import { NextRequest, NextResponse } from "next/server";
import { queryRow } from "@/lib/db";
import {
  parseOrderHTML,
  parseOrderAmendment,
  isAmendment,
  validateOrderTotals,
  processOrderIngestion,
  applyOrderAmendment,
  reconcilePendingOrders,
  OrderParseError,
  NotAReceiptError,
  type MessageMeta,
} from "@/lib/order-ingestion";
import { merchantVerdict, SECOND_CONSUMER_ID } from "@/lib/order-reviews";
import { nudgeBlocks } from "@/lib/slack-blocks";

/**
 * Machine ingest endpoint for order receipts.
 *
 * n8n polls the two mailboxes and POSTs each message here. The parsing lives in
 * the app, not in an n8n Code node, because the n8n sandbox has no `require`
 * and no filesystem — a parser there could not be unit-tested against the real
 * fixture corpus, which is the whole reason this one is trustworthy.
 *
 * Auth is a shared secret, not the Traefik `x-forwarded-user` header: this is
 * called machine-to-machine and there is no browser session to forward.
 */
function authorised(req: NextRequest): boolean {
  const expected = process.env.ORDER_INGEST_TOKEN;
  if (!expected) return false; // fail closed when unconfigured
  const got = req.headers.get("x-ingest-token");
  return !!got && got === expected;
}

/** Does a split with the second consumer already exist on this transaction? */
async function isShared(transactionId: number | null): Promise<boolean> {
  if (!transactionId) return false;
  const row = await queryRow<{ n: string }>(
    `SELECT count(*) AS n FROM transaction_splits
      WHERE transaction_id = $1 AND participant_id = $2`,
    [transactionId, SECOND_CONSUMER_ID]
  );
  return Number(row?.n ?? 0) > 0;
}

export async function POST(req: NextRequest) {
  if (!authorised(req)) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  let body: { html?: string; meta?: MessageMeta; dryRun?: boolean };
  try {
    body = await req.json();
  } catch {
    return NextResponse.json({ error: "invalid JSON" }, { status: 400 });
  }

  const { html, meta, dryRun } = body;
  if (!html || !meta?.messageId || !meta?.subject || !meta?.receivedAt) {
    return NextResponse.json(
      { error: "html and meta{messageId,subject,receivedAt} are required" },
      { status: 400 }
    );
  }

  try {
    // Amendments restate an existing order; they are not receipts.
    if (isAmendment(html)) {
      const amendment = parseOrderAmendment(html, meta);
      if (dryRun) return NextResponse.json({ kind: "amendment", amendment });
      const applied = await applyOrderAmendment(amendment);
      return NextResponse.json({ kind: "amendment", amendment, applied });
    }

    const order = parseOrderHTML(html, meta);

    const check = validateOrderTotals(order, html);
    if (!check.ok) {
      // Refuse rather than record a number we cannot stand behind.
      return NextResponse.json(
        { kind: "rejected", reason: check.reason, order_reference: order.order_reference },
        { status: 422 }
      );
    }

    if (dryRun) return NextResponse.json({ kind: "order", order });

    const result = await processOrderIngestion(order, {
      messageId: meta.messageId,
      subject: meta.subject,
      sender: meta.sender,
    });
    // What we said about this merchant before, so the Slack nudge can warn at
    // the moment the order lands rather than waiting for someone to open the
    // app. `result.transactionId` is excluded because a brand-new order has no
    // verdict yet — anything found is genuinely a previous visit.
    const verdict = result.skipped
      ? null
      : await merchantVerdict(order.merchant_name, result.transactionId);

    return NextResponse.json({
      kind: "order",
      order_reference: order.order_reference,
      merchant: order.merchant_name,
      total: order.totals.total_charged,
      currency: order.currency,
      is_family: order.is_family,
      ...result,
      prior_verdict: verdict && {
        warn: verdict.warn,
        counts: verdict.counts,
        last_note: verdict.history.find((h) => h.note)?.note ?? null,
      },
      // The nudge message is built here, not in n8n expressions: Block Kit in a
      // template string is untestable, and this shape has to stay in step with
      // what /api/slack/interactive renders after a button press. Null when
      // there is no transaction to act on — a card-settled order is parked
      // until its statement arrives, so there is nothing yet to split or rate.
      slack_blocks:
        result.transactionId && !result.skipped
          ? nudgeBlocks({
              transactionId: result.transactionId,
              merchant: order.merchant_name,
              currency: order.currency,
              total: Number(order.totals.total_charged),
              isFamily: order.is_family,
              // Read rather than assume. Today a freshly ingested order has no
              // splits, so `false` would be right — the 140 ingested orders
              // that do carry splits were split by hand after the backfill,
              // not by a rule. But the card's label drives a destructive
              // button: if a split rule is ever added, an assumed `false`
              // would label a shared order "Not shared" and offer to remove
              // the split. One query is cheaper than that failure.
              shared: await isShared(result.transactionId),
              warn: verdict?.warn ?? false,
              warnNote: verdict?.history.find((h) => h.note)?.note ?? null,
            })
          : null,
    });
  } catch (e) {
    // Not a receipt: promotions, delivery updates, adjustment and refund
    // notices. Expected traffic — 200 and silent, or the alert channel fills
    // with noise and stops being read.
    if (e instanceof NotAReceiptError) {
      return NextResponse.json({ kind: "skipped", reason: e.message });
    }

    // IS a receipt, could not be parsed. This is the failure that matters and
    // it must be loud: a provider template change breaks every order at once,
    // and the only other symptom is spend quietly ceasing to appear. Returning
    // 200 here — as this route originally did — made the most likely
    // production failure completely invisible.
    if (e instanceof OrderParseError) {
      return NextResponse.json(
        { kind: "parse_failed", reason: e.message, messageId: e.messageId },
        { status: 422 }
      );
    }

    const message = e instanceof Error ? e.message : String(e);
    return NextResponse.json({ error: message }, { status: 500 });
  }
}

/** Statement-import hook: resolve orders parked awaiting a card statement. */
export async function PATCH(req: NextRequest) {
  if (!authorised(req)) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const out = await reconcilePendingOrders();
  return NextResponse.json(out);
}
