import { SQLBuilder } from "@dto/sql-builder-class";
import {
  ColumnData,
  InsertOptions,
  LimitOffset,
  OrderByField,
  Prettify,
  QueryFunction,
  QueryAction,
  UpdateOptions,
  WhereCondition,
  SoftDeleteOptions,
  FieldAlias,
  PatchOptions,
  CentralFields,
  TableModelConstructor,
  SelectFields,
  InsertValue,
  UpdateValue
} from "@dto/types";
import logger from "@lib/logger";
import type {
  QueryResult,
  ResultSetHeader,
  RowDataPacket
} from "mysql2";
export class TableModel<ColumnKeys extends string, PrimaryKey extends ColumnKeys> {
  tableName: string;
  tableAlias?: string;
  primaryKey: PrimaryKey;
  columns: ColumnKeys[];
  private enablePrimaryKey: boolean;
  private centralFields: CentralFields;
  private queryFn?: QueryFunction;

  constructor(config: TableModelConstructor<ColumnKeys[], PrimaryKey>) {
    this.tableName = config.tableName;
    this.tableAlias = config.tableAlias;
    this.primaryKey = config.primaryKey;
    this.columns = config.columns;
    this.enablePrimaryKey = config.enablePrimaryKey !== false; // default true
    const defaultCentralFields: CentralFields = {
      ctimeField: 'ctime',
      utimeField: 'utime',
      isActiveField: 'is_active',
      isDeletedField: 'is_deleted',
      statusField: 'status'
    };
    this.centralFields = {
      ...defaultCentralFields,
      ...(config?.centralFields || {})
    };
    this.queryFn = config.queryFn;
    this.checkConfig();
  }

  private checkConfig() {
    if (!this.tableName) {
      throw new Error('Table name is required');
    }
    if (this.enablePrimaryKey && !this.primaryKey) {
      throw new Error('Primary key is required when enablePrimaryKey is true');
    }
    if (!Array.isArray(this.columns) || this.columns.length === 0) {
      throw new Error('Table Columns are required');
    }
  }

  private checkEmptyObject(obj: Object) {
    return Object.keys(obj).length === 0;
  }

  private throwEmptyObjectError(obj: Object, message?: string) {
    if (this.checkEmptyObject(obj)) {
      throw new Error(message || 'Object cannot be empty');
    }
  }

  private throwEmptyArrayError(arr: any[], message?: string) {
    if (arr.length === 0) {
      throw new Error(message || 'Array cannot be empty');
    }
  }

  private printPrefixMessage(message: string) {
    return `[Table :: ${this.tableName}] :: ${message}`;
  }

  private cloneData<T>(data: T): T {
    if (Array.isArray(data)) {
      return data.map(item => ({ ...item })) as T;
    }
    return { ...data } as T;
  }

  private removeExtraFieldsAndLog(structuredData: InsertValue<ColumnKeys> & { [key: string]: any }, index?: number) {
    if (Array.isArray(structuredData)) {
      structuredData.forEach((data, index) => {
        this.removeExtraFieldsAndLog(data, index);
      });
      return;
    }

    const removedKeys: string[] = [];
    // Remove extra fields that are not in the columns
    Object.keys(structuredData).forEach(key => {
      if (!this.columns.includes(key as ColumnKeys)) {
        removedKeys.push(key);
        delete structuredData[key as ColumnKeys];
      }
    });

    if (removedKeys.length > 0) {
      logger.warn(this.printPrefixMessage(`Removed unknown fields: ${removedKeys.join(', ')} from data[${index}]`));
    }
  }

  initSQLBuilder<T extends ColumnKeys, QueryReturnType = any>() {
    return new SQLBuilder<T, QueryReturnType>(this.queryFn);
  }

  createWhereCondition() {
    return {} as WhereCondition<ColumnKeys>;
  }

  createOrderByObject(defaultValues?: OrderByField<ColumnKeys>) {
    if (defaultValues && !this.checkEmptyObject(defaultValues)) return defaultValues;
    return {} as OrderByField<ColumnKeys>;
  }

  createOrderByArray() {
    return [] as OrderByField<ColumnKeys>[];
  }

  createSelect() {
    return (values?: {
      fields?: SelectFields<ColumnKeys>,
    }) => {
      const SQLBuild = this.initSQLBuilder<ColumnKeys, RowDataPacket[]>();
      const { fields } = values || {};
      return SQLBuild.select(fields || "*").from(this.tableName, this.tableAlias);
    }
  }

  createUpdate(options?: UpdateOptions) {
    return (values: {
      data: UpdateValue<ColumnKeys>,
      where: WhereCondition<ColumnKeys>,
    }) => {
      const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
      const { data = {}, where = {} } = values || {};
      this.throwEmptyObjectError(where, this.printPrefixMessage('CreateUpdate :: Where condition cannot be empty'));
      this.throwEmptyObjectError(data, this.printPrefixMessage('CreateUpdate :: Data cannot be empty'));
      const handleData = this.cloneData(data);
      if (this.enablePrimaryKey && this.primaryKey && this.primaryKey in handleData) {
        delete handleData[this.primaryKey as unknown as keyof typeof handleData];
      }
      return SQLBuild.update(this.tableName, handleData as UpdateValue<ColumnKeys>, options)
        .where(where);
    }
  }

  createInsert(options?: InsertOptions<ColumnKeys>) {
    return (data: InsertValue<ColumnKeys>) => {
      const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
      const handleData = this.cloneData(data);
      if (Array.isArray(handleData)) {
        this.throwEmptyArrayError(handleData, this.printPrefixMessage('CreateInsert :: Data cannot be empty'));
      } else {
        this.throwEmptyObjectError(handleData, this.printPrefixMessage('CreateInsert :: Data cannot be empty'));
      }
      const structuredData = Array.isArray(handleData) ? handleData : [handleData];
      this.removeExtraFieldsAndLog(structuredData);
      return SQLBuild.insert(this.tableName, structuredData, options);
    }
  }

  createDelete() {
    return (values: {
      where: WhereCondition<ColumnKeys>,
    }) => {
      const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
      const { where = {} } = values || {};
      this.throwEmptyObjectError(where, this.printPrefixMessage('CreateDelete :: Where condition cannot be empty'));
      return SQLBuild.deleteFrom(this.tableName)
        .where(where)
    }
  }

  createCount() {
    return (field: ColumnKeys | (string & {}) | "*" = '*', alias?: string) => {
      const SQLBuild = this.initSQLBuilder<ColumnKeys, RowDataPacket[]>();
      return SQLBuild.count(field, alias).from(this.tableName, this.tableAlias);
    }
  }

  findOne(values: {
    fields?: SelectFields<ColumnKeys>,
    where: WhereCondition<ColumnKeys>,
    orderBy?: OrderByField<ColumnKeys>[],
  }) {
    const { where = {}, orderBy = [], fields } = values || {};
    this.throwEmptyObjectError(where, this.printPrefixMessage('FindOne :: Where condition cannot be empty'));
    const SQLBuild = this.initSQLBuilder<ColumnKeys, { [key in ColumnKeys | (string & {})]?: any }[]>();
    return SQLBuild.select(fields || "*")
      .from(this.tableName, this.tableAlias)
      .where(where)
      .orderBy(orderBy)
      .limit(1);
  }

  findAll(values?: Prettify<{
    fields?: SelectFields<ColumnKeys>,
    where?: WhereCondition<ColumnKeys>,
    orderBy?: OrderByField<ColumnKeys>[],
  } & LimitOffset>) {
    const { where, orderBy = [], fields, limit, offset } = values || {};
    if (where) this.throwEmptyObjectError(where, this.printPrefixMessage('FindAll :: Where condition cannot be empty'));
    const SQLBuild = this.initSQLBuilder<ColumnKeys, { [key in ColumnKeys | (string & {})]?: any }[]>();
    return SQLBuild.select(fields || "*")
      .from(this.tableName, this.tableAlias)
      .where(where as WhereCondition<ColumnKeys>)
      .orderBy(orderBy)
      .limit(limit || -1) // -1 means no limit
      .offset(offset || -1); // -1 means no offset
  }

  updateOne(values: Prettify<{
    data: UpdateValue<ColumnKeys>,
    where: WhereCondition<ColumnKeys>,
    options?: UpdateOptions
  }>) {
    const { data = {}, where = {}, options } = values || {};
    this.throwEmptyObjectError(where, this.printPrefixMessage('UpdateOne :: Where condition cannot be empty'));
    this.throwEmptyObjectError(data, this.printPrefixMessage('UpdateOne :: Data cannot be empty'));
    const handleData = this.cloneData(data);
    if (this.enablePrimaryKey && this.primaryKey && this.primaryKey in handleData) {
      delete handleData[this.primaryKey as unknown as keyof typeof handleData]; // For javascript type checking
    }
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    return SQLBuild.update(this.tableName, handleData as UpdateValue<ColumnKeys>, options)
      .where(where)
      .limit(1) as QueryAction<ResultSetHeader>;
  }

  updateAll(values: Prettify<{
    data: UpdateValue<ColumnKeys>,
    where?: WhereCondition<ColumnKeys>,
    options?: UpdateOptions
  }>) {
    const { data = {}, where = {}, options } = values || {};
    this.throwEmptyObjectError(data, this.printPrefixMessage('UpdateAll :: Data cannot be empty'));
    if (where) this.throwEmptyObjectError(where, this.printPrefixMessage('UpdateAll :: Where condition cannot be empty'));
    const handleData = this.cloneData(data);
    if (this.enablePrimaryKey && this.primaryKey && this.primaryKey in handleData) {
      delete handleData[this.primaryKey as unknown as keyof typeof handleData]; // For javascript type checking
    }
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    return SQLBuild.update(this.tableName, handleData as UpdateValue<ColumnKeys>, options)
      .where(where) as QueryAction<ResultSetHeader>;
  }

  insertRecord(data: InsertValue<ColumnKeys>, options?: InsertOptions<ColumnKeys>) {
    const handleData = this.cloneData(data);
    if (Array.isArray(handleData)) {
      this.throwEmptyArrayError(handleData, this.printPrefixMessage('Create :: Data cannot be empty'));
    } else {
      this.throwEmptyObjectError(handleData, this.printPrefixMessage('Create :: Data cannot be empty'));
    }
    const structuredData = Array.isArray(handleData) ? handleData : [handleData];
    this.removeExtraFieldsAndLog(structuredData);
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    return SQLBuild.insert(this.tableName, structuredData, options);
  }

  removeOne(values: {
    where: WhereCondition<ColumnKeys>,
    orderBy?: OrderByField<ColumnKeys>[]
  }) {
    const { where = {}, orderBy = [] } = values || {};
    this.throwEmptyObjectError(where, this.printPrefixMessage('RemoveOne :: Where condition cannot be empty'));
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    return SQLBuild.deleteFrom(this.tableName)
      .where(where)
      .orderBy(orderBy)
      .limit(1) as QueryAction<ResultSetHeader>;
  }

  remove(values: {
    where: WhereCondition<ColumnKeys>,
  }) {
    const { where = {} } = values || {};
    // Prevent accidental deletion of all records
    this.throwEmptyObjectError(where, this.printPrefixMessage('Remove :: Where condition cannot be empty'));
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    return SQLBuild.deleteFrom(this.tableName)
      .where(where) as QueryAction<ResultSetHeader>;
  }

  patchSingleField<T>(values: {
    patchField: ColumnKeys,
    where: WhereCondition<ColumnKeys>,
    value: T,
    options?: PatchOptions
  }) {
    const { where = {}, value, options, patchField } = values || {};
    if (typeof patchField !== 'string' || patchField.length === 0) {
      throw new Error(this.printPrefixMessage('PatchSingleField :: Patch field is required'));
    }
    this.throwEmptyObjectError(where, this.printPrefixMessage('PatchSingleField :: Where condition cannot be empty'));
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    const data = { [patchField]: value } as ColumnData<ColumnKeys>;
    return SQLBuild.update(this.tableName, data, options)
      .where(where) as QueryAction<ResultSetHeader>;
  }

  softDeleteOne<T>(values: {
    where: WhereCondition<ColumnKeys>,
    value: T,
    options?: SoftDeleteOptions
  }) {
    const { where = {}, value, options } = values || {};
    this.throwEmptyObjectError(where, this.printPrefixMessage('SoftDeleteOne :: Where condition cannot be empty'));
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    const data = { [options?.deleteField || this.centralFields.isDeletedField!]: value } as ColumnData<ColumnKeys>;
    return SQLBuild.update(this.tableName, data, options)
      .where(where)
      .limit(1) as QueryAction<ResultSetHeader>;
  }

  softDelete(values: {
    where: WhereCondition<ColumnKeys>,
    value: any,
    options?: SoftDeleteOptions
  }) {
    const { where = {}, value, options } = values || {};
    this.throwEmptyObjectError(where, this.printPrefixMessage('SoftDelete :: Where condition cannot be empty'));
    const SQLBuild = this.initSQLBuilder<ColumnKeys, ResultSetHeader>();
    const data = { [options?.deleteField || this.centralFields.isDeletedField!]: value } as ColumnData<ColumnKeys>;
    return SQLBuild.update(this.tableName, data, options)
      .where(where) as QueryAction<ResultSetHeader>;
  }

}