import { queryRaw, queryRow } from "@/lib/db";

/**
 * Verdicts on delivery orders — the "don't order from here again" memory.
 *
 * The problem this exists for is not accounting. Orders were placed twice from
 * places we disliked because nobody remembered by the time the next order went
 * in (user, 2026-07-28). The ledger already knew we had been there; it just had
 * nowhere to record what we thought of it.
 *
 * **A verdict is recorded per order per person, but read per merchant.**
 * `order_reviews` keys on `(transaction_id, participant_id)`, because what you
 * are judging is one delivery — this Thai place was bad *that night*, with
 * those items — and because a shared meal produces two opinions that routinely
 * disagree. That disagreement is the useful part; one row per transaction
 * cannot hold it.
 *
 * The signal you need later is about the merchant, so it is derived by
 * aggregating a merchant's orders rather than stored on one. Storing it per
 * merchant instead would mean the second verdict silently overwrites the first
 * and you lose the fact that it was fine twice and awful once.
 *
 * The join key is `expense_metadata.merchant_normalized`, not
 * `transactions.merchant_name`: the latter is a bank descriptor and reads
 * differently for the same restaurant on different nights.
 */

/**
 * Five levels. Three collapsed the distinction that decides a re-order —
 * "loved" and "liked" are both "would order again" but only one is worth going
 * out of your way for, and "ok" is not a recommendation at all. "bad" was added
 * because the jump from "ok" to "never again" is too big and most
 * disappointments live in the gap (user, 2026-07-28).
 *
 * Only "never" raises the warning on a future order. A blacklist that fires for
 * every mediocre meal stops being read, so "bad" records the disappointment
 * without triggering the alarm.
 */
export type Rating = "loved" | "liked" | "ok" | "bad" | "never";

export const RATINGS: Rating[] = ["loved", "liked", "ok", "bad", "never"];

/**
 * Per-item opinions, keyed by the line item's description.
 *
 * Only the poles are offered. A per-item "ok" is noise: the useful question at
 * the next order is "what should I get / what should I avoid here", and a
 * middling dish answers neither.
 */
export type ItemVerdict = "loved" | "never";

export const ITEM_VERDICTS: ItemVerdict[] = ["loved", "never"];

export interface ItemOpinion {
  item: string;
  verdict: ItemVerdict;
}

/**
 * Whose verdict this is by default, and who the other one is.
 *
 * A two-person household with one primary user: the owner records almost every
 * verdict, and the only other consumer is Sonu (user, 2026-07-28). Named rather
 * than inlined so the Slack nudge, the split it creates and the second verdict
 * it asks for cannot drift apart.
 */
export const OWNER_PARTICIPANT_ID = 1;
export const SECOND_CONSUMER_ID = 4;

export interface OrderReview {
  transaction_id: number;
  participant_id: number;
  participant_name?: string;
  rating: Rating | null;
  order_again: boolean | null;
  note: string | null;
  item_verdicts: ItemOpinion[];
  updated_at: string;
}

export interface MerchantVerdict {
  merchant: string;
  /** Verdicts on OTHER orders from this merchant, newest first. */
  history: {
    transaction_id: number;
    participant_id: number;
    participant_name: string;
    rating: Rating | null;
    note: string | null;
    transaction_date: string | null;
  }[];
  counts: Record<Rating, number>;
  /**
   * True when this merchant has ever been marked `never` — and ONLY `never`.
   * "bad" is deliberately excluded: the warning is a blacklist, and one that
   * fires for every mediocre meal is one nobody reads.
   */
  warn: boolean;
  /**
   * What to get and what to avoid here, pooled across every order from this
   * merchant. This is the payoff for recording items at all — the order-level
   * rating tells you whether to come back, this tells you what to order when
   * you do.
   */
  items: { item: string; loved: number; never: number }[];
}

/**
 * `merchant_normalized` for a transaction, resolving both directions.
 *
 * A card-settled order has no transaction of its own — the statement line is
 * the transaction and the receipt points at it through
 * `matched_transaction_id`. Looking only at `transaction_id` misses exactly the
 * orders that were paid by card, which is most of them.
 */
export async function merchantForTransaction(
  transactionId: number
): Promise<string | null> {
  const row = await queryRow<{ merchant_normalized: string | null }>(
    `SELECT merchant_normalized FROM expense_metadata
      WHERE transaction_id = $1 OR matched_transaction_id = $1
      LIMIT 1`,
    [transactionId]
  );
  return row?.merchant_normalized ?? null;
}

/**
 * What we have previously said about a merchant.
 *
 * `exclude` drops the order being looked at, so the panel shows "what you said
 * the other times" rather than echoing the verdict you are currently editing.
 * Pass null when there is no current order — the ingest path, where the whole
 * point is that nothing has been said about this one yet.
 */
export async function merchantVerdict(
  merchant: string | null,
  exclude: number | null = null
): Promise<MerchantVerdict | null> {
  if (!merchant) return null;

  const rows = await queryRaw<{
    transaction_id: number;
    participant_id: number;
    participant_name: string;
    rating: Rating | null;
    note: string | null;
    transaction_date: string | null;
    item_verdicts: ItemOpinion[] | null;
  }>(
    // `rating IS NOT NULL` is deliberately NOT in the WHERE clause: a review
    // can carry item verdicts and no overall rating, and dropping those would
    // lose exactly the "the noodles here are great" signal this exists for.
    `SELECT r.transaction_id, r.participant_id, p.name AS participant_name,
            r.rating, r.note, r.item_verdicts,
            to_char(t.transaction_date, 'YYYY-MM-DD') AS transaction_date
       FROM order_reviews r
       JOIN transactions t ON t.id = r.transaction_id
       JOIN participants p ON p.id = r.participant_id
       JOIN expense_metadata em
         ON em.transaction_id = r.transaction_id
         OR em.matched_transaction_id = r.transaction_id
      -- Case-folded: the platforms capitalise the same restaurant differently
      -- ("TEG Kebabs & Biryani" on Uber Eats, "TEG KEBABS & BIRYANI" on
      -- DoorDash). An exact match split one restaurant's history in two, so a
      -- "never again" recorded through one app never warned you in the other —
      -- silently defeating the whole point of the memory.
      WHERE lower(em.merchant_normalized) = lower($1)
        AND ($2::int IS NULL OR r.transaction_id <> $2)
      ORDER BY t.transaction_date DESC, r.participant_id
      LIMIT 50`,
    [merchant, exclude]
  );

  const counts: Record<Rating, number> = { loved: 0, liked: 0, ok: 0, bad: 0, never: 0 };
  for (const r of rows) if (r.rating) counts[r.rating] += 1;

  // Pool item opinions across orders. Case-folded because the same dish comes
  // back with inconsistent capitalisation between receipts; the first spelling
  // seen is kept for display.
  const pool = new Map<string, { item: string; loved: number; never: number }>();
  for (const r of rows) {
    for (const v of r.item_verdicts ?? []) {
      if (!v?.item) continue;
      const key = v.item.trim().toLowerCase();
      const entry = pool.get(key) ?? { item: v.item.trim(), loved: 0, never: 0 };
      if (v.verdict === "loved") entry.loved += 1;
      else if (v.verdict === "never") entry.never += 1;
      pool.set(key, entry);
    }
  }

  return {
    merchant,
    history: rows
      .filter((r) => r.rating !== null || r.note)
      .map(({ item_verdicts: _drop, ...h }) => h),
    counts,
    warn: counts.never > 0,
    items: [...pool.values()].sort(
      (a, b) => b.loved + b.never - (a.loved + a.never)
    ),
  };
}
