/**
 * sqlc-style query generator. Annotated SQL in `app/db/queries/*.sql` becomes
 * typed TS functions whose params + results are inferred from the TS models and
 * whose rows are mapped back through `model.parse`.
 *
 *   -- name: GetUserByEmail :one
 *   SELECT * FROM users WHERE email = :email;
 *
 * →  GetUserByEmail(db, { email: string }): Promise<{…} | null>
 *
 * Type inference is best-effort (comparisons + INSERT column lists + SELECT list
 * vs the model); anything it can't resolve becomes `unknown`.
 */

import type { Column, Model } from "./schema.ts";
import type { Dialect } from "./sql.ts";

export type QueryKind = "one" | "many" | "exec";

export interface QueryDef {
  name: string;
  kind: QueryKind;
  sql: string;
}

/** A model plus the variable name it is exported under (for imports). */
export interface ModelRef {
  varName: string;
  model: Model;
}

/** Parse annotated queries from one `.sql` file's contents. */
export function parseQueries(content: string): QueryDef[] {
  const out: QueryDef[] = [];
  const re = /--\s*name:\s*(\w+)\s*:(one|many|exec)\b[^\n]*\n([\s\S]*?)(?=--\s*name:|$)/gi;
  let m: RegExpExecArray | null;
  while ((m = re.exec(content))) {
    out.push({
      name: m[1]!,
      kind: m[2]!.toLowerCase() as QueryKind,
      sql: m[3]!.trim().replace(/;\s*$/, ""),
    });
  }
  return out;
}

/** Rewrite `:name` placeholders to positional params, keeping their order. */
function toPositional(sql: string, dialect: Dialect): { sql: string; order: string[] } {
  const order: string[] = [];
  const rewritten = sql.replace(/:([A-Za-z_]\w*)/g, (_m, name: string) => {
    order.push(name);
    return dialect === "postgres" ? `$${order.length}` : "?";
  });
  return { sql: rewritten, order };
}

function uniqueInOrder(names: string[]): string[] {
  const seen = new Set<string>();
  const out: string[] = [];
  for (const n of names) {
    if (!seen.has(n)) {
      seen.add(n);
      out.push(n);
    }
  }
  return out;
}

function tableOf(sql: string): string | undefined {
  const from = /\bFROM\s+["`]?(\w+)["`]?/i.exec(sql);
  if (from) return from[1];
  const into = /\bINTO\s+["`]?(\w+)["`]?/i.exec(sql);
  if (into) return into[1];
  const upd = /\bUPDATE\s+["`]?(\w+)["`]?/i.exec(sql);
  return upd?.[1];
}

function tsOutput(col: Column): string {
  switch (col.def.type) {
    case "id":
    case "int":
    case "real":
      return "number";
    case "bool":
      return "boolean";
    case "timestamp":
      return "Date";
    case "json":
      return "unknown";
    default:
      return "string";
  }
}

function tsInput(col: Column): string {
  return col.def.type === "timestamp" ? "string | Date" : tsOutput(col);
}

interface SelectCol {
  name: string;
  /** A type forced by an aggregate (e.g. COUNT → number), overriding the model. */
  forced?: string;
}

/** Split a SELECT list on top-level commas (respecting `fn(a, b)`). */
function splitTopLevel(list: string): string[] {
  const out: string[] = [];
  let depth = 0;
  let cur = "";
  for (const ch of list) {
    if (ch === "(") depth++;
    else if (ch === ")") depth--;
    if (ch === "," && depth === 0) {
      out.push(cur);
      cur = "";
    } else cur += ch;
  }
  if (cur.trim()) out.push(cur);
  return out;
}

/** Parse the SELECT list into columns, or null for `SELECT *`. */
function selectColumns(sql: string): SelectCol[] | null {
  const m = /SELECT\s+([\s\S]*?)\s+FROM\b/i.exec(sql);
  if (!m) return null;
  const list = m[1]!.trim();
  if (list === "*") return null;
  return splitTopLevel(list).map((seg): SelectCol => {
    const s = seg.trim();
    const alias = /\s+AS\s+["`]?(\w+)["`]?$/i.exec(s);
    const name = alias ? alias[1]! : s.split(".").pop()!.replace(/["`]/g, "");
    const forced = /\b(count|sum|avg|min|max|total)\s*\(/i.test(s) ? "number" : undefined;
    return { name, forced };
  });
}

/** True when every selected column is a plain model column (so `model.parse` fits). */
function columnsMatchModel(cols: SelectCol[] | null, model: Model | undefined): boolean {
  if (!model) return false;
  if (cols === null) return true; // SELECT * → full model row
  return cols.every((c) => !c.forced && !!model.columns[c.name]);
}

function resultType(cols: SelectCol[] | null, model: Model | undefined): string {
  if (cols === null) {
    if (!model) return "Record<string, unknown>";
    return `{ ${Object.entries(model.columns)
      .map(([k, col]) => `${k}: ${tsOutput(col)}`)
      .join("; ")} }`;
  }
  return `{ ${cols
    .map(
      (c) =>
        `${c.name}: ${c.forced ?? (model && model.columns[c.name] ? tsOutput(model.columns[c.name]!) : "unknown")}`,
    )
    .join("; ")} }`;
}

/** Find the column a `:param` is compared to / inserted into, if any. */
function paramColumn(param: string, sql: string): string | undefined {
  const op = "(?:=|!=|<>|<=|>=|<|>|LIKE)";
  const cmp1 = new RegExp(`(\\w+)\\s*${op}\\s*:${param}\\b`, "i").exec(sql);
  if (cmp1) return cmp1[1];
  const cmp2 = new RegExp(`:${param}\\b\\s*${op}\\s*(\\w+)`, "i").exec(sql);
  if (cmp2) return cmp2[1];
  const ins = /INSERT\s+INTO\s+\w+\s*\(([^)]*)\)\s*VALUES\s*\(([^)]*)\)/i.exec(sql);
  if (ins) {
    const cols = ins[1]!.split(",").map((s) => s.trim().replace(/["`]/g, ""));
    const vals = ins[2]!.split(",").map((s) => s.trim());
    const idx = vals.indexOf(`:${param}`);
    if (idx >= 0 && cols[idx]) return cols[idx];
  }
  return undefined;
}

function inferParamType(param: string, sql: string, model: Model | undefined): string {
  if (!model) return "unknown";
  const col = paramColumn(param, sql);
  const column = col ? model.columns[col] : undefined;
  return column ? tsInput(column) : "unknown";
}

/** Generate the full `queries.gen.ts` source. */
export function generateQueriesFile(
  queries: QueryDef[],
  models: ModelRef[],
  dialect: Dialect,
): string {
  const byTable = new Map(models.map((m) => [m.model.name, m]));
  const usedModels = new Set<string>();
  const blocks: string[] = [];

  for (const q of queries) {
    const { sql, order } = toPositional(q.sql, dialect);
    const sqlLit = JSON.stringify(sql);
    const positional = `[${order.map((n) => `args.${n}`).join(", ")}]`;
    const params = uniqueInOrder(order);
    const table = tableOf(q.sql);
    const ref = table ? byTable.get(table) : undefined;

    const argFields = params.map((p) => `${p}: ${inferParamType(p, q.sql, ref?.model)}`);
    const sig = argFields.length ? `db: Db, args: { ${argFields.join("; ")} }` : "db: Db";

    if (q.kind === "exec") {
      blocks.push(
        `export async function ${q.name}(${sig}): Promise<ExecResult> {\n  return db.exec(${sqlLit}, ${positional});\n}`,
      );
      continue;
    }

    const cols = selectColumns(q.sql);
    const row = resultType(cols, ref?.model);
    const ret = q.kind === "one" ? `${row} | null` : `${row}[]`;
    const method = q.kind === "one" ? "one" : "all";
    // Only map through the model when the selected columns are model columns.
    const passModel = !!ref && columnsMatchModel(cols, ref.model);
    let modelArg = "";
    if (passModel && ref) {
      usedModels.add(ref.varName);
      modelArg = `, ${ref.varName}`;
    }
    blocks.push(
      `export async function ${q.name}(${sig}): Promise<${ret}> {\n  return (await db.${method}(${sqlLit}, ${positional}${modelArg})) as ${ret};\n}`,
    );
  }

  const imports = [`import type { Db, ExecResult } from "@wrnexus/db";`];
  if (usedModels.size > 0) {
    imports.push(`import { ${[...usedModels].sort().join(", ")} } from "./schema.ts";`);
  }
  return `// AUTO-GENERATED by \`wrnexus db generate\` — do not edit.\n${imports.join("\n")}\n\n${blocks.join("\n\n")}\n`;
}
