/* Finance and credit control — every commit money makes.
   Kept out of App.jsx so the router stays readable: this file holds the invoice
   ladder (code, issue, pay, dispute, resolve, write off), the chase ladder, and
   the two staged dialogs an instant screen opens.

   The factory is given the commit machinery it needs and nothing else. Two rules
   it exists to keep:
     · No total is ever written. A commit writes a status, a date or a payment
       row; metrics.js sums the rest, so no figure can drift from its invoice.
     · A due date comes from the payer's contracted terms, never a constant.

   Reversibility (AGENTS §3): everything here applies immediately with an undo
   toast, except writing a balance off — irreversible, so it confirms — and,
   above the hospital's write-off threshold, it is not offered at all: it becomes
   a request for approval instead. */
function makeFinanceCommits(ctx) {
  const data = ctx.data, setData = ctx.setData, patch = ctx.patch, restore = ctx.restore;
  const logActivity = ctx.logActivity, unlogActivity = ctx.unlogActivity, pushToast = ctx.pushToast;
  const setForm = ctx.setForm, setConfirm = ctx.setConfirm, viewer = ctx.viewer, now = ctx.now;
  const setPath = ctx.setPath, billing = ctx.billing;

  /* ── Finance ───────────────────────────────────────────────────────────
     An invoice's total and balance are never stored, so a commit here only ever
     writes a status, a date or a payment row — and every figure that divides
     them moves on the next render. */
  /* The due date is the payer's contracted terms, never a constant here. */
  const dueDateFor = (invoice) => now + contractDueDays(data.contracts, invoice.payer) * DAY_MS;

  const issueInvoice = (invoice) => {
    const total = invoiceTotal(invoice);
    const due = dueDateFor(invoice);
    patch("invoices", invoice.id, { status: "issued", issuedAt: now, dueAt: due, codedById: viewer.id });
    const act = logActivity("issued an invoice", { type: "invoice", id: invoice.id }, formatPounds(total) + " to " + labelOf(PAYERS, invoice.payer));
    pushToast({
      variant: "success", title: "Invoice issued",
      description: invoice.reference + " · " + formatPounds(total) + " to " + labelOf(PAYERS, invoice.payer) + " · " + contractDueDays(data.contracts, invoice.payer) + "-day terms, due " + formatWhen(due, now),
      onUndo: () => { restore("invoices", invoice); unlogActivity(act); },
    });
  };

  const bulkIssue = (invoices) => {
    const snapshot = data.invoices;
    const ids = invoices.map((i) => i.id);
    setData((d) => Object.assign({}, d, {
      invoices: d.invoices.map((i) => (ids.indexOf(i.id) > -1 ? Object.assign({}, i, { status: "issued", issuedAt: now, dueAt: now + contractDueDays(d.contracts, i.payer) * DAY_MS, codedById: viewer.id }) : i)),
    }));
    pushToast({
      variant: "success",
      title: invoices.length + (invoices.length === 1 ? " invoice issued" : " invoices issued"),
      description: formatPounds(invoices.reduce((n, i) => n + invoiceTotal(i), 0)) + " asked for across " + invoices.map((i) => labelOf(PAYERS, i.payer)).reduce((list, p) => (list.indexOf(p) > -1 ? list : list.concat([p])), []).length + " payers · each due on its own contracted terms",
      onUndo: () => setData((d) => Object.assign({}, d, { invoices: snapshot })),
    });
  };

  /* Recording money needs three decisions, so this instant screen opens a staged
     dialog. The payment is written as CLEARED, which is what moves the balance —
     and the invoice's status follows the balance rather than being typed. */
  const recordPayment = (invoice) => {
    const openInvoices = data.invoices.filter((i) => INVOICE_FLAGS.billable(i) && invoiceBalance(i, data.payments) > 0);
    const target = invoice || openInvoices[0];
    if (!target) {
      return pushToast({ title: "Nothing to pay", description: "Every issued invoice is settled; there is no balance to record money against." });
    }
    setForm({
      title: invoice ? "Record a payment against " + invoice.reference : "Record a payment",
      description: "Money recorded here is cleared money: it moves the invoice's balance immediately. Advise a payment that has not cleared by leaving it out until it does.",
      submitLabel: "Record payment",
      initial: { invoice: target.id, amount: (invoiceBalance(target, data.payments) / 100).toFixed(2), method: target.payer === "nhs_commissioner" ? "remittance" : "bank_transfer", reference: "" },
      fields: [
        invoice ? null : { id: "invoice", kind: "select", label: "Invoice", options: openInvoices.map((i) => ({ value: i.id, label: i.reference + " — " + patientName(data.patients, i.patientId) + " · " + formatPounds(invoiceBalance(i, data.payments)) + " outstanding" })) },
        { id: "amount", kind: "input", label: "Amount in pounds", hint: "The balance on " + target.reference + " is " + formatPoundsExact(invoiceBalance(target, data.payments)) + "." },
        { id: "method", kind: "select", label: "How it arrived", options: PAYMENT_METHODS },
        { id: "reference", kind: "input", label: "Remittance or transfer reference", placeholder: "REM-88231" },
      ].filter(Boolean),
      validate: (v) => {
        const errors = {};
        const chosen = data.invoices.find((i) => i.id === (invoice ? invoice.id : v.invoice));
        const balance = chosen ? invoiceBalance(chosen, data.payments) : 0;
        const minor = Math.round(Number(v.amount) * 100);
        if (!(minor > 0)) errors.amount = "Enter an amount in pounds, like 1250.00.";
        else if (minor > balance) errors.amount = "That is more than the " + formatPoundsExact(balance) + " outstanding on " + (chosen ? chosen.reference : "the invoice") + ".";
        if (!v.reference || v.reference.trim().length < 4) errors.reference = "Give the reference the payer used.";
        return errors;
      },
      run: (v) => {
        const chosen = data.invoices.find((i) => i.id === (invoice ? invoice.id : v.invoice));
        const minor = Math.round(Number(v.amount) * 100);
        const id = "pay_" + Math.random().toString(36).slice(2, 6);
        const payment = {
          id: id, invoiceId: chosen.id, payer: chosen.payer, method: v.method, status: "cleared",
          amountMinor: minor, receivedAt: now, reference: v.reference.trim(), byId: viewer.id, note: null,
        };
        const settled = invoiceBalance(chosen, data.payments) - minor <= 0;
        setData((d) => Object.assign({}, d, {
          payments: [payment].concat(d.payments),
          invoices: d.invoices.map((i) => (i.id === chosen.id ? Object.assign({}, i, { status: settled ? "paid" : "part_paid" }) : i)),
          activity: [{ id: "act_" + id, actorId: viewer.id, at: now, verb: "recorded a payment", record: { type: "invoice", id: chosen.id }, note: labelOf(PAYMENT_METHODS, v.method) + " " + v.reference.trim() }].concat(d.activity),
        }));
        pushToast({
          variant: "success", title: settled ? "Invoice settled" : "Payment recorded",
          description: formatPoundsExact(minor) + " against " + chosen.reference + " · " + (settled ? "nothing outstanding" : formatPoundsExact(invoiceBalance(chosen, data.payments) - minor) + " still outstanding"),
          onUndo: () => setData((d) => Object.assign({}, d, {
            payments: d.payments.filter((p) => p.id !== id),
            invoices: d.invoices.map((i) => (i.id === chosen.id ? chosen : i)),
            activity: d.activity.filter((a) => a.id !== "act_" + id),
          })),
        });
      },
    });
  };

  const disputeInvoice = (invoice) => setForm({
    title: "Mark " + invoice.reference + " disputed",
    description: "This records that the payer has queried the bill. It stops the invoice being chased and shows the query beside it until somebody answers.",
    submitLabel: "Mark disputed",
    initial: { note: "" },
    fields: [{ id: "note", kind: "text", label: "What the payer is querying", placeholder: "The commissioner queries 11 bed days against a 9-day expected stay.", hint: "Written onto the invoice and shown to anybody who opens it." }],
    validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Record the query — a sentence is enough." } : {}),
    run: (v) => {
      patch("invoices", invoice.id, { status: "disputed", note: v.note.trim() });
      const act = logActivity("marked an invoice disputed", { type: "invoice", id: invoice.id }, null);
      pushToast({
        title: "Marked disputed",
        description: invoice.reference + " · " + formatPounds(invoiceBalance(invoice, data.payments)) + " held behind the query",
        onUndo: () => { restore("invoices", invoice); unlogActivity(act); },
      });
    },
  });

  const onInvoiceTransition = (invoice, transition) => {
    const balance = invoiceBalance(invoice, data.payments);
    if (transition.id === "send_for_coding") {
      patch("invoices", invoice.id, { status: "awaiting_coding" });
      return pushToast({
        title: "Sent for coding",
        description: invoice.reference + " · " + formatPounds(invoiceTotal(invoice)) + " over " + invoice.lines.length + (invoice.lines.length === 1 ? " line" : " lines") + " · a coder checks it before it is issued",
        onUndo: () => restore("invoices", invoice),
      });
    }
    if (transition.id === "issue") return issueInvoice(invoice);
    if (transition.id === "record_payment") return recordPayment(invoice);
    if (transition.id === "dispute") return disputeInvoice(invoice);
    if (transition.id === "resolve") {
      const paid = invoicePaid(data.payments, invoice.id);
      patch("invoices", invoice.id, { status: paid > 0 ? "part_paid" : "issued", note: null });
      const act = logActivity("resolved a dispute", { type: "invoice", id: invoice.id }, null);
      return pushToast({
        variant: "success", title: "Dispute resolved",
        description: invoice.reference + " is being chased again · " + formatPounds(balance) + " outstanding",
        onUndo: () => { restore("invoices", invoice); unlogActivity(act); },
      });
    }
    if (transition.id === "write_off") {
      /* Above the hospital's threshold this is not a local decision (Settings →
         Billing), so the action becomes a request for approval instead. */
      if (billing && balance > billing.writeOffThresholdMinor) {
        return setForm({
          title: "Request approval to write off " + formatPoundsExact(balance),
          description: "The threshold for writing a balance off here is " + formatPounds(billing.writeOffThresholdMinor) + ". Above it, " + billing.approver.toLowerCase() + " approval is needed, so this records the request rather than writing the balance off.",
          submitLabel: "Request approval",
          initial: { note: "" },
          fields: [{ id: "note", kind: "text", label: "Why it should be written off", placeholder: "Chased four times over eleven weeks; the payer has been dissolved.", hint: "Sent to " + billing.approver.toLowerCase() + " with the invoice and its contact history." }],
          validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Say why — a sentence is enough." } : {}),
          run: (v) => {
            const id = "cmt_" + Math.random().toString(36).slice(2, 7);
            setData((d) => Object.assign({}, d, {
              comments: d.comments.concat([{ id: id, record: { type: "invoice", id: invoice.id }, authorId: viewer.id, at: now, body: "Write-off requested for " + formatPoundsExact(balance) + ": " + v.note.trim(), mentions: [], reactions: {} }]),
              activity: [{ id: "act_" + id, actorId: viewer.id, at: now, verb: "requested a write-off", record: { type: "invoice", id: invoice.id }, note: formatPounds(balance) }].concat(d.activity),
            }));
            pushToast({
              title: "Approval requested",
              description: invoice.reference + " · " + formatPoundsExact(balance) + " · with " + billing.approver.toLowerCase() + "; the balance is still owed until they answer",
              onUndo: () => setData((d) => Object.assign({}, d, {
                comments: d.comments.filter((c) => c.id !== id),
                activity: d.activity.filter((x) => x.id !== "act_" + id),
              })),
            });
          },
        });
      }
      return setConfirm({
        title: "Write off " + formatPoundsExact(balance) + " on " + invoice.reference + "?",
        description: "The balance stops being owed and the invoice is closed against " + labelOf(PAYERS, invoice.payer) + ". It cannot be undone, and the amount leaves every revenue figure that counts money still expected.",
        confirmLabel: "Write off " + formatPounds(balance),
        run: () => {
          patch("invoices", invoice.id, { status: "written_off", note: "Balance of " + formatPoundsExact(balance) + " written off by " + viewer.name + "." });
          logActivity("wrote off a balance", { type: "invoice", id: invoice.id }, formatPounds(balance));
          pushToast({ title: "Written off", description: invoice.reference + " · " + formatPoundsExact(balance) + " is no longer expected" });
        },
      });
    }
  };

  /* A new invoice starts as a draft with the one line the biller can state. More
     lines are coded onto it before it is issued — which is why the coding step
     exists and why nothing is invented here. */
  const newInvoice = () => setForm({
    title: "New invoice",
    description: "It starts as a draft with one line. Coding adds the rest from the care record, and only an issued invoice asks anybody for money.",
    submitLabel: "Create draft",
    initial: { patient: data.patients[0].id, payer: billing.defaultPayer, label: "", kind: "procedure", qty: "1", unit: "" },
    fields: [
      { id: "patient", kind: "select", label: "Patient", options: data.patients.map((p) => ({ value: p.id, label: p.name + " — " + p.number })) },
      { id: "payer", kind: "select", label: "Who is billed", options: PAYERS },
      { id: "label", kind: "input", label: "What is billed", placeholder: "Laparoscopic cholecystectomy" },
      { id: "kind", kind: "select", label: "Kind of line", options: BILL_LINE_KINDS },
      { id: "qty", kind: "input", label: "Quantity", hint: "Bed days, doses or one for a procedure." },
      { id: "unit", kind: "input", label: "Unit price in pounds", placeholder: "2940.00", hint: "If the payer's contract prices this item, use that price — Contracts and tariffs flags anything billed off contract." },
    ],
    validate: (v) => {
      const errors = {};
      if (!v.label || v.label.trim().length < 4) errors.label = "Say what is being billed.";
      if (!/^\d+$/.test(v.qty || "") || Number(v.qty) < 1) errors.qty = "A whole number, one or more.";
      if (!(Math.round(Number(v.unit) * 100) > 0)) errors.unit = "Enter a unit price in pounds, like 2940.00.";
      return errors;
    },
    run: (v) => {
      const id = "inv_" + Math.random().toString(36).slice(2, 6);
      const invoice = {
        id: id, patientId: v.patient, payer: v.payer, status: "draft",
        reference: billing.referencePrefix + "-0" + Math.floor(352 + Math.random() * 40), issuedAt: null, dueAt: null, codedById: null,
        note: "Raised by " + viewer.name + "; one line so far.",
        lines: [{ label: v.label.trim(), kind: v.kind, ref: null, qty: Number(v.qty), unitMinor: Math.round(Number(v.unit) * 100) }],
      };
      setData((d) => Object.assign({}, d, { invoices: [invoice].concat(d.invoices) }));
      setPath("/invoices/" + id);
      pushToast({
        variant: "success", title: "Draft invoice created",
        description: invoice.reference + " · " + formatPounds(invoiceTotal(invoice)) + " for " + patientName(data.patients, v.patient) + " · draft, so nobody has been asked for anything",
        onUndo: () => { setData((d) => Object.assign({}, d, { invoices: d.invoices.filter((i) => i.id !== id) })); setPath("/invoices"); },
      });
    },
  });

  /* ── Credit control ────────────────────────────────────────────────────
     A chase is a contact, so it needs what was said and what came of it: a
     staged dialog from an instant screen. Logging one moves the ladder, because
     the next step is derived from the last. */
  const onChase = (row, transition) => setForm({
    title: labelOf(CHASE_STEPS, transition.step) + " — " + row.invoice.reference,
    description: formatPoundsExact(row.balance) + " outstanding with " + labelOf(PAYERS, row.invoice.payer) + " on " + contractDueDays(data.contracts, row.invoice.payer) + "-day terms · " +
      (row.overdue ? formatCountdown(row.invoice.dueAt, now) : "due " + formatWhen(row.invoice.dueAt, now)) +
      (row.last ? " · last contact " + labelOf(CHASE_STEPS, row.last.step).toLowerCase() + " " + formatWhen(row.last.at, now) : " · never chased before"),
    submitLabel: "Log " + labelOf(CHASE_STEPS, transition.step).toLowerCase(),
    initial: { outcome: "awaiting", note: "" },
    fields: [
      { id: "outcome", kind: "radio", label: "What came of it", options: CHASE_OUTCOMES.map((o) => ({ value: o.value, label: o.label })) },
      { id: "note", kind: "text", label: "What was said", placeholder: "Spoke to the finance inbox; they will pay with the April remittance.", hint: "Kept on the contact history for anybody who picks this up next." },
    ],
    validate: (v) => (!v.note || v.note.trim().length < 12 ? { note: "Record what was said — a sentence is enough." } : {}),
    run: (v) => {
      const id = "chs_" + Math.random().toString(36).slice(2, 6);
      const chase = { id: id, invoiceId: row.invoice.id, step: transition.step, at: now, byId: viewer.id, outcome: v.outcome, note: v.note.trim() };
      setData((d) => Object.assign({}, d, {
        chases: d.chases.concat([chase]),
        activity: [{ id: "act_" + id, actorId: viewer.id, at: now, verb: "chased a payer", record: { type: "invoice", id: row.invoice.id }, note: labelOf(CHASE_STEPS, transition.step) }].concat(d.activity),
      }));
      const next = nextChaseStep(transition.step);
      pushToast({
        variant: v.outcome === "promised" ? "success" : undefined,
        title: labelOf(CHASE_STEPS, transition.step) + " logged",
        description: row.invoice.reference + " · " + labelOf(CHASE_OUTCOMES, v.outcome).toLowerCase() + " · " + (next ? "next step is " + labelOf(CHASE_STEPS, next.value).toLowerCase() : "the ladder ends here; the debt agency has it"),
        onUndo: () => setData((d) => Object.assign({}, d, {
          chases: d.chases.filter((c) => c.id !== id),
          activity: d.activity.filter((a) => a.id !== "act_" + id),
        })),
      });
    },
  });

  return {
    onInvoiceTransition: onInvoiceTransition,
    bulkIssue: bulkIssue,
    recordPayment: recordPayment,
    newInvoice: newInvoice,
    onChase: onChase,
    issueInvoice: issueInvoice,
  };
}
window.makeFinanceCommits = makeFinanceCommits;
