package aceboot::orm.common

import std.collection.*

/**
 * 中性列类型(由方言映射到各数据库的具体 SQL 类型)。
 *  - TInt/TText/TReal/TBool:基础标量。
 *  - TDateTime/TDate:日期时间,统一以毫秒整型存储(三库均映射为整型,跨库一致、便于运算)。
 *  - TJson:JSON 文档,文本存储(SQLite TEXT / PG JSONB / MySQL JSON)。
 *  - TBlob:二进制大对象(SQLite BLOB / PG BYTEA / MySQL BLOB),对应 DbValue.DbBytes。
 *  - TDecimal:高精度十进制。仓颉标准库无 Decimal 类型,故以文本存储(PG NUMERIC / MySQL DECIMAL)。
 */
public enum DbType {
    | TInt
    | TText
    | TReal
    | TBool
    | TDateTime
    | TDate
    | TJson
    | TBlob
    | TDecimal
}

/**
 * 一列的中性元数据(@Entity 宏生成;方言据此建表,与具体数据库无关)。
 * 基础 4 字段(name/dbType/isPk/fkRef)保持位置参数;扩展约束走带默认值的命名参数,
 * 故旧调用 `ColumnSpec(name, dbType, isPk, fkRef)` 与既有宏发射保持向后兼容。
 *  - nullable:是否允许 NULL(false 且非主键 → 生成 `NOT NULL`)。
 *  - unique:是否列级唯一(true → `UNIQUE`)。
 *  - defaultSql:默认值 SQL 片段("" 表示无;原样拼入 `DEFAULT <x>`)。
 *  - length:文本长度(>0 且文本列 → `VARCHAR(length)`;0 表示按 DbType 推断)。
 *  - explicitType:显式 SQL 类型("" 表示按 DbType 推断;非空则覆盖)。
 *  - autoIncrement:主键是否自增(true=自增主键;false=手动主键 @PrimaryColumn)。
 *  - fkRefIdColumn:外键所引用的目标表主键列名(默认 "id";目标实体自定义主键列时与 JOIN 目标对齐)。
 */
public class ColumnSpec {
    public let name: String
    public let dbType: DbType
    public let isPk: Bool
    public let fkRef: String // "" 或引用表名(外键 -> ref(id))
    public let nullable: Bool
    public let unique: Bool
    public let defaultSql: String
    public let length: Int64
    public let explicitType: String
    public let autoIncrement: Bool
    public let fkRefIdColumn: String
    // 默认 SELECT 是否包含该列(false=隐藏列,如密码;仅影响 findAll/findById/QB 默认投影,不影响 insert/update/建表)。
    public let selectable: Bool
    // 列注释("" 表示无)。DDL 发射按方言分歧:PG 独立 `COMMENT ON COLUMN`、MySQL 内联 `COMMENT '...'`、SQLite 跳过。
    public let comment: String
    // Decimal 精度 p(0=不指定,回退方言默认);拼 DECIMAL(p,s)/NUMERIC(p,s)。
    public let precision: Int64
    // Decimal 小数位 s(仅 precision>0 时生效)。
    public let scale: Int64
    // 枚举允许值(空=非枚举列;非空则 DDL 追加三库通用的 CHECK(col IN ('a','b',...)))。
    public let enumValues: Array<String>
    // 外键引用动作("" 表示不指定):非空则 FK DDL 追加 `ON DELETE <x>` / `ON UPDATE <x>`。
    // 取值如 "CASCADE"/"SET NULL"/"RESTRICT"/"NO ACTION"(原样拼入,由方言语法负责)。
    public let fkOnDelete: String
    public let fkOnUpdate: String

    public init(name: String, dbType: DbType, isPk: Bool, fkRef: String, nullable!: Bool = true,
        unique!: Bool = false, defaultSql!: String = "", length!: Int64 = 0, explicitType!: String = "",
        autoIncrement!: Bool = true, fkRefIdColumn!: String = "id",
        selectable!: Bool = true, comment!: String = "", precision!: Int64 = 0, scale!: Int64 = 0,
        enumValues!: Array<String> = Array<String>(), fkOnDelete!: String = "", fkOnUpdate!: String = "") {
        this.name = name
        this.dbType = dbType
        this.isPk = isPk
        this.fkRef = fkRef
        this.nullable = nullable
        this.unique = unique
        this.defaultSql = defaultSql
        this.length = length
        this.explicitType = explicitType
        this.autoIncrement = autoIncrement
        this.fkRefIdColumn = fkRefIdColumn
        this.selectable = selectable
        this.comment = comment
        this.precision = precision
        this.scale = scale
        this.enumValues = enumValues
        this.fkOnDelete = fkOnDelete
        this.fkOnUpdate = fkOnUpdate
    }
}

/** 一个索引的中性元数据(@Entity 宏据 @Index 收集;方言据此生成 CREATE INDEX)。 */
public class IndexSpec {
    public let name: String
    public let columns: Array<String>
    public let unique: Bool

    public init(name: String, columns: Array<String>, unique: Bool) {
        this.name = name
        this.columns = columns
        this.unique = unique
    }
}

/**
 * SQL 方言:把「中性列元数据 + 参数占位」翻译成各数据库的具体语法。
 * 让同一套实体/仓储跑在 SQLite / PostgreSQL / MySQL 上(占位符、自增主键、类型映射各异)。
 */
public interface Dialect {
    /** 第 index(0 基)个参数占位符:SQLite/MySQL 为 "?",PostgreSQL 为 "$1/$2..."。 */
    func placeholder(index: Int64): String
    /** 中性列类型映射为本方言的具体 SQL 类型(建表与 ALTER ... ADD COLUMN 共用)。 */
    func sqlType(t: DbType): String
    /** 由列元数据生成 CREATE TABLE 语句(无表级 CHECK;默认委托 3 参重载,向后兼容旧调用点)。 */
    func createTable(table: String, cols: Array<ColumnSpec>): String {
        createTable(table, cols, Array<CheckSpec>())
    }
    /** 由列元数据 + 表级 CHECK 约束生成 CREATE TABLE 语句(各方言实现)。 */
    func createTable(table: String, cols: Array<ColumnSpec>, checks: Array<CheckSpec>): String
    /** 由索引元数据生成 CREATE [UNIQUE] INDEX 语句。 */
    func createIndex(table: String, idx: IndexSpec): String
    /** 插入自增行后取 id 是否需 `RETURNING`(PostgreSQL 为 true;SQLite/MySQL 用驱动取 last-insert-id)。 */
    func usesReturning(): Bool
    /**
     * 生成 upsert 冲突子句,追加到 `INSERT INTO ... VALUES(...)` 之后(对标 TypeORM Repository.upsert):
     *  - SQLite/PostgreSQL:`ON CONFLICT(conflictColumns) DO UPDATE SET c = excluded.c, ...`(无更新列则 DO NOTHING)。
     *  - MySQL:`ON DUPLICATE KEY UPDATE c = VALUES(c), ...`(由唯一键触发,conflictColumns 仅作回退占位)。
     * conflictColumns 为冲突判定列;updateColumns 为冲突时要更新的列(已剔除冲突列与不应覆盖的列)。
     */
    func upsertClause(conflictColumns: Array<String>, updateColumns: Array<String>): String

    /**
     * 悲观锁尾部子句(SELECT 末尾,LIMIT/OFFSET 之后追加):
     *  - "update" → " FOR UPDATE";"share" → " FOR SHARE";其它/"" → ""(不加锁,向后兼容)。
     * 默认给出标准 SQL 行锁子句(PostgreSQL/MySQL 8.0+ 通用);SQLite 无行锁 → 覆盖返回 ""。
     */
    func lockClause(mode: String): String {
        if (mode == "update") {
            return " FOR UPDATE"
        }
        if (mode == "share") {
            return " FOR SHARE"
        }
        return ""
    }

    /**
     * schema 内省:判定表是否存在的查询(返回 SQL + 参数;有任意行即视为存在)。
     * 默认走 SQL 标准 information_schema(PostgreSQL/MySQL 适用);SQLite 覆盖为 sqlite_master。
     */
    func introspectTableExists(table: String): (String, Array<DbValue>) {
        ("SELECT table_name FROM information_schema.tables WHERE table_name = " + placeholder(0), [DbText(table)])
    }
    /**
     * schema 内省:列出某表所有列名的查询(每行含名为 "name" 的列)。
     * 默认走 information_schema.columns;SQLite 覆盖为 PRAGMA table_info。
     */
    func introspectColumns(table: String): (String, Array<DbValue>) {
        ("SELECT column_name AS name FROM information_schema.columns WHERE table_name = " + placeholder(0),
            [DbText(table)])
    }
    /**
     * schema 内省:列出某表所有列「名 + 库现状类型」的查询(每行含 "name" 与 "type" 两列),供改列类型 diff。
     * 默认走 information_schema.columns(data_type);SQLite 覆盖为 PRAGMA table_info(其结果天然含 name/type 列)。
     */
    func introspectColumnTypes(table: String): (String, Array<DbValue>) {
        ("SELECT column_name AS name, data_type AS type FROM information_schema.columns WHERE table_name = " +
            placeholder(0), [DbText(table)])
    }
    /**
     * 「改列类型」DDL:把 col 改成目标类型。返回 None 表示本方言不支持原地改列类型
     * (SQLite 需「建新表+拷贝+换名」较重,本轮 best-effort 跳过);PG/MySQL 覆盖返回具体语句。
     */
    func alterColumnType(_: String, _: ColumnSpec): ?String {
        None
    }
    /** 「删列」DDL(ALTER TABLE ... DROP COLUMN)。三方言通用(SQLite 需 3.35+,本机满足)。 */
    func dropColumn(table: String, col: String): String {
        "ALTER TABLE " + table + " DROP COLUMN " + col
    }
    /**
     * 库现状列类型(currentRaw,来自内省)是否与目标列(target)等价。
     * 默认 true(视为一致 → 不触发改列,best-effort);PG/MySQL 覆盖做规范化比较以驱动 ALTER。
     */
    func columnTypeMatches(_: String, _: ColumnSpec): Bool {
        true
    }

    /**
     * 建表后单独发射的「列注释」DDL(每列一条,建表 exec 之后逐条 exec)。
     * 默认返回空 → SQLite/MySQL 天然继承(SQLite 无列注释;MySQL 走内联 COMMENT,不用独立语句)。
     * 仅 PostgreSQL 覆盖:返回每个含 comment 列的 `COMMENT ON COLUMN <table>.<col> IS '...'`。
     */
    func columnComments(_: String, _: Array<ColumnSpec>): Array<String> {
        Array<String>()
    }
}

/**
 * 规范化 SQL 类型串以做跨方言「库现状 vs 目标」比较:小写、截断括号内长度/精度、归并常见同义词。
 * 例:"VARCHAR(255)"→"varchar"、PG "character varying"→"varchar"、"INTEGER"/"int4"→"int"、"int8"→"bigint"。
 */
protected func normalizeSqlType(raw: String): String {
    let out = ArrayList<UInt8>()
    for (b in raw.toArray()) {
        if (b == 40u8) {
            // '(' 之后是长度/精度,忽略
            break
        }
        var c = b
        if (c >= 65u8 && c <= 90u8) {
            // A-Z → 小写
            c = c + 32u8
        }
        out.add(c)
    }
    let s = String.fromUtf8(out.toArray())
    if (s == "character varying") {
        return "varchar"
    }
    if (s == "integer" || s == "int4") {
        return "int"
    }
    if (s == "int8") {
        return "bigint"
    }
    return s
}

/**
 * 由列元数据生成 `ALTER TABLE <table> ADD COLUMN <def>`(schema sync 补列用)。
 * 列定义复用建表的类型映射与约束拼接,故与 CREATE TABLE 中的列定义一致。
 * 注:SQLite 对已有数据的表加 NOT NULL 列须带 DEFAULT,调用方应在 ColumnSpec 上配 defaultSql 或保持 nullable。
 */
public func buildAddColumn(table: String, c: ColumnSpec, d: Dialect): String {
    let typ = columnType(c, {t => d.sqlType(t)})
    return "ALTER TABLE " + table + " ADD COLUMN " + c.name + " " + typ + columnConstraints(c)
}

// 逗号连接列名(upsert 子句构造用)。
func joinColsComma(cols: Array<String>): String {
    var s = ""
    var first = true
    for (c in cols) {
        if (!first) {
            s = s + ", "
        }
        first = false
        s = s + c
    }
    return s
}

// SQLite/PostgreSQL 的 ON CONFLICT 子句:有更新列则 DO UPDATE SET c = excluded.c,否则 DO NOTHING。
protected func onConflictClause(conflictColumns: Array<String>, updateColumns: Array<String>): String {
    let conf = joinColsComma(conflictColumns)
    if (updateColumns.size == 0) {
        return " ON CONFLICT(" + conf + ") DO NOTHING"
    }
    var sets = ""
    var first = true
    for (c in updateColumns) {
        if (!first) {
            sets = sets + ", "
        }
        first = false
        sets = sets + c + " = excluded." + c
    }
    return " ON CONFLICT(" + conf + ") DO UPDATE SET " + sets
}

// MySQL 的 ON DUPLICATE KEY UPDATE 子句:c = VALUES(c)(无更新列时回退用冲突列做空更新)。
protected func onDuplicateClause(updateColumns: Array<String>, conflictColumns: Array<String>): String {
    var cols = updateColumns
    if (cols.size == 0) {
        cols = conflictColumns
    }
    var sets = ""
    var first = true
    for (c in cols) {
        if (!first) {
            sets = sets + ", "
        }
        first = false
        sets = sets + c + " = VALUES(" + c + ")"
    }
    return " ON DUPLICATE KEY UPDATE " + sets
}

// 列的 SQL 类型:explicitType 优先;否则 Decimal 列带 precision 拼 NUMERIC/DECIMAL(p,s)(SQLite TEXT 忽略精度);
// 否则文本列带 length 用 VARCHAR(length);否则按 DbType 推断。
protected func columnType(c: ColumnSpec, typeOf: (DbType) -> String): String {
    if (c.explicitType != "") {
        return c.explicitType
    }
    if (isDecimalType(c.dbType) && c.precision > 0) {
        // typeOf(TDecimal):MySQL 'DECIMAL(38, 10)' / PG 'NUMERIC' / SQLite 'TEXT'。
        // 先截断 '(' 前的关键字(避免 MySQL 出现 DECIMAL(38, 10)(p,s) 双括号),再拼 (p,s)。
        let base = decimalKeyword(typeOf(TDecimal))
        if (base == "TEXT") {
            // SQLite 无 Decimal 类型(TEXT 存储),精度无意义。
            return "TEXT"
        }
        if (c.scale > 0) {
            return base + "(" + c.precision.toString() + ", " + c.scale.toString() + ")"
        }
        return base + "(" + c.precision.toString() + ")"
    }
    if (c.length > 0 && isTextType(c.dbType)) {
        return "VARCHAR(" + c.length.toString() + ")"
    }
    return typeOf(c.dbType)
}

func isTextType(t: DbType): Bool {
    match (t) {
        case TText => true
        case _ => false
    }
}

func isDecimalType(t: DbType): Bool {
    match (t) {
        case TDecimal => true
        case _ => false
    }
}

// 取原始类型串中 '(' 之前的关键字(保留大小写);无 '(' 则原样返回,顺带去尾部空格。
// 例:"DECIMAL(38, 10)"→"DECIMAL"、"NUMERIC"→"NUMERIC"、"TEXT"→"TEXT"。
func decimalKeyword(raw: String): String {
    let out = ArrayList<UInt8>()
    for (b in raw.toArray()) {
        if (b == 40u8) {
            break
        }
        out.add(b)
    }
    // 去尾部空格。
    while (out.size > 0 && out[out.size - 1] == 32u8) {
        out.remove(at: out.size - 1)
    }
    return String.fromUtf8(out.toArray())
}

// SQL 字符串字面量:单引号包裹,内部单引号加倍转义(enum CHECK 值来自编译期字面量,仍做转义防注入)。
protected func sqlStringLiteral(s: String): String {
    let out = ArrayList<UInt8>()
    out.add(39u8)
    for (b in s.toArray()) {
        if (b == 39u8) {
            out.add(39u8)
        }
        out.add(b)
    }
    out.add(39u8)
    return String.fromUtf8(out.toArray())
}

// 非主键列的内联约束:NOT NULL / UNIQUE / DEFAULT / CHECK(enum)(按需追加,顺序固定)。
// enum 用三库通用的 CHECK(col IN ('a','b',...))。
// commentInline:仅 MySQL 传 true → 列尾追加内联 COMMENT '...'(SQLite/PG 绝不内联,否则建表语法报错)。
func columnConstraints(c: ColumnSpec, commentInline!: Bool = false): String {
    var s = ""
    if (!c.nullable) {
        s = s + " NOT NULL"
    }
    if (c.unique) {
        s = s + " UNIQUE"
    }
    if (c.defaultSql != "") {
        s = s + " DEFAULT " + c.defaultSql
    }
    if (c.enumValues.size > 0) {
        var vals = ""
        var first = true
        for (v in c.enumValues) {
            if (!first) {
                vals = vals + ", "
            }
            first = false
            vals = vals + sqlStringLiteral(v)
        }
        s = s + " CHECK(" + c.name + " IN (" + vals + "))"
    }
    if (commentInline && c.comment != "") {
        s = s + " COMMENT " + sqlStringLiteral(c.comment)
    }
    return s
}

// 拼公共的列定义 + 外键约束 + 表级 CHECK(各方言传入自己的「自增主键定义」与「类型映射」函数)。
// 主键分两类:autoIncrement → 用 pkDef(自增);否则手动主键 `name type PRIMARY KEY`。
// checks:表级 CHECK 约束(默认空,向后兼容);具名 → `, CONSTRAINT n CHECK (expr)`,匿名 → `, CHECK (expr)`。
protected func buildCreateTable(table: String, cols: Array<ColumnSpec>, pkDef: (String) -> String,
    typeOf: (DbType) -> String, checks!: Array<CheckSpec> = Array<CheckSpec>(),
    commentInline!: Bool = false): String {
    var defs = ""
    var fks = ""
    var first = true
    // 主键列数:单主键(==1)走列级 PRIMARY KEY(输出与既有逐字节一致);复合主键(>1)走表级 PRIMARY KEY(a, b)。
    var pkCount = 0
    for (c in cols) {
        if (c.isPk) {
            pkCount += 1
        }
    }
    let pkNames = ArrayList<String>()
    for (c in cols) {
        if (!first) {
            defs = defs + ", "
        }
        first = false
        if (c.isPk && pkCount == 1) {
            if (c.autoIncrement) {
                defs = defs + pkDef(c.name)
            } else {
                defs = defs + c.name + " " + columnType(c, typeOf) + " PRIMARY KEY"
            }
        } else if (c.isPk) {
            // 复合主键列:作为普通列定义(复合主键恒非自增),主键约束在循环后以表级 PRIMARY KEY(...) 追加。
            defs = defs + c.name + " " + columnType(c, typeOf) + columnConstraints(c, commentInline: commentInline)
            pkNames.add(c.name)
        } else {
            defs = defs + c.name + " " + columnType(c, typeOf) + columnConstraints(c, commentInline: commentInline)
        }
        if (c.fkRef != "") {
            // 外键引用目标表主键列(默认 "id";目标实体自定义主键列时与 JOIN 目标对齐)。
            let refId = if (c.fkRefIdColumn == "") {
                "id"
            } else {
                c.fkRefIdColumn
            }
            fks = fks + ", FOREIGN KEY(" + c.name + ") REFERENCES " + c.fkRef + "(" + refId + ")"
            if (c.fkOnDelete != "") {
                fks = fks + " ON DELETE " + c.fkOnDelete
            }
            if (c.fkOnUpdate != "") {
                fks = fks + " ON UPDATE " + c.fkOnUpdate
            }
        }
    }
    if (pkCount > 1) {
        defs = defs + ", PRIMARY KEY(" + joinColsComma(pkNames.toArray()) + ")"
    }
    var cks = ""
    for (ck in checks) {
        let named = if (ck.name != "") {
            "CONSTRAINT " + ck.name + " "
        } else {
            ""
        }
        cks = cks + ", " + named + "CHECK (" + ck.expr + ")"
    }
    return "CREATE TABLE IF NOT EXISTS " + table + "(" + defs + fks + cks + ")"
}

// 拼 CREATE INDEX(ifNotExists 控制是否带 IF NOT EXISTS:MySQL 不支持该子句)。
protected func buildCreateIndex(table: String, idx: IndexSpec, ifNotExists: Bool): String {
    var cols = ""
    var first = true
    for (c in idx.columns) {
        if (!first) {
            cols = cols + ", "
        }
        first = false
        cols = cols + c
    }
    let uniq = if (idx.unique) {
        "UNIQUE "
    } else {
        ""
    }
    let ine = if (ifNotExists) {
        "IF NOT EXISTS "
    } else {
        ""
    }
    return "CREATE " + uniq + "INDEX " + ine + idx.name + " ON " + table + "(" + cols + ")"
}