import { createAdapterFactory, type DBAdapterDebugLogOption } from "better-auth/adapters";
import { jsonify, StringRecordId } from "surrealdb";
import { Surreal } from "surrealdb";

/**
 * SurrealDB adapter configuration
 */
export interface SurrealAdapterConfig {
    /** SurrealDB connection address (e.g., "http://localhost:8000") */
    address: string;
    /** Database username */
    username: string;
    /** Database password */
    password: string;
    /** Namespace to use */
    ns: string;
    /** Database to use */
    db: string;
    /** Enable debug logging */
    debugLogs?: DBAdapterDebugLogOption;
    /** Use plural table names */
    usePlural?: boolean;
}

/**
 * SurrealDB adapter for Better-Auth
 *
 * @example
 * ```typescript
 * import { surrealAdapter } from "surrealdb-better-auth";
 *
 * const auth = betterAuth({
 *     database: surrealAdapter({
 *         address: "http://localhost:8000",
 *         username: "root",
 *         password: "root",
 *         ns: "test",
 *         db: "test",
 *     }),
 * });
 * ```
 */
export const surrealAdapter = (config: SurrealAdapterConfig) => {
    // Connection management
    let db: Surreal | null = null;
    let isConnecting = false;
    let connectionPromise: Promise<Surreal> | null = null;

    const ensureConnection = async (): Promise<Surreal> => {
        if (db) {
            try {
                // Test if connection is still alive
                await db.query("SELECT 1");
                return db;
            } catch {
                // Connection is dead, reset and reconnect
                db = null;
            }
        }

        if (isConnecting && connectionPromise) {
            return connectionPromise;
        }

        isConnecting = true;
        connectionPromise = new Promise((resolve, reject) => {
            const newDb = new Surreal();
            newDb
                .connect(config.address, {
                    namespace: config.ns,
                    database: config.db,
                    auth: {
                        username: config.username,
                        password: config.password,
                    },
                })
                .then(() => {
                    db = newDb;
                    isConnecting = false;
                    connectionPromise = null;
                    resolve(newDb);
                })
                .catch((error) => {
                    isConnecting = false;
                    connectionPromise = null;
                    reject(error);
                });
        });

        return connectionPromise;
    };

    /**
     * Check if a string looks like a SurrealDB RecordId (format: table:id)
     */
    const isRecordIdString = (value: string): boolean => {
        // RecordId format: table:id where table is alphanumeric and id is alphanumeric/uuid
        // e.g., "user:abc123", "session:xyz", "account:microsoft-12345"
        const recordIdPattern = /^[a-zA-Z_][a-zA-Z0-9_]*:[a-zA-Z0-9_\-⟨⟩]+$/;
        return recordIdPattern.test(value);
    };

    /**
     * Fields that are explicitly NOT RecordIds (OAuth/external identifiers)
     */
    const nonRecordIdFields = new Set(["accountId", "providerId"]);

    /**
     * Create serialization functions with schema-aware RecordId detection
     */
    const createSerializationFunctions = (recordIdFieldsMap: Map<string, Set<string>>) => {
        /**
         * Check if a field should be treated as a RecordId
         * Uses schema metadata when available, falls back to heuristics
         */
        const isRecordIdField = (field: string, model?: string): boolean => {
            // id is always a RecordId
            if (field === "id") return true;
            // OAuth/external identifiers are never RecordIds
            if (nonRecordIdFields.has(field)) return false;
            // Check schema-based map if model is provided
            if (model && recordIdFieldsMap.has(model)) {
                return recordIdFieldsMap.get(model)!.has(field);
            }
            // Fallback: field ends with Id (for fields not in schema)
            return field.endsWith("Id");
        };

        /**
         * Serialize a value for SurrealQL queries
         * - RecordId strings (table:id format) in RecordId fields are NOT quoted
         * - Regular strings get JSON-quoted
         * - Dates get converted to ISO strings
         * - Arrays and objects use JSON.stringify
         * - Numbers and booleans are used as-is
         */
        const serializeValue = (value: unknown, field?: string, model?: string): string => {
            if (value === undefined || value === null) {
                return "NONE";
            }
            if (typeof value === "string") {
                // Only treat as RecordId if:
                // 1. The field is a known RecordId field
                // 2. AND the value looks like a RecordId
                if (field && isRecordIdField(field, model) && isRecordIdString(value)) {
                    return value;
                }
                return JSON.stringify(value);
            }
            if (typeof value === "number" || typeof value === "boolean") {
                return String(value);
            }
            if (value instanceof Date) {
                return `d"${value.toISOString()}"`;
            }
            if (Array.isArray(value)) {
                // Pass the field through so array elements are properly handled as RecordIds if needed
                return `[${value.map(v => serializeValue(v, field, model)).join(", ")}]`;
            }
            if (typeof value === "object" && value !== null) {
                // Check if it's a RecordId (has tb property)
                if ("tb" in value) {
                    return jsonify(value);
                }
                return JSON.stringify(value);
            }
            return JSON.stringify(value);
        };

        /**
         * Convert a where clause array to SurrealDB WHERE string
         */
        const buildWhereClause = (
            where: Array<{ field: string; value: unknown; operator?: string }>,
            model?: string
        ): string => {
            if (!where || where.length === 0) return "";

            return where
                .map((clause) => {
                    const field = clause.field;
                    const value = clause.value;
                    const operator = clause.operator || "eq";

                    // Handle null/undefined values
                    if (value === undefined || value === null) {
                        return `${field} = NONE`;
                    }

                    switch (operator) {
                        case "eq":
                            return `${field} = ${serializeValue(value, field, model)}`;
                        case "ne":
                            return `${field} != ${serializeValue(value, field, model)}`;
                        case "gt":
                            return `${field} > ${serializeValue(value, field, model)}`;
                        case "gte":
                            return `${field} >= ${serializeValue(value, field, model)}`;
                        case "lt":
                            return `${field} < ${serializeValue(value, field, model)}`;
                        case "lte":
                            return `${field} <= ${serializeValue(value, field, model)}`;
                        case "in":
                            return `${field} IN ${serializeValue(value, field, model)}`;
                        case "contains":
                            return `${field} CONTAINS ${serializeValue(value, field, model)}`;
                        case "starts_with":
                            return `string::starts_with(${field}, ${serializeValue(value, field, model)})`;
                        case "ends_with":
                            return `string::ends_with(${field}, ${serializeValue(value, field, model)})`;
                        default:
                            return `${field} = ${serializeValue(value, field, model)}`;
                    }
                })
                .join(" AND ");
        };

        return { isRecordIdField, serializeValue, buildWhereClause };
    };

    /**
     * Transform field values for SurrealDB
     * - Convert *Id fields to StringRecordId (using schema-aware detection)
     */
    const createTransformValueForDB = (isRecordIdField: (field: string, model?: string) => boolean) => {
        return (field: string, value: unknown, model?: string): unknown => {
            if (value === undefined || value === null) {
                return value;
            }

            // Convert foreign key fields to RecordId using schema-aware detection
            if (typeof value === "string" && isRecordIdField(field, model) && field !== "id") {
                // Extract the referenced model from field name (e.g., "userId" -> "user")
                const refModel = field.slice(0, -2);
                // Don't convert if it's already a RecordId string format
                if (!value.includes(":")) {
                    return new StringRecordId(`${refModel}:${value}`);
                }
                return new StringRecordId(value);
            }

            return value;
        };
    };

    return createAdapterFactory({
        config: {
            adapterId: "surrealdb",
            adapterName: "SurrealDB",
            debugLogs: config.debugLogs ?? false,
            usePlural: config.usePlural ?? false,
            supportsJSON: true,
            supportsDates: true,
            supportsBooleans: true,
            supportsNumericIds: false, // SurrealDB uses string RecordIds
            // Let the factory handle joins via multiple queries
            supportsJoin: false,
        },
        adapter: ({ debugLog, schema }) => {
            // Build RecordId fields map from schema metadata
            // Fields with `references` property are foreign keys and should be RecordIds
            const recordIdFieldsMap = new Map<string, Set<string>>();

            if (schema) {
                for (const [modelName, modelDef] of Object.entries(schema)) {
                    const recordIdFields = new Set<string>(["id"]);
                    const fields = (modelDef as { fields?: Record<string, { references?: { model: string } }> }).fields;
                    if (fields) {
                        for (const [fieldName, fieldDef] of Object.entries(fields)) {
                            // Check if field has references (foreign key)
                            if (fieldDef.references?.model) {
                                recordIdFields.add(fieldName);
                            }
                        }
                    }
                    recordIdFieldsMap.set(modelName, recordIdFields);
                }
            }

            // Create schema-aware functions
            const { isRecordIdField, buildWhereClause } = createSerializationFunctions(recordIdFieldsMap);
            const transformValueForDB = createTransformValueForDB(isRecordIdField);

            return {
                create: async ({ model, data }) => {
                    const conn = await ensureConnection();

                    // Transform foreign key references to RecordIds
                    const transformedData: Record<string, unknown> = {};
                    for (const [key, value] of Object.entries(data)) {
                        transformedData[key] = transformValueForDB(key, value, model);
                    }

                    debugLog?.("create", { model, data: transformedData });

                    const [result] = await conn.create(model, transformedData);

                    if (!result) {
                        throw new SurrealDBError("Failed to create record");
                    }

                    // Transform output - convert RecordId to string
                    const output: Record<string, unknown> = {};
                    for (const [key, value] of Object.entries(result)) {
                        output[key] = jsonify(value);
                    }

                    return output;
                },

                findOne: async ({ model, where }) => {
                    const conn = await ensureConnection();
                    const whereClause = buildWhereClause(where, model);

                    const query = `SELECT * FROM ${model} WHERE ${whereClause} LIMIT 1`;
                    debugLog?.("findOne", { model, query });

                    const [results] = await conn.query<[Record<string, unknown>[]]>(query);
                    const record = results?.[0];

                    if (!record) {
                        return null;
                    }

                    // Transform output - convert RecordId to string
                    const output: Record<string, unknown> = {};
                    for (const [key, value] of Object.entries(record)) {
                        output[key] = jsonify(value);
                    }

                    return output;
                },

                findMany: async ({ model, where, limit, offset, sortBy }) => {
                    const conn = await ensureConnection();

                    let query = `SELECT * FROM ${model}`;

                    if (where && where.length > 0) {
                        const whereClause = buildWhereClause(where, model);
                        query += ` WHERE ${whereClause}`;
                    }

                    if (sortBy) {
                        query += ` ORDER BY ${sortBy.field} ${sortBy.direction.toUpperCase()}`;
                    }

                    if (limit !== undefined) {
                        query += ` LIMIT ${limit}`;
                    }

                    if (offset !== undefined) {
                        query += ` START ${offset}`;
                    }

                    debugLog?.("findMany", { model, query });

                    const [results] = await conn.query<[Record<string, unknown>[]]>(query);

                    // Transform output - convert RecordId to string
                    return (results || []).map((record) => {
                        const output: Record<string, unknown> = {};
                        for (const [key, value] of Object.entries(record)) {
                            output[key] = jsonify(value);
                        }
                        return output;
                    });
                },

                update: async ({ model, where, update }) => {
                    const conn = await ensureConnection();
                    const whereClause = buildWhereClause(where, model);

                    // Transform foreign key references to RecordIds
                    const transformedUpdate: Record<string, unknown> = {};
                    for (const [key, value] of Object.entries(update)) {
                        transformedUpdate[key] = transformValueForDB(key, value, model);
                    }

                    const query = `UPDATE ${model} MERGE $data WHERE ${whereClause}`;
                    debugLog?.("update", { model, query, data: transformedUpdate });

                    const [results] = await conn.query<[Record<string, unknown>[]]>(query, {
                        data: transformedUpdate,
                    });

                    const record = results?.[0];

                    if (!record) {
                        throw new SurrealDBError("Failed to update record");
                    }

                    // Transform output - convert RecordId to string
                    const output: Record<string, unknown> = {};
                    for (const [key, value] of Object.entries(record)) {
                        output[key] = jsonify(value);
                    }

                    return output;
                },

                updateMany: async ({ model, where, update }) => {
                    const conn = await ensureConnection();
                    const whereClause = buildWhereClause(where, model);

                    // Transform foreign key references to RecordIds
                    const transformedUpdate: Record<string, unknown> = {};
                    for (const [key, value] of Object.entries(update)) {
                        transformedUpdate[key] = transformValueForDB(key, value, model);
                    }

                    const query = `UPDATE ${model} MERGE $data WHERE ${whereClause}`;
                    debugLog?.("updateMany", { model, query, data: transformedUpdate });

                    const [results] = await conn.query<[Record<string, unknown>[]]>(query, {
                        data: transformedUpdate,
                    });

                    return results?.length || 0;
                },

                delete: async ({ model, where }) => {
                    const conn = await ensureConnection();
                    const whereClause = buildWhereClause(where, model);

                    const query = `DELETE FROM ${model} WHERE ${whereClause}`;
                    debugLog?.("delete", { model, query });

                    await conn.query(query);
                },

                deleteMany: async ({ model, where }) => {
                    const conn = await ensureConnection();
                    const whereClause = buildWhereClause(where, model);

                    const query = `DELETE FROM ${model} WHERE ${whereClause}`;
                    debugLog?.("deleteMany", { model, query });

                    const [results] = await conn.query<[Record<string, unknown>[]]>(query);
                    return results?.length || 0;
                },

                count: async ({ model, where }) => {
                    const conn = await ensureConnection();

                    let query: string;
                    if (where && where.length > 0) {
                        const whereClause = buildWhereClause(where, model);
                        query = `SELECT count() FROM ${model} WHERE ${whereClause} GROUP ALL`;
                    } else {
                        query = `SELECT count() FROM ${model} GROUP ALL`;
                    }

                    debugLog?.("count", { model, query });

                    const [results] = await conn.query<[Array<{ count: number }>]>(query);
                    return results?.[0]?.count || 0;
                },

                options: config,
            };
        },
    });
};

/**
 * Custom error class for SurrealDB adapter errors
 */
export class SurrealDBError extends Error {
    constructor(message: string) {
        super(message);
        this.name = "SurrealDBError";
    }
}

// Re-export for backwards compatibility
export { SurrealDBError as SurrealDBQueryError };
