import sqlite3 from "sqlite3";

export interface DatabaseAdapter {
  run(sql: string, params?: readonly unknown[]): Promise<{ lastID: number; changes: number }>;
  get<T>(sql: string, params?: readonly unknown[]): Promise<T | undefined>;
  all<T>(sql: string, params?: readonly unknown[]): Promise<T[]>;
  exec(sql: string): Promise<void>;
  close(): Promise<void>;
}

export class SqliteAdapter implements DatabaseAdapter {
  private readonly db: sqlite3.Database;
  private readonly ready: Promise<void>;

  public constructor(path: string) {
    this.db = new sqlite3.Database(path);
    this.ready = new Promise((resolve, reject) => {
      this.db.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL;", (err: Error | null) => {
        if (err) {
          reject(err);
          return;
        }
        resolve();
      });
    });
  }

  public run(sql: string, params: readonly unknown[] = []): Promise<{ lastID: number; changes: number }> {
    return this.ready.then(
      () =>
        new Promise((resolve, reject) => {
          this.db.run(sql, params as unknown as unknown[], function runCallback(this: sqlite3.RunResult, err: Error | null) {
            if (err) {
              reject(err);
              return;
            }
            resolve({ lastID: this.lastID, changes: this.changes });
          });
        })
    );
  }

  public get<T>(sql: string, params: readonly unknown[] = []): Promise<T | undefined> {
    return this.ready.then(
      () =>
        new Promise((resolve, reject) => {
          this.db.get(sql, params as unknown as unknown[], (err: Error | null, row: T | undefined) => {
            if (err) {
              reject(err);
              return;
            }
            resolve(row);
          });
        })
    );
  }

  public all<T>(sql: string, params: readonly unknown[] = []): Promise<T[]> {
    return this.ready.then(
      () =>
        new Promise((resolve, reject) => {
          this.db.all(sql, params as unknown as unknown[], (err: Error | null, rows: T[]) => {
            if (err) {
              reject(err);
              return;
            }
            resolve(rows);
          });
        })
    );
  }

  public exec(sql: string): Promise<void> {
    return this.ready.then(
      () =>
        new Promise((resolve, reject) => {
          this.db.exec(sql, (err: Error | null) => {
            if (err) {
              reject(err);
              return;
            }
            resolve();
          });
        })
    );
  }

  public close(): Promise<void> {
    return this.ready.then(
      () =>
        new Promise((resolve, reject) => {
          this.db.close((err: Error | null) => {
            if (err) {
              reject(err);
              return;
            }
            resolve();
          });
        })
    );
  }
}
