/**
 * How the team overview is cut, sorted and grouped, and how any screen carrying
 * a backlog collapses its repetition.
 *
 * `groupRuns` is shared with My tasks rather than copied into it. Both screens
 * show the same thing going wrong, one daily duty missed for nine days taking
 * nine rows, and two groupers written separately are two that drift: one gains
 * a tie-break or a window rule the other never hears about, and the same
 * backlog then reads differently depending on which screen somebody opened.
 *
 * Its own module, with nothing under it at all, because the tabs, the filter bar
 * and the tables are client components and every one of them needs to agree with
 * the page about what `?tab=late` means. Taking these names from
 * `lib/services/team.ts` would pull `lib/db/client.ts` into the browser bundle
 * and fail the build on `Can't resolve 'tls'` from inside the Postgres driver,
 * which is the fourth time this repository would have drawn that line after
 * `lib/levels.ts`, `lib/choice.ts` and `lib/task-views.ts`.
 *
 * It takes no row type from the service either. The functions are generic over
 * the handful of fields they actually read, so a service row satisfies them
 * structurally and there is not even a type import to invert the layering.
 */

/** The three questions, and the one on screen. Exactly one table at a time. */
export type TeamTab = "late" | "today" | "waiting"

export const TEAM_TABS: TeamTab[] = ["late", "today", "waiting"]

export function isTeamTab(value: unknown): value is TeamTab {
  return typeof value === "string" && (TEAM_TABS as string[]).includes(value)
}

/**
 * How the Late tab is ordered. Only Late has this: the other two are already
 * ordered by the thing somebody scans them for, a name and a wait.
 */
export type LateSort = "oldest" | "most" | "person"

export const LATE_SORTS: LateSort[] = ["oldest", "most", "person"]

export function isLateSort(value: unknown): value is LateSort {
  return typeof value === "string" && (LATE_SORTS as string[]).includes(value)
}

/**
 * Thai collation, named rather than left to the machine.
 *
 * `localeCompare` with no locale follows whatever the host is configured for,
 * and the host is not the factory. Naming `th` keeps the order the same on a
 * developer's laptop and on the box in Bangkok, and it sorts the handful of
 * Latin display names correctly on the way.
 */
export function byName(a: string, b: string): number {
  return a.localeCompare(b, "th")
}

/**
 * Everything `groupRuns` reads, and nothing else.
 *
 * Deliberately narrower than `ViewRow`: My tasks groups the same repetition and
 * has no person on its rows to read, because every row there already belongs to
 * the reader. Widening this to `ViewRow` would force that screen to invent an
 * assignee and a position it does not have.
 */
export type RunRow = {
  templateId: string
  description: string
  dueDate: string
}

/** Everything the team overview reads from a row, and nothing else. */
export type ViewRow = RunRow & {
  /** Null while the position has nobody in it. The work is still owed. */
  assignee: string | null
  position: string
  department: string
}

/**
 * Who a row is about: the person doing it, or the empty position that owes it.
 * A duty nobody holds is still somebody's job to fill, so it sorts and filters
 * among the people rather than falling to the bottom.
 */
export function personOf(row: ViewRow): string {
  return row.assignee ?? row.position
}

/**
 * The two filters, as they arrive from the url.
 *
 * Substrings and not exact values, because the field they come from is typed
 * into. A name half spelled narrows the list, which is the whole point of a bar
 * that applies as you type.
 *
 * Case is folded with `toLowerCase`, which does nothing at all to Thai: Thai has
 * no letter case, so 183 of the 193 names match on their bytes and the handful
 * of Latin display names match either way round.
 */
export type TeamFilter = { person: string; duty: string }

export const NO_FILTER: TeamFilter = { person: "", duty: "" }

export function isFiltered(filter: TeamFilter): boolean {
  return filter.person !== "" || filter.duty !== ""
}

export function matchesFilter(row: ViewRow, filter: TeamFilter): boolean {
  if (filter.person && !contains(personOf(row), filter.person)) return false
  if (filter.duty && !contains(row.description, filter.duty)) return false
  return true
}

function contains(haystack: string, needle: string): boolean {
  return haystack.toLowerCase().includes(needle.toLowerCase().trim())
}

/**
 * The distinct people, and the distinct duties, in a set of rows.
 *
 * Offered under the two fields so somebody can pick a Thai name rather than
 * type one. They are built from the tab's rows BEFORE the filter is applied:
 * built after, choosing a name would collapse the list to that one name and
 * there would be no way back to any other.
 */
export function peopleIn(rows: ViewRow[]): string[] {
  return [...new Set(rows.map(personOf))].sort(byName)
}

export function dutiesIn(rows: ViewRow[]): string[] {
  return [...new Set(rows.map((r) => r.description))].sort(byName)
}

/**
 * One duty across every date it comes back on, as one line.
 *
 * This is the redesign. Sixteen late rows in Top management are four duties
 * repeated across dates, and the same duty missed seven days running took seven
 * rows and pushed the rest of the department onto page two of a table. Grouped,
 * the seven become one line that says seven, and the pagination that existed to
 * carry the repetition is not needed at all.
 */
export type Run<T extends RunRow> = {
  /** Stable across renders, and unique among the runs of one list. */
  key: string
  description: string
  /** The oldest and the newest date in the run, inclusive. */
  from: string
  to: string
  rows: T[]
}

/**
 * The repetition, collapsed. What the key is, is the caller's question.
 *
 * The two screens that carry a backlog ask it differently and the difference is
 * real, not a preference. The team overview lists other people's work, so a
 * duty is only one line when it is also one person's: a position that changes
 * hands mid-run is two people's records. My tasks lists nothing but the
 * reader's own, so the person is a constant on every row and putting it in the
 * key would be spelling out a value that cannot vary.
 *
 * Neither of them groups on the words on screen. Two departments run duties
 * spelled identically, so the template is what identifies a duty; on My tasks
 * the same holds the moment somebody holds positions in two of them.
 *
 * The order is the oldest run first, which is the order both screens open on
 * and the only one that needs no control to explain it. `groupLate` re-sorts on
 * top for the team overview's three orderings.
 */
export function groupRuns<T extends RunRow>(
  rows: T[],
  keyOf: (row: T) => string,
): Run<T>[] {
  const runs = new Map<string, Run<T>>()

  for (const row of rows) {
    const key = keyOf(row)
    const found = runs.get(key)
    if (found) {
      found.rows.push(row)
      if (row.dueDate < found.from) found.from = row.dueDate
      if (row.dueDate > found.to) found.to = row.dueDate
      continue
    }
    runs.set(key, {
      key,
      description: row.description,
      from: row.dueDate,
      to: row.dueDate,
      rows: [row],
    })
  }

  const list = [...runs.values()]
  // Inside a run the dates read forward, oldest at the top, whatever the runs
  // themselves are sorted by. A run of missed days is a sequence and reversing
  // it for one sort order and not another would make it unreadable.
  for (const run of list) {
    run.rows.sort((a, b) => compare(a.dueDate, b.dueDate))
  }

  list.sort(
    (a, b) => compare(a.from, b.from) || byName(a.description, b.description),
  )

  return list
}

/** A run on the team overview, which is a duty and the person who owes it. */
export type LateGroup<T extends ViewRow> = Run<T> & {
  /** The holder's name, or the position title when nobody holds it. */
  person: string
  /** True when `person` is a position title standing in for nobody. */
  vacant: boolean
  department: string
}

/**
 * Grouped by template and by person, in one of the tab's three orderings.
 *
 * The person is in the key because a position can change hands inside a run of
 * missed days, and those are two people's records rather than one.
 */
export function groupLate<T extends ViewRow>(
  rows: T[],
  sort: LateSort,
): LateGroup<T>[] {
  const list = groupRuns(
    rows,
    (row) => `${row.templateId}|${personOf(row)}`,
  ).map((run) => {
    // Every row in a run shares its person by construction, and its department
    // follows the template, so the earliest date speaks for all of them.
    const first = run.rows[0]!
    return {
      ...run,
      person: personOf(first),
      vacant: first.assignee === null,
      department: first.department,
    }
  })

  list.sort((a, b) => {
    if (sort === "most") {
      // Ties go to the older run: two duties missed three times each are not
      // equally urgent when one of them started a fortnight earlier.
      return b.rows.length - a.rows.length || compare(a.from, b.from)
    }
    if (sort === "person") {
      return byName(a.person, b.person) || byName(a.description, b.description)
    }
    return compare(a.from, b.from) || byName(a.description, b.description)
  })

  return list
}

function compare(a: string, b: string): number {
  return a < b ? -1 : a > b ? 1 : 0
}
