<?php
// api/credentials.php — Gestión de credenciales por cliente
// Incluir desde otros archivos api/

if (!function_exists('dm_require_login')) {
    require_once __DIR__ . '/auth.php';
}

// ── Cifrado de credenciales (AES-256-CBC) ─────────────────────────────────────
// La clave se deriva de constantes de config — no requiere nueva variable
function _dm_cred_key(): string {
    return hash('sha256', DB_NAME . DB_USER . 'sc-cred-2025', true);
}

function dm_encrypt_cred(string $value): string {
    if (empty($value) || !function_exists('openssl_encrypt')) return $value;
    $iv  = random_bytes(16);
    $enc = openssl_encrypt($value, 'AES-256-CBC', _dm_cred_key(), OPENSSL_RAW_DATA, $iv);
    return 'enc:' . base64_encode($iv . $enc);
}

function dm_decrypt_cred(string $value): string {
    if (empty($value) || strpos($value, 'enc:') !== 0) return $value; // texto plano legacy
    $raw = base64_decode(substr($value, 4));
    if (strlen($raw) < 17) return $value;
    $dec = openssl_decrypt(substr($raw, 16), 'AES-256-CBC', _dm_cred_key(), OPENSSL_RAW_DATA, substr($raw, 0, 16));
    return ($dec !== false) ? $dec : $value;
}

/**
 * Guarda una credencial cifrada para un cliente
 */
function dm_set_credential(int $client_id, string $service, string $key_name, string $value): bool {
    global $pdo;
    if (!$pdo) return false;
    try {
        $pdo->prepare('
            INSERT INTO dm_client_credentials (client_id, service, key_name, key_value)
            VALUES (?, ?, ?, ?)
            ON DUPLICATE KEY UPDATE key_value = VALUES(key_value), updated_at = NOW()
        ')->execute([$client_id, $service, $key_name, dm_encrypt_cred($value)]);
        return true;
    } catch (Exception $e) { return false; }
}

/**
 * Lee y descifra una credencial de un cliente
 */
function dm_get_credential(int $client_id, string $service, string $key_name): string {
    global $pdo;
    if (!$pdo) return '';
    try {
        $stmt = $pdo->prepare('
            SELECT key_value FROM dm_client_credentials
            WHERE client_id = ? AND service = ? AND key_name = ?
        ');
        $stmt->execute([$client_id, $service, $key_name]);
        return dm_decrypt_cred($stmt->fetchColumn() ?: '');
    } catch (Exception $e) { return ''; }
}

/**
 * Lee y descifra todas las credenciales de un cliente para un servicio.
 *
 * FASE 8 FIX: aislamiento multi-tenant — si hay usuario logueado y NO es admin,
 * solo devuelve credenciales si el usuario tiene acceso a ese cliente via
 * dm_user_clients. Admins siguen viendo todo (super-admin global por ahora).
 *
 * Cron (sin sesión activa, php_sapi_name() === 'cli') bypassea el check —
 * el contexto cron ya está protegido por CRON_SECRET en sus endpoints.
 */
function dm_get_credentials(int $client_id, string $service): array {
    global $pdo;
    if (!$pdo) return [];

    // Aislamiento multi-tenant: validar acceso si hay sesión web activa
    if (php_sapi_name() !== 'cli' && !empty($_SESSION['dm_user_id'])) {
        $uid  = (int)$_SESSION['dm_user_id'];
        $role = $_SESSION['dm_role'] ?? '';
        if ($role !== 'admin') {
            try {
                $chk = $pdo->prepare('SELECT 1 FROM dm_user_clients WHERE user_id=? AND client_id=? LIMIT 1');
                $chk->execute([$uid, $client_id]);
                if (!$chk->fetchColumn()) {
                    error_log("[dm_get_credentials] user {$uid} sin acceso a cliente {$client_id}");
                    return [];
                }
            } catch (\Throwable $e) { /* tabla puede no existir en deploys viejos — fail-open con audit log */
                error_log("[dm_get_credentials] dm_user_clients check failed: " . $e->getMessage());
            }
        }
    }

    try {
        $stmt = $pdo->prepare('
            SELECT key_name, key_value FROM dm_client_credentials
            WHERE client_id = ? AND service = ?
        ');
        $stmt->execute([$client_id, $service]);
        $result = [];
        foreach ($stmt->fetchAll() as $row) {
            $result[$row['key_name']] = dm_decrypt_cred($row['key_value']);
        }
        return $result;
    } catch (Exception $e) { return []; }
}

/**
 * FASE 7: Secrets globales cifrados (no per-cliente)
 * Reemplaza el patrón actual de guardar GOOGLE_REFRESH_TOKEN/etc en config.php plain text.
 * Storage: dm_settings.value como `enc:base64(iv+aes)`.
 * Lectura ordenada: BD cifrada → constante config.php (fallback de migración).
 */
function dm_set_global_secret(string $name, string $value): bool {
    global $pdo;
    if (!$pdo) return false;
    try {
        $pdo->exec("CREATE TABLE IF NOT EXISTS dm_settings (
            `key`        VARCHAR(80) PRIMARY KEY,
            `value`      TEXT,
            `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        $enc = dm_encrypt_cred($value);
        $pdo->prepare("INSERT INTO dm_settings (`key`,`value`) VALUES (?,?)
                       ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), updated_at=NOW()")
            ->execute([$name, $enc]);
        return true;
    } catch (\Throwable $e) {
        error_log("[dm_set_global_secret] {$name}: " . $e->getMessage());
        return false;
    }
}

function dm_get_global_secret(string $name, string $config_constant_fallback = ''): string {
    global $pdo;
    // 1) BD cifrada
    if ($pdo) {
        try {
            $st = $pdo->prepare("SELECT `value` FROM dm_settings WHERE `key`=? LIMIT 1");
            $st->execute([$name]);
            $val = $st->fetchColumn();
            if ($val !== false && $val !== null && $val !== '') {
                return dm_decrypt_cred((string)$val);
            }
        } catch (\Throwable $e) { /* tabla puede no existir aún */ }
    }
    // 2) Fallback a constante en config.php (migración) — solo si se especificó constante
    if ($config_constant_fallback !== '' && defined($config_constant_fallback)) {
        return (string)constant($config_constant_fallback);
    }
    return '';
}

/**
 * FASE 8: Accesores convenience para los secrets más usados.
 * Cualquier nuevo código debería usar estos en vez de leer CLAUDE_API_KEY/META_SYSTEM_TOKEN
 * directamente como constante — así el día que se borren de config.php todo sigue funcionando.
 */
function dm_claude_api_key(): string {
    return dm_get_global_secret('CLAUDE_API_KEY', 'CLAUDE_API_KEY');
}
function dm_meta_system_token(): string {
    return dm_get_global_secret('META_SYSTEM_TOKEN', 'META_SYSTEM_TOKEN');
}

/**
 * Cache helper — guarda en BD con TTL en minutos
 */
function dm_cache_set(string $key, $data, int $ttl_minutes = 30): void {
    global $pdo;
    if (!$pdo) return;
    try {
        $pdo->prepare('
            INSERT INTO dm_cache (cache_key, data, expires_at)
            VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? MINUTE))
            ON DUPLICATE KEY UPDATE data = VALUES(data), expires_at = VALUES(expires_at)
        ')->execute([$key, json_encode($data), $ttl_minutes]);
        // Purga probabilística (~2% de escrituras): elimina entradas vencidas
        if (rand(1, 50) === 1) {
            $pdo->exec("DELETE FROM dm_cache WHERE expires_at < DATE_SUB(NOW(), INTERVAL 1 HOUR)");
        }
    } catch (Exception $e) {}
}

/**
 * Cache helper — lee de BD, devuelve null si expiró
 */
function dm_cache_get(string $key) {
    global $pdo;
    if (!$pdo) return null;
    try {
        $stmt = $pdo->prepare('
            SELECT data FROM dm_cache
            WHERE cache_key = ? AND expires_at > NOW()
        ');
        $stmt->execute([$key]);
        $row = $stmt->fetchColumn();
        return $row ? json_decode($row, true) : null;
    } catch (Exception $e) { return null; }
}
