import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { queryRaw } from "@/lib/db";
import { getTransactions } from "@/lib/queries";
import { evaluateCondition, type Condition, type Actions } from "@/lib/rules";

/**
 * Dry run: which transactions would this rule touch, and what would change?
 *
 * Nothing is written. This exists so a rule can be inspected before it is
 * applied — the apply path (POST /api/transactions/bulk with action
 * "apply_rule") takes an explicit list of ids, so the flow is preview, pick,
 * then apply.
 *
 * Matches are split into two groups because they need different attention:
 * `changes` are rows the rule would actually alter, `noops` already have the
 * value the rule would set. A merchant-normalisation rule matching 400 rows
 * where 380 are already correct is 20 changes and 380 rows of noise.
 */

interface Change {
  field: "category" | "merchant" | "tags" | "split";
  from: string | null;
  to: string;
}

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const user = await getCurrentUser(req);
  if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });

  const { id } = await params;
  const rules = await queryRaw<{
    id: number; name: string; conditions: unknown; actions: unknown;
  }>(
    `SELECT id, name, conditions, actions FROM rules WHERE id = $1 AND owner_id = $2`,
    [Number(id), user.id]
  );
  if (!rules.length) return NextResponse.json({ error: "Rule not found" }, { status: 404 });

  const rule = rules[0];
  const conditions = (typeof rule.conditions === "string"
    ? JSON.parse(rule.conditions) : rule.conditions) as Condition[];
  const actions = (typeof rule.actions === "string"
    ? JSON.parse(rule.actions) : rule.actions) as Actions;

  const { searchParams } = new URL(req.url);
  const statementId = searchParams.get("statement_id");
  const limit = Math.min(2000, Math.max(1, Number(searchParams.get("limit") || "500")));

  const { data: transactions } = await getTransactions(user.id, {
    limit: 100000,
    offset: 0,
    ...(statementId ? { statement_id: statementId } : {}),
  });

  // A rule with no conditions matches every transaction. That is how the apply
  // endpoint behaves too, so the preview must show it rather than hide it —
  // seeing "matches 3,721" is the point.
  const matchAll = conditions.length === 0;

  const tagIds = new Set(actions.add_tag_ids ?? []);
  const splitTarget = actions.apply_split ?? [];

  const changes: unknown[] = [];
  let noops = 0;

  for (const tx of transactions) {
    if (!matchAll && !conditions.every((c) => evaluateCondition(c, tx))) continue;

    const diff: Change[] = [];

    if (actions.set_category && tx.effective_category !== actions.set_category) {
      diff.push({ field: "category", from: tx.effective_category ?? null, to: actions.set_category });
    }
    if (actions.set_merchant && tx.effective_merchant !== actions.set_merchant) {
      diff.push({ field: "merchant", from: tx.effective_merchant ?? null, to: actions.set_merchant });
    }
    if (tagIds.size) {
      const have = new Set((tx.tags ?? []).map((t) => t.id));
      const missing = [...tagIds].filter((t) => !have.has(t));
      if (missing.length) {
        diff.push({ field: "tags", from: null, to: missing.join(",") });
      }
    }
    if (splitTarget.length) {
      // Compare the participant/share set the rule would write against what is
      // already there; a re-run that changes nothing should not look like work.
      const have = new Map((tx.splits ?? []).map((s) => [s.participant_id, Number(s.share_percent)]));
      const differs = splitTarget.length !== have.size
        || splitTarget.some((s) => have.get(s.participant_id) !== Number(s.share_percent));
      if (differs) {
        diff.push({
          field: "split",
          from: [...have.entries()].map(([p, s]) => `${p}:${s}%`).join(" ") || null,
          to: splitTarget.map((s) => `${s.participant_id}:${s.share_percent}%`).join(" "),
        });
      }
    }

    if (diff.length === 0) { noops++; continue; }

    changes.push({
      id: tx.id,
      transaction_date: tx.transaction_date,
      description: tx.description,
      amount: tx.amount,
      amount_aud: tx.amount_aud,
      currency: tx.currency,
      bank_name: tx.bank_name,
      effective_merchant: tx.effective_merchant,
      effective_category: tx.effective_category,
      changes: diff,
    });
  }

  return NextResponse.json({
    rule: { id: rule.id, name: rule.name, matches_everything: matchAll },
    total_matched: changes.length + noops,
    would_change: changes.length,
    already_correct: noops,
    transactions: changes.slice(0, limit),
    truncated: changes.length > limit,
  });
}
