/**
 * Applying a rule's `actions` to transactions, and snapshotting the before-state
 * so a run can be reverted.
 *
 * Shared by the two callers that write rule actions:
 * - `POST /api/rules/apply` — condition-matched bulk run
 * - `POST /api/transactions/bulk` with `action: "apply_rule"` — quick action
 *   fired against a hand-picked selection (conditions ignored)
 */

import { queryRaw } from "@/lib/db";
import { completeSplit } from "@/lib/splits";
import type { Actions } from "@/lib/rules";

export interface SnapshotEntry {
  transaction_id: number;
  had_override: boolean;
  prev_category_override: string | null;
  prev_merchant_normalized: string | null;
  prev_tag_ids: number[];
  prev_splits: { participant_id: number; share_percent: number; settled: boolean }[];
}

/**
 * Apply `actions` to one transaction.
 *
 * Tags are additive (ON CONFLICT DO NOTHING) — re-running never duplicates and
 * never removes tags the rule doesn't mention. Splits replace the participant
 * set but upsert shares, so `settled` flags survive a re-run; a plain
 * delete+reinsert would silently un-settle reconciled splits.
 */
export async function applyRuleActions(
  transactionId: number,
  actions: Actions,
  opts: { skipSplit?: boolean } = {}
): Promise<void> {
  if (actions.set_category || actions.set_merchant) {
    await queryRaw(
      `INSERT INTO transaction_overrides (transaction_id, category_override, merchant_normalized)
       VALUES ($1, $2, $3)
       ON CONFLICT (transaction_id) DO UPDATE SET
         category_override = COALESCE($2, transaction_overrides.category_override),
         merchant_normalized = COALESCE($3, transaction_overrides.merchant_normalized),
         updated_at = NOW()`,
      [transactionId, actions.set_category || null, actions.set_merchant || null]
    );
  }

  if (actions.add_tag_ids?.length) {
    for (const tagId of actions.add_tag_ids) {
      await queryRaw(
        `INSERT INTO transaction_tags (transaction_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
        [transactionId, tagId]
      );
    }
  }

  if (actions.apply_split?.length && !opts.skipSplit) {
    await queryRaw(
      `DELETE FROM transaction_splits WHERE transaction_id = $1 AND participant_id != ALL($2::int[])`,
      [transactionId, actions.apply_split.map((s) => s.participant_id)]
    );
    for (const s of actions.apply_split) {
      await queryRaw(
        `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent)
         VALUES ($1, $2, $3)
         ON CONFLICT (transaction_id, participant_id)
         DO UPDATE SET share_percent = EXCLUDED.share_percent`,
        [transactionId, s.participant_id, s.share_percent]
      );
    }
    // A rule may name only the other person — "split Woolworths with Sonu 50%"
    // is a complete thought, and ten of the original rules are written that way.
    // The payer's half is implied by it, so write the implication down.
    await completeSplit(transactionId);
  }
}

/** Capture the overrides / tags / splits of `ids` before a run mutates them. */
export async function captureSnapshot(ids: number[]): Promise<SnapshotEntry[]> {
  if (ids.length === 0) return [];

  const overrides = await queryRaw<{
    transaction_id: number;
    category_override: string | null;
    merchant_normalized: string | null;
  }>(
    `SELECT transaction_id, category_override, merchant_normalized
     FROM transaction_overrides WHERE transaction_id = ANY($1::int[])`,
    [ids]
  );
  const overrideMap = new Map(overrides.map((o) => [o.transaction_id, o]));

  const tagRows = await queryRaw<{ transaction_id: number; tag_id: number }>(
    `SELECT transaction_id, tag_id FROM transaction_tags WHERE transaction_id = ANY($1::int[])`,
    [ids]
  );
  const tagMap = new Map<number, number[]>();
  for (const row of tagRows) {
    if (!tagMap.has(row.transaction_id)) tagMap.set(row.transaction_id, []);
    tagMap.get(row.transaction_id)!.push(row.tag_id);
  }

  const splitRows = await queryRaw<{
    transaction_id: number;
    participant_id: number;
    share_percent: number;
    settled: boolean;
  }>(
    `SELECT transaction_id, participant_id, share_percent, settled
     FROM transaction_splits WHERE transaction_id = ANY($1::int[])`,
    [ids]
  );
  const splitMap = new Map<number, { participant_id: number; share_percent: number; settled: boolean }[]>();
  for (const row of splitRows) {
    if (!splitMap.has(row.transaction_id)) splitMap.set(row.transaction_id, []);
    splitMap.get(row.transaction_id)!.push({
      participant_id: row.participant_id,
      share_percent: row.share_percent,
      settled: row.settled,
    });
  }

  return ids.map((id) => {
    const ov = overrideMap.get(id);
    return {
      transaction_id: id,
      had_override: !!ov,
      prev_category_override: ov?.category_override ?? null,
      prev_merchant_normalized: ov?.merchant_normalized ?? null,
      prev_tag_ids: tagMap.get(id) ?? [],
      prev_splits: splitMap.get(id) ?? [],
    };
  });
}
