/**
 * FilterMap evaluation utilities.
 *
 * Used by memory provider for in-memory filtering and
 * by cosmos provider for SQL query generation.
 */

import type { FilterMap, FilterOperator, FilterValue } from './types.js';

/**
 * Evaluate a FilterMap against a document (in-memory).
 * Returns true if the document matches all filter conditions.
 */
export function matchesFilter(doc: Record<string, unknown>, filter: FilterMap): boolean {
  for (const [key, condition] of Object.entries(filter)) {
    if (key === '$or') {
      const orClauses = condition as FilterMap[];
      if (!orClauses.some(clause => matchesFilter(doc, clause))) return false;
      continue;
    }
    if (condition === undefined) continue;

    const value = getNestedValue(doc, key);

    if (isFilterOperator(condition)) {
      if (!matchesOperator(value, condition as FilterOperator)) return false;
    } else {
      // Exact match
      if (value !== condition) return false;
    }
  }
  return true;
}

function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
  const parts = path.split('.');
  let current: unknown = obj;
  for (const part of parts) {
    if (current == null || typeof current !== 'object') return undefined;
    current = (current as Record<string, unknown>)[part];
  }
  return current;
}

function isFilterOperator(value: unknown): value is FilterOperator {
  if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
  const keys = Object.keys(value);
  return keys.some(k => k.startsWith('$'));
}

function matchesOperator(docValue: unknown, op: FilterOperator): boolean {
  if (op.$gt !== undefined && !(compare(docValue, op.$gt) > 0)) return false;
  if (op.$gte !== undefined && !(compare(docValue, op.$gte) >= 0)) return false;
  if (op.$lt !== undefined && !(compare(docValue, op.$lt) < 0)) return false;
  if (op.$lte !== undefined && !(compare(docValue, op.$lte) <= 0)) return false;
  if (op.$ne !== undefined && docValue === op.$ne) return false;

  if (op.$exists !== undefined) {
    const exists = docValue !== undefined && docValue !== null;
    if (op.$exists !== exists) return false;
  }

  if (op.$startsWith !== undefined) {
    if (typeof docValue !== 'string') return false;
    if (!docValue.startsWith(op.$startsWith)) return false;
  }

  if (op.$contains !== undefined) {
    if (Array.isArray(docValue)) {
      if (!docValue.includes(op.$contains)) return false;
    } else if (typeof docValue === 'string' && typeof op.$contains === 'string') {
      if (!docValue.includes(op.$contains)) return false;
    } else {
      return false;
    }
  }

  if (op.$in !== undefined) {
    if (!op.$in.includes(docValue as FilterValue)) return false;
  }

  return true;
}

function compare(a: unknown, b: unknown): number {
  if (a === b) return 0;
  if (a == null) return -1;
  if (b == null) return 1;
  if (typeof a === 'number' && typeof b === 'number') return a - b;
  if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b);
  if (typeof a === 'boolean' && typeof b === 'boolean') return (a ? 1 : 0) - (b ? 1 : 0);
  return String(a).localeCompare(String(b));
}

// ── Cosmos SQL generation ───────────────────────────────────────────────────

export interface SqlQuery {
  query: string;
  parameters: Array<{ name: string; value: unknown }>;
}

/**
 * Convert a FilterMap to a Cosmos SQL WHERE clause.
 */
export function filterToCosmosSQL(filter: FilterMap): SqlQuery {
  const parameters: Array<{ name: string; value: unknown }> = [];
  let paramIndex = 0;

  function nextParam(value: unknown): string {
    const name = `@p${paramIndex++}`;
    parameters.push({ name, value });
    return name;
  }

  function buildCondition(key: string, condition: unknown): string {
    if (condition === null) {
      return `c.${key} = null`;
    }

    if (!isFilterOperator(condition)) {
      return `c.${key} = ${nextParam(condition)}`;
    }

    const op = condition as FilterOperator;
    const parts: string[] = [];

    if (op.$gt !== undefined) parts.push(`c.${key} > ${nextParam(op.$gt)}`);
    if (op.$gte !== undefined) parts.push(`c.${key} >= ${nextParam(op.$gte)}`);
    if (op.$lt !== undefined) parts.push(`c.${key} < ${nextParam(op.$lt)}`);
    if (op.$lte !== undefined) parts.push(`c.${key} <= ${nextParam(op.$lte)}`);
    if (op.$ne !== undefined) parts.push(`c.${key} != ${nextParam(op.$ne)}`);

    if (op.$exists !== undefined) {
      parts.push(op.$exists ? `IS_DEFINED(c.${key})` : `NOT IS_DEFINED(c.${key})`);
    }

    if (op.$startsWith !== undefined) {
      parts.push(`STARTSWITH(c.${key}, ${nextParam(op.$startsWith)})`);
    }

    if (op.$contains !== undefined) {
      // Could be array or string — emit both ARRAY_CONTAINS and CONTAINS.
      // Cosmos returns false (not error) when the function doesn't match the field type,
      // so OR-ing them handles both array membership and string substring correctly.
      const param = nextParam(op.$contains);
      parts.push(`(ARRAY_CONTAINS(c.${key}, ${param}) OR CONTAINS(c.${key}, ${param}))`);
    }

    if (op.$in !== undefined) {
      const placeholders = op.$in.map(v => nextParam(v));
      parts.push(`c.${key} IN (${placeholders.join(', ')})`);
    }

    return parts.join(' AND ');
  }

  function buildFilter(f: FilterMap): string {
    const clauses: string[] = [];

    for (const [key, condition] of Object.entries(f)) {
      if (key === '$or') {
        const orClauses = (condition as FilterMap[]).map(c => `(${buildFilter(c)})`);
        clauses.push(`(${orClauses.join(' OR ')})`);
        continue;
      }
      if (condition === undefined) continue;
      clauses.push(buildCondition(key, condition));
    }

    return clauses.join(' AND ');
  }

  const where = buildFilter(filter);
  return {
    query: where ? `WHERE ${where}` : '',
    parameters,
  };
}
