/* Claims Desk — every figure on a screen comes from one of these helpers, so a
   number rendered twice is one function called twice. Each takes the array it
   summarises; none of them holds data.

   The money chain is the important one: claimed → deductions → assessed → cap
   → payable. No screen subtracts anything itself. */

const sumBy = (rows, key) => rows.reduce((total, row) => total + row[key], 0);
const sumOf = (rows, get) => rows.reduce((total, row) => total + get(row), 0);

/* ── The liability cap the bill of lading imposes ─────────────────────── */
/* Hague-Visby art. IV(5)(a): the HIGHER of 666.67 SDR per package and 2 SDR
   per kilo, converted at one rate held in CLAIM_POLICY. */
function liabilityCap(claim) {
  const byPackageSdr = claim.packages * CLAIM_POLICY.packageLimitSdr;
  const byWeightSdr = claim.weightKg * CLAIM_POLICY.weightLimitSdrPerKg;
  const sdr = Math.max(byPackageSdr, byWeightSdr);
  return {
    byPackageSdr: byPackageSdr,
    byWeightSdr: byWeightSdr,
    sdr: sdr,
    basis: byPackageSdr >= byWeightSdr ? "package" : "weight",
    cents: Math.round(sdr * CLAIM_POLICY.sdrUsdRate * 100),
  };
}

/* The one reconciliation. Pass draft adjustments to see the ledger the adjuster
   is typing; pass nothing and it reads the claim's saved assessment. */
function assess(claim, draft) {
  const adjustments = draft || claim.assessment;
  const cap = liabilityCap(claim);
  if (!adjustments) return { assessed: null, payable: null, cap: cap, capBinds: false };
  const depreciation = adjustments.depreciationCents || 0;
  const salvage = adjustments.salvageCents || 0;
  const deductible = adjustments.deductibleCents || 0;
  const deductions = depreciation + salvage + deductible;
  const assessed = claim.claimedCents - deductions;
  const payable = Math.min(assessed, cap.cents);
  return {
    claimed: claim.claimedCents,
    depreciation: depreciation, salvage: salvage, deductible: deductible, deductions: deductions,
    assessed: assessed, cap: cap, payable: payable,
    capBinds: assessed > cap.cents,
    cappedAway: Math.max(0, assessed - cap.cents),
  };
}

/* What this claim is worth to the customer today. The settlement owns the
   figure once one exists; before that the assessment does, and a rejected claim
   is worth nothing whatever the assessment said. */
function payableOf(claim) {
  if (claim.settlement) return claim.settlement.amountCents;
  if (claim.decision && claim.decision.outcome === "reject") return 0;
  return assess(claim).payable;
}

/* ── The queue ───────────────────────────────────────────────────────── */
const openClaims = (claims) => claims.filter(CLAIM_FLAGS.isOpen);
const closedClaims = (claims) => claims.filter((c) => !CLAIM_FLAGS.isOpen(c));
/* The act-ability predicate, applied to a list. */
const decidableClaims = (claims) => claims.filter(CLAIM_FLAGS.isDecidable);
const waitingClaims = (claims) => claims.filter(CLAIM_FLAGS.isWaitingOnOthers);
/* Ready for the adjuster but not yet decidable — the difference between these
   two arrays is what the queue headline has to explain in words. */
const stalledClaims = (claims) => claims.filter((c) => CLAIM_FLAGS.awaitsAdjuster(c) && !CLAIM_FLAGS.evidenceComplete(c));
const claimsInsideDays = (claims, days, now) => openClaims(claims).filter((c) => CLAIM_FLAGS.daysLeft(c, now) <= days);
const claimedExposure = (rows) => sumBy(rows, "claimedCents");
const byUrgency = (claims, now) => claims.slice().sort((a, b) => CLAIM_FLAGS.daysLeft(a, now) - CLAIM_FLAGS.daysLeft(b, now));

/* Who owes the next move, in words, for a claim the adjuster cannot act on. */
function waitingOn(claim) {
  switch (claim.status) {
    case "awaiting_documents": return "the customer";
    case "awaiting_survey": return "the surveyor";
    case "awaiting_signoff": return CLAIMS_MANAGER.name;
    case "escalated_legal": return "Legal";
    case "approved_awaiting_payment": return claim.settlement && claim.settlement.status === "unscheduled" ? "you, on the payments desk" : "Treasury";
    default: return null;
  }
}

/* ── Payments ────────────────────────────────────────────────────────── */
const settledClaims = (claims) => claims.filter(SETTLEMENT_FLAGS.exists);
const settlementsWith = (claims, status) => settledClaims(claims).filter((c) => c.settlement.status === status);
const settlementsToSchedule = (claims) => settledClaims(claims).filter(SETTLEMENT_FLAGS.needsScheduling);
const settlementTotal = (rows) => sumOf(rows, (c) => c.settlement.amountCents);
/* The release pipeline, in the order money moves. Every stage is a filter over
   the same array the table renders. */
function releasePipeline(claims) {
  return SETTLEMENT_STATUS.map((value) => {
    const rows = settlementsWith(claims, value.id);
    return { id: value.id, label: value.label, rows: rows, count: rows.length, amount: settlementTotal(rows) };
  }).filter((stage) => stage.count > 0);
}
/* Payment runs leave on Wednesdays. One helper, so the toast, the row and the
   dialog all name the same date. */
function nextRunDate(now) {
  const d = new Date(now);
  d.setHours(9, 0, 0, 0);
  const delta = (3 - d.getDay() + 7) % 7 || 7;
  return d.getTime() + delta * 864e5;
}

/* ── Responses ───────────────────────────────────────────────────────── */
const offeredClaims = (claims) => claims.filter(RESPONSE_FLAGS.exists);
const offersWith = (claims, status) => offeredClaims(claims).filter((c) => c.response.status === status);
const disputedOffers = (claims) => offersWith(claims, "disputed");
const answeredOffers = (claims) => offeredClaims(claims).filter((c) => c.response.status !== "awaiting_response");
const disputedGapTotal = (claims) => sumOf(disputedOffers(claims), (c) => RESPONSE_FLAGS.gap(c) || 0);

/* ── Recovery ────────────────────────────────────────────────────────── */
const openRecoveries = (recoveries) => recoveries.filter(RECOVERY_FLAGS.isOpen);
const closedRecoveries = (recoveries) => recoveries.filter(RECOVERY_FLAGS.isClosed);
const recoveriesToFile = (recoveries) => recoveries.filter(RECOVERY_FLAGS.awaitsAdjuster);
/* The figure the managers read. Money-weighted, over closed files only: an
   open recovery has not failed yet, and including it would flatter or damn the
   rate depending only on how much is in flight. */
function recoveryRate(recoveries) {
  const closed = closedRecoveries(recoveries);
  const pursued = sumBy(closed, "soughtCents");
  if (!pursued) return null;
  return sumBy(closed, "recoveredCents") / pursued;
}
function recoveryByKind(recoveries, kinds) {
  const pursued = sumBy(recoveries, "soughtCents");
  return kinds
    .map((kind) => {
      const rows = recoveries.filter((r) => r.counterparty.kind === kind.value);
      return {
        kind: kind.value, label: kind.label, count: rows.length,
        sought: sumBy(rows, "soughtCents"), recovered: sumBy(rows, "recoveredCents"),
        share: pursued ? sumBy(rows, "soughtCents") / pursued : 0,
      };
    })
    .filter((row) => row.count > 0)
    .sort((a, b) => b.sought - a.sought);
}

/* ── Sign-off ────────────────────────────────────────────────────────── */
/* The adjuster's side of the manager's queue: what she has sent up, what it is
   worth, and how long it has been sitting. She signs none of it. */
const awaitingSignoff = (claims) => claims.filter(CLAIM_FLAGS.awaitsSignoff);
const signedOffClaims = (claims) => claims.filter((c) => c.decision && c.decision.signedOffBy);
const decidedClaims = (claims) => claims.filter((c) => !!c.decision);
/* Payable is the figure the manager is being asked to approve, so it is read
   from the one owner rather than recomputed. */
const signoffValue = (rows) => sumOf(rows, (c) => payableOf(c) || 0);
const bySignoffWait = (claims) => awaitingSignoff(claims).slice().sort((a, b) => CLAIM_FLAGS.signoffWaitingSince(a) - CLAIM_FLAGS.signoffWaitingSince(b));
const longestSignoffWait = (claims) => bySignoffWait(claims)[0] || null;
/* Elapsed time, whole days. formatCountdown owns deadline words ("11 days
   over"); a wait is not a deadline, so it gets the plain count and the sentence
   around it says who is holding it. */
const daysSince = (ts, now) => Math.max(0, Math.round((now - ts) / 864e5));

/* ── Claimants ───────────────────────────────────────────────────────── */
/* A claimant record holds no money of its own: every figure below is the
   claimant's own claims read through the helpers the other screens use. */
const claimantById = (claimants, id) => claimants.find((p) => p.id === id) || null;
const claimantOf = (claimants, claim) => (claim ? claimants.find((p) => p.name === claim.customer) || null : null);
const claimsOfClaimant = (claims, claimant) => claims.filter((c) => c.customer === claimant.name);
const releasedClaims = (claims) => claims.filter(SETTLEMENT_FLAGS.isReleased);
const releasedTotal = (claims) => sumOf(releasedClaims(claims), (c) => c.settlement.amountCents);
const recoveriesForClaims = (recoveries, claims) => recoveries.filter((r) => claims.some((c) => c.id === r.claimId));
/* Money has left the account and nobody is pursuing it yet — the only claims a
   new recovery can be opened against. */
const claimsWithoutRecovery = (claims) => claims.filter((c) => SETTLEMENT_FLAGS.isReleased(c) && !c.recoveryId);
function claimantSummary(claimant, claims, recoveries, now) {
  const rows = claimsOfClaimant(claims, claimant);
  const open = openClaims(rows);
  return {
    rows: rows,
    open: open,
    claimed: claimedExposure(rows),
    openClaimed: claimedExposure(open),
    released: releasedClaims(rows),
    releasedTotal: releasedTotal(rows),
    rejected: rows.filter((c) => c.status === "rejected"),
    offers: offeredClaims(rows),
    answered: answeredOffers(rows),
    accepted: offersWith(rows, "accepted"),
    disputed: disputedOffers(rows),
    recoveries: recoveriesForClaims(recoveries, rows),
    firstClaim: rows.slice().sort((a, b) => a.claimedAt - b.claimedAt)[0] || null,
    nearestBar: byUrgency(open, now)[0] || null,
  };
}

/* ── Notifications ───────────────────────────────────────────────────── */
const unreadNotifications = (items, readIds) => items.filter((n) => readIds.indexOf(n.id) === -1);
const notificationsOfKind = (items, kind) => items.filter((n) => n.kind === kind);
/* Grouped for the page, newest first, one heading per calendar day. */
function notificationsByDay(items, now) {
  const days = [];
  items.slice().sort((a, b) => b.at - a.at).forEach((n) => {
    const key = new Date(n.at).toDateString();
    const found = days.find((d) => d.key === key);
    if (found) found.items.push(n);
    else days.push({ key: key, at: n.at, items: [n] });
  });
  return days;
}

/* ── Lookups ─────────────────────────────────────────────────────────── */
const claimById = (claims, id) => claims.find((c) => c.id === id) || null;
const recoveryById = (recoveries, id) => recoveries.find((r) => r.id === id) || null;
const recoveryForClaim = (recoveries, claim) => (claim && claim.recoveryId ? recoveryById(recoveries, claim.recoveryId) : null);

/* The nav badge predicates, resolved by name from nav.js. Each one counts a
   single kind of record, and says so. */
const NAV_BADGES = {
  claimsDecidable: {
    /* Claims you can decide now — ready for decision AND every mandatory
       document on file. The queue headline names the ones this excludes. */
    count: (data) => decidableClaims(data.claims).length,
    label: "claims you can decide now",
  },
  signoffWaiting: {
    /* Not act-ability — nobody on this desk can sign these. It counts what is
       stuck with the manager, which is the reason to open the screen. */
    count: (data) => awaitingSignoff(data.claims).length,
    label: "decisions waiting on a signature",
  },
  settlementsToSchedule: {
    count: (data) => settlementsToSchedule(data.claims).length,
    label: "payments to schedule",
  },
  offersDisputed: {
    count: (data) => disputedOffers(data.claims).length,
    label: "offers the customer has disputed",
  },
  recoveriesToFile: {
    count: (data) => recoveriesToFile(data.recoveries).length,
    label: "recoveries to file",
  },
};

Object.assign(window, {
  sumBy, sumOf, liabilityCap, assess, payableOf,
  openClaims, closedClaims, decidableClaims, waitingClaims, stalledClaims, claimsInsideDays,
  claimedExposure, byUrgency, waitingOn,
  settledClaims, settlementsWith, settlementsToSchedule, settlementTotal, releasePipeline, nextRunDate,
  offeredClaims, offersWith, disputedOffers, answeredOffers, disputedGapTotal,
  openRecoveries, closedRecoveries, recoveriesToFile, recoveryRate, recoveryByKind,
  awaitingSignoff, signedOffClaims, decidedClaims, signoffValue, bySignoffWait, longestSignoffWait, daysSince,
  claimantById, claimantOf, claimsOfClaimant, releasedClaims, releasedTotal, recoveriesForClaims, claimsWithoutRecovery, claimantSummary,
  unreadNotifications, notificationsOfKind, notificationsByDay,
  claimById, recoveryById, recoveryForClaim, NAV_BADGES,
});
