// Sequelize Op → SQL WHERE clause translation

export const Op = {
  eq: Symbol('eq'),
  ne: Symbol('ne'),
  gt: Symbol('gt'),
  gte: Symbol('gte'),
  lt: Symbol('lt'),
  lte: Symbol('lte'),
  in: Symbol('in'),
  notIn: Symbol('notIn'),
  like: Symbol('like'),
  notLike: Symbol('notLike'),
  or: Symbol('or'),
  and: Symbol('and'),
  not: Symbol('not'),
  between: Symbol('between'),
  notBetween: Symbol('notBetween'),
  is: Symbol('is'),
  overlap: Symbol('overlap'),
  contains: Symbol('contains'),
  startsWith: Symbol('startsWith'),
  endsWith: Symbol('endsWith'),
  substring: Symbol('substring'),
  col: Symbol('col'),
};

const OP_MAP = new Map<symbol, string>([
  [Op.eq, '='],
  [Op.ne, '!='],
  [Op.gt, '>'],
  [Op.gte, '>='],
  [Op.lt, '<'],
  [Op.lte, '<='],
  [Op.like, 'LIKE'],
  [Op.notLike, 'NOT LIKE'],
  [Op.is, 'IS'],
]);

const SYM_SET = new Set<symbol>(Object.values(Op) as symbol[]);

function isOpSymbol(s: symbol): boolean {
  return SYM_SET.has(s);
}

// Inline helper to convert literal/fn/col to SQL — avoids circular import with helpers.ts
function helperToSQLInline(value: any): string {
  if (!value || typeof value !== 'object') return String(value);
  if (value.__literal) return value.sql;
  if (value.__col) {
    if (value.name.includes('.')) return `"${value.name.replace('.', '"."')}"`;
    return `"${value.name}"`;
  }
  if (value.__fn) {
    const args = value.args.map((a: any) => {
      if (a === null || a === undefined) return 'NULL';
      if (typeof a === 'string') return `"${a}"`;
      if (typeof a === 'number') return String(a);
      return helperToSQLInline(a);
    });
    return `${value.name}(${args.join(', ')})`;
  }
  return String(value);
}

// Convert a dotted field path like 'metadata.stripe_id' to SQLite json_extract expression
function fieldToSQL(field: string): string {
  if (field.includes('.')) {
    const parts = field.split('.');
    const column = parts[0];
    const jsonPath = '$.' + parts.slice(1).join('.');
    return `json_extract("${column}", '${jsonPath}')`;
  }
  return `"${field}"`;
}

// Handle a single operator condition for a field
function buildOpCondition(
  fieldSQL: string,
  sym: symbol,
  opValue: any,
  conditions: string[],
  values: any[],
): void {
  if (sym === Op.in) {
    const arr = opValue as any[];
    if (arr.length === 0) {
      conditions.push('1=0');
    } else {
      const placeholders = arr.map(() => '?').join(', ');
      conditions.push(`${fieldSQL} IN (${placeholders})`);
      values.push(...arr);
    }
  } else if (sym === Op.notIn) {
    const arr = opValue as any[];
    if (arr.length === 0) {
      conditions.push('1=1');
    } else {
      const placeholders = arr.map(() => '?').join(', ');
      conditions.push(`${fieldSQL} NOT IN (${placeholders})`);
      values.push(...arr);
    }
  } else if (sym === Op.between) {
    conditions.push(`${fieldSQL} BETWEEN ? AND ?`);
    values.push(opValue[0], opValue[1]);
  } else if (sym === Op.notBetween) {
    conditions.push(`${fieldSQL} NOT BETWEEN ? AND ?`);
    values.push(opValue[0], opValue[1]);
  } else if (sym === Op.not) {
    if (opValue === null) {
      conditions.push(`${fieldSQL} IS NOT NULL`);
    } else {
      conditions.push(`${fieldSQL} != ?`);
      values.push(opValue);
    }
  } else if (sym === Op.is) {
    if (opValue === null) {
      conditions.push(`${fieldSQL} IS NULL`);
    } else {
      conditions.push(`${fieldSQL} IS ?`);
      values.push(opValue);
    }
  } else if (sym === Op.or) {
    const orParts = (opValue as any[]).map(() => `${fieldSQL} = ?`);
    conditions.push(`(${orParts.join(' OR ')})`);
    values.push(...(opValue as any[]));
  } else if (sym === Op.contains) {
    // JSON array contains check using SQLite json_each
    const searchVal = Array.isArray(opValue) ? opValue[0] : opValue;
    conditions.push(`EXISTS (SELECT 1 FROM json_each(${fieldSQL}) WHERE value = ?)`);
    values.push(searchVal);
  } else {
    const sqlOp = OP_MAP.get(sym);
    if (sqlOp) {
      conditions.push(`${fieldSQL} ${sqlOp} ?`);
      values.push(opValue);
    } else {
      conditions.push(`${fieldSQL} = ?`);
      values.push(opValue);
    }
  }
}

// sequelizeWhere: wraps Sequelize.where(lhs, rhs) calls
export function sequelizeWhere(lhs: any, rhs: any): any {
  return { __sequelizeWhere: true, lhs, rhs };
}

export function buildWhereClause(where: any): { sql: string; values: any[] } {
  if (!where) return { sql: '', values: [] };

  const conditions: string[] = [];
  const values: any[] = [];

  // Special case: Sequelize.where(literal(...), value)
  if (where.__sequelizeWhere) {
    const { lhs, rhs } = where;
    const lhsSQL = typeof lhs === 'object' ? helperToSQLInline(lhs) : `"${lhs}"`;
    conditions.push(`${lhsSQL} = ?`);
    values.push(rhs);
    return { sql: conditions.join(' AND '), values };
  }

  // Array case
  if (Array.isArray(where)) {
    const parts = where.map((item) => {
      const sub = buildWhereClause(item);
      values.push(...sub.values);
      return sub.sql ? `(${sub.sql})` : '1=1';
    });
    return { sql: parts.join(' AND '), values };
  }

  // Collect string keys
  const stringEntries: [string, any][] = Object.entries(where);
  // Collect Symbol keys — critical for { [Op.or]: [...] } top-level usage
  const symbolKeys: symbol[] = Object.getOwnPropertySymbols(where);

  for (const [key, value] of stringEntries) {
    processWhereEntry(key, value, conditions, values);
  }

  for (const sym of symbolKeys) {
    if (!isOpSymbol(sym)) continue;
    const value = (where as any)[sym];

    if (sym === Op.or) {
      const items = Array.isArray(value) ? value : [value];
      const orParts = items.map((v) => {
        const sub = buildWhereClause(v);
        values.push(...sub.values);
        return sub.sql ? `(${sub.sql})` : '1=1';
      });
      if (orParts.length > 0) conditions.push(`(${orParts.join(' OR ')})`);
    } else if (sym === Op.and) {
      const items = Array.isArray(value) ? value : [value];
      const andParts = items.map((v) => {
        if (v && v.__sequelizeWhere) {
          const sub = buildWhereClause(v);
          values.push(...sub.values);
          return sub.sql ? `(${sub.sql})` : '1=1';
        }
        if (v && v.__literal) {
          return `(${v.sql})`;
        }
        const sub = buildWhereClause(v);
        values.push(...sub.values);
        return sub.sql ? `(${sub.sql})` : '1=1';
      });
      if (andParts.length > 0) conditions.push(`(${andParts.join(' AND ')})`);
    }
  }

  return {
    sql: conditions.length > 0 ? conditions.join(' AND ') : '',
    values: values.map(coerceBindValue),
  };
}

/**
 * D1 only accepts primitive bind values (string, number, boolean, null,
 * BigInt, ArrayBuffer). Sequelize APIs commonly pass `Date` objects in WHERE
 * clauses (e.g. `{ updated_at: { [Op.lt]: new Date(...) } }`); coerce them to
 * ISO strings to match how dates are stored. Without this iap-reconcile and
 * any other time-window query throws `D1_TYPE_ERROR: Type 'object' not
 * supported for value 'Tue Jun 02 2026 …'`.
 */
export function coerceBindValue(v: any): any {
  if (v instanceof Date) return v.toISOString();
  return v;
}

function processWhereEntry(key: string, value: any, conditions: string[], values: any[]): void {
  const fieldSQL = fieldToSQL(key);

  if (key === String(Op.or)) {
    const items = Array.isArray(value) ? value : [value];
    const orParts = items.map((v) => {
      const sub = buildWhereClause(v);
      values.push(...sub.values);
      return sub.sql ? `(${sub.sql})` : '1=1';
    });
    if (orParts.length > 0) conditions.push(`(${orParts.join(' OR ')})`);
    return;
  }

  if (key === String(Op.and)) {
    const items = Array.isArray(value) ? value : [value];
    const andParts = items.map((v) => {
      const sub = buildWhereClause(v);
      values.push(...sub.values);
      return sub.sql ? `(${sub.sql})` : '1=1';
    });
    if (andParts.length > 0) conditions.push(`(${andParts.join(' AND ')})`);
    return;
  }

  if (value === null || value === undefined) {
    conditions.push(`${fieldSQL} IS NULL`);
    return;
  }

  if (typeof value !== 'object' || value instanceof Date) {
    conditions.push(`${fieldSQL} = ?`);
    values.push(value instanceof Date ? value.toISOString() : value);
    return;
  }

  if (value.__sequelizeWhere) {
    const sub = buildWhereClause(value);
    if (sub.sql) {
      conditions.push(sub.sql);
      values.push(...sub.values);
    }
    return;
  }

  if (value.__literal) {
    conditions.push(`${fieldSQL} = (${value.sql})`);
    return;
  }

  const symKeys = Object.getOwnPropertySymbols(value);
  if (symKeys.length > 0) {
    for (const sym of symKeys) {
      if (!isOpSymbol(sym)) continue;
      buildOpCondition(fieldSQL, sym, (value as any)[sym], conditions, values);
    }
    return;
  }

  if (Array.isArray(value)) {
    if (value.length === 0) {
      conditions.push('1=0');
    } else {
      const placeholders = value.map(() => '?').join(', ');
      conditions.push(`${fieldSQL} IN (${placeholders})`);
      values.push(...value);
    }
    return;
  }

  // Nested plain object — JSON equality
  conditions.push(`${fieldSQL} = ?`);
  values.push(JSON.stringify(value));
}
