package aceboot::orm.common

import std.sync.*

/**
 * 读写分离路由驱动:包裹一个 primary(主,写) + 若干 replica(从,读),透明地把读路由到从库、
 * 把写与事务强制留在主库——仓储 / QueryBuilder / EntityManager 全部零改动(它们只认 `ds.driver: Driver`)。
 *
 * 路由规则:
 *  - `exec` / `begin` / `commit` / `rollback` / `close` / `affectedRows` / `lastInsertId` / `dialect` → 一律 primary。
 *  - `query()`:
 *      · 事务内(`aceTxDepth() > 0`)→ primary(保证「读己之写」一致性,避免读到从库未同步的旧数据);
 *      · 写语句(非 SELECT,如 `INSERT ... RETURNING`、`SAVEPOINT`、DDL)→ primary;
 *      · 其余只读 SELECT → 轮询(round-robin)一个 replica。
 * 无 replica 配置时一律走 primary,完全向后兼容单库部署。
 *
 * 约束:主从须同型(同方言);dialect 取 primary。RoutingDriver 自身不持连接,只委派,
 * 与 sqlite perOp / 连接池 / InstrumentedDriver 装饰器可任意叠加。
 */
public class RoutingDriver <: Driver {
    let primary: Driver
    let replicas: Array<Driver>
    let rr = AtomicInt64(0)

    public init(primary: Driver, replicas: Array<Driver>) {
        this.primary = primary
        this.replicas = replicas
    }

    public func dialect(): Dialect {
        primary.dialect()
    }

    public func query(sql: String, params: Array<DbValue>): Array<Row> {
        // 仅「有从库 + 不在事务中 + 纯只读 SELECT」才路由到从库;其余保守地走主库。
        if (replicas.size > 0 && aceTxDepth() == 0 && isReadSql(sql)) {
            let idx = rr.fetchAdd(1) % Int64(replicas.size)
            return replicas[idx].query(sql, params)
        }
        return primary.query(sql, params)
    }

    public func exec(sql: String, params: Array<DbValue>): Int64 {
        primary.exec(sql, params)
    }

    public func begin(): Unit {
        primary.begin()
    }

    public func commit(): Unit {
        primary.commit()
    }

    public func rollback(): Unit {
        primary.rollback()
    }

    public func close(): Unit {
        primary.close()
        for (r in replicas) {
            r.close()
        }
    }

    public func affectedRows(): Int64 {
        primary.affectedRows()
    }

    public func lastInsertId(): Int64 {
        primary.lastInsertId()
    }
}

// 判断 SQL 是否纯只读(首关键字为 SELECT)。保守策略:除明确 SELECT 外一律视为写/走主库,
// 故 INSERT ... RETURNING(经 query 执行)、WITH ... 写 CTE、DDL 都正确留在主库。
func isReadSql(sql: String): Bool {
    leadingKeyword(sql) == "select"
}

// 取 SQL 的首个英文关键字并转小写(跳过前导空白与左括号)。
func leadingKeyword(sql: String): String {
    let bytes = sql.toArray()
    var i = 0
    while (i < bytes.size) {
        let c = bytes[i]
        if (c == 32u8 || c == 9u8 || c == 10u8 || c == 13u8 || c == 40u8) {
            i += 1
        } else {
            break
        }
    }
    let start = i
    while (i < bytes.size) {
        let c = bytes[i]
        if ((c >= 65u8 && c <= 90u8) || (c >= 97u8 && c <= 122u8)) {
            i += 1
        } else {
            break
        }
    }
    if (i <= start) {
        return ""
    }
    let slice = Array<UInt8>(i - start, {k => bytes[start + k]})
    return lowerAscii(String.fromUtf8(slice))
}

// ASCII 小写化(SQL 关键字判定用;不触碰非 ASCII 字节)。
func lowerAscii(s: String): String {
    let bs = s.toArray()
    let out = Array<UInt8>(bs.size, {i =>
        let c = bs[i]
        if (c >= 65u8 && c <= 90u8) {
            c + 32u8
        } else {
            c
        }
    })
    String.fromUtf8(out)
}