import { doltAddAndCommit, doltStatus } from "./commit.js";
import { MergeConflictError, doltMerge } from "./merge.js";
import { sql } from "drizzle-orm";
import { createHash } from "node:crypto";

//#region src/dolt/schema-sync.ts
/**
* The branch that serves as the source of truth for schema.
* All other branches sync their schema from this branch.
*/
const SCHEMA_SOURCE_BRANCH = "main";
/**
* Get the currently active branch
*/
const getActiveBranch = (db) => async () => {
	return (await db.execute(sql`SELECT active_branch() as branch`)).rows[0]?.branch;
};
/**
* Get schema differences between the schema source branch and a target branch.
* Returns an empty array if schemas are identical.
*
* The comparison is done as dolt_schema_diff(targetBranch, SCHEMA_SOURCE_BRANCH),
* so fromCreateStatement shows the target branch's schema and toCreateStatement
* shows main's schema (what we want to sync to).
*
* @param targetBranch - The branch to compare against the schema source
* @returns Array of schema differences (one per table that differs)
*/
const getSchemaDiff = (db) => async (targetBranch) => {
	return (await db.execute(sql.raw(`SELECT * FROM dolt_schema_diff('${targetBranch}', '${SCHEMA_SOURCE_BRANCH}')`))).rows.map((row) => ({
		fromTableName: row.from_table_name,
		toTableName: row.to_table_name,
		fromCreateStatement: row.from_create_statement,
		toCreateStatement: row.to_create_statement
	}));
};
/**
* Check if a branch has schema differences from the schema source branch
*/
const hasSchemaDifferences = (db) => async (targetBranch) => {
	return (await getSchemaDiff(db)(targetBranch)).length > 0;
};
/**
* Check if the current branch has uncommitted changes
*/
const hasUncommittedChanges = (db) => async () => {
	return (await doltStatus(db)()).length > 0;
};
/**
* Commit any pending changes on the current branch
*/
const commitPendingChanges = (db) => async (options) => {
	const message = options.message ?? "Auto-commit pending changes before schema sync";
	await doltAddAndCommit(db)({
		message,
		author: options.author
	});
};
/**
* Advisory lock key prefix for schema sync operations.
* We use a fixed prefix combined with the branch name hash to create unique lock keys.
*/
const SCHEMA_SYNC_LOCK_PREFIX = "schema_sync_";
const getSchemaSyncLockKey = (branchName) => {
	const lockKey = `${SCHEMA_SYNC_LOCK_PREFIX}${branchName}`;
	return createHash("sha256").update(lockKey).digest().readBigInt64BE(0);
};
/**
* Try to acquire a non-blocking advisory lock for schema sync on a branch.
* Uses pg_try_advisory_lock which returns immediately without waiting.
*
* @param branchName - The branch name to lock
* @returns true if lock was acquired, false if another session holds it
*/
const tryAcquireSchemaSyncLock = (db) => async (branchName) => {
	const key = getSchemaSyncLockKey(branchName);
	return (await db.execute(sql`SELECT pg_try_advisory_lock(CAST(${key} AS bigint)) as acquired`)).rows[0]?.acquired === true;
};
/**
* Release the advisory lock for schema sync on a branch.
*
* @param branchName - The branch name to unlock
*/
const releaseSchemaSyncLock = (db) => async (branchName) => {
	const key = getSchemaSyncLockKey(branchName);
	await db.execute(sql`SELECT pg_advisory_unlock(CAST(${key} AS bigint))`);
};
/**
* Get the latest commit hash for the current branch
*/
const getLatestCommitHash = (db) => async () => {
	return (await db.execute(sql`SELECT commit_hash FROM dolt_log LIMIT 1`)).rows[0]?.commit_hash;
};
/**
* Sync schema from the schema source branch (main) into the current branch.
* This performs a merge of the schema source branch into the current branch.
*
* Uses a non-blocking advisory lock to prevent duplicate syncs when multiple
* concurrent requests attempt to sync the same branch. If another request is
* already syncing, this function returns immediately with skippedDueToLock: true.
*
* Prerequisites:
* - Current branch must not have uncommitted changes (unless autoCommitPending is true)
* - Current branch must not be the schema source branch itself
*
* @param options - Sync options
* @returns Result of the sync operation
*/
const syncSchemaFromMain = (db) => async (options = {}) => {
	const currentBranch = await getActiveBranch(db)();
	if (currentBranch === SCHEMA_SOURCE_BRANCH) return {
		synced: true,
		hadDifferences: false,
		error: "Cannot sync schema: already on schema source branch"
	};
	if (!await tryAcquireSchemaSyncLock(db)(currentBranch)) return {
		synced: false,
		hadDifferences: true,
		skippedDueToLock: true
	};
	try {
		const differences = await getSchemaDiff(db)(currentBranch);
		if (differences.length === 0) return {
			synced: false,
			hadDifferences: false
		};
		if (await hasUncommittedChanges(db)()) if (options.autoCommitPending) await commitPendingChanges(db)({
			message: "Auto-commit pending changes before schema sync",
			author: options.author
		});
		else return {
			synced: false,
			hadDifferences: true,
			differences,
			error: "Cannot sync schema: uncommitted changes exist. Commit changes first or set autoCommitPending: true"
		};
		const mergeSchemaMessage = `Synced schema from ${SCHEMA_SOURCE_BRANCH}`;
		const schemaSyncAuthor = {
			name: "Schema Sync System",
			email: "system@inkeep.com"
		};
		try {
			await doltMerge(db)({
				fromBranch: SCHEMA_SOURCE_BRANCH,
				toBranch: currentBranch,
				message: mergeSchemaMessage,
				noFastForward: true,
				author: schemaSyncAuthor
			});
		} catch (error) {
			if (error instanceof MergeConflictError) return {
				synced: false,
				hadDifferences: true,
				differences,
				error: "Schema merge produced conflicts that require manual resolution. Merge has been rolled back."
			};
			throw error;
		}
		return {
			synced: true,
			hadDifferences: true,
			differences,
			mergeCommitHash: await getLatestCommitHash(db)()
		};
	} catch (error) {
		return {
			synced: false,
			hadDifferences: true,
			error: `Schema sync failed: ${error instanceof Error ? error.message : "Unknown error"}`
		};
	} finally {
		try {
			await releaseSchemaSyncLock(db)(currentBranch);
		} catch {}
	}
};
/**
* Ensure the current branch has schema in sync with the schema source branch.
* This is a convenience function for checking and optionally syncing schema.
*
* Use cases:
* - Call before write operations to ensure schema compatibility
* - Call before merge operations to prevent schema conflicts
* - Call on branch checkout to keep branches up-to-date
*
* @param options - Options for the check and optional sync
* @returns Result indicating current sync status
*/
const ensureSchemaSync = (db) => async (options = {}) => {
	const currentBranch = await getActiveBranch(db)();
	if (currentBranch === SCHEMA_SOURCE_BRANCH) return {
		synced: false,
		hadDifferences: false
	};
	const differences = await getSchemaDiff(db)(currentBranch);
	if (differences.length === 0) return {
		synced: false,
		hadDifferences: false
	};
	if (options.autoSync) return syncSchemaFromMain(db)({
		autoCommitPending: options.autoCommitPending,
		commitMessage: options.commitMessage,
		author: options.author
	});
	return {
		synced: false,
		hadDifferences: true,
		differences,
		error: `Branch '${currentBranch}' has ${differences.length} schema difference(s) from '${SCHEMA_SOURCE_BRANCH}'. Set autoSync: true to automatically sync schema.`
	};
};
/**
* Get a human-readable summary of schema differences
*/
const formatSchemaDiffSummary = (differences) => {
	if (differences.length === 0) return "No schema differences";
	const lines = [`${differences.length} table(s) with schema differences:`];
	for (const diff of differences) {
		const tableName = diff.toTableName || diff.fromTableName;
		let changeType;
		if (!diff.fromCreateStatement) changeType = "added";
		else if (!diff.toCreateStatement) changeType = "removed";
		else changeType = "modified";
		lines.push(`  - ${tableName} (${changeType})`);
	}
	return lines.join("\n");
};
/**
* Check if two branches have compatible schemas (both are in sync with main).
* Useful before merging two feature branches.
*
* @param branchA - First branch to check
* @param branchB - Second branch to check
* @returns Object indicating if both branches are schema-compatible
*/
const areBranchesSchemaCompatible = (db) => async (branchA, branchB) => {
	const [diffA, diffB] = await Promise.all([getSchemaDiff(db)(branchA), getSchemaDiff(db)(branchB)]);
	return {
		compatible: diffA.length === 0 && diffB.length === 0,
		branchADifferences: diffA,
		branchBDifferences: diffB
	};
};
const isLocalhostUrl = (url) => {
	if (!url) return false;
	try {
		const parsed = new URL(url);
		return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
	} catch {
		return url.includes("localhost") || url.includes("127.0.0.1");
	}
};

//#endregion
export { SCHEMA_SOURCE_BRANCH, areBranchesSchemaCompatible, ensureSchemaSync, formatSchemaDiffSummary, getActiveBranch, getSchemaDiff, hasSchemaDifferences, hasUncommittedChanges, isLocalhostUrl, syncSchemaFromMain };