/**
 * What a viewer may do with a task, from `machine/lifecycle.json`.
 *
 * Two things decide it together: the status, and the viewer's relation to this
 * task. Neither alone is enough, which is why this is a function and not a
 * lookup on the role. A checker sees nothing to do until somebody has finished
 * the work; a verifier sees nothing until it has been checked.
 *
 * Pure on purpose. It will be tested before it is wired to anything.
 */

import type { MessageKey } from "@/lib/i18n/messages"

export type TaskStatus = "todo" | "started" | "finished" | "checked" | "missed"

export type Relation =
  | "assignee"
  | "checker"
  | "verifier"
  | "one_time_creator"
  | "manager_of_dept"
  | "admin"

export type TaskAction = {
  id:
    | "start"
    | "finish"
    | "report_issue"
    | "approve"
    | "send_back"
    | "verify"
    | "close"
    | "remind"
  /**
   * Dictionary keys, not words. This module decides what may be done and the
   * screen decides what that is called, because what it is called depends on
   * who is reading and this function has never been told.
   */
  label: MessageKey
  /** The one action the viewer came for, if there is one. */
  primary?: boolean
  /** Said at the moment of the act, in the confirmation, never under the button. */
  consequence?: MessageKey
}

/**
 * The two verbs the assignee owns, written once because `missed` offers both
 * and a second copy of either is a copy that drifts from the first.
 */
const START: TaskAction = { id: "start", label: "action.start" }
const FINISH: TaskAction = {
  id: "finish",
  label: "action.finish",
  consequence: "consequence.finish",
}

export function availableActions(
  status: TaskStatus,
  relations: Relation[],
  {
    oneTime = false,
    verified = false,
  }: { oneTime?: boolean; verified?: boolean } = {},
): TaskAction[] {
  const has = (r: Relation) =>
    relations.includes(r) || relations.includes("admin")
  const actions: TaskAction[] = []

  if (has("assignee")) {
    if (status === "todo") {
      actions.push({ ...START, primary: true })
    }
    if (status === "started") {
      actions.push({ ...FINISH, primary: true })
    }
    /**
     * Work that ran out of time is work that is still owed, which is what the
     * client answered on 4 August: nothing writes it off, so the person who
     * owes it has to be able to hand it in. This branch is that sentence.
     *
     * `finish` comes first and is the primary one, which is the one place this
     * differs from `todo`. Pressing Start on a task whose cut-off has already
     * passed changes nothing anybody is waiting for, and tonight's sweep will
     * write `missed` back over `started` anyway, because the task is still not
     * done and still past its cycle. Finishing is the only press that clears
     * the debt. Start stays on offer under it, for somebody picking the work up
     * now and wanting the row to say so until they do.
     *
     * `report_issue` is deliberately not here. `lifecycle.json` names it on
     * `todo|started` and nowhere else, and the client answered whether the work
     * is still owed, not whether the flag outlives the cut-off. ELEVA-156 is
     * the question; inventing an answer to a second one is not.
     */
    if (status === "missed") {
      actions.push({ ...FINISH, primary: true }, START)
    }
    if (status === "todo" || status === "started") {
      actions.push({
        id: "report_issue",
        label: "action.reportIssue",
        consequence: "consequence.reportIssue",
      })
    }
  }

  /**
   * Approving and sending back belong to the checker, and closing belongs to
   * whoever created a one-time task. They were folded together, and the fold
   * was wrong in both directions.
   *
   * `send_back` sat inside a branch that a one-time creator also entered, so a
   * creator could return work to the assignee. `actions_by_relation` gives
   * `one_time_creator` exactly one verb, `close`. This was not cosmetic: the
   * relation never expires, so somebody who created a task, then moved to
   * another department and another tier, kept the power to write to it. That
   * was demonstrated against the database, not argued.
   *
   * And `close` was chosen by `oneTime && !has("checker")`, which an admin can
   * never satisfy, because `has` already treats them as the checker. So the one
   * relation the specification grants everything to was the one relation
   * refused the verb.
   */
  if (status === "finished") {
    if (has("checker")) {
      actions.push({
        id: "approve",
        label: "action.approve",
        primary: true,
        consequence: "consequence.completes",
      })
      actions.push({
        id: "send_back",
        label: "action.sendBack",
        consequence: "consequence.sendBack",
      })
    }

    if (oneTime && has("one_time_creator")) {
      actions.push({
        id: "close",
        // Primary only when there is nothing else to be primary. Somebody who
        // is both the creator and the checker came to approve.
        primary: !has("checker"),
        label: "action.close",
        consequence: "consequence.completes",
      })
    }
  }

  /**
   * Verifying is a sample audit and it happens once. `verified` has to be
   * passed in because the status does not move when it happens: a second verify
   * would otherwise pass every guard and overwrite who audited the task and
   * when, which is the one record the audit exists to leave.
   */
  if (has("verifier") && status === "checked" && !verified) {
    actions.push({
      id: "verify",
      label: "action.verify",
      primary: true,
      consequence: "consequence.verify",
    })
  }

  // Chasing follows the debt. A manager who could nudge a task on Tuesday and
  // not on Thursday would lose the reminder exactly when it started to matter.
  if (has("manager_of_dept") && isOwed(status)) {
    actions.push({ id: "remind", label: "action.remind" })
  }

  return actions
}

/**
 * Work that is still owed: begun or not, past its cut-off or not.
 *
 * The one question every "how much is left" count on the screens is asking, and
 * it is not the same question as `isLateOnDay` below. `missed` belongs here
 * because a missed task was never done and nothing writes it off; it does not
 * belong in `late`, which the specification defines as `status IN (todo,
 * started)` and which the screens draw as a second badge beside the status.
 *
 * It lives here rather than in a service because two pages count with it and
 * two services query with it, and a page is a place a client component can
 * already take a value from. One answer, asked four ways.
 */
export function isOwed(status: TaskStatus): boolean {
  return status === "todo" || status === "started" || status === "missed"
}

/**
 * The same answer as a list, for the `WHERE status IN (...)` that asks it.
 * `readonly` because two services hold the same array: a copy is what they
 * spread, never this.
 */
export const OWED: readonly TaskStatus[] = ["todo", "started", "missed"]

/**
 * Work that was done: handed in, or handed in and approved.
 *
 * `finished` counts because the work was done. It is sitting in somebody's
 * check queue, and a figure that called it undone would blame the person who
 * did the work for their checker being slow.
 *
 * Here beside `OWED` because /team's completion figure and /kpi's are the same
 * figure, and the day one of them stops counting `finished` the other has to
 * stop with it. Two services counting "done" from two lists is how a manager
 * and the person they are judging end up reading different numbers about the
 * same month, which is exactly what ELEVA-116 is about.
 */
export const DONE: readonly TaskStatus[] = ["finished", "checked"]

/**
 * Still open: nobody has handed it in, and its cycle has not been judged over.
 *
 * The other half of `isLateOnDay`, pulled out because /kpi counts lateness in
 * SQL over a month while /team computes it per row in TypeScript. One list, so
 * the aggregate and the badge cannot come to disagree about which statuses are
 * eligible to be late.
 *
 * Narrower than `OWED`: `missed` is owed and is not late. `lifecycle.json`
 * defines late as `status IN (todo, started)` and the screens draw Missed where
 * they would otherwise draw Late.
 */
export const OPEN: readonly TaskStatus[] = ["todo", "started"]

/** Late is never stored. It is this, asked at the moment somebody looks. */
/**
 * Late, for a task whose due date is a day rather than an instant.
 *
 * Every recurring instance is dated with a Bangkok calendar day, so lateness is
 * a comparison between days and never between moments. `isLate` below takes two
 * `Date`s and answers about a `once` task's deadline, which is a real instant;
 * using it here would call a task late at some hour of its own due day,
 * depending on where the server happens to be standing.
 *
 * Late is not missed. A weekly task due Tuesday and still open on Wednesday is
 * late, and it has until Sunday before it has run out of time. `isPastCutOff` in
 * lib/recurrence.ts answers that second question, and only that one writes a
 * status: late is computed for display and never stored.
 *
 * `missed` reads as not late, which is `lifecycle.json`'s own definition of
 * late and not a fact being thrown away. Nothing is stored, so nothing can be
 * lost: the day somebody picks a missed task back up and it moves to `started`,
 * this answers late again from the due date it always had. The screens say the
 * same thing in the other direction, drawing Missed where they would otherwise
 * draw Late rather than both at once.
 */
export function isLateOnDay(
  status: TaskStatus,
  dueDate: string,
  today: string,
): boolean {
  if (!OPEN.includes(status)) return false
  return dueDate < today
}

/** Late, for a `once` task whose deadline is a moment. */
export function isLate(
  status: TaskStatus,
  due: Date | null,
  now: Date,
): boolean {
  if (!due) return false
  if (!OPEN.includes(status)) return false
  return now > due
}
