import { Brackets, DataSource, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm";
import { dataSource } from "../data-source";
import { ConditionStructure, DynamicQueryStructure } from "../type/dynamicQueries";
import { TableMeta } from "../type/tableMeta";

export default abstract class DynamicQueryBuilder {
  public static allowedTables: Array<string> = [
    "award",
    "communication",
    "communicationType",
    "executivePosition",
    "membershipStatus",
    "qualification",
    "member",
    "memberAwards",
    "memberExecutivePositions",
    "memberQualifications",
    "membership",
    "memberView",
    "memberExecutivePositionsView",
    "memberQualificationsView",
    "membershipView",
  ];

  public static getTableMeta(tableName: string): TableMeta {
    let { name, columns, relations } = dataSource.getMetadata(tableName);

    const uniqueColumns = columns.map((c) => ({ column: c.propertyName, type: c.type }));

    return {
      tableName: name,
      columns: [
        ...uniqueColumns,
        ...relations
          .filter((r) => !uniqueColumns.some((c) => r.propertyName == c.column))
          .map((r) => ({
            column: r.propertyName,
            type: r.inverseEntityMetadata?.columns.find((col) => col.propertyName === r.inverseSidePropertyPath)?.type,
          })),
      ],
      relations: relations.map((r) => ({
        column: r.propertyName,
        relationType: r.relationType,
        referencedTableName: r.inverseEntityMetadata?.tableName,
      })),
    };
  }

  public static getAllTableMeta(): Array<TableMeta> {
    return this.allowedTables.map((table) => this.getTableMeta(table));
  }

  public static buildQuery(
    queryObj: DynamicQueryStructure,
    offset: number = 0,
    count: number = 25
  ): SelectQueryBuilder<ObjectLiteral> {
    let affix = Math.random().toString(36).substring(2);
    let query = dataSource.getRepository(queryObj.table).createQueryBuilder(`${queryObj.table}_${affix}`);

    this.buildDynamicQuery(query, queryObj, affix);

    query.offset(offset);
    query.limit(count);

    return query;
  }

  private static buildDynamicQuery(
    query: SelectQueryBuilder<ObjectLiteral>,
    queryObject: DynamicQueryStructure,
    affix: string = "",
    depth: number = 0
  ): void {
    const alias = queryObject.table + "_" + affix;
    let firstSelect = true;
    let selects: Array<string> = [];

    if (queryObject.select == "*") {
      let meta = this.getTableMeta(queryObject.table);
      let relCols = meta.relations.map((r) => r.column);
      selects = meta.columns.map((c) => c.column).filter((c) => !relCols.includes(c));
    } else {
      selects = queryObject.select;
    }

    for (const select of selects) {
      if (firstSelect && depth == 0) {
        query.select(`${alias}.${select}`);
        firstSelect = false;
      } else {
        query.addSelect(`${alias}.${select}`);
      }
    }

    if (queryObject.where) {
      this.applyWhere(query, queryObject.where, alias);
    }

    if (queryObject.join) {
      for (const join of queryObject.join) {
        let subaffix = Math.random().toString(36).substring(2);
        query.leftJoinAndSelect(`${alias}.${join.foreignColumn}`, join.table + "_" + subaffix);

        this.buildDynamicQuery(query, join, subaffix, depth + 1);
      }
    }

    if (queryObject.orderBy) {
      queryObject.orderBy.forEach((order) => {
        query.addOrderBy(`${alias}.${order.column}`, order.order);
      });
    }
  }

  public static applyWhere(
    query: SelectQueryBuilder<ObjectLiteral> | WhereExpressionBuilder,
    conditions: Array<ConditionStructure>,
    alias: string
  ): void {
    for (const condition of conditions) {
      if (condition.structureType == "condition") {
        const whereClause = this.buildConditionClause(condition, alias);

        if (condition.concat == "_" || condition.concat == "AND") {
          query.andWhere(whereClause.query, whereClause.parameters);
        } else {
          query.orWhere(whereClause.query, whereClause.parameters);
        }
      } else {
        if (condition.concat == "_" || condition.concat == "AND") {
          query.andWhere(
            condition.invert == undefined || condition.invert == true
              ? new Brackets((qb) => {
                  this.applyWhere(qb, condition.conditions, alias);
                })
              : new NotBrackets((qb) => {
                  this.applyWhere(qb, condition.conditions, alias);
                })
          );
        } else {
          query.orWhere(
            condition.invert == undefined || condition.invert == true
              ? new Brackets((qb) => {
                  this.applyWhere(qb, condition.conditions, alias);
                })
              : new NotBrackets((qb) => {
                  this.applyWhere(qb, condition.conditions, alias);
                })
          );
        }
      }
    }
  }

  private static buildConditionClause(
    condition: ConditionStructure & { structureType: "condition" },
    alias: string
  ): { query: string; parameters: Record<string, unknown> } {
    const parameterKey = `${alias}_${condition.column}_${Math.random().toString(36).substring(2)}`;
    let query = `${alias}.${condition.column}`;
    let parameters: Record<string, unknown> = {};

    switch (condition.operation) {
      case "eq":
        query += ` = :${parameterKey}`;
        parameters[parameterKey] = condition.value;
        break;
      case "neq":
        query += ` != :${parameterKey}`;
        parameters[parameterKey] = condition.value;
        break;
      case "lt":
        query += ` < :${parameterKey}`;
        parameters[parameterKey] = condition.value;
        break;
      case "lte":
        query += ` <= :${parameterKey}`;
        parameters[parameterKey] = condition.value;
        break;
      case "gt":
        query += ` > :${parameterKey}`;
        parameters[parameterKey] = condition.value;
        break;
      case "gte":
        query += ` >= :${parameterKey}`;
        parameters[parameterKey] = condition.value;
        break;
      case "in":
        query += ` IN (:...${parameterKey})`;
        parameters[parameterKey] = condition.value;
        break;
      case "notIn":
        query += ` NOT IN (:...${parameterKey})`;
        parameters[parameterKey] = condition.value;
        break;
      case "between":
        query += ` BETWEEN :${parameterKey}_start AND :${parameterKey}_end`;
        parameters[`${parameterKey}_start`] = (condition.value as { start: any }).start;
        parameters[`${parameterKey}_end`] = (condition.value as { end: any }).end;
        break;
      case "null":
        query += ` IS NULL`;
        break;
      case "notNull":
        query += ` IS NOT NULL`;
        break;
      case "contains":
        query += ` LIKE :${parameterKey}`;
        parameters[parameterKey] = `%${condition.value}%`;
        break;
      case "notContains":
        query += ` NOT LIKE :${parameterKey}`;
        parameters[parameterKey] = `%${condition.value}%`;
        break;
      case "startsWith":
        query += ` LIKE :${parameterKey}`;
        parameters[parameterKey] = `${condition.value}%`;
        break;
      case "endsWith":
        query += ` LIKE :${parameterKey}`;
        parameters[parameterKey] = `%${condition.value}`;
        break;
      case "timespanEq":
        query += ` BETWEEN :${parameterKey}_start AND :${parameterKey}_end`;
        parameters[`${parameterKey}_start`] = new Date(new Date().getFullYear() - (condition.value as number), 0, 1);
        parameters[`${parameterKey}_end`] = new Date(new Date().getFullYear() - (condition.value as number), 11, 31);
    }

    return { query, parameters };
  }
}