/* Meridian — status vocabulary.
   A value not listed here does not exist. If a screen needs one, it is added
   here first, with a tone grouping, a reason, and at least one fixture.
   Anything computed from other fields is NOT a value — derived flags live at
   the bottom of this file, and take their clock as an argument. */

/* payment.status — where one inbound charge sits in its lifecycle.
   Tone answers "is our money there yet": neutral = nothing owed to us yet or
   given back, info = authorised but not captured, success = settled,
   danger = it failed and someone must act.
   Deliberately absent: `disputed` (a dispute is its own record, and the
   payment stays succeeded while it runs) and `late` (derived from expectedAt). */
const PAYMENT_STATUS = [
  { id: "pending", label: "Pending", tone: "neutral" },
  { id: "authorized", label: "Authorized", tone: "info" },
  { id: "succeeded", label: "Succeeded", tone: "success" },
  { id: "refunded", label: "Refunded", tone: "neutral" },
  { id: "failed", label: "Failed", tone: "danger" },
];
/* ids: "pending" | "authorized" | "succeeded" | "refunded" | "failed" */

/* payout.status — an outbound transfer to a customer's bank.
   The field has five values and the primary screen asks one question — "who is
   this waiting on" — so: warning = waiting on us, info = waiting on the
   network, success = done, danger = it came back.
   Deliberately absent: `blocked` (derived from blockedBy) and `late`
   (derived from expectedAt). */
const PAYOUT_STATUS = [
  { id: "awaiting_approval", label: "Awaiting approval", tone: "warning" },
  { id: "approved", label: "Approved", tone: "info" },
  { id: "in_transit", label: "In transit", tone: "info" },
  { id: "paid", label: "Paid", tone: "success" },
  { id: "rejected", label: "Rejected", tone: "neutral" },
  { id: "returned", label: "Returned", tone: "danger" },
];
/* ids: "awaiting_approval" | "approved" | "in_transit" | "paid" | "rejected" | "returned"
   `rejected` is neutral, not danger: turning a payout down is a deliberate
   ending we chose, while `returned` is the network failing us. */

/* dispute.status — a chargeback's own lifecycle.
   warning = the clock is ours, info = the clock is the network's.
   Deliberately absent: `closing_soon` (derived from evidenceDueAt). */
const DISPUTE_STATUS = [
  { id: "needs_response", label: "Needs response", tone: "warning" },
  { id: "under_review", label: "Under review", tone: "info" },
  { id: "won", label: "Won", tone: "success" },
  { id: "lost", label: "Lost", tone: "danger" },
];
/* ids: "needs_response" | "under_review" | "won" | "lost" */

/* expense.status — one card transaction's review lifecycle.
   warning = waiting on us, info = waiting on the person who spent it,
   neutral = nothing to review yet or a deliberate ending we chose,
   success = signed off.
   Deliberately absent: `missing_receipt` (derived from receiptAt and the
   workspace's receipt policy) and `over_limit` (derived from the card's
   month-to-date spend). */
const EXPENSE_STATUS = [
  { id: "pending", label: "Pending", tone: "neutral" },
  { id: "needs_review", label: "Needs review", tone: "warning" },
  { id: "queried", label: "Queried", tone: "info" },
  { id: "approved", label: "Approved", tone: "success" },
  { id: "denied", label: "Denied", tone: "neutral" },
];
/* ids: "pending" | "needs_review" | "queried" | "approved" | "denied"
   `denied` is neutral for the same reason a rejected payout is: we chose it. */

/* card.status — a corporate card's own lifecycle.
   The question the cards screen asks is "can this card be spent on":
   info = waiting on the holder, success = yes, warning = deliberately stopped,
   neutral = closed for good.
   Deliberately absent: `near_limit` and `expiring` — both derived. */
const CARD_STATUS = [
  { id: "pending_activation", label: "Not activated", tone: "info" },
  { id: "active", label: "Active", tone: "success" },
  { id: "frozen", label: "Frozen", tone: "warning" },
  { id: "cancelled", label: "Cancelled", tone: "neutral" },
];
/* ids: "pending_activation" | "active" | "frozen" | "cancelled" */

/* Classification lists — read through labelOf, never reaching a Badge. */
const PAYMENT_METHODS = [
  { value: "card", label: "Card" },
  { value: "ach", label: "ACH" },
  { value: "wire", label: "Wire" },
  { value: "wallet", label: "Wallet" },
];
const EXPENSE_CATEGORIES = [
  { value: "software", label: "Software" },
  { value: "travel", label: "Travel" },
  { value: "meals", label: "Meals and entertainment" },
  { value: "advertising", label: "Advertising" },
  { value: "equipment", label: "Equipment" },
  { value: "services", label: "Professional services" },
];
const CARD_TYPES = [
  { value: "physical", label: "Physical" },
  { value: "virtual", label: "Virtual" },
];
const TEAM_ROLES = [
  { value: "admin", label: "Admin", note: "Approves up to their own limit, issues cards, edits workspace settings." },
  { value: "approver", label: "Approver", note: "Approves payouts and expenses up to their own limit." },
  { value: "member", label: "Member", note: "Spends on a card and sends receipts in. Approves nothing." },
];

/* Policy — the numbers the derived flags below read, and the user edits on
   /settings/spend-controls. Never inlined into a predicate, never in lib/: one
   object owns them, every predicate takes it as an argument, so saving the form
   really does recount the sidebar badge and every tile that reads it. */
const MERIDIAN_POLICY_DEFAULTS = {
  receiptThresholdCents: 7500,
  receiptCategories: ["meals", "travel"],
  cardWarnShare: 0.8,
  cardDefaultLimitCents: 500000,
};

/* Derived flags. Clock injected, never read from the module. */
const PAYMENT_FLAGS = {
  hasOpenDispute: (payment, disputes) => disputes.some((d) => d.paymentId === payment.id && DISPUTE_FLAGS.isOpen(d)),
  needsAttention: (payment, disputes) => payment.status === "failed" || PAYMENT_FLAGS.hasOpenDispute(payment, disputes),
};

const PAYOUT_FLAGS = {
  awaitsApproval: (payout) => payout.status === "awaiting_approval",
  isBlocked: (payout) => !!payout.blockedBy,
  /* Above the approver's own limit. A permission fact about a person and a
     payout together, so it is derived and the approver is injected — never a
     value on the record and never read from the module. */
  isAboveLimit: (payout, approver) => !approver || payout.amount > approver.approvalLimit,
  /* Waiting on us, nothing in the way, AND inside this person's limit — the
     act-ability predicate the row, the figure, the bulk bar and the sidebar
     badge all call, with the same approver passed to each. */
  isApprovable: (payout, approver) => PAYOUT_FLAGS.awaitsApproval(payout) && !PAYOUT_FLAGS.isBlocked(payout) && !PAYOUT_FLAGS.isAboveLimit(payout, approver),
  isPending: (payout) => ["awaiting_approval", "approved", "in_transit"].indexOf(payout.status) > -1,
  isLate: (payout, now) => !!payout.expectedAt && payout.expectedAt < now && PAYOUT_FLAGS.isPending(payout),
};

/* Expenses. The receipt rule is policy, so it is passed in rather than read. */
const EXPENSE_FLAGS = {
  isSettled: (expense) => expense.status !== "pending",
  awaitsReview: (expense) => expense.status === "needs_review",
  needsReceipt: (expense, policy) => !expense.receiptAt && (expense.amount >= policy.receiptThresholdCents || policy.receiptCategories.indexOf(expense.category) > -1),
  /* Waiting on us AND nothing in the way — the act-ability predicate the row,
     the tile, the bulk bar and the sidebar badge all call. */
  isReviewable: (expense, policy) => EXPENSE_FLAGS.awaitsReview(expense) && !EXPENSE_FLAGS.needsReceipt(expense, policy),
  waitsOnSpender: (expense, policy) => expense.status === "queried" || (EXPENSE_FLAGS.awaitsReview(expense) && EXPENSE_FLAGS.needsReceipt(expense, policy)),
};

/* Cards. Month-to-date spend is computed in metrics.js and injected, like the
   clock: nothing here reaches for data. */
const CARD_FLAGS = {
  isSpendable: (card) => card.status === "active",
  hasLimit: (card) => card.spendLimit > 0,
  isOverLimit: (card, spentCents) => CARD_FLAGS.hasLimit(card) && spentCents >= card.spendLimit,
  isNearLimit: (card, spentCents, policy) => CARD_FLAGS.hasLimit(card) && !CARD_FLAGS.isOverLimit(card, spentCents) && spentCents / card.spendLimit >= policy.cardWarnShare,
  isExpiringSoon: (card, now) => !!card.expiresAt && card.status !== "cancelled" && card.expiresAt - now < 60 * 24 * 3600e3,
};

/* customer.status — whether this company is trading with us.
   Tone answers "can money move to and from them": success = trading normally,
   warning = we paused it ourselves and can resume, danger = risk stopped it,
   neutral = a deliberate ending.
   Deliberately absent: `disputing` (a dispute is its own record and
   CUSTOMER_FLAGS.hasOpenDispute reads it) and `dormant` (derived from the date
   of their last payment). */
const CUSTOMER_STATUS = [
  { id: "live", label: "Live", tone: "success" },
  { id: "on_hold", label: "On hold", tone: "warning" },
  { id: "suspended", label: "Suspended", tone: "danger" },
  { id: "closed", label: "Closed", tone: "neutral" },
];
/* ids: "live" | "on_hold" | "suspended" | "closed" */

const DISPUTE_FLAGS = {
  isOpen: (dispute) => ["needs_response", "under_review"].indexOf(dispute.status) > -1,
  needsEvidence: (dispute) => dispute.status === "needs_response",
  isClosingSoon: (dispute, now) => !!dispute.evidenceDueAt && dispute.evidenceDueAt - now < 48 * 3600e3,
};

/* Transitions, named once and derived from the current status: one function per
   entity, called by the row menu, the detail header and the bulk bar alike.
   kind: "primary" the record's normal path · "undo" instant + consequential,
   raises an undo toast · "confirm" irreversible, opens a confirm dialog. */
function paymentTransitions(payment) {
  switch (payment.status) {
    case "authorized": return [{ id: "capture", label: "Capture payment", kind: "undo", primary: true }];
    case "succeeded": return [{ id: "refund", label: "Refund payment", kind: "confirm", destructive: true }];
    case "failed": return [{ id: "retry", label: "Retry payment", kind: "undo", primary: true }];
    default: return [];
  }
}
/* A held payout offers nothing — the row names the team holding it. Above the
   approver's own limit, Approve exists and cannot be taken: the precondition is
   visible, so it is disabled with the reason rather than omitted. Reject stays
   live, because turning a payout down keeps the money and is not what the limit
   gates. */
function payoutTransitions(payout, approver) {
  if (!PAYOUT_FLAGS.awaitsApproval(payout) || PAYOUT_FLAGS.isBlocked(payout)) return [];
  const above = PAYOUT_FLAGS.isAboveLimit(payout, approver);
  return [
    { id: "approve", label: "Approve payout", kind: "undo", primary: true, disabled: above, disabledReason: above ? "Above your " + formatMoney(approver.approvalLimit) + " approval limit — somebody with a higher one has to release it." : undefined },
    { id: "reject", label: "Reject payout", kind: "confirm", destructive: true },
  ];
}
/* A dispute is answered or conceded; under review the clock belongs to the
   network, so nothing is offered and the screen says who is being waited on. */
function disputeTransitions(dispute) {
  switch (dispute.status) {
    case "needs_response": return [
      { id: "submit_evidence", label: "Submit evidence", kind: "form", primary: true },
      { id: "accept_dispute", label: "Accept dispute", kind: "confirm", destructive: true },
    ];
    default: return [];
  }
}

/* An expense is signed off, sent back, or turned down. Nothing is offered while
   the card authorisation is still settling, or once the money is dealt with; a
   queried expense is waiting on the person who spent it, so the row says so
   instead of carrying a button. Approve exists but cannot be taken until the
   receipt policy is met — that is a visible precondition, so it is disabled
   with the reason rather than omitted. */
function expenseTransitions(expense, policy) {
  if (expense.status !== "needs_review") return [];
  const missing = EXPENSE_FLAGS.needsReceipt(expense, policy);
  return [
    { id: "approve_expense", label: "Approve expense", kind: "undo", primary: true, disabled: missing, disabledReason: missing ? "A receipt is required before this can be approved — " + labelOf(EXPENSE_CATEGORIES, expense.category).toLowerCase() + " spend, " + Math.round(policy.receiptThresholdCents / 100) + " dollar threshold." : undefined },
    { id: "query_expense", label: "Query expense", kind: "form" },
    { id: "deny_expense", label: "Deny expense", kind: "confirm", destructive: true },
  ];
}
/* A card is frozen and unfrozen freely; cancelling one cannot be undone. A card
   the holder has not activated yet is waiting on them, so freezing it is
   meaningless and is omitted rather than disabled. */
function cardTransitions(card) {
  switch (card.status) {
    case "active": return [
      { id: "freeze_card", label: "Freeze card", kind: "undo" },
      { id: "cancel_card", label: "Cancel card", kind: "confirm", destructive: true },
    ];
    case "frozen": return [
      { id: "unfreeze_card", label: "Unfreeze card", kind: "undo", primary: true },
      { id: "cancel_card", label: "Cancel card", kind: "confirm", destructive: true },
    ];
    case "pending_activation": return [{ id: "cancel_card", label: "Cancel card", kind: "confirm", destructive: true }];
    default: return [];
  }
}

/* A customer is paused, resumed, or ended. Putting one on hold is reversible,
   so it is not destructive; closing an account is, so it opens a confirm
   dialog. Nothing is offered on a closed account. */
function customerTransitions(customer) {
  switch (customer.status) {
    case "live": return [
      { id: "hold_customer", label: "Put on hold", kind: "undo" },
      { id: "close_customer", label: "Close account", kind: "confirm", destructive: true },
    ];
    case "on_hold": return [
      { id: "resume_customer", label: "Resume payouts", kind: "undo", primary: true },
      { id: "close_customer", label: "Close account", kind: "confirm", destructive: true },
    ];
    case "suspended": return [
      { id: "resume_customer", label: "Resume payouts", kind: "undo", primary: true },
      { id: "close_customer", label: "Close account", kind: "confirm", destructive: true },
    ];
    default: return [];
  }
}

/* Workspace settings — classification lists, not statuses. */
const PAYOUT_SCHEDULES = [
  { value: "daily", label: "Daily", note: "Every business day, two days after the money settles." },
  { value: "weekly", label: "Weekly, on Fridays", note: "One transfer a week, covering the week to Wednesday." },
  { value: "manual", label: "Manual", note: "Nothing leaves the balance until somebody asks for it." },
];
const NOTIFICATION_KINDS = [
  { value: "payout_approval", label: "Payouts waiting on my approval" },
  { value: "dispute_evidence", label: "Disputes with evidence due" },
  { value: "payment_failed", label: "Failed payments" },
  { value: "balance_low", label: "Available balance below the floor" },
  { value: "expense_review", label: "Card expenses waiting on my review" },
  { value: "card_limit", label: "A card near its monthly limit" },
];

/* What Meridian charges this workspace. Policy, not status: the plan is a
   workspace setting, and these are the rates behind it. /settings/billing
   computes every line from these constants over the payment, payout, dispute,
   card and member arrays — so a fee on that screen can always be checked
   against the rows on the destination it names. */
const BILLING_PLANS = [
  { value: "Launch", label: "Launch", monthlyCents: 0, seatsIncluded: 2, cardsIncluded: 2, note: "No monthly fee; every rate is the standard one." },
  { value: "Scale", label: "Scale", monthlyCents: 39900, seatsIncluded: 5, cardsIncluded: 10, note: "Five seats, ten live cards, approval limits per person and same-day wires." },
  { value: "Enterprise", label: "Enterprise", monthlyCents: 149900, seatsIncluded: 25, cardsIncluded: 50, note: "Negotiated processing, a named treasury contact and audit exports." },
];
const FEE_SCHEDULE = {
  /* Per method, because how the money arrives is what it costs. */
  method: {
    card: { rate: 0.029, fixedCents: 30 },
    wallet: { rate: 0.031, fixedCents: 30 },
    ach: { rate: 0.008, capCents: 500 },
    wire: { rate: 0, fixedCents: 1500 },
  },
  payoutFixedCents: 25,
  disputeFixedCents: 1500,
  extraSeatCents: 900,
  extraCardCents: 300,
};

const CUSTOMER_FLAGS = {
  /* Both derived, so neither is a status value. The clock is injected. */
  hasOpenDispute: (customer, disputes) => disputes.some((d) => d.customer === customer.name && DISPUTE_FLAGS.isOpen(d)),
  isDormant: (customer, payments, now) => {
    const theirs = payments.filter((p) => p.customer === customer.name);
    return customer.status !== "closed" && (!theirs.length || now - Math.max.apply(null, theirs.map((p) => p.createdAt)) > 45 * 24 * 3600e3);
  },
};

/* Notes have NO status field. A note is a sentence somebody wrote, and the only
   question a screen asks about it — is a reply still owed — is computed from the
   thread rather than stored: the person mentioned has not written on that record
   since. Adding `answered` here would be a second place for the same truth. */
const COMMENT_FLAGS = {
  mentions: (comment, userId) => comment.mentions.indexOf(userId) > -1,
  isAwaitingReply: (comment, userId, all) =>
    comment.mentions.indexOf(userId) > -1 &&
    !all.some((c) => c.record.type === comment.record.type && c.record.id === comment.record.id && c.authorId === userId && c.at > comment.at),
};

const STATUS_FIELDS = {
  "payment.status": { entity: "payment", field: "status", means: "Charge lifecycle", values: PAYMENT_STATUS },
  "payout.status": { entity: "payout", field: "status", means: "Transfer lifecycle", values: PAYOUT_STATUS },
  "dispute.status": { entity: "dispute", field: "status", means: "Chargeback lifecycle", values: DISPUTE_STATUS },
  "expense.status": { entity: "expense", field: "status", means: "Card spend review", values: EXPENSE_STATUS },
  "card.status": { entity: "card", field: "status", means: "Card lifecycle", values: CARD_STATUS },
  "customer.status": { entity: "customer", field: "status", means: "Whether money can move to and from them", values: CUSTOMER_STATUS },
};

/* Coverage — values with no fixture yet, so nobody assumes they are unused:
   dispute.status `won` and `lost`. Both are one action away in the kit —
   submitting evidence moves a dispute to `under_review` and accepting one
   moves it to `lost`. Every value of expense.status and card.status has at
   least one fixture, and each is also reachable by an action: approving,
   querying and denying an expense; freezing, unfreezing and cancelling a card;
   and a newly issued card starts at `pending_activation`. */

Object.assign(window, {
  PAYMENT_STATUS, PAYOUT_STATUS, DISPUTE_STATUS, EXPENSE_STATUS, CARD_STATUS, CUSTOMER_STATUS,
  PAYMENT_METHODS, EXPENSE_CATEGORIES, CARD_TYPES, TEAM_ROLES, PAYOUT_SCHEDULES, NOTIFICATION_KINDS,
  BILLING_PLANS, FEE_SCHEDULE,
  MERIDIAN_POLICY_DEFAULTS,
  PAYMENT_FLAGS, PAYOUT_FLAGS, DISPUTE_FLAGS, EXPENSE_FLAGS, CARD_FLAGS, CUSTOMER_FLAGS, COMMENT_FLAGS,
  customerTransitions,
  paymentTransitions, payoutTransitions, disputeTransitions, expenseTransitions, cardTransitions, STATUS_FIELDS,
});
