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

/**
 * What a task needs, and what it does not, decided by its occurrence.
 *
 * From `machine/task-fields.tsv`: `deadline` is conditional, `fixed_day` exists
 * only where a month has a day to fix, and `general` never becomes a task at
 * all. Showing thirteen fields at once produces a form nobody fills, so the
 * form asks the occurrence early and lets the answer decide the rest.
 *
 * Pure, and tested. What it imports is a type and `lib/calendar.ts`, which
 * imports nothing at all, so it stays importable from a client component.
 */

export const OCCURRENCES = [
  "once",
  "daily",
  "weekly",
  "monthly",
  "quarterly",
  "general",
] as const

export type Occurrence = (typeof OCCURRENCES)[number]

/** The four that repeat. `once` happens and stops; `general` never happens. */
export const RECURRING_OCCURRENCES = [
  "daily",
  "weekly",
  "monthly",
  "quarterly",
] as const satisfies readonly Occurrence[]

/**
 * Which of the six a tier may create, from the client's answer of 3 August to
 * open point G: "Recurring self to-dos are saved and visible to the manager.
 * One-time self to-dos they assign themselves are not needed."
 *
 * So a staff member gets the four that repeat and neither of the other two.
 * `once` is the one the client turned down. `general` is a line in a job
 * description rather than work, and the screen that writes one, `/positions/
 * [id]`, is an admin's.
 *
 * Here rather than in the service because both sides need the same answer: the
 * form draws the radio group from this and `createDuty` asks it again on the
 * way in, the way it already asks about the position twice.
 */
export function occurrencesFor(
  tier: "admin" | "manager" | "staff",
): readonly Occurrence[] {
  return tier === "staff" ? RECURRING_OCCURRENCES : OCCURRENCES
}

export type Schedule =
  /** A single date. The task exists once and is due then. */
  | { kind: "deadline" }
  /** Every working day of the assignee's pattern. Nothing more to ask. */
  | { kind: "none" }
  /** One day of the week, 1 Monday to 7 Sunday. */
  | { kind: "day_of_week" }
  /** A day of the month, 1 to 31, shifted off weekends and holidays. */
  | { kind: "day_of_month" }
  /** Not a task. It belongs to the job description and generates nothing. */
  | { kind: "not_a_task" }

export function scheduleFor(occurrence: Occurrence): Schedule {
  switch (occurrence) {
    case "once":
      return { kind: "deadline" }
    case "daily":
      return { kind: "none" }
    case "weekly":
      return { kind: "day_of_week" }
    case "monthly":
    case "quarterly":
      return { kind: "day_of_month" }
    case "general":
      return { kind: "not_a_task" }
  }
}

/** A general duty is listed in the job description and never generated. */
export function becomesTasks(occurrence: Occurrence): boolean {
  return scheduleFor(occurrence).kind !== "not_a_task"
}

/** Only a recurring duty belongs to a position for good. */
export function isRecurring(occurrence: Occurrence): boolean {
  return becomesTasks(occurrence) && occurrence !== "once"
}

/**
 * Everything the form holds, as the browser holds it: strings, because that is
 * what an input gives back. Turning them into what the database wants happens
 * once, on the way out, and never in thirteen places.
 */
export type Draft = {
  positionId: string
  description: string
  occurrence: Occurrence
  deadline: string
  dayOfWeek: string
  dayOfMonth: string
  checker: string
  /** Optional. `functional-spec.md` Q1: an audit of a sample, not a step. */
  verifier: string
  report: string
  sopUrl: string
  exampleUrl: string
  minutes: string
  customer: string
}

/**
 * The fields somebody can leave empty and must not, which is fewer than the
 * fields the form has. The five under "What it produces" are optional in
 * `machine/task-fields.tsv` and so is the verifier, so none of them can ever be
 * missing and none of them belongs in this union.
 */
export type RequiredField =
  | "positionId"
  | "description"
  | "deadline"
  | "dayOfWeek"
  | "dayOfMonth"
  | "checker"

/**
 * What is missing before this can be created, in the order the form asks for it.
 * A field that does not apply to the chosen occurrence is never missing.
 */
export function missingFields(draft: Draft): RequiredField[] {
  const missing: RequiredField[] = []
  if (!draft.positionId) missing.push("positionId")
  if (!draft.description.trim()) missing.push("description")

  const schedule = scheduleFor(draft.occurrence)
  if (schedule.kind === "not_a_task") return missing

  if (schedule.kind === "deadline" && !draft.deadline) missing.push("deadline")
  if (schedule.kind === "day_of_week" && !draft.dayOfWeek)
    missing.push("dayOfWeek")
  if (schedule.kind === "day_of_month" && !draft.dayOfMonth)
    missing.push("dayOfMonth")

  // Every task is checked by somebody. That is what completes it.
  if (!draft.checker) missing.push("checker")
  return missing
}

/**
 * A number that is not the number it has to be.
 *
 * Separate from `missingFields` because the answer is a different sentence: an
 * empty field is named and asked for, a field holding 99 has to be told what a
 * day of the month is. Left to the database, 99 becomes a check-constraint
 * violation, which the screen reports as "the system did not answer" and which
 * says nothing whatever about the number somebody typed.
 */
export type InvalidField = "dayOfMonth" | "minutes"

/** 1 to 31, whole, whatever it arrived as. The column checks the same range. */
export function isDayOfMonth(value: unknown): value is number {
  return (
    typeof value === "number" &&
    Number.isInteger(value) &&
    value >= 1 &&
    value <= 31
  )
}

/**
 * A whole number of minutes, 1 or more. Nothing in the schema says so, because
 * `time_to_do_minutes` is a plain integer, but it feeds the workload sum and a
 * duty that takes minus ten minutes is a workload figure nobody can read.
 */
export function isMinutes(value: unknown): value is number {
  return typeof value === "number" && Number.isInteger(value) && value >= 1
}

/**
 * 1 Monday to 7 Sunday, the numbering the column checks and the radio group
 * sets. The radio group cannot send anything else; a posted body can, and 99
 * left to the database is a check-constraint violation the screen reports as
 * silence, which says nothing about the day.
 */
export function isWeekday(value: unknown): value is Weekday {
  return (
    typeof value === "number" &&
    Number.isInteger(value) &&
    value >= 1 &&
    value <= 7
  )
}

/**
 * A day on the Bangkok wall calendar, spelled the way `lib/calendar.ts` spells
 * every day in this system. February the 30th is refused here rather than
 * turned into an Invalid Date, which is what a `new Date` of it produces and
 * what the insert then fails on without ever naming the date.
 */
export function isDeadline(value: unknown): value is string {
  return typeof value === "string" && isIsoDate(value)
}

/** Empty means the field was left alone, which is not the same as a zero. */
function typedNumber(text: string): number | null {
  return text.trim() === "" ? null : Number(text)
}

/** What the draft carries as a number, or null where nothing was typed. */
export function dayOfMonthOf(draft: Draft): number | null {
  return typedNumber(draft.dayOfMonth)
}

export function minutesOf(draft: Draft): number | null {
  return typedNumber(draft.minutes)
}

/** What is filled in and unusable, said before the round trip rather than after. */
export function invalidFields(draft: Draft): InvalidField[] {
  const invalid: InvalidField[] = []

  const dayOfMonth = dayOfMonthOf(draft)
  if (
    scheduleFor(draft.occurrence).kind === "day_of_month" &&
    dayOfMonth !== null &&
    !isDayOfMonth(dayOfMonth)
  ) {
    invalid.push("dayOfMonth")
  }

  const minutes = minutesOf(draft)
  if (minutes !== null && !isMinutes(minutes)) invalid.push("minutes")

  return invalid
}

/**
 * Why a create was refused.
 *
 * Named here and not in the service that decides it, so the screen can turn
 * each one into a sentence without importing a module that reaches a database.
 * The service imports this type instead; a type is erased and carries nothing
 * with it.
 */
export type Refusal =
  | "position_out_of_scope"
  | "checker_out_of_scope"
  | "verifier_out_of_scope"
  | "occurrence_not_allowed"
  | "day_of_month_out_of_range"
  | "day_of_week_out_of_range"
  | "deadline_not_a_date"
  | "time_to_do_not_a_number"

/** 1 Monday to 7 Sunday, as the database and the form both spell it. */
export const WEEKDAYS = [1, 2, 3, 4, 5, 6, 7] as const

export type Weekday = (typeof WEEKDAYS)[number]

/** The form only ever sets 1 to 7, and a value that is not one is not a day. */
function asWeekday(value: string): Weekday | null {
  const day = Number(value)
  return isWeekday(day) ? day : null
}

/**
 * A key the dictionary carries. Misspell one and this stops compiling here,
 * rather than printing itself onto the screen the way `"status.todo"` did.
 */
type Key<K extends MessageKey> = K

/**
 * What the chosen schedule will do: which sentence, and the numbers it needs.
 *
 * **Not a sentence.** This file decides the case; it has no idea who is
 * reading, and 183 of the 193 people here read Thai. `lib/notice.ts` reached
 * the same conclusion from the other side, for the same reason: a notification
 * stores its key and not its words, because the words have to be chosen at the
 * moment somebody looks rather than at the moment something happened.
 *
 * **The data stays data.** `day` is a `YYYY-MM-DD` and never a formatted date,
 * so it reaches the reader through `lib/i18n/dates.ts` and a Thai reader gets
 * the Buddhist year that is on every document in the building. `weekday` is a
 * number the dictionary names, and `dayOfMonth` is a number the sentence puts
 * wherever its own language puts it.
 *
 * **A union and not an optional bag** of maybe-present fields, so a caller that
 * forgets to format the date, or leaves one of the four shapes unhandled, fails
 * to compile rather than printing `undefined` at somebody.
 */
export type ScheduleLine =
  | {
      key: Key<
        | "taskForm.schedule.notATask"
        | "taskForm.schedule.onceUndecided"
        | "occurrence.dailyWorking"
        | "taskForm.schedule.weeklyUndecided"
        | "taskForm.schedule.monthlyUndecided"
        | "taskForm.schedule.quarterlyUndecided"
      >
    }
  | { key: Key<"taskForm.schedule.onceOn">; day: string }
  | { key: Key<"taskForm.schedule.everyWeekday">; weekday: Weekday }
  | {
      key: Key<
        | "taskForm.schedule.monthlyOn"
        | "taskForm.schedule.monthlyOnLate"
        | "taskForm.schedule.quarterlyOn"
        | "taskForm.schedule.quarterlyOnLate"
      >
      dayOfMonth: number
    }

/**
 * The schedule named, for the line that says what will happen before somebody
 * creates it.
 *
 * A month and a quarter get their own key, and so does the day that some months
 * do not have, rather than a word or a clause dropped into a shared sentence.
 * Thai puts neither where English does, and a sentence assembled from pieces is
 * a sentence only one language can be assembled into.
 */
export function describeSchedule(draft: Draft): ScheduleLine {
  const schedule = scheduleFor(draft.occurrence)
  switch (schedule.kind) {
    case "not_a_task":
      return { key: "taskForm.schedule.notATask" }
    case "deadline":
      return draft.deadline
        ? { key: "taskForm.schedule.onceOn", day: draft.deadline }
        : { key: "taskForm.schedule.onceUndecided" }
    case "none":
      return { key: "occurrence.dailyWorking" }
    case "day_of_week": {
      const weekday = asWeekday(draft.dayOfWeek)
      return weekday
        ? { key: "taskForm.schedule.everyWeekday", weekday }
        : { key: "taskForm.schedule.weeklyUndecided" }
    }
    case "day_of_month": {
      const quarterly = draft.occurrence === "quarterly"
      if (!draft.dayOfMonth) {
        return {
          key: quarterly
            ? "taskForm.schedule.quarterlyUndecided"
            : "taskForm.schedule.monthlyUndecided",
        }
      }
      const dayOfMonth = Number(draft.dayOfMonth)
      // February has 28 days, so a duty fixed past that lands on a month that
      // has no such date, and the sentence has to say what happens then.
      const late = dayOfMonth > 28
      return {
        key: quarterly
          ? late
            ? "taskForm.schedule.quarterlyOnLate"
            : "taskForm.schedule.quarterlyOn"
          : late
            ? "taskForm.schedule.monthlyOnLate"
            : "taskForm.schedule.monthlyOn",
        dayOfMonth,
      }
    }
  }
}
