import { randomUUID } from "node:crypto";
import {
  AlephaError,
  type Static,
  type TObject,
  type TSchema,
  z,
} from "alepha";
import { type BuildColumns, sql } from "drizzle-orm";
import * as pg from "drizzle-orm/sqlite-core";
import {
  check,
  foreignKey,
  index,
  type SQLiteColumnBuilderBase,
  type SQLiteTableWithColumns,
  sqliteTable,
  unique,
  uniqueIndex,
} from "drizzle-orm/sqlite-core";
import {
  PG_CREATED_AT,
  PG_GENERATED,
  PG_IDENTITY,
  PG_PRIMARY_KEY,
  PG_REF,
  PG_SERIAL,
  PG_UPDATED_AT,
  type PgGeneratedOptions,
  type PgRefOptions,
} from "../constants/PG_SYMBOLS.ts";
import type { EntityPrimitive } from "../primitives/$entity.ts";
import type { SequencePrimitive } from "../primitives/$sequence.ts";
import { ModelBuilder } from "./ModelBuilder.ts";

export class SqliteModelBuilder extends ModelBuilder {
  public buildTable(
    entity: EntityPrimitive<any>,
    options: {
      tables: Map<string, unknown>;
      enums: Map<string, unknown>;
      schemas: Map<string, unknown>;
      schema: string;
    },
  ) {
    const tableName = entity.name;
    if (options.tables.has(tableName)) {
      return;
    }

    const columns = this.schemaToSqliteColumns(
      tableName,
      entity.schema,
      options.enums,
      options.tables,
    );

    // Build the config function for SQLite
    const configFn = this.getTableConfig(entity, options.tables);

    const table = sqliteTable(tableName, columns, configFn);

    options.tables.set(tableName, table);
  }

  public buildSequence(
    sequence: SequencePrimitive,
    options: {
      sequences: Map<string, unknown>;
      schema: string;
    },
  ) {
    throw new AlephaError("SQLite does not support sequences");
  }

  /**
   * Get SQLite-specific config builder for the table.
   */
  protected getTableConfig(
    entity: EntityPrimitive,
    tables: Map<string, unknown>,
  ): ((self: BuildColumns<string, any, "sqlite">) => any) | undefined {
    // SQLite-specific builders
    const sqliteBuilders = {
      index,
      uniqueIndex,
      unique,
      check,
      foreignKey,
    };

    // Table resolver function
    const tableResolver = (entityName: string) => {
      return tables.get(entityName) as any;
    };

    return this.buildTableConfig<any, BuildColumns<string, any, "sqlite">>(
      entity,
      sqliteBuilders,
      tableResolver,
      (config, self) => {
        // SQLite custom config handler
        const customConfigs = (config as any)(self);
        return Array.isArray(customConfigs) ? customConfigs : [];
      },
    );
  }

  // -------------------------------------------------------------------------------------------------------------------

  schemaToSqliteColumns = <T extends TObject>(
    tableName: string,
    schema: T,
    enums: Map<string, unknown>,
    tables: Map<string, unknown>,
  ): SchemaToSqliteBuilder<T> => {
    return Object.entries(schema.properties as Record<string, any>).reduce<
      Partial<SchemaToSqliteBuilder<T>>
    >((columns, [key, value]) => {
      let col = this.mapFieldToSqliteColumn(tableName, key, value, enums);

      const defaultValue = z.schema.getDefault(value);
      if (defaultValue != null) {
        col = col.default(defaultValue as any);
      }

      if (PG_PRIMARY_KEY in value) {
        col = col.primaryKey();
      }

      if (PG_REF in value) {
        const config = value[PG_REF] as PgRefOptions;
        col = col.references(() => {
          const ref = config.ref();
          const table = tables.get(
            ref.entity.name,
          ) as SQLiteTableWithColumns<any>;

          if (!table) {
            throw new AlephaError(
              `Referenced table ${ref.entity.name} not found for ${tableName}.${key}`,
            );
          }

          const target = table[ref.name];
          if (!target) {
            throw new AlephaError(
              `Referenced column ${ref.name} not found in table ${ref.entity.name} for ${tableName}.${key}`,
            );
          }

          return target;
        }, config.actions);
      }

      if (PG_GENERATED in value) {
        const gen = value[PG_GENERATED] as PgGeneratedOptions;
        col = col.generatedAlwaysAs(gen.expression, {
          mode: gen.mode ?? "virtual",
        });
      }

      if (z.schema.requiredKeys(schema).includes(key)) {
        col = col.notNull();
      }

      (columns as Record<string, unknown>)[key] = col;
      return columns;
    }, {}) as SchemaToSqliteBuilder<T>;
  };

  mapFieldToSqliteColumn = (
    tableName: string,
    fieldName: string,
    value: any,
    enums: Map<string, any>,
  ) => {
    const key = this.toColumnName(fieldName);

    // Peel optional / nullable / default wrappers to the underlying type.
    value = z.schema.unwrap(value);

    if (z.schema.isInteger(value)) {
      if (PG_SERIAL in value || PG_IDENTITY in value) {
        return pg
          .integer(key, { mode: "number" })
          .primaryKey({ autoIncrement: true });
      }

      return pg.integer(key);
    }

    if (z.schema.isBigInt(value)) {
      if (PG_PRIMARY_KEY in value || PG_IDENTITY in value) {
        return pg
          .integer(key, { mode: "number" })
          .primaryKey({ autoIncrement: true });
      }

      return pg.integer(key, { mode: "number" });
    }

    if (z.schema.isNumber(value)) {
      if (PG_IDENTITY in value) {
        return pg
          .integer(key, { mode: "number" })
          .primaryKey({ autoIncrement: true });
      }

      // Native real column — returns a JS number (sqlite `numeric` yields a
      // string, which strict `z.number()` validation rejects).
      return pg.real(key);
    }

    if (z.schema.isString(value)) {
      return this.mapStringToSqliteColumn(key, value);
    }

    if (z.schema.isBoolean(value)) {
      return this.sqliteBool(key, value);
    }

    if (z.schema.isObject(value)) {
      return this.sqliteJson(key, value);
    }

    if (z.schema.isRecord(value)) {
      return this.sqliteJson(key, value);
    }

    if (z.schema.isAny(value)) {
      return this.sqliteJson(key, value);
    }

    if (z.schema.isArray(value)) {
      if (z.schema.isObject(value.items)) {
        return this.sqliteJson(key, value);
      }
      if (z.schema.isRecord(value.items)) {
        return this.sqliteJson(key, value);
      }
      if (z.schema.isAny(value.items)) {
        return this.sqliteJson(key, value);
      }
      if (z.schema.isString(value.items)) {
        return this.sqliteJson(key, value);
      }
      if (z.schema.isInteger(value.items)) {
        return this.sqliteJson(key, value);
      }
      if (z.schema.isNumber(value.items)) {
        return this.sqliteJson(key, value);
      }
      if (z.schema.isBoolean(value.items)) {
        return this.sqliteJson(key, value);
      }
      if (z.schema.isEnum(value.items)) {
        return this.sqliteJson(key, value);
      }
    }

    if (z.schema.isEnum(value)) {
      return this.mapStringToSqliteColumn(key, value);
    }

    throw new AlephaError(
      `Unsupported schema for field '${tableName}.${fieldName}' (schema: ${JSON.stringify(value)})`,
    );
  };

  mapStringToSqliteColumn = (key: string, value: any) => {
    if (z.schema.format(value) === "uuid") {
      if (PG_PRIMARY_KEY in value) {
        return pg
          .text(key)
          .primaryKey()
          .$defaultFn(() => randomUUID());
      }

      return pg.text(key);
    }

    if (z.schema.format(value) === "binary") {
      return this.sqliteJson(key, value);
    }

    if (z.schema.format(value) === "date-time") {
      if (PG_CREATED_AT in value) {
        return this.sqliteDateTime(key, {}).default(
          sql`(unixepoch('subsec') * 1000)`,
        );
      }
      if (PG_UPDATED_AT in value) {
        return this.sqliteDateTime(key, {}).default(
          sql`(unixepoch('subsec') * 1000)`,
        );
      }
      return this.sqliteDateTime(key, {});
    }

    if (z.schema.format(value) === "date") {
      return this.sqliteDate(key, {});
    }

    return pg.text(key);
  };

  sqliteJson = <TDocument extends TSchema>(name: string, document: TDocument) =>
    pg
      .customType<{
        data: Static<TDocument>;
        driverData: string;
        config: { document: TDocument };
        configRequired: true;
      }>({
        dataType: () => "text",
        toDriver: (value) => JSON.stringify(value),
        fromDriver: (value: TDocument | string) => {
          return value && typeof value === "string" ? JSON.parse(value) : value;
        },
      })(name, { document })
      .$type<Static<TDocument>>();

  sqliteDateTime = pg.customType<{
    data: string;
    driverData: number;
    configRequired: true;
  }>({
    dataType: () => "integer",
    toDriver: (value) => new Date(value).getTime(),
    fromDriver: (value) => {
      return new Date(value).toISOString();
    },
  });

  sqliteBool = pg.customType<{
    data: boolean;
    driverData: number;
    configRequired: true;
  }>({
    dataType: () => "integer",
    toDriver: (value) => (value ? 1 : 0),
    fromDriver: (value) => value === 1,
  });

  sqliteDate = pg.customType<{
    data: string;
    driverData: number;
    configRequired: true;
  }>({
    dataType: () => "integer",
    toDriver: (value) => new Date(value).getTime(),
    fromDriver: (value) => {
      return new Date(value).toISOString().split("T")[0];
    },
  });
}

export type SchemaToSqliteBuilder<T extends TObject> = {
  [key in keyof T["properties"]]: SQLiteColumnBuilderBase;
};
