import { BN, fromTokenToUnit } from '@ocap/util';
import Joi from 'joi';
import { Op } from 'sequelize';
import SqlWhereParser from 'sql-where-parser';
import type { OrderDirection, OrderInput, OrderItem } from '../store/models';

const parser = new SqlWhereParser();

export function handleOperator(operator: string, operands: any[]): any {
  switch (operator) {
    case 'AND':
    case 'OR':
      return handleCondition({ [operator]: operands });
    case 'IN':
      if (operands[0].NOT) {
        return { [operands[0].NOT]: { [Op.notIn]: operands[1] } };
      }
      return { [operands[0]]: { [Op.in]: operands[1] } };
    case 'LIKE':
      if (operands[0].NOT) {
        return { [operands[0].NOT]: { [Op.notLike]: operands[1] } };
      }
      return { [operands[0]]: { [Op.like]: operands[1] } };
    case 'BETWEEN':
      if (operands[0].NOT) {
        return { [operands[0].NOT]: { [Op.notBetween]: [operands[1], operands[2]] } };
      }
      return { [operands[0]]: { [Op.between]: [operands[1], operands[2]] } };
    case 'IS':
      if (operands[1] && operands[1].NOT && Array.isArray(operands[1].NOT) && operands[1].NOT.length) {
        return { [operands[0]]: { [Op.ne]: operands[1].NOT[0] } };
      }
      return { [operands[0]]: operands[1] };
    case '=':
      return { [operands[0]]: operands[1] };
    case '!=':
      return { [operands[0]]: { [Op.ne]: operands[1] } };
    case '>':
      return { [operands[0]]: { [Op.gt]: operands[1] } };
    case '>=':
      return { [operands[0]]: { [Op.gte]: operands[1] } };
    case '<':
      return { [operands[0]]: { [Op.lt]: operands[1] } };
    case '<=':
      return { [operands[0]]: { [Op.lte]: operands[1] } };
    default:
      console.error('Invalid operator', { operator, operands });
      throw new Error('Operator not supported when parse query');
  }
}

export function handleItem(item: Record<string, any>) {
  const operator = Object.keys(item)[0] as string;
  const operands = item[operator];
  return handleOperator(operator, operands);
}

export function handleCondition(condition: any) {
  if (condition.AND) {
    return { [Op.and]: condition.AND.map(handleItem) };
  }
  if (condition.OR) {
    return { [Op.or]: condition.OR.map(handleItem) };
  }
  return handleItem(condition);
}

export function getWhereFromQuery(query: string) {
  if (!query) {
    throw new Error('Can not parse empty query');
  }

  const parsed = parser.parse(query);
  return handleCondition(parsed);
}

export const getWhereFromKvQuery = (query?: string) => {
  if (!query) {
    return {};
  }
  const out: any = {};
  const likes: any = [];
  const fn = (kv: string) => {
    const [k, v] = kv.split(':');
    if (k && !v && !k.includes('like')) {
      out[k as string] = {
        [Op.in]: [],
      };
    }
    if (v) {
      let value = decodeURIComponent(v).replace('+', ' ');
      if (value.includes(',')) {
        value = {
          [Op.in]: v.split(','),
        } as any;
        out[k as string] = value;
      } else if ((k as any).includes('like')) {
        const kk = k?.slice(5);
        value = { [kk as any]: { [Op.like]: `%${v}%` } } as any;
        likes.push(value);
      } else {
        out[k as string] = value;
      }
    }
  };
  query.split(' ').forEach(fn);
  if (likes.length) {
    out[Op.or] = likes;
  }
  return out;
};

export function normalizeOrder(
  order: OrderInput | undefined,
  defaultOrder: OrderItem[] = [['created_at', 'DESC']]
): OrderItem[] {
  if (!order) return defaultOrder;

  let normalizedOrder: OrderItem[];

  if (typeof order === 'string') {
    const [field, direction] = order.split(':');
    normalizedOrder = [[field || '', (direction?.toUpperCase() as OrderDirection) || 'ASC']];
  } else if (Array.isArray(order)) {
    if (order.length === 0) return defaultOrder;

    if (Array.isArray(order[0])) {
      normalizedOrder = order.map(([field, direction]) => [
        field || '',
        (direction?.toUpperCase() as OrderDirection) || 'ASC',
      ]);
    } else {
      normalizedOrder = order.map((item) => {
        const [field, direction] = (item as string).split(':');
        return [field || '', (direction?.toUpperCase() as OrderDirection) || 'ASC'];
      });
    }
  } else {
    return defaultOrder;
  }

  // merge default order with normalized order
  const orderFields = new Set(normalizedOrder.map(([field]) => field));
  const remainingDefaultOrders = defaultOrder.filter(([field]) => !orderFields.has(field));

  return [...normalizedOrder, ...remainingDefaultOrders];
}

export function getOrder(query: Record<string, any>, defaultOrder?: OrderItem[]): OrderItem[] {
  if (query.order) {
    return normalizeOrder(query.order, defaultOrder);
  }

  if (query.o) {
    const direction = query.o.toUpperCase() as OrderDirection;
    return defaultOrder?.map(([field]) => [field, direction]) || [['created_at', direction]];
  }

  return defaultOrder || [['created_at', 'DESC']];
}

const fieldRegex = /^[\w.]+$/i;
const directionRegex = /(asc|desc)$/i;

const ORDER_ERROR_MESSAGES = {
  FIELD_PATTERN: 'Field name can only contain letters, numbers, dots and underscores',
  DIRECTION_PATTERN: 'Direction must be either "asc" or "desc"',
  SEPARATOR: 'Must use ":" to separate field and direction',
  TUPLE_LENGTH: 'Each tuple must contain exactly 2 elements',
  ARRAY_TYPE: 'Each item must be an array',
  INVALID_FORMAT:
    'Invalid order format. Must be one of:\n' +
    '- Single field: "field:direction"\n' +
    '- Multiple fields: ["field1:asc", "field2:desc"]\n' +
    '- Tuple format: [["field1", "ASC"], ["field2", "DESC"]]\n' +
    'where field contains only letters, numbers, dots and underscores, ' +
    'and direction is either "asc" or "desc" (case insensitive)',
};

const validateField = (field: any): boolean => typeof field === 'string' && fieldRegex.test(field);

const validateDirection = (direction: any): boolean => typeof direction === 'string' && directionRegex.test(direction);

// Function to validate the order parameter
export const validateOrderInput = (value: any, helpers: any) => {
  // If it does not exist or is null, skip validation
  if (value == null) return value;

  // String format: "field:direction"
  if (typeof value === 'string') {
    const parts = value.split(':');

    if (parts.length !== 2) {
      return helpers.message(ORDER_ERROR_MESSAGES.SEPARATOR);
    }

    const [field, direction] = parts;

    if (!validateField(field)) {
      return helpers.message(ORDER_ERROR_MESSAGES.FIELD_PATTERN);
    }

    if (!validateDirection(direction)) {
      return helpers.message(ORDER_ERROR_MESSAGES.DIRECTION_PATTERN);
    }

    return value;
  }

  // Array format
  if (Array.isArray(value)) {
    // Empty array, return directly
    if (value.length === 0) return value;

    // String array format
    if (typeof value[0] === 'string') {
      const invalidItem = value.find((item) => typeof item !== 'string');
      if (invalidItem !== undefined) {
        return helpers.message(ORDER_ERROR_MESSAGES.INVALID_FORMAT);
      }

      for (const item of value) {
        const parts = item.split(':');

        if (parts.length !== 2) {
          return helpers.message(ORDER_ERROR_MESSAGES.SEPARATOR);
        }

        const [field, direction] = parts;

        if (!validateField(field)) {
          return helpers.message(ORDER_ERROR_MESSAGES.FIELD_PATTERN);
        }

        if (!validateDirection(direction)) {
          return helpers.message(ORDER_ERROR_MESSAGES.DIRECTION_PATTERN);
        }
      }

      return value;
    }

    // Multiple tuples format
    if (Array.isArray(value[0])) {
      for (const tuple of value) {
        if (!Array.isArray(tuple)) {
          return helpers.message(ORDER_ERROR_MESSAGES.ARRAY_TYPE);
        }

        if (tuple.length !== 2) {
          return helpers.message(ORDER_ERROR_MESSAGES.TUPLE_LENGTH);
        }

        const [field, direction] = tuple;

        if (!validateField(field)) {
          return helpers.message(ORDER_ERROR_MESSAGES.FIELD_PATTERN);
        }

        if (!validateDirection(direction)) {
          return helpers.message(ORDER_ERROR_MESSAGES.DIRECTION_PATTERN);
        }
      }

      return value;
    }
  }
  return helpers.message(ORDER_ERROR_MESSAGES.INVALID_FORMAT);
};

export function createListParamSchema<T>(schema: any, pageSize: number = 20) {
  return Joi.object<
    T & {
      page: number;
      pageSize: number;
      livemode?: boolean;
      q?: string;
      o?: string;
      order?: OrderInput;
    }
  >({
    page: Joi.number()
      .integer()
      .default(1)
      .custom((value) => (value < 1 ? 1 : value), 'page should be valid'),
    pageSize: Joi.number()
      .integer()
      .default(pageSize)
      .custom((value) => {
        if (value > 100) return 100;
        if (value < 1) return 1;
        return value;
      }, 'pageSize should be valid'),
    livemode: Joi.boolean().empty(''),
    q: Joi.string().empty(''),
    o: Joi.string().valid('asc', 'desc').insensitive().empty(''),
    order: Joi.any().custom(validateOrderInput).optional(),
    ...schema,
  });
}

export const BNPositiveValidator = Joi.string().custom((value, helpers) => {
  if (!/^\d+(\.\d+)?$/.test(value)) {
    return helpers.error('any.invalid');
  }
  try {
    const valueInWei = fromTokenToUnit(value, 18).toString();
    const bnValue = new BN(valueInWei);
    if (bnValue.isNeg() || bnValue.isZero()) {
      return helpers.error('any.invalid');
    }
  } catch (error) {
    return helpers.error('any.invalid');
  }
  return value;
}, 'BN Positive Validation');

function validateMetadataValue(value: any, helpers: any) {
  if (value && typeof value === 'string' && value.length > 256) {
    return helpers.message({ custom: 'Metadata value should be less than 256 characters' });
  }
  return value;
}
export const MetadataSchema = Joi.alternatives()
  .try(
    Joi.object()
      .pattern(Joi.string().max(40), Joi.any().custom(validateMetadataValue, 'Custom Validation'))
      .min(0)
      .allow(null),
    Joi.array()
      .items(
        Joi.object({
          key: Joi.string().max(40).required(),
          value: Joi.any().custom(validateMetadataValue, 'Custom Validation').required(),
        })
      )
      .min(0)
  )
  .optional();
