import { Knex } from 'knex';
import flatten from 'lodash.flatten';
import { SchemaInspector } from '../types/schema-inspector';
import { Table } from '../types/table';
import { Column } from '../types/column';
import extractMaxLength from '../utils/extract-max-length';
import extractType from '../utils/extract-type';
import { ForeignKey } from '../types/foreign-key';
import { stripQuotes } from '../utils/strip-quotes';

type RawColumn = {
  cid: number;
  name: string;
  type: string;
  notnull: 0 | 1;
  unique: 0 | 1;
  dflt_value: any;
  pk: 0 | 1;
  hidden: number;
};

type RawForeignKey = {
  id: number;
  seq: number;
  table: string;
  from: string;
  to: string;
  on_update: ForeignKey['on_update'];
  on_delete: ForeignKey['on_delete'];
  match: string;
};

export function parseDefaultValue(value: string | null): string | null {
  if (value === null || value.trim().toLowerCase() === 'null') return null;

  return stripQuotes(value);
}

export default class SQLite implements SchemaInspector {
  knex: Knex;

  constructor(knex: Knex) {
    this.knex = knex;
  }

  // Tables
  // ===============================================================================================

  /**
   * List all existing tables in the current schema/database
   */
  async tables(): Promise<string[]> {
    const records = await this.knex
      .select('name')
      .from('sqlite_master')
      .whereRaw(`type = 'table' AND name NOT LIKE 'sqlite_%'`);
    return records.map(({ name }) => name) as string[];
  }

  /**
   * Get the table info for a given table. If table parameter is undefined, it will return all tables
   * in the current schema/database
   */
  tableInfo(): Promise<Table[]>;
  tableInfo(table: string): Promise<Table>;
  async tableInfo(table?: string) {
    const query = this.knex
      .select('name', 'sql')
      .from('sqlite_master')
      .where({ type: 'table' })
      .andWhereRaw(`name NOT LIKE 'sqlite_%'`);

    if (table) {
      query.andWhere({ name: table });
    }

    let records = await query;

    records = records.map((table) => ({
      name: table.name,
      sql: table.sql,
    }));

    if (table) {
      return records[0];
    }

    return records;
  }

  /**
   * Check if a table exists in the current schema/database
   */
  async hasTable(table: string): Promise<boolean> {
    const results = await this.knex
      .select(1)
      .from('sqlite_master')
      .where({ type: 'table', name: table });
    return results.length > 0;
  }

  // Columns
  // ===============================================================================================

  /**
   * Get all the available columns in the current schema/database. Can be filtered to a specific table
   */
  async columns(table?: string): Promise<{ table: string; column: string }[]> {
    if (table) {
      const columns = await this.knex.raw<RawColumn[]>(
        `PRAGMA table_xinfo(??)`,
        table
      );
      return columns.map((column) => ({
        table,
        column: column.name,
      }));
    }

    const tables = await this.tables();
    const columnsPerTable = await Promise.all(
      tables.map(async (table) => await this.columns(table))
    );
    return flatten(columnsPerTable);
  }

  /**
   * Get the column info for all columns, columns in a given table, or a specific column.
   */
  columnInfo(): Promise<Column[]>;
  columnInfo(table: string): Promise<Column[]>;
  columnInfo(table: string, column: string): Promise<Column>;
  async columnInfo(table?: string, column?: string) {
    const getColumnsForTable = async (table: string): Promise<Column[]> => {
      const tablesWithAutoIncrementPrimaryKeys = (
        await this.knex
          .select('name')
          .from('sqlite_master')
          .whereRaw(`sql LIKE '%AUTOINCREMENT%'`)
      ).map(({ name }) => name);

      const columns: RawColumn[] = await this.knex.raw(
        `PRAGMA table_xinfo(??)`,
        table
      );

      const foreignKeys = await this.knex.raw<
        { table: string; from: string; to: string }[]
      >(`PRAGMA foreign_key_list(??)`, table);

      const indexList = await this.knex.raw<
        { name: string; unique: boolean }[]
      >(`PRAGMA index_list(??)`, table);

      const indexInfoList = await Promise.all(
        indexList.map((index) =>
          this.knex.raw<{ seqno: number; cid: number; name: string }[]>(
            `PRAGMA index_info(??)`,
            index.name
          )
        )
      );

      return columns.map((raw): Column => {
        const foreignKey = foreignKeys.find((fk) => fk.from === raw.name);

        const indexIndex = indexInfoList.findIndex((list) =>
          list.find((fk) => fk.name === raw.name)
        );
        const index = indexList[indexIndex];
        const indexInfo = indexInfoList[indexIndex];

        return {
          name: raw.name,
          table: table,
          data_type: extractType(raw.type),
          default_value: parseDefaultValue(raw.dflt_value),
          max_length: extractMaxLength(raw.type),
          /** @NOTE SQLite3 doesn't support precision/scale */
          numeric_precision: null,
          numeric_scale: null,
          is_generated: raw.hidden !== 0,
          generation_expression: null,
          is_nullable: raw.notnull === 0,
          is_unique: !!index?.unique && indexInfo?.length === 1,
          is_primary_key: raw.pk === 1,
          has_auto_increment:
            raw.pk === 1 && tablesWithAutoIncrementPrimaryKeys.includes(table),
          foreign_key_column: foreignKey?.to || null,
          foreign_key_table: foreignKey?.table || null,
        };
      });
    };

    if (!table) {
      const tables = await this.tables();
      const columnsPerTable = await Promise.all(
        tables.map(async (table) => await getColumnsForTable(table))
      );
      return flatten(columnsPerTable);
    }

    if (table && !column) {
      return await getColumnsForTable(table);
    }

    const columns = await getColumnsForTable(table);
    return columns.find((columnInfo) => columnInfo.name === column);
  }

  /**
   * Check if a table exists in the current schema/database
   */
  async hasColumn(table: string, column: string): Promise<boolean> {
    let isColumn = false;
    const results = await this.knex.raw(
      `SELECT COUNT(*) AS ct FROM pragma_table_xinfo('${table}') WHERE name='${column}'`
    );
    const resultsVal = results[0]['ct'];
    if (resultsVal !== 0) {
      isColumn = true;
    }
    return isColumn;
  }

  /**
   * Get the primary key column for the given table
   */
  async primary(table: string) {
    const columns = await this.knex.raw<RawColumn[]>(
      `PRAGMA table_xinfo(??)`,
      table
    );
    const pkColumn = columns.find((col) => col.pk !== 0);
    return pkColumn?.name || null;
  }

  // Foreign Keys
  // ===============================================================================================

  async foreignKeys(table?: string): Promise<ForeignKey[]> {
    if (table) {
      const keys = await this.knex.raw(`PRAGMA foreign_key_list(??)`, table);

      return keys.map(
        (key: RawForeignKey): ForeignKey => ({
          table,
          column: key.from,
          foreign_key_table: key.table,
          foreign_key_column: key.to,
          on_update: key.on_update,
          on_delete: key.on_delete,
          constraint_name: null,
        })
      );
    }

    const tables = await this.tables();

    const keysPerTable = await Promise.all(
      tables.map(async (table) => await this.foreignKeys(table))
    );

    return flatten(keysPerTable);
  }
}
