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

/**
 * The job description overview, cut the way the admin screen reads it.
 *
 * A job description belongs to a position and this screen is a list of people,
 * so the whole of this module lives in that mismatch. The roster has 193 people
 * and 193 positions, but they do not pair off: a transfer can empty one chair
 * and double up another, and somebody who has left keeps their history without
 * keeping their seat. Expanding chairs into rows is therefore a decision, not a
 * join, and it is made here where it can be read and tested rather than inside
 * a query.
 *
 * Its own module, with no import from `lib/services/`, for the reason
 * `lib/team-view.ts` sets out: the table and its columns are client components,
 * and taking a type from the service would pull `lib/db/client.ts` into the
 * browser bundle and fail the build inside the Postgres driver.
 */

export type Holder = { id: string; name: string; archived: boolean }

/** A position, its job description, and whoever is sitting in it today. */
export type Chair = {
  positionId: string
  title: string
  department: string
  /** The prose. Null or blank means nobody has written one. */
  jobDescription: string | null
  /**
   * Duties that exist only to be read. `general` never generates a task, so
   * these are the job description's own content rather than a side effect of
   * the work.
   */
  generalDuties: number
  /** Duties that also become tasks. Shown, but they do not order the screen. */
  recurringDuties: number
  holders: Holder[]
}

export type JobDescriptionRow = {
  positionId: string
  title: string
  department: string
  /** Null when nobody holds the chair. */
  person: { id: string; name: string } | null
  hasText: boolean
  generalDuties: number
  recurringDuties: number
}

/**
 * Blank prose is no prose.
 *
 * `setJobDescription` already stores an empty string as null, so this is for
 * the rows that predate it and for anything that arrives another way. A screen
 * that reported a page of spaces as "written" would send somebody looking for
 * a job description that is not there.
 */
const written = (text: string | null): boolean => (text ?? "").trim() !== ""

/**
 * The three numbers above the table.
 *
 * Counted from the chairs and never from the rows, because the two do not
 * agree: a position with two holders is one position and two lines, so summing
 * a column of the table would report a factory with more positions than it has
 * and one more job description left to write than is actually missing.
 */
export function jobDescriptionSummary(chairs: Chair[]): {
  positions: number
  rows: number
  unwritten: number
} {
  return {
    positions: chairs.length,
    rows: chairs.reduce(
      (n, chair) =>
        n + Math.max(1, chair.holders.filter((h) => !h.archived).length),
      0,
    ),
    unwritten: chairs.filter((chair) => !written(chair.jobDescription)).length,
  }
}

/**
 * One row per person, and one row for the chair nobody is in.
 *
 * The vacant chair is kept on purpose. It is counter-intuitive on a screen
 * listing people, but the spec is explicit that a job description is what the
 * next holder reads to learn the job — so the empty chair is the one whose
 * description matters most, and hiding it would hide exactly the gap this
 * screen exists to find.
 *
 * Two holders give two rows rather than one shared line. `liveTemplates` has
 * to refuse to pick between them because a task can only be done once; a job
 * description is read, not done, and both of them need to read it.
 */
export function jobDescriptionRows(chairs: Chair[]): JobDescriptionRow[] {
  const rows = chairs.flatMap((chair): JobDescriptionRow[] => {
    const seated = chair.holders.filter((holder) => !holder.archived)
    const shared = {
      positionId: chair.positionId,
      title: chair.title,
      department: chair.department,
      hasText: written(chair.jobDescription),
      generalDuties: chair.generalDuties,
      recurringDuties: chair.recurringDuties,
    }

    if (seated.length === 0) return [{ ...shared, person: null }]
    return seated.map((holder) => ({
      ...shared,
      person: { id: holder.id, name: holder.name },
    }))
  })

  /**
   * Emptiest first, because the screen is a worklist for whoever has to write
   * the missing ones. A chair with no general duties and no prose says nothing
   * at all about its job and belongs at the top; a chair with four duties and a
   * paragraph belongs at the bottom.
   *
   * Recurring duties are deliberately not part of this. Forty daily tasks
   * describe what somebody does today, not what the job is for, and letting
   * them count would bury the busiest positions whose description is thinnest.
   */
  return rows.sort(
    (a, b) =>
      a.generalDuties - b.generalDuties ||
      Number(a.hasText) - Number(b.hasText) ||
      byName(a.person?.name ?? a.title, b.person?.name ?? b.title),
  )
}

/* -------------------------------------------------------------------------- */
/* One person's own                                                           */
/* -------------------------------------------------------------------------- */

/**
 * Everything the staff screen reads off a duty, and nothing else.
 *
 * Structural rather than imported from `lib/services/duties.ts`, like the row
 * types in `lib/team-view.ts`: a service row satisfies this on its own and
 * there is not even a type import pointing the wrong way down the layers.
 */
type ReadableDuty = { occurrence: string; active: boolean }

export type OwnJobDescription<D extends ReadableDuty> = {
  /** Listed and never generated. These are the job description's own content. */
  listed: D[]
  /** Also becomes a task on a schedule. Shown, because it is part of the job. */
  generated: D[]
  hasText: boolean
  /**
   * Nothing has been written and nothing has been listed.
   *
   * Its own flag rather than something the page works out, because the two
   * halves can be empty separately and only the pair of them means "there is
   * nothing here to read". A position with no paragraph but with duties on it
   * describes the job in the only way anybody has described it so far, and a
   * page that called that empty would be throwing away the answer it has.
   *
   * On 1 September this is true for roughly half the roster, so it is the
   * ordinary screen and not the edge case.
   */
  isEmpty: boolean
}

/**
 * A position's job description, as the person holding it reads it.
 *
 * Read-only everywhere it is used, and that is not an oversight. The
 * description belongs to the position: somebody editing their own would be
 * editing their successor's, and the spec puts writing it with the people who
 * own the position rather than the people sitting in it.
 */
export function ownJobDescription<D extends ReadableDuty>(input: {
  jobDescription: string | null
  duties: D[]
}): OwnJobDescription<D> {
  const owed = input.duties.filter((duty) => duty.active)
  const listed = owed.filter((duty) => duty.occurrence === "general")
  const generated = owed.filter((duty) => duty.occurrence !== "general")
  const hasText = written(input.jobDescription)

  return {
    listed,
    generated,
    hasText,
    isEmpty: !hasText && owed.length === 0,
  }
}
