import { byName } from "@/lib/team-view"

/**
 * The arithmetic behind /kpi, with nothing under it.
 *
 * It is here and not in `lib/services/kpi.ts` for the same reason
 * `lib/team-view.ts` exists beside `lib/services/team.ts`: what a query returns
 * and what a screen is owed are two different shapes, and the step between them
 * is the part worth being able to test without a database in the room. A KPI
 * screen that is wrong is worse than no KPI screen, and "wrong" here means the
 * rows not adding up to the total above them.
 */

export type KpiFigures = {
  /** How many instances fell due inside the window. */
  due: number
  /** Of those, how many are `finished` or `checked`. See `DONE`. */
  done: number
  /** Still open with the due day behind it. See `isLateOnDay`. */
  late: number
  /** Judged out of time by the nightly sweep. See `isPastCutOff`. */
  missed: number
}

/**
 * One group as the query returns it: a person and a position together, because
 * work on an empty chair has no person to group on and the position is the only
 * thing that tells one empty chair from another.
 */
export type KpiGroup = {
  /** Null when nobody holds the position the work belongs to. */
  personId: string | null
  /** Null in exactly the cases `personId` is. */
  name: string | null
  positionId: string
  position: string
  /** Somebody who has left. Their work still happened and still counts. */
  left: boolean
  figures: KpiFigures
}

/**
 * One line of the department's figures.
 *
 * Vacant work is a row rather than a footnote because the rows have to add up
 * to the totals above them. A department with ten instances on an empty chair
 * would otherwise show a completion figure nobody in the table accounts for,
 * and the first thing anybody does with a number that does not add up is stop
 * believing the ones that do.
 */
export type KpiRow = {
  /** The person, or the position when there is nobody in it. */
  key: string
  /** Null while the position is vacant, and then `position` is what is shown. */
  name: string | null
  position: string
  left: boolean
  figures: KpiFigures
}

const ZERO: KpiFigures = { due: 0, done: 0, late: 0, missed: 0 }

function add(a: KpiFigures, b: KpiFigures): KpiFigures {
  return {
    due: a.due + b.due,
    done: a.done + b.done,
    late: a.late + b.late,
    missed: a.missed + b.missed,
  }
}

/**
 * One row per person, in the order a list should show them.
 *
 * The fold is on the person and not on the position, because ELEVA-116 turns on
 * a manager's line for somebody being the same figure that person reads about
 * themselves. Somebody holding two positions would come back from the query as
 * two groups; folded on their id they are one row, which is what their own
 * screen shows them. Vacant work cannot fold that way and folds on the position
 * instead, so two empty chairs stay two lines.
 *
 * Name order, and no other. A table sorted worst-first by a service would be
 * the service deciding who belongs at the top of a list about people; the
 * screen's own column headers do that when a manager asks for it.
 */
export function foldGroups(groups: readonly KpiGroup[]): KpiRow[] {
  const rows = new Map<string, KpiRow>()
  for (const group of groups) {
    const key = group.personId ?? `position:${group.positionId}`
    const at = rows.get(key)
    if (at) {
      at.figures = add(at.figures, group.figures)
      continue
    }
    rows.set(key, {
      key,
      name: group.name,
      position: group.position,
      left: group.left,
      figures: { ...group.figures },
    })
  }
  return [...rows.values()].sort((a, b) =>
    byName(a.name ?? a.position, b.name ?? b.position),
  )
}

/**
 * The total, added up from the rows and never counted a second time.
 *
 * A second aggregate over the same window would be a second chance to be wrong,
 * and the one thing a reader does with a table under a total is check that it
 * adds up.
 *
 * Null when there are no rows at all, which is not the same answer as a row of
 * noughts: a person who was owed nothing has not failed at anything, and a
 * screen of zeroes reads as a month of failure.
 */
export function totalOf(rows: readonly KpiRow[]): KpiFigures | null {
  if (rows.length === 0) return null
  return rows.reduce((sum, row) => add(sum, row.figures), ZERO)
}

/**
 * The completion percentage, rounded once and in one place.
 *
 * `due` is never nought where this is called: a row exists only where an
 * instance did, and the total is null rather than zero when nothing was due. It
 * is asserted rather than guarded so that a caller who finds a way to break
 * that hears about it.
 */
export function percentDone(figures: KpiFigures): number {
  return Math.round((figures.done / figures.due) * 100)
}
