/* Marque Talent Desk — 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.

   Agency policy constants live here too: they are part of this product's
   vocabulary (the commission floor, the usage uplifts, the notice window), and
   nothing outside this folder may read them. */

const AGENCY_POLICY = {
  /* Standard commission. Deals may be negotiated below it; the deal stores its
     own rate and the ledger reads that, never this. */
  standardCommissionRate: 0.2,
  /* A creator counts as "coming free" this many days before their booking ends,
     because that is how far ahead brands ask us to hold someone. */
  comingFreeDays: 30,
  /* Deliverables inside this window are "due now" on the week ladder and in the
     nav badge. One number, one meaning. */
  dueSoonDays: 7,
  /* Invoices are net-30 from issue. One term, one place. */
  paymentTermsDays: 30,
  /* Usage rights inside this many days are "expiring": the window in which a
     renewal is still a conversation rather than a takedown. */
  rightsNoticeDays: 60,
  /* Sign-in attempts before the account locks (AGENTS §7.3.6: lockout is a
     designed state, not a toast). One number, read by the form and by the
     copy that counts down. */
  signInAttempts: 3,
  /* A brief whose per-creator budget cannot buy anyone on the books is below
     the floor. The floor is derived from the roster, never typed — see
     rosterFloor() in metrics.js. */
};

/* creator.status — whether we can sell this creator today.
   Three values, three tones, grouped by the question the roster asks: "can I
   pitch them?"
     success = pitchable now
     info    = working, and the roster says until when
     neutral = they have asked us to stop pitching them
   Deliberately absent because they are derived: `coming_free` (bookedUntil
   inside AGENCY_POLICY.comingFreeDays), `conflicted` (an exclusivity that bites
   against one particular brief — it is a property of the pair, not of the
   creator), and `overbooked`. */
const CREATOR_STATUS = [
  { id: "available", label: "Free", tone: "success" },
  { id: "booked", label: "Booked", tone: "info" },
  { id: "paused", label: "Paused", tone: "neutral" },
];
/* ids: "available" | "booked" | "paused" */

/* brief.status — an inbound brand request, from arrival to outcome.
   warning = on the talent manager's desk · info = with the brand ·
   success = it became a deal · neutral = closed, by them or by us.
   `passed` is neutral, not danger: turning work down is a decision we made and
   can defend. Deliberately absent because derived: `expiring` (the brand's
   respond-by date against the clock). */
const BRIEF_STATUS = [
  { id: "to_triage", label: "To triage", tone: "warning" },
  { id: "pitching", label: "Pitched", tone: "info" },
  { id: "won", label: "Won", tone: "success" },
  { id: "passed", label: "Passed", tone: "neutral" },
  { id: "lost", label: "Lost", tone: "neutral" },
];
/* ids: "to_triage" | "pitching" | "won" | "passed" | "lost" */

/* contract.status — where the paper on a deal stands.
   warning = on us to move it · info = with the brand and the creator ·
   success = executed. A deal exists from the moment terms are agreed, so
   `drafted` is the first state and there is no "none". */
const CONTRACT_STATUS = [
  { id: "drafted", label: "Drafted", tone: "warning" },
  { id: "out_for_signature", label: "Out for signature", tone: "info" },
  { id: "signed", label: "Signed", tone: "success" },
  { id: "void", label: "Void", tone: "neutral" },
];
/* ids: "drafted" | "out_for_signature" | "signed" | "void"
   `void` exists because voiding a deal has to land somewhere: a deal killed
   before signature is not "drafted" any more and never became "signed". It is
   neutral, not danger — walking away from unsigned paper is a decision we made
   and can defend. Only reachable while nothing is signed and nothing is live. */

/* deliverable.status — one post or asset on one deal.
   neutral = nothing to do yet · info = with the brand · warning = it has come
   back and somebody has to fix it · success = live.
   Deliberately absent because derived: `overdue` (dueAt against the clock — a
   revision that is late is still `revisions_requested`) and `due_soon`. */
const DELIVERABLE_STATUS = [
  { id: "scheduled", label: "Scheduled", tone: "neutral" },
  { id: "submitted", label: "With brand", tone: "info" },
  { id: "revisions_requested", label: "Revisions requested", tone: "warning" },
  { id: "approved", label: "Approved", tone: "info" },
  { id: "published", label: "Published", tone: "success" },
];
/* ids: "scheduled" | "submitted" | "revisions_requested" | "approved" | "published" */

/* invoice.status — what the brand owes the agency.
   warning = on us to issue it · info = with the brand · success = banked.
   Deliberately absent because derived: `overdue` (dueAt < now and not paid). */
const INVOICE_STATUS = [
  { id: "draft", label: "Not issued", tone: "warning" },
  { id: "issued", label: "Issued", tone: "info" },
  { id: "paid", label: "Paid", tone: "success" },
];
/* ids: "draft" | "issued" | "paid" */

/* payout.status — what the agency owes the creator, after commission.
   neutral = we cannot pay yet because the brand has not paid us, and the row
   says which brand · warning = the money is in and it is on us · success =
   gone. A payout is never `overdue`: if it is late it is late because the
   invoice is, and that is the invoice's fact to carry. */
const PAYOUT_STATUS = [
  { id: "awaiting_funds", label: "Awaiting funds", tone: "neutral" },
  { id: "due", label: "Due", tone: "warning" },
  { id: "paid", label: "Paid", tone: "success" },
];
/* ids: "awaiting_funds" | "due" | "paid" */

/* ── Classification lists — read through labelOf, never reaching a Badge. ── */

const CATEGORIES = [
  { value: "beauty", label: "Beauty" },
  { value: "fitness", label: "Fitness" },
  { value: "food", label: "Food & drink" },
  { value: "home", label: "Home & interiors" },
  { value: "fashion", label: "Fashion" },
  { value: "tech", label: "Technology" },
  { value: "travel", label: "Travel" },
  { value: "dating", label: "Dating & social" },
];

const PLATFORMS = [
  { value: "instagram", label: "Instagram", handlePrefix: "@" },
  { value: "tiktok", label: "TikTok", handlePrefix: "@" },
  { value: "youtube", label: "YouTube", handlePrefix: "" },
];

/* The rate card is keyed by these. A creator without a rate for a kind cannot
   quote for a brief that asks for it — that is a rate miss, not a zero. */
const DELIVERABLE_KINDS = [
  { value: "ig_reel", label: "Instagram Reel", platform: "instagram" },
  { value: "ig_story", label: "Story frame", platform: "instagram" },
  { value: "tiktok", label: "TikTok video", platform: "tiktok" },
  { value: "yt_integration", label: "YouTube integration", platform: "youtube" },
  { value: "ugc_pack", label: "UGC asset pack", platform: "instagram" },
];

/* Usage rights. The uplift is a multiplier on the organic fee — one number per
   scope, in one place, so the quote on the match screen and the fee on the deal
   sheet can never disagree. */
const USAGE_SCOPES = [
  { value: "organic", label: "Organic only", uplift: 0, months: 0 },
  { value: "paid_social_3m", label: "Paid social", uplift: 0.35, months: 3 },
  { value: "whitelisting_6m", label: "Whitelisting", uplift: 0.6, months: 6 },
  { value: "buyout_12m", label: "Full buyout", uplift: 1.2, months: 12 },
];

/* Why we turned a brief down. One of these is required to pass on a brief. */
const PASS_REASONS = [
  { value: "below_floor", label: "Budget below our floor" },
  { value: "wrong_category", label: "No one on the books in this category" },
  { value: "no_availability", label: "Nobody free in the brand's window" },
  { value: "brand_conflict", label: "Exclusivity conflict with a competing brand" },
];

/* The one password constant: the live checklist on the set-new screen and the
   validation that blocks its submit read this same list (AGENTS §7.5). */
const PASSWORD_RULES = [
  { id: "length", label: "At least 12 characters", test: (v) => v.length >= 12 },
  { id: "case", label: "An upper and a lower case letter", test: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) },
  { id: "number", label: "A number", test: (v) => /[0-9]/.test(v) },
];
const passwordMeets = (value) => PASSWORD_RULES.every((rule) => rule.test(value || ""));

/* ── Derived flags. Clock injected, never read from the module. ─────────── */

const CREATOR_FLAGS = {
  isPitchable: (creator) => creator.status === "available",
  /* Booked, but the booking ends inside the notice window — the answer to the
     question brands ask most often. */
  isComingFree: (creator, now) =>
    creator.status === "booked" &&
    creator.bookedUntil - now <= AGENCY_POLICY.comingFreeDays * 864e5 &&
    creator.bookedUntil >= now,
  daysUntilFree: (creator, now) => Math.round((creator.bookedUntil - now) / 864e5),
  /* Exclusivities that are still running today. */
  liveExclusivities: (creator, now) => (creator.exclusivities || []).filter((e) => e.until > now),
};

const BRIEF_FLAGS = {
  /* THE act-ability predicate for briefs. The nav badge, the triage deck and
     the tab count all call this one function. */
  needsTriage: (brief) => brief.status === "to_triage",
  isOpen: (brief) => ["to_triage", "pitching"].indexOf(brief.status) > -1,
  isClosed: (brief) => ["passed", "lost"].indexOf(brief.status) > -1,
  daysToRespond: (brief, now) => Math.round((brief.respondByAt - now) / 864e5),
  /* The brand wants the campaign live inside this window; availability is
     judged against its start, never against today. */
  windowStart: (brief) => brief.liveFromAt,
};

const CONTRACT_FLAGS = {
  isSigned: (deal) => deal.contract.status === "signed",
  isVoid: (deal) => deal.contract.status === "void",
  /* On the talent manager to move the paper. */
  awaitsAgency: (deal) => deal.contract.status === "drafted",
  awaitsCountersign: (deal) => deal.contract.status === "out_for_signature",
  daysOut: (deal, now) => (deal.contract.sentAt ? Math.round((now - deal.contract.sentAt) / 864e5) : null),
};

const DELIVERABLE_FLAGS = {
  isLive: (item) => item.status === "published",
  isWithBrand: (item) => ["submitted", "approved"].indexOf(item.status) > -1,
  needsRevision: (item) => item.status === "revisions_requested",
  /* Handed over. A deliverable that is with the brand cannot be late: we did
     our part on time and the clock is theirs now. */
  isDelivered: (item) => ["submitted", "approved", "published"].indexOf(item.status) > -1,
  /* Late, and it is still ours to fix. */
  isOverdue: (item, now) => item.dueAt < now && !DELIVERABLE_FLAGS.isDelivered(item),
  isDueSoon: (item, now) =>
    item.dueAt >= now &&
    item.dueAt - now <= AGENCY_POLICY.dueSoonDays * 864e5 &&
    !DELIVERABLE_FLAGS.isDelivered(item),
  /* THE act-ability predicate for deliverables: work the talent manager has to
     move this week. The nav badge, the "Needs you" section and the week ladder
     all call this. */
  needsAttention: (item, now) =>
    DELIVERABLE_FLAGS.needsRevision(item) || DELIVERABLE_FLAGS.isOverdue(item, now) || DELIVERABLE_FLAGS.isDueSoon(item, now),
};

const INVOICE_FLAGS = {
  /* Net-30 from issue. The due date is derived, never stored, so changing the
     terms moves every date and every overdue count with it. */
  dueAt: (invoice) => (invoice.issuedAt ? invoice.issuedAt + AGENCY_POLICY.paymentTermsDays * 864e5 : null),
  isSettled: (invoice) => invoice.status === "paid",
  isOutstanding: (invoice) => invoice.status !== "paid",
  isOverdue: (invoice, now) => invoice.status === "issued" && INVOICE_FLAGS.dueAt(invoice) < now,
  awaitsAgency: (invoice) => invoice.status === "draft",
  daysOverdue: (invoice, now) => Math.round((now - INVOICE_FLAGS.dueAt(invoice)) / 864e5),
};

/* Usage rights on a signed deal. Not a status field: what a brand may run and
   until when is computed from the scope it bought and the date the window
   opened, so it is a set of derived flags and `usageEndsAt` in metrics.js
   delegates here rather than working it out a second time. An organic deal
   grants nothing that can expire, so it has no date and never flags. */
const RIGHTS_FLAGS = {
  monthsOf: (deal) => (USAGE_SCOPES.find((u) => u.value === deal.usage) || USAGE_SCOPES[0]).months,
  isTimeLimited: (deal) => RIGHTS_FLAGS.monthsOf(deal) > 0,
  endsAt: (deal) => {
    if (!RIGHTS_FLAGS.isTimeLimited(deal)) return null;
    const end = new Date(deal.usageStartAt);
    end.setMonth(end.getMonth() + RIGHTS_FLAGS.monthsOf(deal));
    return end.getTime();
  },
  /* Still running, and close enough that renewing is a conversation. */
  isExpiring: (deal, now) => {
    const end = RIGHTS_FLAGS.endsAt(deal);
    return end !== null && end >= now && end - now <= AGENCY_POLICY.rightsNoticeDays * 864e5;
  },
  hasLapsed: (deal, now) => {
    const end = RIGHTS_FLAGS.endsAt(deal);
    return end !== null && end < now;
  },
};

const PAYOUT_FLAGS = {
  isSettled: (payout) => payout.status === "paid",
  /* THE act-ability predicate for money going out: the brand has paid and it is
     on us. `awaiting_funds` is blocked, not actionable, and the row names the
     brand it is waiting on. */
  isReleasable: (payout) => payout.status === "due",
  isBlocked: (payout) => payout.status === "awaiting_funds",
};

/* ── Transitions, named once, derived from the current status. ────────────
   One function per entity, called by the row action, the record header and the
   section alike. A transition that does not apply is omitted, never disabled.
   kind: "primary" the record's normal path · "nav" moves to another screen ·
   "undo" instant + consequential, raises an undo toast · "confirm"
   irreversible, opens a confirm dialog · "form" opens a staged dialog. */

/* The brief's normal path is to match it to the roster. When nothing on the
   book fits it, the normal path is to say no — so the primary weight follows
   the fit count, decided here rather than on the screen. */
function briefTransitions(brief, fitCount) {
  const nobodyFits = fitCount === 0;
  switch (brief.status) {
    case "to_triage": return [
      { id: "pitch_creators", label: "Match creators", kind: "nav", primary: !nobodyFits },
      { id: "pass_brief", label: "Pass on brief", kind: "form", primary: nobodyFits },
    ];
    case "pitching": return [
      /* The brief's whole point is to become a deal, so booking one is the
         primary path the moment creators are in front of the brand. */
      { id: "book_deal", label: "Book the deal", kind: "form", primary: true },
      { id: "chase_brand", label: "Chase the brand", kind: "undo" },
      { id: "pass_brief", label: "Pass on brief", kind: "form" },
    ];
    case "passed":
    case "lost": return [{ id: "reopen_brief", label: "Reopen brief", kind: "undo", primary: true }];
    default: return [];
  }
}

function contractTransitions(deal) {
  switch (deal.contract.status) {
    case "drafted": return [{ id: "send_contract", label: "Send for signature", kind: "undo", primary: true }];
    case "out_for_signature": return [
      { id: "mark_signed", label: "Mark signed", kind: "undo", primary: true },
      { id: "chase_signature", label: "Chase signature", kind: "undo" },
    ];
    default: return [];
  }
}

/* Voiding a deal is the only irreversible thing on the deal sheet. It is
   offered only while the paper is unsigned and nothing has gone live — after
   either of those the transition is omitted, never disabled. */
function dealTransitions(deal, publishedCount) {
  if (publishedCount > 0) return [];
  if (["drafted", "out_for_signature"].indexOf(deal.contract.status) === -1) return [];
  return [{ id: "void_deal", label: "Void the deal", kind: "confirm", destructive: true }];
}

function deliverableTransitions(item) {
  switch (item.status) {
    case "scheduled": return [
      { id: "submit_to_brand", label: "Submit to brand", kind: "undo", primary: true },
      { id: "chase_creator", label: "Chase the creator", kind: "undo" },
    ];
    case "revisions_requested": return [
      { id: "submit_to_brand", label: "Submit to brand", kind: "undo", primary: true },
      { id: "chase_creator", label: "Chase the creator", kind: "undo" },
    ];
    case "submitted": return [
      { id: "mark_approved", label: "Mark approved", kind: "undo", primary: true },
      { id: "record_revisions", label: "Record revisions", kind: "form" },
    ];
    case "approved": return [{ id: "mark_published", label: "Mark published", kind: "form", primary: true }];
    default: return [];
  }
}

function invoiceTransitions(invoice) {
  switch (invoice.status) {
    case "draft": return [{ id: "issue_invoice", label: "Issue invoice", kind: "undo", primary: true }];
    case "issued": return [
      { id: "record_payment", label: "Record payment", kind: "undo", primary: true },
      { id: "chase_payment", label: "Chase payment", kind: "undo" },
    ];
    default: return [];
  }
}

function payoutTransitions(payout) {
  /* Money leaving the agency's account cannot be recalled. */
  return payout.status === "due" ? [{ id: "release_payout", label: "Release payout", kind: "confirm", primary: true }] : [];
}

const STATUS_FIELDS = {
  "creator.status": { entity: "creator", field: "status", means: "Whether we can sell this creator today", values: CREATOR_STATUS },
  "brief.status": { entity: "brief", field: "status", means: "An inbound brand request, from arrival to outcome", values: BRIEF_STATUS },
  "contract.status": { entity: "contract", field: "status", means: "Where the paper on a deal stands", values: CONTRACT_STATUS },
  "deliverable.status": { entity: "deliverable", field: "status", means: "One post or asset on one deal", values: DELIVERABLE_STATUS },
  "invoice.status": { entity: "invoice", field: "status", means: "What the brand owes the agency", values: INVOICE_STATUS },
  "payout.status": { entity: "payout", field: "status", means: "What the agency owes the creator after commission", values: PAYOUT_STATUS },
};

/* Coverage — values with no fixture yet, so nobody assumes they are unused:
   deliverable.status `approved` and contract.status `void`. Both are reachable
   only by taking the action that leads to them (approving something the brand
   has back, voiding an unsigned deal), which the kit does. `contract.status:
   drafted` has no fixture either and is where every booked deal starts, so
   booking one from a pitched brief lands on it.
   Every other value in every field appears at least once in fixtures.js. */

Object.assign(window, {
  AGENCY_POLICY,
  CREATOR_STATUS, BRIEF_STATUS, CONTRACT_STATUS, DELIVERABLE_STATUS, INVOICE_STATUS, PAYOUT_STATUS,
  CATEGORIES, PLATFORMS, DELIVERABLE_KINDS, USAGE_SCOPES, PASS_REASONS, PASSWORD_RULES, passwordMeets,
  CREATOR_FLAGS, BRIEF_FLAGS, CONTRACT_FLAGS, DELIVERABLE_FLAGS, INVOICE_FLAGS, PAYOUT_FLAGS, RIGHTS_FLAGS,
  briefTransitions, contractTransitions, dealTransitions, deliverableTransitions, invoiceTransitions, payoutTransitions,
  STATUS_FIELDS,
});
