import type { HasID, Where, DB, SQLArgs } from "./index"
import { schema, } from "./schema"
import { buildWhere, type WhereClause } from "./where-builder"

export interface Mapper<T extends HasID> {
    find(id: string | number): Promise<T | null>
    create(data: Partial<T>): Promise<T>
    update(id: string | number, data: Partial<T>): Promise<T | null>
    delete(id: string | number): Promise<boolean>
    all(where?: string | Where<T>, args?: Where<T>): Promise<T[]>
    one(where?: string | Where<T>, args?: Where<T>): Promise<T | null>
    query(sql: string, args?: SQLArgs): Promise<T[]>
    queryOne(sql: string, args?: SQLArgs): Promise<T | null>
    schema(): string
}

export class BaseMapper<T extends HasID> implements Mapper<T> {
    constructor(
        protected db: DB,
        protected table: string,
        protected construct: new (data: any) => T,
        protected id: string,
    ) { }

    protected initializeModel(data: any): T {
        // Create instance
        const model = new this.construct(data)

        // Use Object.assign for property assignment
        Object.assign(model, data)

        return model
    }

    async find(id: string | number): Promise<T | null> {
        const sql = `SELECT * FROM ${this.table} WHERE ${this.id} = $id`
        const args = { id }
        return await this.db.queryOne<any, T>(
            sql,
            args,
            (row) => this.initializeModel(row),
        )
    }

    async create(data: Partial<T>): Promise<T> {
        const columns = Object.keys(data)
        const placeholders = columns.map(c => `$${c}`).join(", ")

        const args = columns.reduce((acc, col) => {
            acc[col] = (data as any)[col]
            return acc
        }, {} as Record<string, any>)

        const result = await this.db.queryOne<any, T>(
            `INSERT INTO ${this.table}(${columns.join(", ")})
        VALUES(${placeholders})
        RETURNING * `,
            args,
            (row) => this.initializeModel(row),
        )

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

        return result
    }

    async update(id: string | number, data: Partial<T>): Promise<T | null> {
        const updates = Object.keys(data)
            .map((key) => `${key} = $${key}`)
            .join(", ")

        const args = {
            id,
            ...Object.keys(data).reduce((acc, key) => {
                acc[key] = (data as any)[key]
                return acc
            }, {} as Record<string, any>),
        }

        return await this.db.queryOne<any, T>(
            `UPDATE ${this.table}
        SET ${updates}
        WHERE ${this.id} = $id
        RETURNING * `,
            args,
            (row) => this.initializeModel(row),
        )
    }

    async delete(id: string | number): Promise<boolean> {
        await this.db.run(
            `DELETE FROM ${this.table} WHERE ${this.id} = $id`,
            { id },
        )
        return true
    }

    async one(where?: string | Where<T>, args?: Where<T>): Promise<T | null> {
        return (await this.all(where, args ? { ...args, limit: 1 } : { limit: 1 }))[0] ?? null
    }

    async all(where?: string | Where<T>, args?: Where<T>): Promise<T[]> {
        let query = `SELECT * FROM ${this.table}`

        if (where) {
            if (typeof where === "string") {
                const whereClause = buildWhere(where, args as WhereClause)
                if (!whereClause.startsWith("LIMIT") && !whereClause.startsWith("OFFSET") && !whereClause.startsWith("ORDER")) {
                    query += ` ${whereClause}`
                } else {
                    query += ` ${whereClause}`
                }
                return await this.db.query<any, T>(
                    query,
                    args as Record<string, any>,
                    (row) => this.initializeModel(row),
                )
            } else {
                const whereClause = buildWhere(where as WhereClause)
                if (whereClause) query += ` ${whereClause}`

                return await this.db.query<any, T>(
                    query,
                    where as Record<string, any>,
                    (row) => this.initializeModel(row),
                )
            }
        }

        return await this.db.query<any, T>(
            query,
            args as Record<string, any>,
            (row) => this.initializeModel(row),
        )
    }

    async query(sql: string, args?: SQLArgs): Promise<T[]> {
        return await this.db.query<any, T>(sql, args, (row) => this.initializeModel(row))
    }

    async queryOne(sql: string, args?: SQLArgs): Promise<T | null> {
        return await this.db.queryOne<any, T>(sql, args, (row) => this.initializeModel(row))
    }

    schema(): string {
        return schema(this.construct, this.table)
    }
}

