import { Database, RestBindParameters } from "@db/sqlite";
import { err, getMessageFromError, ok, Result } from "@shared/utils/result.ts";
import { QueryExecutionError } from "@lib/errors.ts";
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
import log from "@shared/utils/logger.ts";

export class DatabaseClient {
    constructor(private readonly db: Database) {}

    private safeExecute<T>(fn: () => T): Result<T, QueryExecutionError> {
        try {
            return ok(fn());
        } catch (e) {
            const message = getMessageFromError(e);
            log.error(`Failed to execute sql! Error: ${e}`);
            return err(new QueryExecutionError(message));
        }
    }

    exec(
        sql: string,
        ...params: RestBindParameters
    ): Result<number, QueryExecutionError> {
        return this.safeExecute(() => this.db.exec(sql, params));
    }

    first<T extends object>(
        sql: string,
        ...params: RestBindParameters
    ): Result<Option<T>, QueryExecutionError> {
        return this.safeExecute(() =>
            fromNullableVal(this.db.prepare(sql).get<T>(params))
        );
    }

    all<T extends object>(
        sql: string,
        ...params: RestBindParameters
    ): Result<Option<T[]>, QueryExecutionError> {
        return this.safeExecute(() => this.db.prepare(sql).all<T>(params)).map(
            (results) => (results.length > 0 ? some(results) : none),
        );
    }

    prepareFetch<
        T extends object,
        P extends RestBindParameters = RestBindParameters,
    >(sql: string): PreparedStatement<T> {
        const stmt = this.db.prepare(sql);

        const get = (
            ...params: P
        ): Result<Option<NonNullable<T>>, QueryExecutionError> =>
            this.safeExecute(() => fromNullableVal(stmt.get<T>(params)));

        const all = (
            ...params: P
        ): Result<Option<NonNullable<T[]>>, QueryExecutionError> =>
            this.safeExecute(() => stmt.all<T>(params)).map((result) =>
                result.length > 0 ? some(result) : none
            );

        return { get, all };
    }

    prepareExec<P extends RestBindParameters = RestBindParameters>(
        sql: string,
    ): (...params: P) => Result<number, QueryExecutionError> {
        const stmt = this.db.prepare(sql);

        return (...params: P): Result<number, QueryExecutionError> => {
            return this.safeExecute(() => stmt.run(params));
        };
    }
}

interface PreparedStatement<T extends object> {
    get(...params: RestBindParameters): Result<Option<T>, QueryExecutionError>;
    all(
        ...params: RestBindParameters
    ): Result<Option<T[]>, QueryExecutionError>;
}
