import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
import { createHash } from "node:crypto";
import { readFile, readdir } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import postgres from "postgres";
import * as schema from "./schema/index.js";
const MIGRATIONS_FOLDER = fileURLToPath(new URL("./migrations", import.meta.url));
const DRIZZLE_MIGRATIONS_TABLE = "__drizzle_migrations";
const MIGRATIONS_JOURNAL_JSON = fileURLToPath(new URL("./migrations/meta/_journal.json", import.meta.url));
const LEGACY_COLUMN_RENAMES = [
    { tableName: "agents", from: "adapter_type", to: "agent_runtime_type" },
    { tableName: "agents", from: "adapter_config", to: "agent_runtime_config" },
    { tableName: "agent_runtime_state", from: "adapter_type", to: "agent_runtime_type" },
    { tableName: "agent_task_sessions", from: "adapter_type", to: "agent_runtime_type" },
    { tableName: "join_requests", from: "adapter_type", to: "agent_runtime_type" },
    { tableName: "finance_events", from: "execution_adapter_type", to: "execution_agent_runtime_type" },
    { tableName: "issues", from: "assignee_adapter_overrides", to: "assignee_agent_runtime_overrides" },
];
function createUtilitySql(url) {
    return postgres(url, { max: 1, onnotice: () => { } });
}
function buildPostgresConnectionString(opts) {
    const host = opts.host ?? "127.0.0.1";
    const database = opts.database ?? "postgres";
    return `postgres://${encodeURIComponent(opts.user)}:${encodeURIComponent(opts.password)}@${host}:${opts.port}/${database}`;
}
function isSafeIdentifier(value) {
    return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
}
function quoteIdentifier(value) {
    if (!isSafeIdentifier(value))
        throw new Error(`Unsafe SQL identifier: ${value}`);
    return `"${value.replaceAll("\"", "\"\"")}"`;
}
function quoteLiteral(value) {
    return `'${value.replaceAll("'", "''")}'`;
}
function splitMigrationStatements(content) {
    return content
        .split("--> statement-breakpoint")
        .map((statement) => statement.trim())
        .filter((statement) => statement.length > 0);
}
export function createDb(url) {
    const sql = postgres(url);
    return drizzlePg(sql, { schema });
}
async function readPostgresDataDirectory(url) {
    const sql = createUtilitySql(url);
    try {
        const rows = await sql `
      SELECT current_setting('data_directory', true) AS data_directory
    `;
        const actual = rows[0]?.data_directory;
        return typeof actual === "string" && actual.length > 0 ? actual : null;
    }
    catch {
        return null;
    }
    finally {
        await sql.end();
    }
}
export async function getPostgresDataDirectory(url) {
    try {
        return await readPostgresDataDirectory(url);
    }
    catch {
        return null;
    }
}
export async function ensurePostgresRolePassword(options) {
    const candidatePasswords = Array.from(new Set([options.preferredPassword, ...(options.fallbackPasswords ?? [])].filter((value) => value.length > 0)));
    let lastError = null;
    for (const password of candidatePasswords) {
        const connectionString = buildPostgresConnectionString({
            host: options.host,
            port: options.port,
            user: options.user,
            password,
            database: options.database,
        });
        try {
            const actualDataDir = await readPostgresDataDirectory(connectionString);
            if (options.expectedDataDir &&
                (!actualDataDir || resolve(actualDataDir) !== resolve(options.expectedDataDir))) {
                continue;
            }
            if (password === options.preferredPassword) {
                return {
                    connectionString,
                    password,
                    normalized: false,
                };
            }
            const sql = createUtilitySql(connectionString);
            try {
                await sql.unsafe(`ALTER ROLE ${quoteIdentifier(options.user)} WITH PASSWORD ${quoteLiteral(options.preferredPassword)}`);
            }
            finally {
                await sql.end();
            }
            const normalizedConnectionString = buildPostgresConnectionString({
                host: options.host,
                port: options.port,
                user: options.user,
                password: options.preferredPassword,
                database: options.database,
            });
            const normalizedDataDir = await readPostgresDataDirectory(normalizedConnectionString);
            if (options.expectedDataDir &&
                (!normalizedDataDir || resolve(normalizedDataDir) !== resolve(options.expectedDataDir))) {
                throw new Error("PostgreSQL password normalization reached an unexpected data directory");
            }
            return {
                connectionString: normalizedConnectionString,
                password: options.preferredPassword,
                normalized: true,
            };
        }
        catch (error) {
            lastError = error;
        }
    }
    if (lastError instanceof Error)
        throw lastError;
    throw new Error(`Unable to authenticate PostgreSQL user ${options.user}`);
}
async function listMigrationFiles() {
    const entries = await readdir(MIGRATIONS_FOLDER, { withFileTypes: true });
    return entries
        .filter((entry) => entry.isFile() && entry.name.endsWith(".sql"))
        .map((entry) => entry.name)
        .sort((a, b) => a.localeCompare(b));
}
async function listJournalMigrationEntries() {
    try {
        const raw = await readFile(MIGRATIONS_JOURNAL_JSON, "utf8");
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed.entries))
            return [];
        return parsed.entries
            .map((entry, entryIndex) => {
            if (typeof entry?.tag !== "string")
                return null;
            if (typeof entry?.when !== "number" || !Number.isFinite(entry.when))
                return null;
            const order = Number.isInteger(entry.idx) ? Number(entry.idx) : entryIndex;
            return { fileName: `${entry.tag}.sql`, folderMillis: entry.when, order };
        })
            .filter((entry) => entry !== null);
    }
    catch {
        return [];
    }
}
async function listJournalMigrationFiles() {
    const entries = await listJournalMigrationEntries();
    return entries.map((entry) => entry.fileName);
}
async function readMigrationFileContent(migrationFile) {
    return readFile(new URL(`./migrations/${migrationFile}`, import.meta.url), "utf8");
}
async function orderMigrationsByJournal(migrationFiles) {
    const journalEntries = await listJournalMigrationEntries();
    const orderByFileName = new Map(journalEntries.map((entry) => [entry.fileName, entry.order]));
    return [...migrationFiles].sort((left, right) => {
        const leftOrder = orderByFileName.get(left);
        const rightOrder = orderByFileName.get(right);
        if (leftOrder === undefined && rightOrder === undefined)
            return left.localeCompare(right);
        if (leftOrder === undefined)
            return 1;
        if (rightOrder === undefined)
            return -1;
        if (leftOrder === rightOrder)
            return left.localeCompare(right);
        return leftOrder - rightOrder;
    });
}
async function runInTransaction(sql, action) {
    await sql.unsafe("BEGIN");
    try {
        await action();
        await sql.unsafe("COMMIT");
    }
    catch (error) {
        try {
            await sql.unsafe("ROLLBACK");
        }
        catch {
            // Ignore rollback failures and surface the original error.
        }
        throw error;
    }
}
async function latestMigrationCreatedAt(sql, qualifiedTable) {
    const rows = await sql.unsafe(`SELECT created_at FROM ${qualifiedTable} ORDER BY created_at DESC NULLS LAST LIMIT 1`);
    const value = Number(rows[0]?.created_at ?? Number.NaN);
    return Number.isFinite(value) ? value : null;
}
function normalizeFolderMillis(value) {
    if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
        return Math.trunc(value);
    }
    return Date.now();
}
async function ensureMigrationJournalTable(sql) {
    let migrationTableSchema = await discoverMigrationTableSchema(sql);
    if (!migrationTableSchema) {
        const drizzleSchema = quoteIdentifier("drizzle");
        const migrationTable = quoteIdentifier(DRIZZLE_MIGRATIONS_TABLE);
        await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS ${drizzleSchema}`);
        await sql.unsafe(`CREATE TABLE IF NOT EXISTS ${drizzleSchema}.${migrationTable} (id SERIAL PRIMARY KEY, hash text NOT NULL, created_at bigint)`);
        migrationTableSchema = (await discoverMigrationTableSchema(sql)) ?? "drizzle";
    }
    const columnNames = await getMigrationTableColumnNames(sql, migrationTableSchema);
    return { migrationTableSchema, columnNames };
}
async function migrationHistoryEntryExists(sql, qualifiedTable, columnNames, migrationFile, hash) {
    const predicates = [];
    if (columnNames.has("hash"))
        predicates.push(`hash = ${quoteLiteral(hash)}`);
    if (columnNames.has("name"))
        predicates.push(`name = ${quoteLiteral(migrationFile)}`);
    if (predicates.length === 0)
        return false;
    const rows = await sql.unsafe(`SELECT 1 AS one FROM ${qualifiedTable} WHERE ${predicates.join(" OR ")} LIMIT 1`);
    return rows.length > 0;
}
async function recordMigrationHistoryEntry(sql, qualifiedTable, columnNames, migrationFile, hash, folderMillis) {
    const insertColumns = [];
    const insertValues = [];
    if (columnNames.has("hash")) {
        insertColumns.push(quoteIdentifier("hash"));
        insertValues.push(quoteLiteral(hash));
    }
    if (columnNames.has("name")) {
        insertColumns.push(quoteIdentifier("name"));
        insertValues.push(quoteLiteral(migrationFile));
    }
    if (columnNames.has("created_at")) {
        const latestCreatedAt = await latestMigrationCreatedAt(sql, qualifiedTable);
        const createdAt = latestCreatedAt === null
            ? normalizeFolderMillis(folderMillis)
            : Math.max(latestCreatedAt + 1, normalizeFolderMillis(folderMillis));
        insertColumns.push(quoteIdentifier("created_at"));
        insertValues.push(quoteLiteral(String(createdAt)));
    }
    if (insertColumns.length === 0)
        return;
    await sql.unsafe(`INSERT INTO ${qualifiedTable} (${insertColumns.join(", ")}) VALUES (${insertValues.join(", ")})`);
}
async function applyPendingMigrationsManually(url, pendingMigrations) {
    if (pendingMigrations.length === 0)
        return;
    const orderedPendingMigrations = await orderMigrationsByJournal(pendingMigrations);
    const journalEntries = await listJournalMigrationEntries();
    const folderMillisByFileName = new Map(journalEntries.map((entry) => [entry.fileName, normalizeFolderMillis(entry.folderMillis)]));
    const sql = createUtilitySql(url);
    try {
        const { migrationTableSchema, columnNames } = await ensureMigrationJournalTable(sql);
        const qualifiedTable = `${quoteIdentifier(migrationTableSchema)}.${quoteIdentifier(DRIZZLE_MIGRATIONS_TABLE)}`;
        for (const migrationFile of orderedPendingMigrations) {
            const migrationContent = await readMigrationFileContent(migrationFile);
            const hash = createHash("sha256").update(migrationContent).digest("hex");
            const existingEntry = await migrationHistoryEntryExists(sql, qualifiedTable, columnNames, migrationFile, hash);
            if (existingEntry)
                continue;
            await runInTransaction(sql, async () => {
                for (const statement of splitMigrationStatements(migrationContent)) {
                    await sql.unsafe(statement);
                }
                await recordMigrationHistoryEntry(sql, qualifiedTable, columnNames, migrationFile, hash, folderMillisByFileName.get(migrationFile) ?? Date.now());
            });
        }
    }
    finally {
        await sql.end();
    }
}
async function mapHashesToMigrationFiles(migrationFiles) {
    const mapped = new Map();
    await Promise.all(migrationFiles.map(async (migrationFile) => {
        const content = await readMigrationFileContent(migrationFile);
        const hash = createHash("sha256").update(content).digest("hex");
        mapped.set(hash, migrationFile);
    }));
    return mapped;
}
async function getMigrationTableColumnNames(sql, migrationTableSchema) {
    const columns = await sql.unsafe(`
      SELECT column_name
      FROM information_schema.columns
      WHERE table_schema = ${quoteLiteral(migrationTableSchema)}
        AND table_name = ${quoteLiteral(DRIZZLE_MIGRATIONS_TABLE)}
    `);
    return new Set(columns.map((column) => column.column_name));
}
async function tableExists(sql, tableName) {
    const rows = await sql `
    SELECT EXISTS (
      SELECT 1
      FROM information_schema.tables
      WHERE table_schema = 'public'
        AND table_name = ${tableName}
    ) AS exists
  `;
    return rows[0]?.exists ?? false;
}
async function columnExists(sql, tableName, columnName) {
    const rows = await sql `
    SELECT EXISTS (
      SELECT 1
      FROM information_schema.columns
      WHERE table_schema = 'public'
        AND table_name = ${tableName}
        AND column_name = ${columnName}
    ) AS exists
  `;
    return rows[0]?.exists ?? false;
}
async function getColumnMetadata(sql, tableName, columnName) {
    const rows = await sql `
    SELECT true AS exists,
      column_default AS "columnDefault",
      is_nullable AS "isNullable"
    FROM information_schema.columns
    WHERE table_schema = 'public'
      AND table_name = ${tableName}
      AND column_name = ${columnName}
    LIMIT 1
  `;
    const row = rows[0];
    if (!row)
        return null;
    return {
        exists: row.exists,
        columnDefault: row.columnDefault,
        isNullable: row.isNullable === "YES",
    };
}
async function indexExists(sql, indexName) {
    const rows = await sql `
    SELECT EXISTS (
      SELECT 1
      FROM pg_class c
      JOIN pg_namespace n ON n.oid = c.relnamespace
      WHERE n.nspname = 'public'
        AND c.relkind = 'i'
        AND c.relname = ${indexName}
    ) AS exists
  `;
    return rows[0]?.exists ?? false;
}
async function constraintExists(sql, constraintName) {
    const rows = await sql `
    SELECT EXISTS (
      SELECT 1
      FROM pg_constraint c
      JOIN pg_namespace n ON n.oid = c.connamespace
      WHERE n.nspname = 'public'
        AND c.conname = ${constraintName}
    ) AS exists
  `;
    return rows[0]?.exists ?? false;
}
export async function normalizeLegacyColumnNames(url) {
    const sql = createUtilitySql(url);
    const repaired = [];
    try {
        for (const rename of LEGACY_COLUMN_RENAMES) {
            const fromExists = await columnExists(sql, rename.tableName, rename.from);
            if (!fromExists)
                continue;
            const toExists = await columnExists(sql, rename.tableName, rename.to);
            if (toExists)
                continue;
            await sql.unsafe(`ALTER TABLE ${quoteIdentifier(rename.tableName)} RENAME COLUMN ${quoteIdentifier(rename.from)} TO ${quoteIdentifier(rename.to)}`);
            repaired.push(`${rename.tableName}.${rename.from}->${rename.to}`);
        }
    }
    finally {
        await sql.end();
    }
    return repaired;
}
async function migrationStatementAlreadyApplied(sql, statement) {
    const normalized = statement.replace(/\s+/g, " ").trim();
    const createTableMatch = normalized.match(/^CREATE TABLE(?: IF NOT EXISTS)? "([^"]+)"/i);
    if (createTableMatch) {
        return tableExists(sql, createTableMatch[1]);
    }
    const addColumnMatch = normalized.match(/^ALTER TABLE "([^"]+)" ADD COLUMN(?: IF NOT EXISTS)? "([^"]+)"/i);
    if (addColumnMatch) {
        return columnExists(sql, addColumnMatch[1], addColumnMatch[2]);
    }
    const createIndexMatch = normalized.match(/^CREATE (?:UNIQUE )?INDEX(?: IF NOT EXISTS)? "([^"]+)"/i);
    if (createIndexMatch) {
        return indexExists(sql, createIndexMatch[1]);
    }
    const addConstraintMatch = normalized.match(/^ALTER TABLE "([^"]+)" ADD CONSTRAINT "([^"]+)"/i);
    if (addConstraintMatch) {
        return constraintExists(sql, addConstraintMatch[2]);
    }
    const alterColumnSetDefaultMatch = normalized.match(/^ALTER TABLE "([^"]+)" ALTER COLUMN "([^"]+)" SET DEFAULT (.+)$/i);
    if (alterColumnSetDefaultMatch) {
        const [, tableName, columnName, nextDefault] = alterColumnSetDefaultMatch;
        const metadata = await getColumnMetadata(sql, tableName, columnName);
        if (!metadata?.exists)
            return false;
        if (metadata.columnDefault == null)
            return false;
        const normalizeDefault = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
        return normalizeDefault(metadata.columnDefault).includes(normalizeDefault(nextDefault));
    }
    const alterColumnDropNotNullMatch = normalized.match(/^ALTER TABLE "([^"]+)" ALTER COLUMN "([^"]+)" DROP NOT NULL$/i);
    if (alterColumnDropNotNullMatch) {
        const [, tableName, columnName] = alterColumnDropNotNullMatch;
        const metadata = await getColumnMetadata(sql, tableName, columnName);
        return metadata?.exists === true && metadata.isNullable;
    }
    // If we cannot reason about a statement safely, require manual migration.
    return false;
}
async function migrationContentAlreadyApplied(sql, migrationContent) {
    const statements = splitMigrationStatements(migrationContent);
    if (statements.length === 0)
        return false;
    for (const statement of statements) {
        const applied = await migrationStatementAlreadyApplied(sql, statement);
        if (!applied)
            return false;
    }
    return true;
}
async function loadAppliedMigrations(sql, migrationTableSchema, availableMigrations) {
    const quotedSchema = quoteIdentifier(migrationTableSchema);
    const qualifiedTable = `${quotedSchema}.${quoteIdentifier(DRIZZLE_MIGRATIONS_TABLE)}`;
    const columnNames = await getMigrationTableColumnNames(sql, migrationTableSchema);
    if (columnNames.has("name")) {
        const rows = await sql.unsafe(`SELECT name FROM ${qualifiedTable} ORDER BY id`);
        return rows.map((row) => row.name).filter((name) => Boolean(name));
    }
    if (columnNames.has("hash")) {
        const rows = await sql.unsafe(`SELECT hash FROM ${qualifiedTable} ORDER BY id`);
        const hashesToMigrationFiles = await mapHashesToMigrationFiles(availableMigrations);
        const appliedFromHashes = rows
            .map((row) => hashesToMigrationFiles.get(row.hash))
            .filter((name) => Boolean(name));
        if (appliedFromHashes.length > 0) {
            // Best-effort: when all hashes resolve, this is authoritative.
            if (appliedFromHashes.length === rows.length)
                return appliedFromHashes;
            // Partial hash resolution can happen when files have changed; return what we can trust.
            return appliedFromHashes;
        }
        // Fallback only when hashes are unavailable/unresolved.
        if (columnNames.has("created_at")) {
            const journalEntries = await listJournalMigrationEntries();
            if (journalEntries.length > 0) {
                const lastDbRows = await sql.unsafe(`SELECT created_at FROM ${qualifiedTable} ORDER BY created_at DESC LIMIT 1`);
                const lastCreatedAt = Number(lastDbRows[0]?.created_at ?? -1);
                if (Number.isFinite(lastCreatedAt) && lastCreatedAt >= 0) {
                    return journalEntries
                        .filter((entry) => availableMigrations.includes(entry.fileName))
                        .filter((entry) => entry.folderMillis <= lastCreatedAt)
                        .map((entry) => entry.fileName)
                        .slice(0, rows.length);
                }
            }
        }
    }
    const rows = await sql.unsafe(`SELECT id FROM ${qualifiedTable} ORDER BY id`);
    const journalMigrationFiles = await listJournalMigrationFiles();
    const appliedFromIds = rows
        .map((row) => journalMigrationFiles[row.id - 1])
        .filter((name) => Boolean(name));
    if (appliedFromIds.length > 0)
        return appliedFromIds;
    return availableMigrations.slice(0, Math.max(0, rows.length));
}
export async function reconcilePendingMigrationHistory(url) {
    const state = await inspectMigrations(url);
    if (state.status !== "needsMigrations" || state.reason !== "pending-migrations") {
        return { repairedMigrations: [], remainingMigrations: [] };
    }
    const sql = createUtilitySql(url);
    const repairedMigrations = [];
    try {
        const journalEntries = await listJournalMigrationEntries();
        const folderMillisByFile = new Map(journalEntries.map((entry) => [entry.fileName, entry.folderMillis]));
        const migrationTableSchema = await discoverMigrationTableSchema(sql);
        if (!migrationTableSchema) {
            return { repairedMigrations, remainingMigrations: state.pendingMigrations };
        }
        const columnNames = await getMigrationTableColumnNames(sql, migrationTableSchema);
        const qualifiedTable = `${quoteIdentifier(migrationTableSchema)}.${quoteIdentifier(DRIZZLE_MIGRATIONS_TABLE)}`;
        for (const migrationFile of state.pendingMigrations) {
            const migrationContent = await readMigrationFileContent(migrationFile);
            const hash = createHash("sha256").update(migrationContent).digest("hex");
            const folderMillis = folderMillisByFile.get(migrationFile) ?? Date.now();
            const existingByHash = columnNames.has("hash")
                ? await sql.unsafe(`SELECT created_at FROM ${qualifiedTable} WHERE hash = ${quoteLiteral(hash)} ORDER BY created_at DESC LIMIT 1`)
                : [];
            const existingByName = columnNames.has("name")
                ? await sql.unsafe(`SELECT created_at FROM ${qualifiedTable} WHERE name = ${quoteLiteral(migrationFile)} ORDER BY created_at DESC LIMIT 1`)
                : [];
            if (existingByHash.length > 0 || existingByName.length > 0) {
                if (columnNames.has("created_at")) {
                    const existingHashCreatedAt = Number(existingByHash[0]?.created_at ?? -1);
                    if (existingByHash.length > 0 && Number.isFinite(existingHashCreatedAt) && existingHashCreatedAt < folderMillis) {
                        await sql.unsafe(`UPDATE ${qualifiedTable} SET created_at = ${quoteLiteral(String(folderMillis))} WHERE hash = ${quoteLiteral(hash)} AND created_at < ${quoteLiteral(String(folderMillis))}`);
                    }
                    const existingNameCreatedAt = Number(existingByName[0]?.created_at ?? -1);
                    if (existingByName.length > 0 && Number.isFinite(existingNameCreatedAt) && existingNameCreatedAt < folderMillis) {
                        await sql.unsafe(`UPDATE ${qualifiedTable} SET created_at = ${quoteLiteral(String(folderMillis))} WHERE name = ${quoteLiteral(migrationFile)} AND created_at < ${quoteLiteral(String(folderMillis))}`);
                    }
                }
                repairedMigrations.push(migrationFile);
                continue;
            }
            const alreadyApplied = await migrationContentAlreadyApplied(sql, migrationContent);
            if (!alreadyApplied)
                break;
            const insertColumns = [];
            const insertValues = [];
            if (columnNames.has("hash")) {
                insertColumns.push(quoteIdentifier("hash"));
                insertValues.push(quoteLiteral(hash));
            }
            if (columnNames.has("name")) {
                insertColumns.push(quoteIdentifier("name"));
                insertValues.push(quoteLiteral(migrationFile));
            }
            if (columnNames.has("created_at")) {
                insertColumns.push(quoteIdentifier("created_at"));
                insertValues.push(quoteLiteral(String(folderMillis)));
            }
            if (insertColumns.length === 0)
                break;
            await sql.unsafe(`INSERT INTO ${qualifiedTable} (${insertColumns.join(", ")}) VALUES (${insertValues.join(", ")})`);
            repairedMigrations.push(migrationFile);
        }
    }
    finally {
        await sql.end();
    }
    const refreshed = await inspectMigrations(url);
    return {
        repairedMigrations,
        remainingMigrations: refreshed.status === "needsMigrations" ? refreshed.pendingMigrations : [],
    };
}
async function discoverMigrationTableSchema(sql) {
    const rows = await sql `
    SELECT n.nspname AS "schemaName"
    FROM pg_class c
    JOIN pg_namespace n ON n.oid = c.relnamespace
    WHERE c.relname = ${DRIZZLE_MIGRATIONS_TABLE} AND c.relkind = 'r'
  `;
    if (rows.length === 0)
        return null;
    const drizzleSchema = rows.find(({ schemaName }) => schemaName === "drizzle");
    if (drizzleSchema)
        return drizzleSchema.schemaName;
    const publicSchema = rows.find(({ schemaName }) => schemaName === "public");
    if (publicSchema)
        return publicSchema.schemaName;
    return rows[0]?.schemaName ?? null;
}
async function clearMigrationJournal(url) {
    const sql = createUtilitySql(url);
    try {
        const migrationTableSchema = await discoverMigrationTableSchema(sql);
        if (!migrationTableSchema)
            return;
        const qualifiedTable = `${quoteIdentifier(migrationTableSchema)}.${quoteIdentifier(DRIZZLE_MIGRATIONS_TABLE)}`;
        await sql.unsafe(`DROP TABLE IF EXISTS ${qualifiedTable}`);
        if (migrationTableSchema === "drizzle") {
            await sql.unsafe(`DROP SCHEMA IF EXISTS ${quoteIdentifier("drizzle")}`);
        }
    }
    finally {
        await sql.end();
    }
}
export async function inspectMigrations(url) {
    const sql = createUtilitySql(url);
    try {
        const availableMigrations = await listMigrationFiles();
        const tableCountResult = await sql `
      select count(*)::int as count
      from information_schema.tables
      where table_schema = 'public'
        and table_type = 'BASE TABLE'
    `;
        const tableCount = tableCountResult[0]?.count ?? 0;
        const migrationTableSchema = await discoverMigrationTableSchema(sql);
        if (!migrationTableSchema) {
            if (tableCount > 0) {
                return {
                    status: "needsMigrations",
                    tableCount,
                    availableMigrations,
                    appliedMigrations: [],
                    pendingMigrations: availableMigrations,
                    reason: "no-migration-journal-non-empty-db",
                };
            }
            return {
                status: "needsMigrations",
                tableCount,
                availableMigrations,
                appliedMigrations: [],
                pendingMigrations: availableMigrations,
                reason: "no-migration-journal-empty-db",
            };
        }
        const appliedMigrations = await loadAppliedMigrations(sql, migrationTableSchema, availableMigrations);
        const pendingMigrations = availableMigrations.filter((name) => !appliedMigrations.includes(name));
        if (pendingMigrations.length === 0) {
            const hasOrganizationsTable = await tableExists(sql, "organizations");
            if (!hasOrganizationsTable) {
                return {
                    status: "needsMigrations",
                    tableCount,
                    availableMigrations,
                    appliedMigrations,
                    pendingMigrations: availableMigrations,
                    reason: "missing-core-schema",
                };
            }
            return {
                status: "upToDate",
                tableCount,
                availableMigrations,
                appliedMigrations,
            };
        }
        return {
            status: "needsMigrations",
            tableCount,
            availableMigrations,
            appliedMigrations,
            pendingMigrations,
            reason: "pending-migrations",
        };
    }
    finally {
        await sql.end();
    }
}
export async function applyPendingMigrations(url) {
    await normalizeLegacyColumnNames(url);
    const initialState = await inspectMigrations(url);
    if (initialState.status === "upToDate")
        return;
    if (initialState.reason === "missing-core-schema") {
        if (initialState.tableCount > 0) {
            throw new Error("Migration journal claims the database is up to date, but required core tables are missing. " +
                "Refusing automatic repair because the public schema is not empty.");
        }
        await clearMigrationJournal(url);
    }
    const repairedInitialState = await inspectMigrations(url);
    if (repairedInitialState.status === "upToDate")
        return;
    if (repairedInitialState.reason === "no-migration-journal-empty-db") {
        const sql = createUtilitySql(url);
        try {
            const db = drizzlePg(sql);
            await migratePg(db, { migrationsFolder: MIGRATIONS_FOLDER });
        }
        finally {
            await sql.end();
        }
        let bootstrappedState = await inspectMigrations(url);
        if (bootstrappedState.status === "upToDate")
            return;
        if (bootstrappedState.reason === "pending-migrations") {
            const repair = await reconcilePendingMigrationHistory(url);
            if (repair.repairedMigrations.length > 0) {
                bootstrappedState = await inspectMigrations(url);
            }
            if (bootstrappedState.status === "needsMigrations" && bootstrappedState.reason === "pending-migrations") {
                await applyPendingMigrationsManually(url, bootstrappedState.pendingMigrations);
                bootstrappedState = await inspectMigrations(url);
            }
        }
        if (bootstrappedState.status === "upToDate")
            return;
        throw new Error(`Failed to bootstrap migrations: ${bootstrappedState.pendingMigrations.join(", ")}`);
    }
    if (repairedInitialState.reason === "no-migration-journal-non-empty-db") {
        throw new Error("Database has tables but no migration journal; automatic migration is unsafe. Initialize migration history manually.");
    }
    let state = await inspectMigrations(url);
    if (state.status === "upToDate")
        return;
    const repair = await reconcilePendingMigrationHistory(url);
    if (repair.repairedMigrations.length > 0) {
        state = await inspectMigrations(url);
        if (state.status === "upToDate")
            return;
    }
    if (state.status !== "needsMigrations" || state.reason !== "pending-migrations") {
        throw new Error("Migrations are still pending after migration-history reconciliation; run inspectMigrations for details.");
    }
    await applyPendingMigrationsManually(url, state.pendingMigrations);
    const finalState = await inspectMigrations(url);
    if (finalState.status !== "upToDate") {
        throw new Error(`Failed to apply pending migrations: ${finalState.pendingMigrations.join(", ")}`);
    }
}
export async function migratePostgresIfEmpty(url) {
    const sql = createUtilitySql(url);
    try {
        const migrationTableSchema = await discoverMigrationTableSchema(sql);
        const tableCountResult = await sql `
      select count(*)::int as count
      from information_schema.tables
      where table_schema = 'public'
        and table_type = 'BASE TABLE'
    `;
        const tableCount = tableCountResult[0]?.count ?? 0;
        if (migrationTableSchema) {
            return { migrated: false, reason: "already-migrated", tableCount };
        }
        if (tableCount > 0) {
            return { migrated: false, reason: "not-empty-no-migration-journal", tableCount };
        }
        const db = drizzlePg(sql);
        await migratePg(db, { migrationsFolder: MIGRATIONS_FOLDER });
        return { migrated: true, reason: "migrated-empty-db", tableCount: 0 };
    }
    finally {
        await sql.end();
    }
}
export async function ensurePostgresDatabase(url, databaseName) {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(databaseName)) {
        throw new Error(`Unsafe database name: ${databaseName}`);
    }
    const sql = createUtilitySql(url);
    try {
        const existing = await sql `
      select 1 as one from pg_database where datname = ${databaseName} limit 1
    `;
        if (existing.length > 0)
            return "exists";
        await sql.unsafe(`create database "${databaseName}" encoding 'UTF8' lc_collate 'C' lc_ctype 'C' template template0`);
        return "created";
    }
    finally {
        await sql.end();
    }
}
//# sourceMappingURL=client.js.map