/* Marketing Desk — this product's status vocabulary.
   A value not listed here does not exist. If a screen needs a status this file
   does not carry, the fix is here, never a string literal at the call site and
   never a tone assigned in a screen.

   Enums hold STORED state only. Anything computed from other fields is a
   derived flag (below) and is deliberately absent from the enums: a journey is
   never "failing" or "stalled", a broadcast is never "overdue" — those are
   read at render from the record and the clock. */

/* Journey lifecycle — what the automation is doing right now.
   Grouped by the question the journeys screen asks: can I act on it?
   live      success  — sending, and the only state that can be paused
   paused    warning  — a deliberate state someone chose; enrolments sit still
   draft     neutral  — never run; nothing has been sent
   stopped   neutral  — finished for good; enrolments were emptied
   `warning` is the deliberate state needing attention, `danger` is a failure
   event — a journey never fails as a whole, so it has no danger value. Delivery
   failure is a step-level number and reaches the screen as a derived flag. */
const JOURNEY_STATUS = [
  { id: "draft", label: "Draft", tone: "neutral" },
  { id: "live", label: "Live", tone: "success" },
  { id: "paused", label: "Paused", tone: "warning" },
  { id: "stopped", label: "Stopped", tone: "neutral" },
];

/* Broadcast lifecycle — a one-off send's position between written and delivered.
   Grouped by who it is waiting on:
   draft      neutral  — waiting on its author
   in_review  warning  — waiting on an approver; the only actionable inbox state
   scheduled  info     — waiting on the clock
   sending    info     — waiting on the sending infrastructure
   sent       success  — done
   failed     danger   — a failure event, not a state anyone chose
   Deliberately absent because derived: "overdue review" (in_review with a send
   time inside 48 hours) and "partially delivered". */
const BROADCAST_STATUS = [
  { id: "draft", label: "Draft", tone: "neutral" },
  { id: "in_review", label: "In review", tone: "warning" },
  { id: "scheduled", label: "Scheduled", tone: "info" },
  { id: "sending", label: "Sending", tone: "info" },
  { id: "sent", label: "Sent", tone: "success" },
  { id: "failed", label: "Failed", tone: "danger" },
];

/* Template lifecycle — whether an email can be sent.
   published  success  — it can send, and something may be pointing at it
   draft      neutral  — not finished; nothing may send it
   archived   neutral  — deliberately retired; kept for the record
   draft and archived share a tone because they answer the primary screen's
   question identically: no, this cannot send. What separates them is direction,
   which the label carries. */
const CONTENT_STATUS = [
  { id: "draft", label: "Draft", tone: "neutral" },
  { id: "published", label: "Published", tone: "success" },
  { id: "archived", label: "Archived", tone: "neutral" },
];

/* One DNS check on one sending domain.
   pass     success  — the record resolves and matches
   failing  danger   — a failure event: the record is there and wrong
   missing  warning  — nothing published yet; a deliberate state to fix */
const DOMAIN_CHECKS = [
  { id: "pass", label: "Pass", tone: "success" },
  { id: "failing", label: "Failing", tone: "danger" },
  { id: "missing", label: "Not set up", tone: "warning" },
];

/* Classification lists — read through labelOf, never through a Badge. */
const STEP_KINDS = [
  { value: "email", label: "Email", icon: "mail" },
  { value: "wait", label: "Wait", icon: "clock" },
  { value: "branch", label: "Branch", icon: "git-branch" },
  { value: "sms", label: "SMS", icon: "message-square" },
  { value: "tag", label: "Tag", icon: "tag" },
];

const CHANNELS = [
  { value: "email", label: "Email" },
  { value: "sms", label: "SMS" },
];

const AUDIENCE_KINDS = [
  { value: "segment", label: "Rule-based segment" },
  { value: "list", label: "Static list" },
];

const LIFECYCLES = [
  { value: "lead", label: "Lead" },
  { value: "trial", label: "Trial" },
  { value: "customer", label: "Customer" },
  { value: "churned", label: "Churned" },
];

/* Who somebody is on this workspace. The entitlement below is what the product
   actually reads — the role is the thing people are given, the entitlement is
   the thing it buys. */
const TEAM_ROLES = [
  { value: "owner", label: "Owner" },
  { value: "admin", label: "Admin" },
  { value: "editor", label: "Editor" },
  { value: "analyst", label: "Analyst" },
];

const ROLE_CAN_APPROVE = { owner: true, admin: true, editor: false, analyst: false };
const ROLE_CAN_EDIT = { owner: true, admin: true, editor: true, analyst: false };

const PLANS = [
  { value: "free", label: "Free" },
  { value: "starter", label: "Starter" },
  { value: "pro", label: "Pro" },
  { value: "scale", label: "Scale" },
];

/* Password requirements. The checklist on the set-new screen and the validation
   that gates its submit read this one list, so what the screen promises and what
   it enforces cannot drift. */
const PASSWORD_RULES = [
  { id: "length", label: "At least 10 characters", test: (v) => v.length >= 10 },
  { 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) },
  { id: "common", label: "Not a password everybody uses", test: (v) => v.length > 0 && ["password", "password1", "letmein", "qwerty123", "northwind1"].indexOf(v.toLowerCase()) === -1 },
];

/* How long a reset link lives. The sent-confirmation states it and the set-new
   screen counts down against it — one number, two screens. */
const RESET_LINK_MINUTES = 30;

/* The scope control's options. Days, so the figures and the chart read one
   number. Thirty is the far end because thirty days is how much delivery
   history the workspace keeps. */
const PERIODS = [
  { value: "7", label: "Last 7 days" },
  { value: "14", label: "Last 14 days" },
  { value: "30", label: "Last 30 days" },
];

/* The zones the workspace can send in. A domain value, so it lives here rather
   than in lib/. */
const TIMEZONES = [
  { value: "Europe/London", label: "London — GMT+1" },
  { value: "Europe/Berlin", label: "Berlin — GMT+2" },
  { value: "America/New_York", label: "New York — GMT−4" },
  { value: "America/Los_Angeles", label: "Los Angeles — GMT−7" },
  { value: "Asia/Singapore", label: "Singapore — GMT+8" },
];

const DAY_MS = 864e5;

/* The product's one number for "this is not noise", and the one comparison that
   reads it. It compares at the precision the screens display, so a rate that
   renders as "2.0%" is never left untoned beside a toned one — the count in a
   footnote and the rows above it always agree. */
const FAILURE_THRESHOLD = 0.02;const isOverFailureThreshold = (failed, attempted) =>
  attempted > 0 && Number(((failed / attempted) * 100).toFixed(1)) >= FAILURE_THRESHOLD * 100;

/* Derived flags — computed at render, never stored, clock injected.
   Every altitude that counts one of these calls the function here: the row, the
   section heading and the sidebar badge. */
const JOURNEY_FLAGS = {
  /* A step whose delivery failures are at or above the product's threshold. */
  failingStep: (journey) => journey.steps.find((s) => isOverFailureThreshold(s.failed, s.entered)) || null,
  /* Live, but nothing has gone out for a week — the trigger has stopped
     matching anyone. Measured from the last send, or from publication when it
     has never sent, so a journey published a minute ago is not stalled. */
  isStalled: (journey, now) => journey.status === "live" && now - (journey.lastSendAt || journey.publishedAt || 0) > 7 * DAY_MS,
  needsAttention: (journey, now) =>
    journey.status === "live" && (!!JOURNEY_FLAGS.failingStep(journey) || JOURNEY_FLAGS.isStalled(journey, now)),
  /* Recession, not disabling: work nobody can act on yet. */
  isRecessed: (journey) => journey.status === "draft" || journey.status === "stopped",
};

const BROADCAST_FLAGS = {
  /* In review, somebody other than the viewer submitted it, AND the viewer's
     role is entitled to approve. This is the act-ability predicate: the row's
     button, the section count and the nav badge all read it, so the badge never
     claims work the rows refuse — and changing a role on the team screen moves
     all three at once. */
  isApprovableBy: (broadcast, viewer) => broadcast.status === "in_review" && broadcast.submittedById !== viewer.id && !!ROLE_CAN_APPROVE[viewer.teamRole],
  isMine: (broadcast, viewer) => broadcast.submittedById === viewer.id,
  /* Waiting on an approver with less than two days before its send time. */
  isReviewUrgent: (broadcast, now) => broadcast.status === "in_review" && !!broadcast.scheduledAt && broadcast.scheduledAt - now < 2 * DAY_MS,
  countsTowardDelivery: (broadcast) => ["sent", "sending", "failed"].indexOf(broadcast.status) > -1,
  /* A SETTLED send that finished with fewer delivered than attempted. A send
     still in flight is not partial — the messages it has not handed off yet are
     pending, not failures, and counting them would put a running send in the
     failure table at a made-up rate. */
  isPartial: (broadcast) => ["sent", "failed"].indexOf(broadcast.status) > -1 && broadcast.delivered < broadcast.sends,
};

const CONTENT_FLAGS = {
  /* A template nobody points at. Computed from the journeys and broadcasts that
     name it, never stored on the template. */
  isUnused: (item, journeys, broadcasts) =>
    !journeys.some((j) => j.steps.some((s) => s.contentId === item.id)) &&
    !broadcasts.some((b) => b.contentId === item.id),
  isRecessed: (item) => item.status === "archived",
};

/* Transitions, named once, product-wide, after the move. The set derives from
   the current status: a transition that does not apply is omitted, never
   disabled. Disabled means the transition exists and this user cannot take it
   yet, with the reason stated.

   Journey verbs: Publish journey · Pause journey · Resume journey · Stop journey
   Broadcast verbs: Send for review · Approve and schedule · Send back ·
   Cancel schedule · Retry send */
/* One question, one answer: can this step send? The publish gate and the step
   editor's problem list both read it, so the two screens cannot contradict each
   other about whether a ladder is finished. Returns the reason as a phrase that
   completes “‹step› …”, or null when the step is fine. */
function stepSendProblem(step, content) {
  if (step.kind !== "email") return null;
  if (step.ready === false) return "has no subject line yet";
  if (!step.contentId) return "has no template picked yet";
  const item = (content || []).find((c) => c.id === step.contentId);
  if (!item) return "points at a template that has been deleted";
  if (!item.subject) return "points at a template with no subject line";
  if (item.status !== "published") return "points at a template that is not published";
  return null;
}

const journeyUnsendableSteps = (journey, content) => journey.steps.filter((s) => stepSendProblem(s, content));

function journeyTransitions(journey, content) {
  if (journey.status === "draft") {
    const blocked = journeyUnsendableSteps(journey, content);
    const first = blocked[0];
    return [{
      id: "publish",
      label: "Publish journey",
      primary: true,
      disabled: blocked.length > 0,
      disabledReason: first ? "“" + (first.label || "An email step") + "” " + stepSendProblem(first, content) + "." : undefined,
    }];
  }
  if (journey.status === "live") {
    return [
      { id: "pause", label: "Pause journey", primary: true },
      { id: "stop", label: "Stop journey", destructive: true },
    ];
  }
  if (journey.status === "paused") {
    return [
      { id: "resume", label: "Resume journey", primary: true },
      { id: "stop", label: "Stop journey", destructive: true },
    ];
  }
  return [];
}

function broadcastTransitions(broadcast, viewer) {
  if (broadcast.status === "draft") {
    const allowed = !!ROLE_CAN_EDIT[viewer.teamRole];
    return [{
      id: "send_for_review", label: "Send for review", primary: true,
      disabled: !allowed,
      disabledReason: allowed ? undefined : "An analyst can read the workspace but not send anything for review.",
    }];
  }
  if (broadcast.status === "in_review") {
    const mine = BROADCAST_FLAGS.isMine(broadcast, viewer);
    const entitled = !!ROLE_CAN_APPROVE[viewer.teamRole];
    const reason = mine
      ? "You submitted this one — someone else has to approve it."
      : entitled ? undefined : "Approving a send needs an admin or the workspace owner.";
    return [
      { id: "approve", label: "Approve and schedule", primary: true, disabled: !!reason, disabledReason: reason },
      { id: "send_back", label: "Send back", disabled: !!reason, disabledReason: mine ? "It is already yours to edit." : reason },
    ];
  }
  if (broadcast.status === "scheduled") return [{ id: "cancel_schedule", label: "Cancel schedule", primary: true }];
  if (broadcast.status === "failed") return [{ id: "retry", label: "Retry send", primary: true }];
  /* `sending` and `sent` return nothing on purpose: a send in flight and a send
     that has finished have no transition anybody can take, so their rows are
     recessed rather than carrying a disabled button (§5.4). */
  return [];
}

/* Content verbs: Publish template · Archive template · Restore template. A
   template something points at cannot be archived — the transition exists and is
   refused with the reason, because the user could make it possible by pointing
   the journey or broadcast somewhere else. */
function contentTransitions(item, uses) {
  const inUse = uses.journeys.length + uses.broadcasts.length;
  if (item.status === "draft") {
    const ready = !!item.subject;
    return [{
      id: "publish_content", label: "Publish template", primary: true,
      disabled: !ready, disabledReason: ready ? undefined : "It has no subject line yet.",
    }];
  }
  if (item.status === "published") {
    return [{
      id: "archive_content", label: "Archive template",
      disabled: inUse > 0,
      disabledReason: inUse > 0 ? inUse + (inUse === 1 ? " thing still sends it." : " things still send it.") : undefined,
    }];
  }
  if (item.status === "archived") return [{ id: "restore_content", label: "Restore template", primary: true }];
  return [];
}

/* What a journey can be triggered by. The builder offers these and nothing else:
   a trigger the product cannot listen for is not a trigger. Every journey in the
   fixture uses one of these labels. */
const JOURNEY_TRIGGERS = [
  { value: "A trial starts", label: "A trial starts" },
  { value: "Trial day 3 with nothing sent", label: "Trial day 3 with nothing sent" },
  { value: "A trial converts to a paid plan", label: "A trial converts to a paid plan" },
  { value: "A subscription is cancelled", label: "A subscription is cancelled" },
  { value: "A customer passes 80% of their plan\u2019s sending limit", label: "A customer passes 80% of their plan\u2019s sending limit" },
  { value: "Somebody joins the audience", label: "Somebody joins the audience" },
];

/* What the step editor can add, and the waits it offers. Hours, so the summary
   and the step label read one number. */
const BUILDER_STEP_KINDS = [
  { value: "email", label: "Email", icon: "mail" },
  { value: "wait", label: "Wait", icon: "clock" },
  { value: "branch", label: "Branch", icon: "git-branch" },
];

const WAIT_OPTIONS = [
  { value: "24", label: "1 day" },
  { value: "48", label: "2 days" },
  { value: "72", label: "3 days" },
  { value: "96", label: "4 days" },
  { value: "120", label: "5 days" },
  { value: "168", label: "7 days" },
];

/* The segment vocabulary the audience builder offers. Each rule carries its own
   predicate and knows when it is inactive, so the builder never has to special-
   case a rule and the preview runs the real test over the real sample. */
const SEGMENT_RULES = [
  {
    id: "lifecycle", label: "Lifecycle", kind: "chips", options: LIFECYCLES,
    active: (v) => !!v && v.length > 0,
    test: (contact, v) => v.indexOf(contact.lifecycle) > -1,
    describe: (v) => "Lifecycle is " + v.map((id) => labelOf(LIFECYCLES, id)).join(" or "),
  },
  {
    id: "plan", label: "Plan", kind: "chips", options: PLANS,
    active: (v) => !!v && v.length > 0,
    test: (contact, v) => v.indexOf(contact.plan) > -1,
    describe: (v) => "plan is " + v.map((id) => labelOf(PLANS, id)).join(" or "),
  },
  {
    id: "engagement", label: "Engagement", kind: "select",
    options: [
      { value: "any", label: "However they engage" },
      { value: "open7", label: "Opened something in the last 7 days" },
      { value: "open30", label: "Opened something in the last 30 days" },
      { value: "quiet14", label: "Nothing opened for 14 days" },
      { value: "never", label: "Never opened anything" },
    ],
    active: (v) => v && v !== "any",
    test: (contact, v, now) => {
      if (v === "never") return !contact.lastOpenAt;
      if (!contact.lastOpenAt) return false;
      const since = now - contact.lastOpenAt;
      if (v === "open7") return since <= 7 * DAY_MS;
      if (v === "open30") return since <= 30 * DAY_MS;
      if (v === "quiet14") return since > 14 * DAY_MS;
      return true;
    },
    describe: (v) => labelOf(SEGMENT_RULES[2].options, v).toLowerCase(),
  },
  {
    id: "tenure", label: "How long they have been here", kind: "select",
    options: [
      { value: "any", label: "However long" },
      { value: "new30", label: "Joined in the last 30 days" },
      { value: "over90", label: "Here more than 90 days" },
    ],
    active: (v) => v && v !== "any",
    test: (contact, v, now) => (v === "new30" ? now - contact.signupAt <= 30 * DAY_MS : now - contact.signupAt > 90 * DAY_MS),
    describe: (v) => labelOf(SEGMENT_RULES[3].options, v).toLowerCase(),
  },
];

/* The registry, so a screen can name a field without importing its array. */
const STATUS_FIELDS = [
  { entity: "journey", field: "status", means: "What the automation is doing right now", values: JOURNEY_STATUS },
  { entity: "broadcast", field: "status", means: "Where a one-off send sits between written and delivered", values: BROADCAST_STATUS },
  { entity: "content", field: "status", means: "Whether an email template can be sent", values: CONTENT_STATUS },
  { entity: "domain", field: "check", means: "The result of one DNS check on one sending domain", values: DOMAIN_CHECKS },
];

/* Coverage: every value in all four enums has at least one fixture, except
   journey `stopped`, which is reached only by taking the transition (nothing
   ships stopped, and a stopped journey with enrolments would be a lie). */

/* ── The social layer's vocabulary ─────────────────────────────────────────
   A note has no status — nothing about it moves through states — so there is no
   comment enum here. What there IS is a derived flag: a mention is owed until the
   person mentioned writes on that record. Derived, never stored, computed at
   render with the comment list injected (§8.1). */
const COMMENT_FLAGS = {
  isAwaitingReply: (comment, memberId, comments) => comment.mentions.indexOf(memberId) > -1
    && !comments.some((other) => other.authorId === memberId
      && other.record.type === comment.record.type && other.record.id === comment.record.id
      && other.at > comment.at),
  /* A thread is the notes on one record. It is waiting on the viewer when any
     note in it holds a mention of the viewer that is still owed — the same
     predicate the inbox, the nav badge and the note's own line read. */
  isThreadAwaiting: (comments, memberId) => comments.some((c) => COMMENT_FLAGS.isAwaitingReply(c, memberId, comments)),
};

/* What a person did, one label per verb. The feed reads this rather than
   printing the stored word, so the same act is never described two ways. */
const ACTIVITY_VERBS = [
  { value: "created", label: "created", icon: "plus" },
  { value: "edited", label: "edited", icon: "pencil" },
  { value: "submitted", label: "submitted for review", icon: "send" },
  { value: "approved", label: "approved", icon: "check" },
  { value: "scheduled", label: "scheduled", icon: "calendar-clock" },
  { value: "published", label: "published", icon: "play" },
  { value: "paused", label: "paused", icon: "pause" },
  { value: "moved", label: "moved sending domain for", icon: "arrow-right-left" },
  { value: "commented", label: "wrote on", icon: "message-circle" },
  { value: "grew", label: "added contacts to", icon: "user-plus" },
  { value: "exported", label: "exported", icon: "download" },
];

/* An integration is either working, needs the user to do something, or is not
   connected at all. Three values, three tones, and the reason the grouping is
   this shape: the question the channels screen asks is "can I rely on it". */
const CHANNEL_STATE = [
  { value: "connected", label: "Connected", tone: "success" },
  { value: "needs_reauth", label: "Needs reconnecting", tone: "warning" },
  { value: "available", label: "Not connected", tone: "neutral" },
];

const KEY_SCOPES = [
  { value: "read", label: "Read only" },
  { value: "write", label: "Read and write" },
];

const WEBHOOK_STATE = [
  { value: "healthy", label: "Delivering", tone: "success" },
  { value: "failing", label: "Failing", tone: "danger" },
];

const INVOICE_STATE = [
  { value: "paid", label: "Paid", tone: "success" },
  { value: "open", label: "Due", tone: "warning" },
  { value: "failed", label: "Payment failed", tone: "danger" },
];

/* How a contact is doing, derived from when they last opened anything — never
   stored on the contact, because it changes on its own with the clock. */
const CONTACT_FLAGS = {
  isEngaged: (contact, now) => contact.lastOpenAt != null && now - contact.lastOpenAt <= 7 * 864e5,
  isQuiet: (contact, now) => contact.lastOpenAt != null && now - contact.lastOpenAt > 30 * 864e5,
  isNeverOpened: (contact) => contact.lastOpenAt == null,
};

Object.assign(window, {
  JOURNEY_STATUS, BROADCAST_STATUS, CONTENT_STATUS, DOMAIN_CHECKS,
  STEP_KINDS, CHANNELS, AUDIENCE_KINDS, LIFECYCLES, PLANS, PERIODS, TIMEZONES, TEAM_ROLES,
  ROLE_CAN_APPROVE, ROLE_CAN_EDIT, FAILURE_THRESHOLD, isOverFailureThreshold,
  PASSWORD_RULES, RESET_LINK_MINUTES, JOURNEY_TRIGGERS, BUILDER_STEP_KINDS, WAIT_OPTIONS, SEGMENT_RULES,
  JOURNEY_FLAGS, BROADCAST_FLAGS, CONTENT_FLAGS, COMMENT_FLAGS, CONTACT_FLAGS,
  ACTIVITY_VERBS, CHANNEL_STATE, KEY_SCOPES, WEBHOOK_STATE, INVOICE_STATE,
  journeyTransitions, broadcastTransitions, contentTransitions, stepSendProblem, journeyUnsendableSteps, STATUS_FIELDS, DAY_MS,
});
