import { DEFAULT_WORK_PATTERN, type WorkPattern } from "@/lib/calendar"

/**
 * Who a position's duties fall to on one given day, and whose working week
 * decides which days those are.
 *
 * Pure. No database, no schema. It is here rather than inside the generator's
 * query for the same reason `lib/recurrence.ts` is: the answer depends on a
 * window with two inclusive ends and on a fallback with three cases, and none of
 * those are easier to see through a WHERE clause. A boundary written in SQL is a
 * boundary no test can move.
 *
 * The one thing this module knows about temporary cover is that it *replaces*
 * the holder for the days it runs, and nothing else. A stand-in's own duties
 * hang off their own position and are resolved by their own call to this
 * function, which is why cover adds work to somebody rather than swapping it.
 */

/** One of the people who hold a position today, with the week they work. */
export type Holder = {
  id: string
  pattern: WorkPattern
}

/**
 * A stand-in arrangement, flattened for the decision: which position, who is
 * standing in, the week *they* work, and the days it runs.
 */
export type Cover = {
  positionId: string
  personId: string
  /**
   * The stand-in's own working week, and deliberately not the holder's.
   *
   * The week decides whether a Saturday exists at all for the person the work
   * is going to, and the person who has to be at the factory that Saturday is
   * the stand-in. Taking the holder's week would put work on a day the person
   * receiving it does not work, and it would do it silently: the instance is
   * created, dated, assigned, and simply never seen by anybody who is there.
   */
  pattern: WorkPattern
  /** Inclusive. A Bangkok calendar day, `YYYY-MM-DD`. */
  from: string
  /** Inclusive. The last day the stand-in receives the work. */
  until: string
}

/**
 * A stretch of days with two inclusive ends, which is all anything below needs
 * to know about a cover.
 *
 * `Cover` is already one of these, structurally, so the generator's flattened
 * shape passes wherever this is asked for. It is written as its own type
 * because the screen holds a database row rather than that shape, and neither
 * should have to grow the other's fields to ask where today sits.
 */
export type Window = { from: string; until: string }

/**
 * Where a day sits relative to a window.
 *
 * Three answers and not a boolean, because a cover that has not started and one
 * that is running are different facts and the screen has to say which. They
 * also end differently: a running cover is shortened to today and keeps every
 * task it has already produced, and one that has not started is deleted,
 * because nothing was ever generated from it. A screen holding only "in force
 * or not" would have to offer one sentence for both.
 */
export type Standing = "running" | "scheduled" | "over"

/**
 * String comparison, and it is exact rather than lucky: `YYYY-MM-DD` is
 * zero-padded and fixed-width, so lexicographic order is calendar order. The
 * whole codebase carries days as these strings for this reason, and the moment
 * one of them becomes a `Date` the comparison starts depending on a timezone.
 */
export function standingOn(window: Window, day: string): Standing {
  if (day < window.from) return "scheduled"
  if (window.until < day) return "over"
  return "running"
}

/**
 * Is this cover in force on this day?
 *
 * Inclusive at both ends: a cover from the 3rd to the 5th covers the 3rd, the
 * 4th and the 5th. That is what somebody writing "I am away Monday to Friday"
 * means, and it is what the exclusion constraint in the schema uses (`'[]'`),
 * so the two agree about which day two covers would collide on.
 *
 * Written through `standingOn` rather than beside it. The boundary is the thing
 * that goes wrong here, and two spellings of it are two places to get it right.
 */
export function isInForce(cover: Cover, date: string): boolean {
  return standingOn(cover, date) === "running"
}

/**
 * The cover running on a position on one day, or null.
 *
 * At most one can exist: the exclusion constraint on `position_covers` refuses
 * a second overlapping window, because two covers on one day would send the
 * same task to two people and neither would know who owed it. Should the data
 * ever hold two anyway (a constraint dropped, a restore from a dump taken
 * before it existed), this takes the one that started first rather than
 * whichever row the query happened to return, so that two runs of the generator
 * at least agree with each other.
 */
export function coverInForce(
  covers: readonly Cover[],
  positionId: string,
  date: string,
): Cover | null {
  let chosen: Cover | null = null
  for (const cover of covers) {
    if (cover.positionId !== positionId) continue
    if (!isInForce(cover, date)) continue
    if (!chosen || cover.from < chosen.from) chosen = cover
  }
  return chosen
}

/** Who receives a position's work on a day, and the week that shapes it. */
export type DutyHolder = {
  /** Null when nobody can be named: the generated task is left unassigned. */
  assignee: string | null
  pattern: WorkPattern
}

/**
 * The assignee and the working week for one position on one day.
 *
 * Three cases, in this order.
 *
 * **A cover in force wins**, whatever the position's holders look like. It wins
 * over a vacant position too, which is the case the client's spreadsheet does
 * not contain and a factory certainly does: somebody stands in for a job nobody
 * currently holds.
 *
 * **Otherwise the single holder**, with their own week.
 *
 * **Otherwise nobody**, on the fallback week. Vacant and shared land here
 * together, and the task is left unassigned rather than given to one of several
 * holders picked at random; ELEVA-137 records that question rather than
 * answering it. The week is the schema's own default, read from
 * `DEFAULT_WORK_PATTERN` rather than written out again: it used to be spelled
 * `mon_fri` in the generator while the column said `mon_sat`, and duties on
 * vacant positions quietly stopped generating on Saturdays.
 */
export function dutyHolderOn(
  positionId: string,
  holders: readonly Holder[],
  covers: readonly Cover[],
  date: string,
): DutyHolder {
  const cover = coverInForce(covers, positionId, date)
  if (cover) return { assignee: cover.personId, pattern: cover.pattern }

  const alone = holders.length === 1 ? holders[0]! : null
  return {
    assignee: alone?.id ?? null,
    pattern: alone?.pattern ?? DEFAULT_WORK_PATTERN,
  }
}
