/**
 * D1 Remote Database Adapter
 *
 * Calls a ZinTrust Cloudflare Worker proxy over HTTPS.
 */
import { RemoteSignedJson } from '../../common/RemoteSignedJson.js';
import { Env } from '../../config/env.js';
import { Logger } from '../../config/logger.js';
import { ErrorFactory } from '../../exceptions/ZintrustError.js';
import { AdaptersEnum } from '../../migrations/enum/index.js';
import { isRecord } from '../adapters/SqlProxyAdapterUtils.js';
import { createStatementId } from '../adapters/SqlProxyRegistryMode.js';
import { QueryBuilder } from '../QueryBuilder.js';
import { SchemaWriter } from '../SchemaStatemenWriter.js';
import { isMutatingSql } from '../../proxy/isMutatingSql.js';
let warnedFallbackCredentials = false;
const resolveSigningPrefix = (baseUrl) => {
    try {
        const parsed = new URL(baseUrl);
        const path = parsed.pathname.endsWith('/') ? parsed.pathname.slice(0, -1) : parsed.pathname;
        if (path === '' || path === '/')
            return undefined;
        return path;
    }
    catch {
        return undefined;
    }
};
const createRemoteConfig = () => {
    const directKeyId = Env.get('D1_REMOTE_KEY_ID', '').trim();
    const directSecret = Env.get('D1_REMOTE_SECRET', '').trim();
    // Intentionally pass empty values when missing so RemoteSignedJson's credential normalizer
    // derives the fallback keyId/secret (APP_NAME/APP_KEY) in a consistent, safe way.
    const keyId = directKeyId === '' ? '' : directKeyId;
    const secret = directSecret === '' ? '' : directSecret;
    if (directKeyId === '' || directSecret === '') {
        if (!warnedFallbackCredentials) {
            warnedFallbackCredentials = true;
            Logger.warn('D1_REMOTE_KEY_ID / D1_REMOTE_SECRET missing; using fallback signing credentials (APP_NAME / APP_KEY).');
        }
    }
    const envName = (Env.get('NODE_ENV', 'development') || 'development').trim().toLowerCase();
    const defaultMode = envName === 'production' ? 'registry' : 'sql';
    const settings = {
        baseUrl: Env.get('D1_REMOTE_URL'),
        keyId,
        secret,
        mode: Env.get('D1_REMOTE_MODE', defaultMode) ?? defaultMode,
        timeoutMs: Env.getInt('ZT_PROXY_TIMEOUT_MS', Env.REQUEST_TIMEOUT),
    };
    const remote = {
        baseUrl: settings.baseUrl,
        keyId: settings.keyId,
        secret: settings.secret,
        timeoutMs: settings.timeoutMs,
        signaturePathPrefixToStrip: resolveSigningPrefix(settings.baseUrl),
        missingUrlMessage: 'D1 remote proxy URL is missing (D1_REMOTE_URL)',
        missingCredentialsMessage: 'D1 remote signing credentials are missing (D1_REMOTE_KEY_ID / D1_REMOTE_SECRET). Fallbacks: APP_NAME and APP_KEY.',
        messages: {
            unauthorized: 'D1 remote proxy unauthorized',
            forbidden: 'D1 remote proxy forbidden',
            rateLimited: 'D1 remote proxy rate limited',
            rejected: 'D1 remote proxy rejected request',
            error: 'D1 remote proxy error',
            timedOut: 'D1 remote proxy request timed out',
        },
    };
    return { mode: settings.mode, remote };
};
const createStatementPayload = async (sql, parameters) => {
    const statementId = await createStatementId(sql);
    await SchemaWriter(sql);
    return { statementId, params: parameters };
};
const isQueryResponse = (value) => isRecord(value) &&
    Array.isArray(value['rows']) &&
    typeof value['rowCount'] === 'number' &&
    value['rows'].every((r) => isRecord(r));
const isQueryOneResponse = (value) => isRecord(value) && 'row' in value && (value['row'] === null || isRecord(value['row']));
const getExecMeta = (value) => {
    if (!isRecord(value) || typeof value['ok'] !== 'boolean')
        return { changes: 0 };
    const meta = value['meta'];
    if (!isRecord(meta))
        return { changes: 0 };
    const changes = typeof meta['changes'] === 'number' ? meta['changes'] : 0;
    const lastRowIdCandidate = meta['lastRowId'] ??
        meta['last_row_id'] ??
        meta['lastInsertRowid'] ??
        meta['last_insert_rowid'];
    const lastRowId = typeof lastRowIdCandidate === 'number' ||
        typeof lastRowIdCandidate === 'string' ||
        typeof lastRowIdCandidate === 'bigint'
        ? lastRowIdCandidate
        : undefined;
    return { changes, lastRowId };
};
const queryRegistry = async (settings, sql, parameters) => {
    const payload = await createStatementPayload(sql, parameters);
    const out = await RemoteSignedJson.request(settings, '/zin/d1/statement', payload);
    if (isQueryResponse(out)) {
        return { rows: out.rows, rowCount: out.rowCount };
    }
    if (isQueryOneResponse(out)) {
        const row = out.row;
        return { rows: row ? [row] : [], rowCount: row ? 1 : 0 };
    }
    const meta = getExecMeta(out);
    return { rows: [], rowCount: meta.changes, lastInsertId: meta.lastRowId };
};
const querySqlMode = async (settings, sql, parameters) => {
    await SchemaWriter(sql);
    if (isMutatingSql(sql)) {
        const out = await RemoteSignedJson.request(settings, '/zin/d1/exec', {
            sql,
            params: parameters,
        });
        const meta = getExecMeta(out);
        return { rows: [], rowCount: meta.changes, lastInsertId: meta.lastRowId };
    }
    const out = await RemoteSignedJson.request(settings, '/zin/d1/query', {
        sql,
        params: parameters,
    });
    return { rows: out.rows, rowCount: out.rowCount };
};
const requireConnected = (getConnected) => {
    if (!getConnected())
        throw ErrorFactory.createConnectionError('Database not connected');
};
const createConnectMethod = (setConnected) => async () => {
    setConnected(true);
    return Promise.resolve(); // NOSONAR
};
const createDisconnectMethod = (setConnected) => async () => {
    setConnected(false);
    return Promise.resolve(); // NOSONAR
};
const createQueryMethod = (getConnected, mode, remote) => async (sql, parameters) => {
    requireConnected(getConnected);
    if (mode === 'registry') {
        return queryRegistry(remote, sql, parameters);
    }
    return querySqlMode(remote, sql, parameters);
};
const createQueryOneMethod = (getConnected, mode, remote) => async (sql, parameters) => {
    requireConnected(getConnected);
    if (mode === 'registry') {
        const payload = await createStatementPayload(sql, parameters);
        const out = await RemoteSignedJson.request(remote, '/zin/d1/statement', payload);
        if (isQueryOneResponse(out))
            return out.row;
        if (isQueryResponse(out))
            return out.rows[0] ?? null;
        return null;
    }
    await SchemaWriter(sql);
    const out = await RemoteSignedJson.request(remote, '/zin/d1/queryOne', {
        sql,
        params: parameters,
    });
    return out.row;
};
const createPingMethod = (getConnected, methods) => async () => {
    requireConnected(getConnected);
    const sql = QueryBuilder.create('').select('1').toSQL();
    await methods.queryOne(sql, []);
};
const createTransactionMethod = (getConnected, methods) => async (callback) => {
    requireConnected(getConnected);
    try {
        return await callback(methods);
    }
    catch (error) {
        throw ErrorFactory.createTryCatchError('Transaction failed', error);
    }
};
const createEnsureMigrationsTableMethod = (getConnected, methods) => async () => {
    requireConnected(getConnected);
    // D1 is SQLite under the hood; this schema matches the core migrator's expectations.
    // Note: d1-remote migrations are expected to run in SQL mode (the CLI enforces this).
    await methods.query(`CREATE TABLE IF NOT EXISTS migrations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        scope TEXT NOT NULL DEFAULT 'global',
        service TEXT NOT NULL DEFAULT '',
        batch INTEGER NOT NULL,
        status TEXT NOT NULL,
        applied_at TEXT NULL,
        created_at TEXT NOT NULL,
        UNIQUE(name, scope, service)
      )`, []);
};
const createRawQueryMethod = (methods) => async (sql, parameters = []) => {
    const out = await methods.query(sql, parameters);
    return out.rows;
};
const createAdapterMethods = (getConnected, setConnected, mode, remote) => {
    const methods = {};
    methods.connect = createConnectMethod(setConnected);
    methods.disconnect = createDisconnectMethod(setConnected);
    methods.query = createQueryMethod(getConnected, mode, remote);
    methods.queryOne = createQueryOneMethod(getConnected, mode, remote);
    methods.ping = createPingMethod(getConnected, methods);
    methods.transaction = createTransactionMethod(getConnected, methods);
    methods.ensureMigrationsTable = createEnsureMigrationsTableMethod(getConnected, methods);
    methods.rawQuery = createRawQueryMethod(methods);
    methods.getType = () => AdaptersEnum.d1Remote;
    methods.isConnected = () => getConnected();
    methods.getPlaceholder = (_index) => '?';
    return methods;
};
const createAdapter = (getConnected, setConnected, mode, remote) => createAdapterMethods(getConnected, setConnected, mode, remote);
export const D1RemoteAdapter = Object.freeze({
    create(_config) {
        let connected = false;
        const { mode, remote } = createRemoteConfig();
        const getConnected = () => connected;
        return createAdapter(getConnected, (value) => {
            connected = value;
        }, mode, remote);
    },
});
export default D1RemoteAdapter;
