import { Database } from "bun:sqlite"
import { connect, Connection, type Config as PlanetScaleConfig, type ExecutedQuery } from '@planetscale/database'
import { BaseMapper, type Mapper } from './mapper'
import type { HasID, SQLArgs, Atom } from "./index"

type DBConfig = {
  sqlite?: string
  planetscale?: PlanetScaleConfig
}

type ModelConfig = {
  table?: string
  id?: string
}

let db: Database | null = null

// open the db connection
// const db = await openDB({ sqlite: "my-db-name.sqlite")
// const user = await db.queryOne<User>("SELECT * FROM users WHERE id = $id", { id: 1 })
// console.log("It's", user.name,"!")
export async function openDB(config: DBConfig): Promise<DB> {
  if (config.planetscale) {
    const conn = connect(config.planetscale)
    return new PlanetScaleDB(conn)
  } else if (config.sqlite) {
    return new SQLiteDB(new Database(config.sqlite, { strict: true }))
  } else {
    throw new Error("openDB() requires a config for either sqlite or planetscale")
  }
}

// Base DB interface
export abstract class DB {
  private mappers: Map<string, Mapper<any>> = new Map()

  abstract query<T = unknown, U = T>(sql: string, args?: SQLArgs, map?: (row: T) => U): Promise<U[]>

  async close(): Promise<void> { }

  async queryOne<T = unknown, U = T>(sql: string, args?: SQLArgs, map?: (row: T) => U): Promise<U | null> {
    return (await this.query<T, U>(sql, args, map))[0] ?? null
  }

  async run(sql: string, args?: SQLArgs): Promise<void> {
    await this.query(sql, args)
  }

  abstract transaction<T>(fn: (tx: DB) => Promise<T>): Promise<T>


  model<Self extends DB, T extends HasID, Name extends string>(
    this: Self,
    modelClass: new (...args: any[]) => T,
    propName: Name,
    cfg?: ModelConfig
  ): Self & { [K in Name]: Mapper<T> }

  // Implementation
  // mapping: .model(ModelClass, 'propName'[, { table: 'table_name', id: 'user_id' }])
  model(this: DB, modelClass: new (...args: any[]) => HasID, propName: string, cfg?: ModelConfig): DB {
    const tableName = cfg?.table ?? propName
    const id = cfg?.id ?? 'id'
    const mapper = new BaseMapper(this, tableName, modelClass, id)
    this.mappers.set(propName, mapper)
      ; (this as any)[propName] = mapper
    return this as any
  }
}

// SQLite implementation
class SQLiteDB extends DB {
  constructor(private __db: Database) {
    super()
  }

  async close(): Promise<void> {
    await this.__db.close()
  }

  async query<T = unknown, U = T>(sql: string, args?: SQLArgs, map?: (row: T) => U): Promise<U[]> {
    // console.log("Query:", sql, args)
    const rows = this.__db.prepare(sql).all(args || {}) as T[]
    return map ? rows.map(map) : (rows as unknown as U[])
  }

  async transaction<T>(fn: (tx: DB) => Promise<T>): Promise<T> {
    await this.run('BEGIN')
    try {
      const result = await fn(this)
      await this.run('COMMIT')
      return result
    } catch (e) {
      await this.run('ROLLBACK')
      throw e
    }
  }
}

// PlanetScale implementation
class PlanetScaleDB extends DB {
  constructor(private __conn: { execute: (sql: string, args?: Record<string, Atom>) => Promise<ExecutedQuery> }) {
    super()
  }

  private convertArgs(args?: SQLArgs): Record<string, Atom> | undefined {
    if (!args) return undefined

    // Convert SQLite $param style to PlanetScale :param style
    const converted: Record<string, Atom> = {}
    for (const [key, value] of Object.entries(args)) {
      // Remove the $ prefix if it exists and add : prefix
      const newKey = key.startsWith('$') ? key.slice(1) : key
      converted[newKey] = value as Atom
    }
    return converted
  }

  private convertQuery(sql: string): string {
    // Convert $param to :param in the query
    return sql.replace(/\$(\w+)/g, ':$1')
  }

  private async executeQuery<T>(sql: string, args?: Record<string, Atom>): Promise<T[]> {
    const result = await this.__conn.execute(sql, args)
    return result.rows as T[]
  }

  async query<T = unknown, U = T>(sql: string, args?: SQLArgs, map?: (row: T) => U): Promise<U[]> {
    const convertedSql = this.convertQuery(sql)
    const convertedArgs = this.convertArgs(args)
    const rows = await this.executeQuery<T>(convertedSql, convertedArgs)
    return map ? rows.map(map) : (rows as unknown as U[])
  }

  async transaction<T>(fn: (tx: DB) => Promise<T>): Promise<T> {
    if ('transaction' in this.__conn) {
      return await (this.__conn as Connection).transaction(async (tx) => {
        const transactionDB = new PlanetScaleDB(tx)
        return await fn(transactionDB)
      })
    }
    throw new Error("Nested transactions are not supported")
  }
}
