import { FILTER_TYPE } from '../constant';
import { QueryModelEntity } from '../entities/query-model.entity';
import {
  ReportColumnConfigEntity,
  ReportConfigEntity,
} from '../entities/report-config.entity';

export class ReportQueryBuilder {
  public reportConfig: ReportConfigEntity;
  public queryModel: QueryModelEntity;

  constructor(reportConfig: ReportConfigEntity, queryModel: QueryModelEntity) {
    this.reportConfig = reportConfig;
    this.queryModel = queryModel;
  }

  getBaseConfig() {
    const tableSchema = this.reportConfig.table_schema;
    const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';

    const selectSql = this.createSelectSql();
    const selectSqlExport = this.createSelectSqlExport();

    const whereSql = this.createWhereSql();
    const limitSql = this.createLimitSql();

    const orderBySql = this.createOrderBySql();
    const orderBySqlExport = this.createOrderBySqlExport();

    const groupBy = this.createGroupBySql();
    const groupByExport = this.createGroupBySqlExport();

    return {
      tableSchema,
      mainTableAlias,
      selectSql,
      selectSqlExport,
      whereSql,
      limitSql,
      orderBySql,
      orderBySqlExport,
      ...groupBy,
      ...groupByExport,
    };
  }

  getSql(): string {
    const {
      selectSql,
      tableSchema,
      whereSql,
      groupByQuery,
      orderBySql,
      limitSql,
    } = this.getBaseConfig();
    return `SELECT ${selectSql} FROM ${tableSchema} ${whereSql} ${groupByQuery} ${orderBySql} ${limitSql}` as string;
  }

  getSqlCount(): string {
    const { groupByColumn, mainTableAlias, tableSchema, whereSql } =
      this.getBaseConfig();

    return `SELECT COUNT(${
      groupByColumn ? `DISTINCT ${groupByColumn}` : `${mainTableAlias}.id`
    }) ${
      groupByColumn
        ? `+ COUNT(DISTINCT CASE WHEN ${groupByColumn} IS NULL THEN 1 END)`
        : ''
    } AS count FROM ${tableSchema} ${whereSql}` as string;
  }

  getSqlExport(): string {
    const {
      selectSqlExport,
      tableSchema,
      whereSql,
      groupByQueryExport,
      orderBySqlExport,
    } = this.getBaseConfig();
    return `SELECT ${selectSqlExport} FROM ${tableSchema} ${whereSql} ${groupByQueryExport} ${orderBySqlExport}` as string;
  }

  getSqlCountExport(): string {
    const { groupByColumnExport, mainTableAlias, tableSchema, whereSql } =
      this.getBaseConfig();
    return `SELECT COUNT(${
      groupByColumnExport
        ? `DISTINCT ${groupByColumnExport}`
        : `${mainTableAlias}.id`
    }) ${
      groupByColumnExport
        ? `+ COUNT(DISTINCT CASE WHEN ${groupByColumnExport} IS NULL THEN 1 END)`
        : ''
    } AS count FROM ${tableSchema} ${whereSql}` as string;
  }

  isDoingGrouping(rowGroupCols, groupKeys) {
    // we are not doing grouping if at the lowest level. we are at the lowest level
    // if we are grouping by more columns than we have keys for (that means the user
    // has not expanded a lowest level group, OR we are not grouping at all).
    return rowGroupCols.length > groupKeys.length;
  }

  interpolate(str, o) {
    return str.replace(/{([^{}]*)}/g, function (a, b) {
      const r = o[b];
      return typeof r === 'string' || typeof r === 'number' ? r : a;
    });
  }

  createLimitSql() {
    const startRow = this.queryModel.startRow;
    const endRow = this.queryModel.endRow;
    const pageSize = endRow - startRow;
    return ' LIMIT ' + (pageSize + 1) + ' OFFSET ' + startRow;
  }

  getRowCount(results) {
    if (results === null || results === undefined || results.length === 0) {
      return null;
    }
    const currentLastRow = this.queryModel.startRow + results.length;
    return currentLastRow <= this.queryModel.endRow ? currentLastRow : -1;
  }

  cutResultsToPageSize(results) {
    const pageSize = this.queryModel.endRow - this.queryModel.startRow;
    if (results && results.length > pageSize) {
      return results.splice(0, pageSize);
    } else {
      return results;
    }
  }

  findQueryConfig(column: string) {
    const configColumns = this.reportConfig.column_configs ?? [];

    const queryColumn: ReportColumnConfigEntity = configColumns.find(
      (el) => el.column === column,
    );
    const customQueryColumn = this.reportConfig?.customQueryColumn
      ? this.reportConfig.customQueryColumn(column)
      : undefined;

    if (customQueryColumn) return customQueryColumn;
    else if (queryColumn) return queryColumn.query;

    return column.replace('__', '.');
  }

  // GENERATE SELECT QUERY ============================================
  createSelectSql(): string {
    const configColumns = this.reportConfig.column_configs;
    const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';

    const rowGroupCols = this.queryModel.rowGroupCols;
    const valueCols = this.queryModel.valueCols;
    const groupKeys = this.queryModel.groupKeys;

    if (this.isDoingGrouping(rowGroupCols, groupKeys)) {
      const colsToSelect = [];
      const rowGroupCol = rowGroupCols[groupKeys.length];
      const rowGroupColChildCount = rowGroupCols[groupKeys.length + 1];

      colsToSelect.push(
        this.findQueryConfig(rowGroupCol.field) + ` AS ${rowGroupCol.field}`,
      );

      if (rowGroupColChildCount) {
        colsToSelect.push(
          `COUNT(DISTINCT ${this.findQueryConfig(
            rowGroupColChildCount.field,
          )}) AS count_child_group`,
        );
      } else {
        colsToSelect.push(`COUNT(${mainTableAlias}.id) AS count_child_group`);
      }

      // eslint-disable-next-line @typescript-eslint/no-this-alias
      const thisSelf = this;
      valueCols.forEach(function (valueCol) {
        colsToSelect.push(
          `${valueCol.aggFunc} (${thisSelf.findQueryConfig(
            valueCol.field,
          )}) AS ${valueCol.field}`,
        );
      });
      return colsToSelect.join(', ');
    }

    const columns = configColumns.map(
      (item) => `${item.query} AS ${item.column}`,
    );

    return columns.join(', ');
  }

  createSelectSqlExport(): string {
    const configColumns = this.reportConfig.column_configs;

    const rowGroupCols = this.queryModel.rowGroupCols;
    const valueCols = this.queryModel.valueCols;

    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const thisSelf = this;

    if (rowGroupCols.length > 0) {
      const colsToSelect = [];
      rowGroupCols.forEach(function (rowGroupCol) {
        colsToSelect.push(
          thisSelf.findQueryConfig(rowGroupCol.field) +
            ` AS ${rowGroupCol.field}`,
        );
      });

      valueCols.forEach(function (valueCol) {
        colsToSelect.push(
          `${valueCol.aggFunc} (${thisSelf.findQueryConfig(
            valueCol.field,
          )}) AS ${valueCol.field}`,
        );
      });

      return colsToSelect.join(', ');
    }

    const columns = configColumns.map((item) => `${item.query} ${item.column}`);

    return columns.join(', ');
  }
  // ==================================================================

  // GENERATE WHERE QUERY =============================================
  createFilterSql(column: string, item: { type: FILTER_TYPE; filter: any }) {
    switch (item.type) {
      // TEXT
      case FILTER_TYPE.TEXT_EQUAL:
        return `${column} = '${item.filter}'`;

      case FILTER_TYPE.TEXT_NOT_EQUAL:
        return `${column} != '${item.filter}'`;

      case FILTER_TYPE.TEXT_CONTAINS:
        return `${column} ILIKE '%${item.filter}%'`;

      case FILTER_TYPE.TEXT_NOT_CONTAINS:
        return `${column} NOT ILIKE '%${item.filter}%'`;

      case FILTER_TYPE.TEXT_START_WITH:
        return `${column} ILIKE '${item.filter}%'`;

      case FILTER_TYPE.TEXT_END_WITH:
        return `${column} ILIKE '%${item.filter}'`;

      case FILTER_TYPE.TEXT_IN_MEMBER:
        return item.filter?.length > 0
          ? `${column} IN(${item.filter.map((i) => `'${i}'`).join(', ')})`
          : null;

      case FILTER_TYPE.TEXT_MULTIPLE_CONTAINS:
        return item.filter?.length > 0
          ? `${column} ILIKE ANY(ARRAY[${item.filter
              .map((i) => `'%${i}%'`)
              .join(', ')}])`
          : null;

      case FILTER_TYPE.TEXT_MULTIPLE_REGEXP_CONTAINS:
        return item.filter?.length > 0
          ? `${column} REGEXP '${item.filter.join('|')}'`
          : null;

      case FILTER_TYPE.DATE_IN_RANGE_EPOCH:
        return `(${column} >= ${item.filter[0]} AND ${column} <= ${item.filter[1]})`;

      case FILTER_TYPE.DATE_IN_RANGE_TIMESTAMP:
        return `(${column} BETWEEN '${item.filter[0]}' AND '${item.filter[1]}')`;

      case FILTER_TYPE.TEXT_IN_RANGE:
        return `(${column} >= '${item.filter[0]}' AND ${column} <= '${item.filter[1]}')`;

      // NUMBER
      case FILTER_TYPE.NUMBER_EQUAL:
        return `${column} = ${item.filter}`;

      case FILTER_TYPE.NUMBER_NOT_EQUAL:
        return `${column} != ${item.filter}`;

      case FILTER_TYPE.NUMBER_GREATER_THAN:
        return `${column} > ${item.filter}`;

      case FILTER_TYPE.NUMBER_GREATER_THAN_OR_EQUAL:
        return `${column} >= ${item.filter}`;

      case FILTER_TYPE.NUMBER_LESS_THAN:
        return `${column} < ${item.filter}`;

      case FILTER_TYPE.NUMBER_LESS_THAN_OR_EQUAL:
        return `${column} <= ${item.filter}`;

      case FILTER_TYPE.NUMBER_IN_RANGE:
        return `(${column} >= ${item.filter[0]} AND ${column} <= ${item.filter[1]})`;

      default:
        console.log('UNKNOWN FILTER TYPE:', item.type);
        return 'true';
    }
  }

  createWhereSql() {
    const configFilters = this.reportConfig.filter_configs ?? [];
    const ignoreFilterKeys = this.reportConfig.ignore_filter_keys ?? [];
    const ignoreFilter = configFilters
      .filter((el) => el.hide_field)
      .map((el) => el.filter_column);

    const ignoreFilterKey = [...ignoreFilter, ...ignoreFilterKeys];

    const whereCondition = this.reportConfig?.whereCondition
      ? this.reportConfig.whereCondition(this.queryModel.filterModel)
      : [];

    const rowGroupCols = this.queryModel.rowGroupCols;
    const groupKeys = this.queryModel.groupKeys;
    const filterModel = this.queryModel.filterModel;

    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const thisSelf = this;

    const whereParts = [];
    if (groupKeys.length > 0) {
      groupKeys.forEach(function (key, index) {
        const colName = rowGroupCols[index].field;
        // whereParts.push(colName + ' = "' + key + '"');
        if (!key) {
          whereParts.push(`${thisSelf.findQueryConfig(colName)} is null`);
        } else {
          whereParts.push(`${thisSelf.findQueryConfig(colName)} = '${key}'`);
        }
      });
    }

    if (filterModel) {
      const keySet = Object.keys(filterModel);
      keySet.forEach(function (key) {
        if (!ignoreFilterKey.includes(key)) {
          const item = filterModel[key];
          const newKey = thisSelf.findQueryConfig(key);
          whereParts.push(thisSelf.createFilterSql(newKey, item));
        }
      });
    }

    // set default where conditions
    const defaultConditions = this.reportConfig.whereDefaultConditions;
    const defaultWhereOptions = [];
    if (defaultConditions) {
      defaultConditions.forEach((condition) => {
        defaultWhereOptions.push(
          this.createFilterSql(condition.column, {
            type: condition.filter_type,
            filter: condition.values,
          }),
        );
      });
    }

    const tableWhereConditions = [...whereCondition, ...whereParts].filter(
      Boolean,
    );

    const defaultWhereConditions = defaultWhereOptions.filter(Boolean);

    if (tableWhereConditions.length > 0) {
      return `WHERE (${
        defaultWhereConditions.length
          ? defaultWhereConditions?.filter(Boolean).join(' AND ') + ' AND '
          : ' '
      } ${tableWhereConditions.filter(Boolean).join(' AND ')})`;
    } else if (defaultWhereConditions.length) {
      return `WHERE (${defaultWhereConditions.filter(Boolean).join(' AND ')})`;
    } else {
      return '';
    }
  }
  // ==================================================================

  // GENERATE ORDER QUERY =============================================
  createOrderBySql() {
    const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';
    const defaultOrderBy = this.reportConfig.defaultOrderBy ?? [];
    const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? [];

    const rowGroupCols = this.queryModel.rowGroupCols;
    const groupKeys = this.queryModel.groupKeys;
    const sortModel = this.queryModel.sortModel;

    const grouping = this.isDoingGrouping(rowGroupCols, groupKeys);

    const sortParts = [];
    if (sortModel) {
      const groupColIds = rowGroupCols
        .map((groupCol) => groupCol.id)
        .slice(0, groupKeys.length + 1);

      sortModel.forEach(function (item) {
        if (grouping && groupColIds.indexOf(item.colId) < 0) {
          // ignore
        } else {
          sortParts.push(item.colId + ' ' + item.sort);
        }
      });
    }

    const defaultOrder = defaultOrderBy[0]
      ? defaultOrderBy
      : [`${mainTableAlias}.created_at DESC`];

    const lowLevelOrder = lowLevelOrderBy[0]
      ? lowLevelOrderBy
      : [`${mainTableAlias}.id DESC`];

    if (sortParts.length > 0) {
      if (rowGroupCols?.length > 0) {
        if (groupKeys.length > 0) {
          const sortBy = sortParts[groupKeys.length];
          if (sortBy) return ' ORDER BY ' + sortBy;
          // return '';
        }

        return ' ORDER BY ' + sortParts.join(', ');
      }
      return (
        ' ORDER BY ' + sortParts.join(', ') + `, ${lowLevelOrder.join(', ')}`
      );
    } else {
      if (rowGroupCols?.length > 0) {
        const sortGroupData = rowGroupCols[groupKeys.length];
        if (sortGroupData) return ' ORDER BY ' + `${sortGroupData['id']} DESC`;
        // return '';
      }

      return (
        ` ORDER BY ` + defaultOrder.join(', ') + `, ${lowLevelOrder.join(', ')}`
      );
    }
  }

  createOrderBySqlExport() {
    const mainTableAlias = this.reportConfig.main_table_alias ?? 'main';
    const defaultOrderBy = this.reportConfig.defaultOrderBy ?? [];
    const lowLevelOrderBy = this.reportConfig.lowLevelOrderBy ?? [];

    const rowGroupCols = this.queryModel.rowGroupCols;
    const sortModel = this.queryModel.sortModel;

    const defaultOrder = defaultOrderBy[0]
      ? defaultOrderBy
      : [`${mainTableAlias}.created_at DESC`];

    const lowLevelOrder = lowLevelOrderBy[0]
      ? lowLevelOrderBy
      : [`${mainTableAlias}.id DESC`];

    if (sortModel.length > 0) {
      const sortParts = sortModel.map((i) => `${i.colId} ${i.sort}`);
      return ' ORDER BY ' + `${sortParts.join(', ')}`;
    } else {
      if (rowGroupCols?.length > 0) {
        const sortParts = rowGroupCols.map((i) => `${i.id} DESC`);
        return ' ORDER BY ' + `${sortParts.join(', ')}`;
      }

      return (
        ` ORDER BY ` + defaultOrder.join(', ') + `, ${lowLevelOrder.join(', ')}`
      );
    }
  }
  // ==================================================================

  // GENERATE GROUP BY QUERY ==========================================
  createGroupBySql() {
    const rowGroupCols = this.queryModel.rowGroupCols;
    const groupKeys = this.queryModel.groupKeys;

    if (this.isDoingGrouping(rowGroupCols, groupKeys)) {
      const colsToGroupBy = [];

      const rowGroupCol = rowGroupCols[groupKeys.length];
      colsToGroupBy.push(this.findQueryConfig(rowGroupCol.field));

      return {
        groupByQuery: ' GROUP BY ' + colsToGroupBy.join(', '),
        groupByColumn: colsToGroupBy.join(', '),
      };
    } else {
      return {
        groupByQuery: '',
        groupByColumn: null,
      };
    }
  }

  createGroupBySqlExport() {
    const rowGroupCols = this.queryModel.rowGroupCols;

    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const thisSelf = this;

    if (rowGroupCols.length > 0) {
      const colsToGroupBy = [];
      rowGroupCols.forEach(function (rowGroupCol) {
        colsToGroupBy.push(thisSelf.findQueryConfig(rowGroupCol.field));
      });

      return {
        groupByQueryExport: ' GROUP BY ' + colsToGroupBy.join(', '),
        groupByColumnExport: colsToGroupBy.join(', '),
      };
    } else {
      return {
        groupByQueryExport: '',
        groupByColumnExport: null,
      };
    }
  }
  // ==================================================================
}
