import { ApiError } from '../../common/error/api-error';
import { ApiErrorCodes } from '../../common/error/api-error-codes';
import {
  AssetPropsSelection,
  AssetPropsSelectionBase,
  AssetPropsSelectionField,
  AssetPropsSelectionFieldObj,
  AssetPropsSelectionOrder,
} from './PropsSelection';
import {
  AssetPropWhere,
  AssetPropWhereAccount,
  AssetPropWhereAsset,
  AssetPropWhereCondition,
  AssetPropWhereEnum,
  AssetPropWhereFile,
  AssetPropWhereOp,
  AssetPropWhereOpKind,
  AssetPropWhereProp,
  AssetPropWhereTask,
  AssetPropWhereValue,
  getAssetPropWhereProp,
} from './PropsWhere';

function makeErrorValueExample(val: any) {
  let error_value = undefined;
  try {
    error_value = JSON.stringify(val);
  } catch (err: any) {
    error_value = 'example stringify failed: ' + err.message;
  }
  return {
    type: typeof val,
    value: error_value,
  };
}

function validateAssetPropsWhereConditionOp(
  obj: Record<string, any>,
): AssetPropWhereOp {
  if (!obj.op) {
    throw new ApiError(
      'Asset where: unexpected type of condition',
      ApiErrorCodes.PARAM_BAD_FORMAT,
      makeErrorValueExample(obj),
    );
  }
  if (obj.v === undefined) {
    throw new ApiError(
      'Asset where: value of condition not set',
      ApiErrorCodes.PARAM_BAD_FORMAT,
      makeErrorValueExample(obj),
    );
  } else {
    const obj_with_op = obj as AssetPropWhereOp;
    switch (obj_with_op.op) {
      // Равенство / не равенство
      // - допускаются любые значения и свойства
      case AssetPropWhereOpKind.EQUAL:
      case AssetPropWhereOpKind.EQUAL_NOT:
        if (!getAssetPropWhereProp(obj_with_op.v)) {
          validateAssetPropsWhereConditionValue(obj_with_op.v);
        }
        break;
      // - допускаются массив любых значений
      case AssetPropWhereOpKind.ANY:
      case AssetPropWhereOpKind.ANY_NOT:
        if (!Array.isArray(obj_with_op.v)) {
          throw new ApiError(
            'Asset where: unexpected type of op value',
            ApiErrorCodes.PARAM_BAD_FORMAT,
            makeErrorValueExample(obj),
          );
        }
        obj_with_op.v.forEach((v) => validateAssetPropsWhereConditionValue(v));
        break;
      // Больше / меньше:
      // - допускается либо строка, либо число, либо свойство
      case AssetPropWhereOpKind.LESS:
      case AssetPropWhereOpKind.LESS_EQUAL:
      case AssetPropWhereOpKind.MORE:
      case AssetPropWhereOpKind.MORE_EQUAL:
        if (!getAssetPropWhereProp(obj_with_op.v)) {
          if (
            typeof obj_with_op.v !== 'string' &&
            typeof obj_with_op.v !== 'number'
          ) {
            throw new ApiError(
              'Asset where: unexpected type of op value',
              ApiErrorCodes.PARAM_BAD_FORMAT,
              makeErrorValueExample(obj),
            );
          }
        }
        break;
      // Содержит подстроку
      // - допускается только строка
      case AssetPropWhereOpKind.LIKE:
      case AssetPropWhereOpKind.LIKE_NOT:
        if (typeof obj_with_op.v !== 'string') {
          throw new ApiError(
            'Asset where: unexpected type of op value',
            ApiErrorCodes.PARAM_BAD_FORMAT,
            makeErrorValueExample(obj),
          );
        }
        break;
      // Объединение условий через ИЛИ
      // - допускается массив подусловий
      case AssetPropWhereOpKind.OR:
      case AssetPropWhereOpKind.AND:
        if (!Array.isArray(obj_with_op.v)) {
          throw new ApiError(
            'Asset where: unexpected type of op value',
            ApiErrorCodes.PARAM_BAD_FORMAT,
            makeErrorValueExample(obj),
          );
        }
        obj_with_op.v.forEach((v) => validateAssetPropsWhere(v));
        break;
      // Проверка длины массива или строки
      // - допускается только число, либо свойство
      case AssetPropWhereOpKind.LEN_EQUAL:
      case AssetPropWhereOpKind.LEN_LESS:
      case AssetPropWhereOpKind.LEN_LESS_EQUAL:
      case AssetPropWhereOpKind.LEN_EQUAL_NOT:
      case AssetPropWhereOpKind.LEN_MORE:
      case AssetPropWhereOpKind.LEN_MORE_EQUAL:
        if (typeof obj_with_op.v !== 'number') {
          throw new ApiError(
            'Asset where: unexpected type of op value',
            ApiErrorCodes.PARAM_BAD_FORMAT,
            makeErrorValueExample(obj),
          );
        }
        break;
      // Проверка на пустоту
      // - допускается только лог. тип
      case AssetPropWhereOpKind.EMPTY:
      case AssetPropWhereOpKind.CHECKED:
        if (typeof obj_with_op.v !== 'boolean') {
          throw new ApiError(
            'Asset where: unexpected type of op value',
            ApiErrorCodes.PARAM_BAD_FORMAT,
            makeErrorValueExample(obj),
          );
        }
        break;
      // Проверка по регулярному выражению
      // - допускается только строка
      case AssetPropWhereOpKind.MATCH:
        if (typeof obj_with_op.v !== 'string') {
          throw new ApiError(
            'Asset where: unexpected type of op value',
            ApiErrorCodes.PARAM_BAD_FORMAT,
            makeErrorValueExample(obj),
          );
        }
        try {
          new RegExp(obj_with_op.v);
        } catch (err) {
          throw new ApiError(
            'Asset where: bad regexp',
            ApiErrorCodes.PARAM_BAD_FORMAT,
            {
              ...makeErrorValueExample(obj),
              regexpError: err.message,
            },
          );
        }

        break;

      default:
        throw new ApiError(
          'Asset where: unexpected type of op',
          ApiErrorCodes.PARAM_BAD_FORMAT,
          makeErrorValueExample(obj),
        );
    }
    return obj_with_op;
  }
}

function validateAssetPropsWhereConditionValue(val: any): AssetPropWhereValue {
  if (val === null || val === undefined) return null;
  else if (typeof val === 'string') return val;
  else if (typeof val === 'boolean') return val;
  else if (typeof val === 'number') return val;
  else if (typeof val === 'object' && (val as AssetPropWhereFile).fileId) {
    return val;
  } else if (
    typeof val === 'object' &&
    (val as AssetPropWhereAccount).accountId
  ) {
    return val;
  } else if (typeof val === 'object' && (val as AssetPropWhereTask).taskNum) {
    return val;
  } else if (typeof val === 'object' && (val as AssetPropWhereAsset).assetId) {
    return val;
  } else if (
    typeof val === 'object' &&
    (val as AssetPropWhereEnum).enum &&
    (val as AssetPropWhereEnum).name
  ) {
    return val;
  } else {
    throw new ApiError(
      'Asset where: unexpected type of value',
      ApiErrorCodes.PARAM_BAD_FORMAT,
      makeErrorValueExample(val),
    );
  }
}

function validateAssetPropsWhereCondition(cond: any): AssetPropWhereCondition {
  if (cond === null || cond === undefined) return null;
  else if (Array.isArray(cond)) {
    return cond.map((c) => validateAssetPropsWhereConditionValue(c));
  } else if (typeof cond === 'object' && cond.op) {
    return validateAssetPropsWhereConditionOp(cond);
  } else return validateAssetPropsWhereConditionValue(cond);
}

export function validateAssetPropsWhere(obj: any): AssetPropWhere {
  if (!obj) {
    return {};
  }
  if (typeof obj !== 'object') {
    throw new ApiError(
      'Asset where: where must be object',
      ApiErrorCodes.PARAM_BAD_FORMAT,
      makeErrorValueExample(obj),
    );
  }
  const res: AssetPropWhere = {};
  for (const [prop, val] of Object.entries(obj)) {
    res[prop] = validateAssetPropsWhereCondition(val);
  }
  return res;
}

export function validateAssetPropsSelectionField(
  prop: any,
): AssetPropsSelectionField {
  if (typeof prop === 'string') {
    return prop;
  } else if (typeof prop === 'object') {
    if (!prop.prop || typeof prop.prop !== 'string') {
      throw new ApiError(
        'Asset prop: wrong prop',
        ApiErrorCodes.PARAM_BAD_FORMAT,
        makeErrorValueExample(prop),
      );
    }
    const field_as_obj = prop as AssetPropsSelectionFieldObj;
    if (field_as_obj.as && typeof field_as_obj.as !== 'string') {
      throw new ApiError(
        'Asset prop: wrong prop as',
        ApiErrorCodes.PARAM_BAD_FORMAT,
        makeErrorValueExample(prop),
      );
    }
    if (field_as_obj.func && typeof field_as_obj.func !== 'string') {
      throw new ApiError(
        'Asset prop: wrong prop func',
        ApiErrorCodes.PARAM_BAD_FORMAT,
        makeErrorValueExample(prop),
      );
    }
    return field_as_obj;
  } else {
    throw new ApiError(
      'Asset prop: wrong type',
      ApiErrorCodes.PARAM_BAD_FORMAT,
      makeErrorValueExample(prop),
    );
  }
}

export function validateAssetPropsSelectionFieldOrder(
  prop: any,
): AssetPropsSelectionField {
  const validated = validateAssetPropsSelectionField(
    prop,
  ) as AssetPropsSelectionOrder;
  if (
    validated &&
    typeof validated === 'object' &&
    validated.desc &&
    typeof validated.desc !== 'boolean'
  ) {
    throw new ApiError(
      'Asset prop: wrong prop desc',
      ApiErrorCodes.PARAM_BAD_FORMAT,
      makeErrorValueExample(prop),
    );
  }
  return validated;
}

export function validateAssetPropsSelectionBase(
  obj: Record<string, any>,
): AssetPropsSelectionBase {
  const res: AssetPropsSelectionBase = {};
  if (obj.where) {
    res.where = validateAssetPropsWhere(obj.where);
  }
  if (obj.group) {
    if (!Array.isArray(obj.group)) {
      throw new ApiError(
        'Asset group: must be array',
        ApiErrorCodes.PARAM_BAD_FORMAT,
      );
    }
    res.group = obj.group.map((g) => validateAssetPropsSelectionField(g));
  }
  if (obj.order) {
    if (!Array.isArray(obj.order)) {
      throw new ApiError(
        'Asset order: must be array',
        ApiErrorCodes.PARAM_BAD_FORMAT,
      );
    }
    res.order = obj.order.map((g) => validateAssetPropsSelectionFieldOrder(g));
  }
  if (obj.offset !== undefined && obj.offset !== null) {
    let offset = obj.offset;
    if (typeof offset === 'string') {
      offset = parseInt(offset);
      if (isNaN(offset)) {
        throw new ApiError(
          'Asset offset: wrong type',
          ApiErrorCodes.PARAM_BAD_FORMAT,
        );
      }
    }
    if (typeof offset !== 'number') {
      throw new ApiError(
        'Asset offset: wrong type',
        ApiErrorCodes.PARAM_BAD_FORMAT,
      );
    }
    if (offset < 0) {
      throw new ApiError(
        'Asset offset: must be non-negative',
        ApiErrorCodes.PARAM_BAD_FORMAT,
      );
    }
    res.offset = offset;
  }
  if (obj.count !== undefined && obj.count !== null) {
    let count = obj.count;
    if (typeof count === 'string') {
      count = parseInt(count);
      if (isNaN(count)) {
        throw new ApiError(
          'Asset count: wrong type',
          ApiErrorCodes.PARAM_BAD_FORMAT,
        );
      }
    }
    if (typeof count !== 'number') {
      throw new ApiError(
        'Asset count: wrong type',
        ApiErrorCodes.PARAM_BAD_FORMAT,
      );
    }
    if (count < 0) {
      throw new ApiError(
        'Asset count: must be non-negative',
        ApiErrorCodes.PARAM_BAD_FORMAT,
      );
    }
    res.count = count;
  }
  return res;
}

export function validateAssetPropsSelection(
  obj: Record<string, any>,
): AssetPropsSelection {
  const base = validateAssetPropsSelectionBase(obj);
  if (!obj.select) {
    throw new ApiError(
      'Asset selection: select is not set',
      ApiErrorCodes.PARAM_BAD_FORMAT,
    );
  }
  if (!Array.isArray(obj.select)) {
    throw new ApiError(
      'Asset selection: select must be array',
      ApiErrorCodes.PARAM_BAD_FORMAT,
    );
  }
  return {
    ...base,
    select: obj.select.map((s) => validateAssetPropsSelectionField(s)),
  };
}
