import { ApiError } from '../../../common/error/api-error';
import { ApiErrorCodes } from '../../../common/error/api-error-codes';
import { decodeBigNumberKey } from '../../../utils/big-number-key';
import { escapeLiteral } from 'pg';
import {
  AssetSelectorQueryBuilding,
  AssetSelectorQueryFieldSelector,
} from '../AssetSelectorQuery';
import { AssetSelectorQueryFieldBase } from '../AssetSelectorQueryFieldBase';
import {
  AssetPropKeyStruct,
  AssetPropType,
  AssetPropValueAccount,
  AssetPropValueAsset,
  AssetPropValueEnum,
  AssetPropValueFile,
  AssetPropValueTask,
  getAssetPropType,
  parseAssetBlockPropKeyRef,
  splitPropParts,
  stringifyAssetBlockRef,
} from '../Props';
import {
  AssetPropWhereOp,
  getAssetPropWhereProp,
  AssetPropWhereOpKind,
  AssetPropWhereValue,
  AssetPropWhereOpAny,
  AssetPropWhereOpLen,
  AssetPropWhereOpLike,
  AssetPropWhereOpMatch,
} from '../PropsWhere';
import { WhereExpressionBuilder } from 'typeorm';

export class AssetSelectorQueryFieldBlock extends AssetSelectorQueryFieldBase {
  parsedProp: AssetPropKeyStruct;
  blockKey: string;
  blockPropParts: (string | number)[];
  constructor(blockkeyprop: string) {
    super(blockkeyprop);

    try {
      this.parsedProp = parseAssetBlockPropKeyRef(blockkeyprop);

      const split = splitPropParts(this.parsedProp.propKey);
      const prop_parts: (string | number)[] = [];
      for (let s = 0; s < split.length; s++) {
        if (/^\d+$/.test(split[s])) {
          prop_parts.push(parseInt(split[s]));
        } else {
          if (
            prop_parts.length === 0 ||
            typeof prop_parts[prop_parts.length - 1] === 'number'
          ) {
            prop_parts.push(split[s]);
          } else {
            prop_parts[prop_parts.length - 1] += '\\' + split[s];
          }
        }
      }
      this.blockPropParts = prop_parts;

      this.blockKey = stringifyAssetBlockRef(
        this.parsedProp.blockName,
        this.parsedProp.blockTitle,
        this.parsedProp.blockType,
      );
    } catch (err: any) {
      throw new ApiError('Invalid prop', ApiErrorCodes.PARAM_BAD_VALUE, {
        prop: blockkeyprop,
        error: err.message,
      });
    }
  }

  private _joinBlock(qb: AssetSelectorQueryBuilding): string {
    let alias = qb.joinedTableMarks.get(`block:${this.blockKey}`);
    if (alias == undefined) {
      const alias_num = ++qb.namesCounter;
      qb.query.leftJoin(
        'asset_block_keys',
        `block_keys_${alias_num}`,
        `
          block_keys_${alias_num}.project_id = :project_id_${alias_num} AND
          block_keys_${alias_num}.block_name = :block_name_${alias_num} AND
          block_keys_${alias_num}.block_title = :block_title_${alias_num} AND
          block_keys_${alias_num}.block_type = :block_type_${alias_num}
        `,
        {
          [`project_id_${alias_num}`]: decodeBigNumberKey(qb.context.projectId),
          [`block_name_${alias_num}`]: this.parsedProp.blockName,
          [`block_title_${alias_num}`]: this.parsedProp.blockTitle,
          [`block_type_${alias_num}`]: this.parsedProp.blockType,
        },
      );
      qb.query.leftJoin(
        'asset_block_comps',
        `block_comps_${alias_num}`,
        `
          block_comps_${alias_num}.block_key = block_keys_${alias_num}.block_key AND
          block_comps_${alias_num}.project_id = block_keys_${alias_num}.project_id AND
          block_comps_${alias_num}.asset_id = a.id
        `,
      );
      alias = `block_comps_${alias_num}`;
      qb.joinedTableMarks.set(`block:${this.blockKey}`, alias);
    }
    return alias;
  }

  requestProp(qb: AssetSelectorQueryBuilding): string {
    const alias = this._joinBlock(qb);
    let ref = ``;
    for (let i = 0; i < this.blockPropParts.length; i++) {
      const part = this.blockPropParts[i];
      if (typeof part === 'number') {
        ref = `${
          ref ? ref : escapeLiteral('')
        } || '\\' || (${alias}.computed_props->(${
          ref ? ref : escapeLiteral('')
        })->>${part})`;
      } else {
        ref = (ref ? `${ref} || '\\' || ` : '') + ` ${escapeLiteral(part)}`;
      }
    }
    return `${alias}.computed_props->(${ref})`;
  }

  selectProp(
    qb: AssetSelectorQueryBuilding,
    as: string,
  ): AssetSelectorQueryFieldSelector {
    const alias = this._joinBlock(qb);
    return {
      ref: `${alias}.computed_props`,
      as: `${alias}_props`,
      reader: (row, res) => {
        const props = row[alias + '_props'];
        if (!props) {
          res[as] = null;
          return;
        }
        let ref = ``;
        let empty = false;
        for (let i = this.blockPropParts.length - 1; i >= 0; i--) {
          const part = this.blockPropParts[i];
          if (typeof part === 'number') {
            if (!props.hasOwnProperty(ref)) {
              empty = true;
              break;
            }
            const arr = props[ref];
            if (!Array.isArray(arr) || arr.length <= part) {
              empty = true;
              break;
            }
            ref = (ref ? `${ref}\\` : '') + arr[part];
          } else {
            ref = (ref ? `${ref}\\` : '') + part;
          }
        }
        if (!empty) {
          empty = true;
          for (const [prop, value] of Object.entries(props)) {
            if (prop === ref) {
              res[as] = value as any;
              empty = false;
            } else if (prop.startsWith(ref + '\\')) {
              res[as + prop.substring(ref.length)] = value as any;
              empty = false;
            }
          }
        }
        if (empty) res[as] = null;
      },
    };
  }

  where(
    qb: AssetSelectorQueryBuilding,
    qwhere: WhereExpressionBuilder,
    cond_op: AssetPropWhereOp,
  ) {
    const oprnd_prop = getAssetPropWhereProp(cond_op.v as any);
    if (!oprnd_prop) {
      switch (cond_op.op) {
        case AssetPropWhereOpKind.EQUAL:
        case AssetPropWhereOpKind.EQUAL_NOT:
        case AssetPropWhereOpKind.LESS:
        case AssetPropWhereOpKind.LESS_EQUAL:
        case AssetPropWhereOpKind.MORE:
        case AssetPropWhereOpKind.MORE_EQUAL:
        case AssetPropWhereOpKind.ANY:
        case AssetPropWhereOpKind.ANY_NOT: {
          const self_ref = qb.getPropField(this.name).requestProp(qb);
          let op_type: AssetPropType;
          if (Array.isArray(cond_op.v)) {
            op_type = AssetPropType.NULL;
            if (cond_op.v.length > 0) {
              op_type = getAssetPropType(cond_op.v[0] as any);
              for (let i = 1; i < cond_op.v.length; i++) {
                const op_type_next = getAssetPropType(cond_op.v[1] as any);
                if (op_type !== op_type_next) {
                  throw new ApiError(
                    'Cannot have array paramenter with different types',
                    ApiErrorCodes.PARAM_BAD_VALUE,
                  );
                }
              }
            }
          } else op_type = getAssetPropType(cond_op.v as any);

          const add_cur_condition = (self_ref_upd, valfn?: (x: any) => any) => {
            const param_num = ++qb.namesCounter;
            switch (cond_op.op) {
              case AssetPropWhereOpKind.EQUAL:
                qwhere.andWhere(`${self_ref_upd} = :val${param_num}`, {
                  [`val${param_num}`]: valfn ? valfn(cond_op.v) : cond_op.v,
                });
                break;
              case AssetPropWhereOpKind.EQUAL_NOT:
                qwhere.andWhere(`${self_ref_upd} <> :val${param_num}`, {
                  [`val${param_num}`]: valfn ? valfn(cond_op.v) : cond_op.v,
                });
                break;
              case AssetPropWhereOpKind.LESS:
                qwhere.andWhere(`${self_ref_upd} < :val${param_num}`, {
                  [`val${param_num}`]: valfn ? valfn(cond_op.v) : cond_op.v,
                });
                break;
              case AssetPropWhereOpKind.LESS_EQUAL:
                qwhere.andWhere(`${self_ref_upd} <= :val${param_num}`, {
                  [`val${param_num}`]: valfn ? valfn(cond_op.v) : cond_op.v,
                });
                break;
              case AssetPropWhereOpKind.MORE:
                qwhere.andWhere(`${self_ref_upd} > :val${param_num}`, {
                  [`val${param_num}`]: valfn ? valfn(cond_op.v) : cond_op.v,
                });
                break;
              case AssetPropWhereOpKind.MORE_EQUAL:
                qwhere.andWhere(`${self_ref_upd} >= :val${param_num}`, {
                  [`val${param_num}`]: valfn ? valfn(cond_op.v) : cond_op.v,
                });
                break;
              case AssetPropWhereOpKind.ANY:
                qwhere.andWhere(`${self_ref_upd} = ANY(:val${param_num})`, {
                  [`val${param_num}`]: valfn
                    ? cond_op.v.map((v) => valfn(v))
                    : cond_op.v,
                });
                break;
              case AssetPropWhereOpKind.ANY_NOT:
                qwhere.andWhere(`${self_ref_upd} <> ANY(:val${param_num})`, {
                  [`val${param_num}`]: valfn
                    ? cond_op.v.map((v) => valfn(v))
                    : cond_op.v,
                });
                break;
            }
          };

          switch (op_type) {
            case AssetPropType.NULL:
              if (
                cond_op.op === AssetPropWhereOpKind.EQUAL ||
                cond_op.op === AssetPropWhereOpKind.ANY_NOT
              ) {
                qwhere.andWhere(
                  `(${self_ref} IS NULL OR ${self_ref} = 'null'::jsonb)`,
                );
              } else {
                qwhere.andWhere(
                  `(${self_ref} IS NOT NULL AND ${self_ref} <> 'null'::jsonb)`,
                );
              }
              break;
            case AssetPropType.NUMBER:
              add_cur_condition(`imc_to_double(${self_ref})`);
              break;
            case AssetPropType.STRING:
              add_cur_condition(`imc_to_string(${self_ref})`);
              break;
            case AssetPropType.BOOLEAN:
              add_cur_condition(`imc_to_boolean(${self_ref})`);
              break;
            case AssetPropType.ASSET:
              add_cur_condition(
                `(${self_ref}->>'assetId')::uuid`,
                (x: AssetPropValueAsset) => x.assetId,
              );
              break;
            case AssetPropType.ACCOUNT:
              add_cur_condition(
                `(${self_ref}->>'accountId')::integer`,
                (x: AssetPropValueAccount) => x.accountId,
              );
              break;
            case AssetPropType.FILE:
              add_cur_condition(
                `(${self_ref}->>'fileId')::uuid`,
                (x: AssetPropValueFile) => x.fileId,
              );
              break;
            case AssetPropType.TASK:
              add_cur_condition(
                `(${self_ref}->>'taskNum')::integer`,
                (x: AssetPropValueTask) => x.taskNum,
              );
              break;
            case AssetPropType.ENUM:
              add_cur_condition(
                `(${self_ref}->>'name')::varchar`,
                (x: AssetPropValueEnum) => x.name,
              );
              break;
            default:
              throw new ApiError(
                'Parameter with specified type is not supported',
                ApiErrorCodes.PARAM_BAD_VALUE,
              );
          }

          return;
        }
      }
    }

    super.where(qb, qwhere, cond_op);
  }
}
