import { AppShell } from "@/components/app-shell"
import { dayIn } from "@/lib/calendar"
import { translator, type Translate } from "@/lib/i18n"
import { fullDay } from "@/lib/i18n/dates"
import { listDepartmentNames } from "@/lib/services/positions"
import { requireAtLeast } from "@/lib/services/session"
import { teamOverview, type TeamRow } from "@/lib/services/team"
import { isOwed } from "@/lib/task-actions"
import {
  dutiesIn,
  groupLate,
  isLateSort,
  isFiltered,
  isTeamTab,
  matchesFilter,
  peopleIn,
  type LateSort,
  type TeamFilter,
  type TeamTab,
} from "@/lib/team-view"
import { DepartmentPicker } from "./department-picker"
import {
  DueTodayTable,
  LateGroupedTable,
  LateTable,
  WaitingTable,
} from "./team-tables"
import { TeamFilters } from "./team-filters"
import { TeamTiles } from "./team-tiles"

/**
 * My department, today.
 *
 * Three questions in the order somebody actually asks them. Is anything behind.
 * Where does today stand, person by person. Is finished work stuck at the
 * check. They were three sections stacked down the page and are now three tabs,
 * because on any day the department had anything behind it the check queue sat
 * below the fold and the thing somebody scrolled past was the thing they came
 * for. The buckets are still cut so no task can appear in two of them.
 *
 * The fourth tile is not a question but a measure: how much of the last thirty
 * days' work was done. It answers the thing a manager asks after the three
 * lists, "is this a bad week or is this normal", and no list can.
 *
 * ## The whole state is the url
 *
 * The tab, the two filters, the ordering and the grouping all live in
 * `searchParams` and are read here. A view is therefore a link somebody can
 * send, Back returns to the one they came from, and nothing is mirrored in a
 * browser to go stale. The client components write the url and read nothing
 * else.
 *
 * ## What the rows may do
 *
 * There are buttons now, and they are not a widening. Each row carries the
 * actions `availableActions` gave it for THIS viewer and THIS task, decided
 * from the relation the duty names them in. Nothing branches on the tier and
 * nothing is assumed from the tab; most rows carry nothing and show nothing.
 * Every button reaches `/tasks/[id]`'s own Server Actions, which read the
 * session and re-derive the relation before writing, so the list having drawn
 * one is not a permission.
 *
 * Two things the mockup showed are still deliberately absent: Excuse and Close
 * the run. Both forgive work that was not done, and the client answered
 * ELEVA-156 on 4 August in the other direction: work owed is work owed, and
 * there is no way to write it off. The third, marking a missed task done, is
 * built now, because that answer requires it: the 57 late rows are all `missed`,
 * their assignees can see and finish them again, and their manager can chase
 * them. What a row offers is still `availableActions` per row and never the tab.
 *
 * Manager and above. `routes.json` puts the team overview under the manager
 * guard, and unlike `/review` there is nothing data-driven to argue with: this
 * is somebody else's work, shown because the reader runs the department.
 */
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
  const user = await requireAtLeast("manager")

  const params = await searchParams
  /**
   * Handed straight to the service, which drops it for anybody below admin.
   * The page never resolves the departments itself.
   *
   * Repeated rather than comma separated, and read with the same shape
   * `/positions` uses, because a department name may contain a comma and
   * because two screens should not disagree about what `?dept=` means.
   */
  const dept = Array.isArray(params.dept)
    ? params.dept
    : params.dept
      ? [params.dept]
      : []

  const [overview, departmentNames] = await Promise.all([
    teamOverview(user, { departments: dept }),
    // Only an admin is offered a choice, so only an admin costs a second query.
    user.tier === "admin" ? listDepartmentNames(user) : Promise.resolve([]),
  ])

  const t = translator(user.language)
  // The date is on screen even when there is no department to put in front of
  // it, because a screen with one sentence on it should still say which day it
  // is refusing to answer for.
  const today = overview?.today ?? dayIn(new Date())

  // More than one department in view is what makes the department column worth
  // a cell on every row. With one it would repeat the line above the tiles.
  const many = (overview?.departments.length ?? 0) > 1

  /*
    Anything unreadable in the url falls back to the default rather than
    erroring. These arrive from an address bar, where anybody can type anything,
    and a screen that 500s on `?tab=banana` is a screen somebody breaks by
    mistyping a link they were sent.
  */
  const tab: TeamTab = isTeamTab(params.tab) ? params.tab : "late"
  const sort: LateSort = isLateSort(params.sort) ? params.sort : "oldest"
  // Grouped is the default and the point of the redesign, so only turning it
  // off costs a parameter.
  const grouped = params.group !== "off"
  const filter: TeamFilter = {
    person: text(params.person),
    duty: text(params.duty),
  }

  /*
    An empty table means two different things and has to say two different
    sentences: 42 of the 46 departments have nothing late at all, which is the
    answer, and a filter matching nothing is an instruction to widen it.
  */
  const narrowed = isFiltered(filter)

  const rows = tabRows(overview, tab)
  // Offered under the filter fields, and built from the tab's rows BEFORE the
  // filter: built after, choosing a name would collapse the list to that name
  // and there would be no way back to any other.
  const people = peopleIn(rows)
  const duties = dutiesIn(rows)
  const shownRows = rows.filter((row) => matchesFilter(row, filter))
  const groups = groupLate(shownRows, sort)

  const lateGrouped = tab === "late" && grouped
  /*
    Dates on every tab, including the grouped one, and not lines on screen.
    Grouped, four lines can stand for seventeen dates, and the number a reader
    compares against is the seventeen in the tile above: counting the lines
    would give them a figure that matches nothing else on the page.
  */
  const shown = t("team.shownRows", {
    shown: shownRows.length,
    total: rows.length,
  })

  return (
    <AppShell title="team.title" user={user}>
      <div className="flex flex-col gap-4">
        <div className="flex flex-wrap items-center justify-between gap-3">
          {/*
            Data and a date, so no dictionary key for the department: the name is
            spelled the way the roster spells it and nothing here translates it.
            The headcount is between them because it is what turns a count of
            late work into a rate somebody can judge: sixteen behind in a
            department of two is not sixteen behind in a department of twelve.
          */}
          <p className="text-foreground/80">
            {overview
              ? `${overview.departments.join(", ")} · ${headcount(t, overview.headcount)} · ${fullDay(today, user.language)}`
              : fullDay(today, user.language)}
          </p>
          {user.tier === "admin" ? (
            <DepartmentPicker departments={departmentNames} />
          ) : null}
        </div>

        {/*
          No department. Two different situations wearing the same shape: an
          admin who has not chosen yet, who is one click from an answer, and a
          manager between positions, for whom nothing on this screen can be
          made to work until somebody in HR acts. The picker above is already
          the admin's way out, so all that is left to say is which of the two
          this is.
        */}
        {!overview ? (
          <p className="text-foreground/80">
            {user.tier === "admin"
              ? t("team.chooseDepartment")
              : t("team.noDepartment")}
          </p>
        ) : (
          <>
            {/*
              The tiles count the department and not the filter. They are what
              the department's state is; the line beside the filter bar is what
              this reader has narrowed it to, and the two are different
              questions asked one above the other.
            */}
            <TeamTiles
              counts={{
                late: overview.late.length,
                today: overview.dueToday.length,
                waiting: overview.waitingCheck.length,
                lateDuties: groupLate(overview.late, "oldest").length,
                // Still owed and not merely not-yet-begun. Six of today's
                // fifteen rows are `missed`, carried in from a cycle that
                // closed, and a subtitle reading "9 still to do" over a table
                // of fifteen outstanding tasks is the wrong number twice.
                todayOpen: overview.dueToday.filter((r) => isOwed(r.status))
                  .length,
                waitingCheckers: new Set(
                  overview.waitingCheck
                    .map((r) => r.checker)
                    .filter((name): name is string => name !== null),
                ).size,
              }}
              completion={overview.completion}
              selected={tab}
            />

            <TeamFilters
              people={people}
              duties={duties}
              sort={sort}
              grouped={grouped}
              lateTab={tab === "late"}
              shown={shown}
            />

            <div
              role="tabpanel"
              id="team-panel"
              aria-labelledby={`team-tab-${tab}`}
            >
              {tab === "late" ? (
                lateGrouped ? (
                  <LateGroupedTable
                    groups={groups}
                    many={many}
                    narrowed={narrowed}
                  />
                ) : (
                  <LateTable rows={shownRows} many={many} narrowed={narrowed} />
                )
              ) : tab === "today" ? (
                <DueTodayTable
                  rows={shownRows}
                  many={many}
                  narrowed={narrowed}
                />
              ) : (
                <WaitingTable
                  rows={shownRows}
                  many={many}
                  narrowed={narrowed}
                />
              )}
            </div>
          </>
        )}
      </div>
    </AppShell>
  )
}

/**
 * "one person" and not "1 people". Seven of the 46 departments hold exactly one
 * person, so this is not a corner: it is a seventh of the screens.
 */
function headcount(t: Translate, count: number): string {
  return count === 1 ? t("team.peopleOne") : t("team.people", { count })
}

/** A single url value, trimmed. Repeated ones are somebody's mistake, not a set. */
function text(value: string | string[] | undefined): string {
  return (Array.isArray(value) ? (value[0] ?? "") : (value ?? "")).trim()
}

function tabRows(
  overview: {
    late: TeamRow[]
    dueToday: TeamRow[]
    waitingCheck: TeamRow[]
  } | null,
  tab: TeamTab,
): TeamRow[] {
  if (!overview) return []
  if (tab === "late") return overview.late
  if (tab === "today") return overview.dueToday
  return overview.waitingCheck
}
