/* Parley — the status vocabulary, the classification lists and the derived
   flags. A value not listed here does not exist: if a screen needs one, it is
   added here first, with a tone grouping and a reason, or the screen is wrong.

   Three status fields in this product. Everything else a screen shows about a
   record — how full its context is, whether a file needs a person, whether a
   prompt has gone stale — is DERIVED at render from the fields below and must
   never be added to an enum.

   Verbs, named once and used by every surface (row menu, header, bulk bar):
     New chat · New project · New prompt · Send · Stop · Retry · Continue ·
     Branch · Copy · Rename · Archive · Restore · Delete · Export ·
     Upload files · Reindex · Share with workspace · Make private ·
     Create public link · Disable link · Sign in · Create account · Sign out ·
     Use · Edit · Invite people · Resend invite · Revoke invite ·
     Remove from workspace · Change access · Change plan · Update card ·
     Retry payment · Set spend cap · Rename workspace */

/* ── Status fields ─────────────────────────────────────────────────────── */

/* Who can reach this thread or project. One field, one vocabulary, used by both
   entities because it means exactly the same thing on each.
   Tones answer "is anyone outside this account able to read it": private is the
   resting state (neutral), workspace is a deliberate widening that is still
   inside the company (info), a public link is a deliberate state that wants
   reviewing (warning). No danger value — nothing here is a failure. */
const VISIBILITY = [
  { id: "private", label: "Private", tone: "neutral" },
  { id: "workspace", label: "Workspace", tone: "info" },
  { id: "link", label: "Public link", tone: "warning" },
];

/* A single assistant turn. Tones answer "can I read this answer and move on":
   complete needs nothing (neutral), streaming is in flight (info), stopped was
   the user's own choice and can be continued (warning), failed is a failure
   event with a retry (danger).
   Coverage: `streaming` has no fixture — it exists only between Send and the
   last token, so it is reached by using the composer, never by loading a
   thread. Every other value has at least one message in fixtures.js. */
const MESSAGE_STATUS = [
  { id: "complete", label: "Complete", tone: "neutral" },
  { id: "streaming", label: "Writing", tone: "info" },
  { id: "stopped", label: "Stopped", tone: "warning" },
  { id: "failed", label: "Failed", tone: "danger" },
];

/* An uploaded file's place in the index. Tones answer "can the model read it
   yet": ready yes (success), indexing not yet but nothing is wrong (info),
   failed is a failure event (danger), unsupported is a deliberate refusal that
   needs a person to convert the file (warning). */
const FILE_STATUS = [
  { id: "ready", label: "Indexed", tone: "success" },
  { id: "indexing", label: "Indexing", tone: "info" },
  { id: "failed", label: "Failed", tone: "danger" },
  { id: "unsupported", label: "Unsupported", tone: "warning" },
];

/* An issued invoice. Tones answer "does this need a person": paid needs nothing
   (success), a failed payment is a failure event and the only value carrying an
   action (danger), void was withdrawn before it was ever charged (neutral).
   No value for the current cycle: it has not been invoiced, so it is derived in
   metrics.js rather than stored here.
   Coverage: every value has at least one invoice in fixtures.js. */
const INVOICE_STATUS = [
  { id: "paid", label: "Paid", tone: "success" },
  { id: "failed", label: "Payment failed", tone: "danger" },
  { id: "void", label: "Void", tone: "neutral" },
];

const STATUS_FIELDS = [
  { entity: "thread", field: "visibility", means: "Who can open it", values: VISIBILITY },
  { entity: "project", field: "visibility", means: "Who can open it", values: VISIBILITY },
  { entity: "message", field: "status", means: "How the turn ended", values: MESSAGE_STATUS },
  { entity: "file", field: "status", means: "Whether the model can read it", values: FILE_STATUS },
  { entity: "invoice", field: "status", means: "Whether it has been paid", values: INVOICE_STATUS },
];

/* ── Classifications — read through labelOf, never through a Badge ─────── */

/* The model catalogue. Rates are cents per million tokens; every cost figure in
   the product multiplies these, and no screen types a price. */
const MODELS = [
  { value: "corvus-l", label: "Corvus L", contextTokens: 200000, inRate: 300, outRate: 1500, blurb: "Deepest reasoning. Slowest, and the only model with the 200k window." },
  { value: "corvus-m", label: "Corvus M", contextTokens: 128000, inRate: 80, outRate: 400, blurb: "The everyday model. Fast enough to sit and read the answer as it lands." },
  { value: "corvus-s", label: "Corvus S", contextTokens: 32000, inRate: 15, outRate: 60, blurb: "Short tasks, rewrites and classification. Cheapest by an order of magnitude." },
];

/* What an account may do in the workspace. A permission level, not a status:
   it renders through labelOf and a Select, never through a Badge. Widest
   first, which is the order the Select offers them. */
const ACCESS_LEVELS = [
  { value: "owner", label: "Owner", note: "Owns the workspace and the bill. Exactly one, and it cannot be removed or changed from here." },
  { value: "admin", label: "Admin", note: "Invites people, changes the plan, and reads everything shared with the workspace." },
  { value: "member", label: "Member", note: "Their own threads and projects, plus anything shared with the workspace." },
];

/* The plans. Every figure on the billing screen multiplies one of these, and
   no screen types a price. `seatLimit` is what makes a plan unavailable to a
   workspace this size — the control is disabled with the reason, never hidden. */
const PLANS = [
  { value: "solo", label: "Solo", seatPrice: 1200, seatLimit: 3, includedTokens: 300000, blurb: "One or two people. Everything private, nothing shared." },
  { value: "studio", label: "Studio", seatPrice: 2400, seatLimit: 25, includedTokens: 1200000, blurb: "Shared projects, shared files, one bill." },
  { value: "company", label: "Company", seatPrice: 4000, seatLimit: 250, includedTokens: 4000000, blurb: "Adds enforced single sign-on, and four times the included tokens." },
];

/* How long a deleted thread's contents are kept before they go for good.
   Values are strings because a Select carries strings; the workspace stores the
   number of days. */
const RETENTION_OPTIONS = [
  { value: "0", label: "Keep forever" },
  { value: "30", label: "Delete after 30 days" },
  { value: "90", label: "Delete after 90 days" },
  { value: "365", label: "Delete after a year" },
];

/* What the search screen looks through. One entry per fixture array it reads. */
const SEARCH_KINDS = [
  { value: "threads", label: "Chats" },
  { value: "projects", label: "Projects" },
  { value: "files", label: "Files" },
  { value: "prompts", label: "Prompts" },
];

const PROMPT_CATEGORIES = [
  { value: "writing", label: "Writing" },
  { value: "code", label: "Code" },
  { value: "research", label: "Research" },
  { value: "ops", label: "Operations" },
];

const FEEDBACK = [
  { value: "up", label: "Helpful" },
  { value: "down", label: "Not helpful" },
];

const DELETE_REASONS = [
  { value: "finished", label: "The work is finished" },
  { value: "wrong_turn", label: "It went the wrong way" },
  { value: "sensitive", label: "It holds something it should not" },
  { value: "duplicate", label: "There is a better thread on this" },
];

/* ── Derived flags — computed at render, never stored, never an enum ──── */

const MESSAGE_FLAGS = {
  isAssistant: (m) => m.role === "assistant",
  needsAction: (m) => m.status === "failed" || m.status === "stopped",
};

const THREAD_FLAGS = {
  /* One predicate, called by the thread header, the rail meter and the
     composer's warning alike. The clock-equivalent here is the file list, and
     it is injected rather than reached for. */
  isNearLimit: (thread, files) => contextRatio(thread, files) >= 0.75,
  isFull: (thread, files) => contextRatio(thread, files) >= 1,
  isShared: (thread) => thread.visibility !== "private",
  lastMessage: (thread) => thread.messages[thread.messages.length - 1] || null,
  lastActivityAt: (thread) => (thread.messages.length ? thread.messages[thread.messages.length - 1].at : thread.createdAt),
  hasUnfinishedTurn: (thread) => thread.messages.some(MESSAGE_FLAGS.needsAction),
  isArchived: (thread) => !!thread.archivedAt,
};

const FILE_FLAGS = {
  /* The one act-ability predicate for files. The row tone, the project's
     "needs you" count and the sidebar badge all call this. */
  needsAttention: (f) => f.status === "failed" || f.status === "unsupported",
  inContext: (f) => f.status === "ready",
};

const PROJECT_FLAGS = {
  isArchived: (p) => !!p.archivedAt,
};

/* Sixty days without signing in. The member row's recession, the seat note and
   the workspace figure all call this one predicate. Clock injected. */
const PERSON_FLAGS = {
  isDormant: (p, now) => now - p.lastSeenAt > 60 * 864e5,
};

/* Whether an invite is still standing is a comparison, not a stored status —
   and it is the difference between holding a seat and not. */
const INVITE_FLAGS = {
  isExpired: (i, now) => i.expiresAt <= now,
  isStanding: (i, now) => i.expiresAt > now,
};

const INVOICE_FLAGS = {
  /* A void invoice was never charged, so it is excluded from every total that
     claims to be money. */
  isCharged: (i) => i.status !== "void",
  needsAction: (i) => i.status === "failed",
};

const PAYMENT_FLAGS = {
  isExpired: (card, now) => new Date(card.expYear, card.expMonth, 0, 23, 59).getTime() < now,
};

const PROMPT_FLAGS = {
  /* Sixty days without a use. Clock injected — never Date.now() inside. */
  isStale: (p, now) => !p.lastUsedAt || now - p.lastUsedAt > 60 * 864e5,
};

/* ── Transitions — derived from state, never a fixed set ───────────────── */

/* Named after the move. A transition that does not apply is omitted; one that
   applies but the user cannot take yet is disabled with the reason. */
function threadTransitions(thread) {
  const list = [];
  /* An archived thread offers one move and nothing else: it cannot be replied
     to, shared, pinned or renamed until it is back. */
  if (THREAD_FLAGS.isArchived(thread)) {
    list.push({ id: "restore", label: "Restore", primary: true });
    list.push({ id: "export", label: "Export" });
    list.push({ id: "delete", label: "Delete", destructive: true });
    return list;
  }
  if (thread.visibility === "private") list.push({ id: "share_workspace", label: "Share with workspace" });
  if (thread.visibility === "workspace") {
    list.push({ id: "make_private", label: "Make private" });
    list.push({ id: "create_link", label: "Create public link" });
  }
  if (thread.visibility === "link") list.push({ id: "disable_link", label: "Disable link" });
  list.push({ id: "rename", label: "Rename" });
  list.push({ id: thread.pinned ? "unpin" : "pin", label: thread.pinned ? "Unpin" : "Pin to top" });
  list.push({ id: "archive", label: "Archive" });
  list.push({ id: "export", label: "Export" });
  list.push({ id: "delete", label: "Delete", destructive: true });
  return list;
}

/* What a single assistant turn offers. Retry belongs to a failure, Continue to
   a stop — never both, and never a Retry on a turn that finished. */
function messageTransitions(message) {
  if (message.role !== "assistant") return [];
  const list = [];
  if (message.status === "failed") list.push({ id: "retry", label: "Retry", primary: true });
  if (message.status === "stopped") list.push({ id: "continue", label: "Continue", primary: true });
  if (message.status === "complete" || message.status === "stopped") {
    list.push({ id: "copy", label: "Copy" });
    list.push({ id: "branch", label: "Branch from here" });
  }
  return list;
}

function fileTransitions(file) {
  const list = [];
  if (file.status === "failed") list.push({ id: "reindex", label: "Reindex", primary: true });
  if (file.status === "indexing") list.push({ id: "reindex", label: "Reindex", disabled: true, disabledReason: "It is still indexing. Give it a minute." });
  if (file.status === "unsupported") list.push({ id: "reindex", label: "Reindex", disabled: true, disabledReason: "This format cannot be read. Convert it to text or PDF and upload it again." });
  if (file.projectId) list.push({ id: "detach", label: "Move to the workspace" });
  list.push({ id: "delete", label: "Delete", destructive: true });
  return list;
}

/* A saved prompt. Using it is the normal path, so it is the primary. */
function promptTransitions(prompt) {
  return [
    { id: "use", label: "Use", primary: true },
    { id: "edit", label: "Edit" },
    { id: "rename", label: "Rename" },
    { id: "copy", label: "Copy the prompt" },
    { id: "delete", label: "Delete", destructive: true },
  ];
}

/* A member. Access is changed by the row's own Select, so it is not a
   transition; what remains is removal, which the owner is exempt from. */
function memberTransitions(person, viewer) {
  const list = [];
  if (person.id === viewer.id) return list;
  if (person.access === "owner") {
    list.push({ id: "remove", label: "Remove from workspace", disabled: true, disabledReason: "The owner cannot be removed. Transfer ownership first." });
    return list;
  }
  list.push({ id: "remove", label: "Remove from workspace", destructive: true });
  return list;
}

function inviteTransitions(invite, now) {
  const list = [];
  if (INVITE_FLAGS.isExpired(invite, now)) list.push({ id: "resend", label: "Resend invite", primary: true });
  else list.push({ id: "copy_link", label: "Copy invite link" });
  list.push({ id: "revoke", label: "Revoke invite", destructive: true });
  return list;
}

function invoiceTransitions(invoice) {
  const list = [];
  if (invoice.status === "failed") list.push({ id: "retry", label: "Retry payment", primary: true });
  if (INVOICE_FLAGS.isCharged(invoice)) list.push({ id: "download", label: "Download" });
  return list;
}

function projectTransitions(project) {
  const list = [];
  if (PROJECT_FLAGS.isArchived(project)) list.push({ id: "restore", label: "Restore", primary: true });
  else {
    if (project.visibility === "private") list.push({ id: "share_workspace", label: "Share with workspace" });
    if (project.visibility === "workspace") list.push({ id: "make_private", label: "Make private" });
    list.push({ id: "archive", label: "Archive" });
  }
  list.push({ id: "delete", label: "Delete", destructive: true });
  return list;
}

Object.assign(window, {
  VISIBILITY, MESSAGE_STATUS, FILE_STATUS, INVOICE_STATUS, STATUS_FIELDS,
  MODELS, ACCESS_LEVELS, PLANS, RETENTION_OPTIONS, SEARCH_KINDS, PROMPT_CATEGORIES, FEEDBACK, DELETE_REASONS,
  MESSAGE_FLAGS, THREAD_FLAGS, FILE_FLAGS, PROJECT_FLAGS, PROMPT_FLAGS, PERSON_FLAGS, INVITE_FLAGS, INVOICE_FLAGS, PAYMENT_FLAGS,
  threadTransitions, messageTransitions, fileTransitions, projectTransitions,
  promptTransitions, memberTransitions, inviteTransitions, invoiceTransitions,
});
