import { Resource, ResourceKind } from "../resource.js";
import { Scope } from "../scope.js";
import { logger } from "../util/logger.js";
import { CloudflareApiError } from "./api-error.js";
import { extractCloudflareResult } from "./api-response.js";
import { createCloudflareApi, } from "./api.js";
import { cloneD1Database } from "./d1-clone.js";
import { importD1Database } from "./d1-import.js";
import { applyLocalD1Migrations } from "./d1-local-migrations.js";
import { applyMigrations } from "./d1-migrations.js";
import { listSqlFiles, readSqlFile } from "./d1-sql-file.js";
import { deleteMiniflareBinding } from "./miniflare/delete.js";
const DEFAULT_MIGRATIONS_TABLE = "d1_migrations";
export function isD1Database(resource) {
    return resource?.[ResourceKind] === "cloudflare::D1Database";
}
/**
 * Creates and manages Cloudflare D1 Databases.
 *
 * D1 Databases provide serverless SQL databases built on SQLite with
 * automatic data replication for high availability.
 *
 * @example
 * // Create a basic D1 database with default settings
 * const basicDatabase = await D1Database("my-app-db", {
 *   name: "my-app-db"
 * });
 *
 * @example
 * // Create a database with location hint for optimal performance
 * const westUsDatabase = await D1Database("west-us-db", {
 *   name: "west-us-db",
 *   primaryLocationHint: "wnam"
 * });
 *
 * @example
 * // Adopt an existing database if it already exists instead of failing
 * const existingDb = await D1Database("existing-db", {
 *   name: "existing-db",
 *   adopt: true,
 *   readReplication: {
 *     mode: "auto"
 *   }
 * });
 *
 * @example
 * // Create a database with migrations
 * const dbWithMigrations = await D1Database("mydb", {
 *   name: "mydb",
 *   migrationsDir: "./migrations",
 * });
 *
 * @example
 * // Create a database with migrations using a custom migration table (compatible with Drizzle)
 * const dbWithCustomMigrations = await D1Database("mydb", {
 *   name: "mydb",
 *   migrationsDir: "./migrations",
 *   migrationsTable: "drizzle_migrations",
 * });
 *
 * @example
 * // Create a database with custom migration table and ID column for maximum compatibility
 * const dbWithCustomMigrations = await D1Database("mydb", {
 *   name: "mydb",
 *   migrationsDir: "./migrations",
 *   migrationsTable: "custom_migrations",
 *   migrationsIdColumn: "migration_name", // explicit column name override
 * });
 *
 * @example
 * // Clone an existing database by ID
 * const clonedDb = await D1Database("cloned-db", {
 *   name: "cloned-db",
 *   clone: otherDb
 * });
 *
 * @example
 * // Clone an existing database by ID
 * const clonedDb = await D1Database("cloned-db", {
 *   name: "cloned-db",
 *   clone: { id: "existing-db-uuid" }
 * });
 *
 * @example
 * // Clone an existing database by name
 * const clonedDb = await D1Database("cloned-db", {
 *   name: "cloned-db",
 *   clone: { name: "existing-db-name" }
 * });
 *
 * @see https://developers.cloudflare.com/d1/
 */
export async function D1Database(id, props = {}) {
    const [migrationsFiles, importFiles] = await Promise.all([
        props.migrationsDir ? await listSqlFiles(props.migrationsDir) : [],
        props.importFiles
            ? await Promise.all(props.importFiles.map((file) => readSqlFile(Scope.current.rootDir, file)))
            : [],
    ]);
    return _D1Database(id, {
        ...props,
        migrationsFiles,
        importFiles,
        dev: {
            ...(props.dev ?? {}),
            // force local migrations to run even if the database was already deployed live
            // this property will oscillate from true to false depending on the dev vs live deployment
            force: Scope.current.local,
        },
    });
}
const _D1Database = Resource("cloudflare::D1Database", async function (id, props) {
    const databaseName = props.name ?? this.output?.name ?? this.scope.createPhysicalName(id);
    const jurisdiction = props.jurisdiction ?? "default";
    if (this.phase === "update" && this.output?.name !== databaseName) {
        this.replace();
    }
    const local = this.scope.local && !props.dev?.remote;
    const dev = {
        id: this.output?.dev?.id ?? this.output?.id ?? databaseName,
        remote: props.dev?.remote ?? false,
    };
    const adopt = props.adopt ?? this.scope.adopt;
    if (local) {
        if (props.migrationsFiles?.length || props.importFiles?.length) {
            await applyLocalD1Migrations({
                databaseId: dev.id,
                migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
                migrations: props.migrationsFiles ?? [],
                imports: props.importFiles ?? [],
                rootDir: this.scope.rootDir,
            });
        }
        return {
            type: "d1",
            id: this.output?.id ?? "",
            name: databaseName,
            readReplication: props.readReplication,
            primaryLocationHint: props.primaryLocationHint,
            migrationsDir: props.migrationsDir,
            migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
            dev,
            jurisdiction,
        };
    }
    const api = await createCloudflareApi(props);
    if (this.phase === "delete") {
        if (this.output.dev?.id) {
            await deleteMiniflareBinding(this.scope, "d1", this.output.dev.id);
        }
        if (props.delete !== false && this.output?.id) {
            await deleteDatabase(api, this.output.id);
        }
        // Return void (a deleted database has no content)
        return this.destroy();
    }
    let dbData;
    if (this.phase === "create" ||
        // this is true IFF the database was created locally before any live deployment
        // in that case, we should still go through the create flow for "update"
        // after that, the ID will remain the UUID for the lifetime of the database
        !this.output?.id) {
        try {
            dbData = await createDatabase(api, databaseName, props);
            // If clone property is provided, perform cloning after database creation
            if (props.clone && dbData.uuid) {
                await cloneDb(api, props.clone, dbData.uuid);
            }
        }
        catch (error) {
            // Check if this is a "database already exists" error and adopt is enabled
            if (adopt &&
                error instanceof CloudflareApiError &&
                error.message.includes("already exists")) {
                logger.log(`Database ${databaseName} already exists, adopting it`);
                // Find the existing database by name
                const databases = await listDatabases(api, databaseName);
                const existingDb = databases.find((db) => db.name === databaseName);
                if (!existingDb) {
                    throw new Error(`Failed to find existing database '${databaseName}' for adoption`);
                }
                // Get the database details using its ID
                dbData = await getDatabase(api, existingDb.uuid);
                // Update the database with the provided properties
                if (props.readReplication) {
                    dbData = await updateReadReplicationMode(api, existingDb.uuid, props.readReplication?.mode);
                }
            }
            else {
                // Re-throw the error if adopt is false or it's not a "database already exists" error
                throw error;
            }
        }
    }
    else if (this.output?.id) {
        // Only read_replication can be modified in update
        if (props.primaryLocationHint &&
            props.primaryLocationHint !== this.output?.primaryLocationHint) {
            throw new Error(`Cannot update primaryLocationHint from '${this.output.primaryLocationHint}' to '${props.primaryLocationHint}' after database creation.`);
        }
        if ((props.jurisdiction ?? "default") !==
            (this.output?.jurisdiction ?? "default")) {
            throw new Error(`Cannot update jurisdiction from '${this.output.jurisdiction}' to '${props.jurisdiction}' after database creation.`);
        }
        // Update the database with new properties
        dbData = await updateReadReplicationMode(api, this.output.id, props.readReplication?.mode ?? "disabled");
    }
    else {
        // If no ID exists, fall back to creating a new database
        dbData = await createDatabase(api, databaseName, props);
    }
    // Run migrations if provided
    if (props.migrationsFiles && props.migrationsFiles.length > 0) {
        try {
            const migrationsTable = props.migrationsTable || DEFAULT_MIGRATIONS_TABLE;
            await applyMigrations({
                migrationsFiles: props.migrationsFiles,
                migrationsTable,
                accountId: api.accountId,
                databaseId: dbData.uuid,
                api,
            });
        }
        catch (migrationErr) {
            logger.error("Failed to apply D1 migrations:", migrationErr);
            throw migrationErr;
        }
    }
    if (props.importFiles?.length) {
        await Promise.all(props.importFiles.map(async (file) => {
            await importD1Database(api, {
                databaseId: dbData.uuid,
                sqlData: file.sql,
                filename: file.id,
            });
        }));
    }
    return {
        type: "d1",
        id: dbData.uuid,
        name: databaseName,
        readReplication: props.readReplication,
        primaryLocationHint: props.primaryLocationHint,
        dev,
        migrationsDir: props.migrationsDir,
        migrationsTable: props.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE,
        jurisdiction,
    };
});
/**
 * Create a new D1 database
 */
export async function createDatabase(api, databaseName, props) {
    // Create new D1 database
    const createPayload = {
        name: databaseName,
        jurisdiction: props.jurisdiction !== "default" ? props.jurisdiction : undefined,
        primary_location_hint: props.primaryLocationHint,
    };
    const database = await extractCloudflareResult(`create D1 database "${databaseName}"`, api.post(`/accounts/${api.accountId}/d1/database`, createPayload));
    if (!database.uuid) {
        // this is included in the spec as optional... why is it optional? we may never know...
        throw new Error("Missing UUID for created database");
    }
    if (props.readReplication?.mode) {
        return await updateReadReplicationMode(api, database.uuid, props.readReplication?.mode);
    }
    return database;
}
/**
 * Get a D1 database
 */
export async function getDatabase(api, databaseId) {
    return await extractCloudflareResult(`get D1 database "${databaseId}"`, api.get(`/accounts/${api.accountId}/d1/database/${databaseId}`));
}
/**
 * Delete a D1 database
 */
export async function deleteDatabase(api, databaseId) {
    try {
        await extractCloudflareResult(`delete D1 database "${databaseId}"`, api.delete(`/accounts/${api.accountId}/d1/database/${databaseId}`));
    }
    catch (error) {
        if (error instanceof CloudflareApiError && error.status === 404) {
            return;
        }
        throw error;
    }
}
/**
 * List all D1 databases in an account
 */
export async function listDatabases(api, name) {
    // Construct query string if name is provided
    const queryParams = name ? `?name=${encodeURIComponent(name)}` : "";
    // TODO(john): handle pagination (wasn't handled originally)
    return await extractCloudflareResult(`list D1 databases${name ? ` with name "${name}"` : ""}`, api.get(`/accounts/${api.accountId}/d1/database${queryParams}`));
}
/**
 * Update a D1 database
 *
 * Note: According to Cloudflare API, only read_replication.mode can be modified during updates.
 */
export async function updateReadReplicationMode(api, databaseId, readReplicationMode) {
    return await extractCloudflareResult(`update read replication mode for D1 database "${databaseId}"`, api.patch(`/accounts/${api.accountId}/d1/database/${databaseId}`, {
        read_replication: {
            mode: readReplicationMode,
        },
    }));
}
/**
 * Helper function to clone data from a source database to a target database
 * Resolves the source database ID from different input formats and performs the cloning operation
 *
 * @param api CloudflareApi instance
 * @param sourceDb Source database specification (can be an ID, a name, or a D1Database object)
 * @param targetDbId Target database ID
 */
async function cloneDb(api, sourceDb, targetDbId) {
    let sourceId;
    // Determine source database ID
    if ("id" in sourceDb && sourceDb.id) {
        // Use provided ID directly
        sourceId = sourceDb.id;
    }
    else if ("name" in sourceDb && sourceDb.name) {
        // Look up ID by name
        const databases = await listDatabases(api, sourceDb.name);
        const foundDb = databases.find((db) => db.name === sourceDb.name);
        if (!foundDb) {
            throw new Error(`Source database with name '${sourceDb.name}' not found for cloning`);
        }
        sourceId = foundDb.uuid;
    }
    else if ("type" in sourceDb && sourceDb.type === "d1" && "id" in sourceDb) {
        // It's a D1Database object
        sourceId = sourceDb.id;
    }
    else {
        throw new Error("Invalid clone property: must provide either id or name");
    }
    // Perform the cloning
    logger.log(`Cloning data from database ${sourceId} to ${targetDbId}`);
    await cloneD1Database(api, {
        sourceDatabaseId: sourceId,
        targetDatabaseId: targetDbId,
    });
}
//# sourceMappingURL=d1-database.js.map