"use client"

import * as React from "react"
import Link from "next/link"
import type { ColumnDef } from "@tanstack/react-table"
import { IconChevronRight } from "@tabler/icons-react"

import { useLocale, useT } from "@/components/locale-provider"
import { Badge } from "@/components/ui/badge"
import { DataTable, DataTableColumnHeader } from "@/components/ui/data-table"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import { dayAndMonth, moment, shortDay } from "@/lib/i18n/dates"
import type { Locale, Translate } from "@/lib/i18n"
import type { MessageKey } from "@/lib/i18n/messages"
import type { TeamRow } from "@/lib/services/team"
import type { LateGroup } from "@/lib/team-view"
import { STATUS } from "@/app/tasks/columns"
import { TaskActionsPanel } from "@/app/tasks/[id]/task-actions-panel"

/**
 * The four tables, on the client, because their headers are words and the words
 * come from the reader.
 *
 * The page cannot build the columns: a column definition holds render
 * functions, and a Server Component cannot hand a function to a client one. So
 * the page passes rows and the words are chosen here, from `useT()` and
 * `useLocale()`.
 *
 * Four builders and not one with flags. They answer different questions and
 * share only the task link, and a single column set switched on a mode is how a
 * table ends up with a column that means nothing in one of them.
 *
 * None of them paginates. Grouping is what took the Late tab from 57 rows to
 * 15, and the other two are a day's work and a check queue: they were never the
 * reason a pager was there.
 */

/** Every row leads into the task, where the acts that exist already live. */
function taskLink(row: TeamRow) {
  return (
    <Link href={`/tasks/${row.id}`} className="font-medium hover:underline">
      {row.description}
    </Link>
  )
}

/**
 * The assignee, or the position when nobody holds it.
 *
 * Dimmed rather than left blank, because a duty with nobody on it is still
 * somebody's job to fill, and a manager reading a blank cell learns nothing
 * about whose work is missing.
 */
function person(row: TeamRow) {
  if (row.assignee) return row.assignee
  return <span className="text-foreground/70">{row.position}</span>
}

/** What a row is sorted and filtered on when the cell shows a name or a title. */
function personValue(row: TeamRow): string {
  return row.assignee ?? row.position
}

/**
 * The status alone, without the "late" badge the task list carries.
 *
 * On the Late tab that badge would repeat the tile above it, and on Due today
 * it can never be true: lateness is `due < today` and every row there is due
 * today.
 */
function statusBadge(t: Translate, row: TeamRow) {
  const { label, variant } = STATUS[row.status]
  return <Badge variant={variant}>{t(label)}</Badge>
}

function statusColumn(t: Translate): ColumnDef<TeamRow> {
  return {
    accessorKey: "status",
    meta: { label: t("tasks.status") },
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title={t("tasks.status")} />
    ),
    cell: ({ row }) => statusBadge(t, row.original),
  }
}

function personColumn(t: Translate): ColumnDef<TeamRow> {
  return {
    id: "person",
    accessorFn: personValue,
    meta: { label: t("team.person") },
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title={t("team.person")} />
    ),
    // The name is data, in Thai for 183 of the 193 people, and nothing here
    // translates it.
    cell: ({ row }) => person(row.original),
  }
}

/**
 * Which department a row belongs to, and only when there is more than one.
 *
 * An admin may hold several at once, and then two services running the same
 * duty under the same words become one list of work belonging to nobody. With a
 * single department in view the column would repeat the line under the title on
 * every row, so it is not built at all.
 */
function departmentColumn(t: Translate): ColumnDef<TeamRow> {
  return {
    accessorKey: "department",
    meta: { label: t("team.department") },
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title={t("team.department")} />
    ),
  }
}

function taskColumn(t: Translate): ColumnDef<TeamRow> {
  return {
    accessorKey: "description",
    meta: { label: t("tasks.task") },
    header: ({ column }) => (
      <DataTableColumnHeader column={column} title={t("tasks.task")} />
    ),
    cell: ({ row }) => taskLink(row.original),
  }
}

/**
 * What this viewer may do with this row, and nothing to do with which tab it is
 * on or what rank the reader holds.
 *
 * The list arrives on the row from `availableActions`, asked per task with the
 * viewer's relation to that task. Most rows come back empty and then there is
 * no cell content at all: an empty flex box would still be a column of nothing
 * suggesting a control somebody has not found.
 *
 * The panel is `/tasks/[id]`'s own, reused rather than copied. It carries the
 * confirmations, the notes those acts require, and the refusal wording, and
 * every button in it reaches a Server Action that asks who is signed in and
 * re-derives the relation before it writes. A button drawn here is not a
 * permission, and neither is one that is missing.
 */
function actionColumn(t: Translate): ColumnDef<TeamRow> {
  return {
    id: "actions",
    enableSorting: false,
    enableHiding: false,
    meta: { label: t("team.action") },
    header: () => <span>{t("team.action")}</span>,
    cell: ({ row }) =>
      row.original.actions.length ? (
        <TaskActionsPanel
          taskId={row.original.id}
          actions={row.original.actions}
        />
      ) : null,
  }
}

/**
 * Late, ungrouped, behind the toggle. Due date first, because that is the
 * damage.
 *
 * It carries the action column now, like the other two tabs. It did not, and
 * the reason was worth writing down: every one of the 57 late rows here is
 * `missed`, and a missed task used to offer nobody a verb, so the column was
 * empty on every row. That was the defect rather than the design. With the work
 * still owed the manager reading this tab can chase any of these rows, and the
 * one person who could clear them can do so from their own list.
 */
function lateColumns(
  t: Translate,
  locale: Locale,
  many: boolean,
): ColumnDef<TeamRow>[] {
  return [
    {
      accessorKey: "dueDate",
      meta: { label: t("tasks.due") },
      header: ({ column }) => (
        <DataTableColumnHeader column={column} title={t("tasks.due")} />
      ),
      cell: ({ row }) => (
        <span className="whitespace-nowrap tabular-nums">
          {shortDay(row.original.dueDate, locale)}
        </span>
      ),
    },
    taskColumn(t),
    personColumn(t),
    ...(many ? [departmentColumn(t)] : []),
    statusColumn(t),
    actionColumn(t),
  ]
}

/** Where today stands, read down the names. */
function dueTodayColumns(t: Translate, many: boolean): ColumnDef<TeamRow>[] {
  return [
    personColumn(t),
    ...(many ? [departmentColumn(t)] : []),
    taskColumn(t),
    statusColumn(t),
    actionColumn(t),
  ]
  // No Due column. Every row is due today and the tile says so.
}

/**
 * What has stopped at the gate.
 *
 * The checker's name is the whole point of this table. `/review` never needs
 * it, because there the reader is the checker; here the reader is the person
 * who has to go and ask, unless they are the checker themselves, in which case
 * the action column hands them Approve without a second screen.
 */
function waitingColumns(
  t: Translate,
  locale: Locale,
  many: boolean,
): ColumnDef<TeamRow>[] {
  return [
    taskColumn(t),
    ...(many ? [departmentColumn(t)] : []),
    {
      id: "doneBy",
      accessorFn: personValue,
      meta: { label: t("review.doneBy") },
      header: ({ column }) => (
        <DataTableColumnHeader column={column} title={t("review.doneBy")} />
      ),
      cell: ({ row }) => person(row.original),
    },
    {
      accessorKey: "checker",
      meta: { label: t("team.waitingOn") },
      enableSorting: false,
      header: ({ column }) => (
        <DataTableColumnHeader column={column} title={t("team.waitingOn")} />
      ),
      cell: ({ row }) => row.original.checker ?? "—",
    },
    {
      accessorKey: "finishedAt",
      meta: { label: t("review.since") },
      header: ({ column }) => (
        <DataTableColumnHeader column={column} title={t("review.since")} />
      ),
      cell: ({ row }) => (
        <span className="whitespace-nowrap tabular-nums">
          {row.original.finishedAt
            ? moment(row.original.finishedAt, locale)
            : "—"}
        </span>
      ),
    },
    actionColumn(t),
  ]
}

/**
 * What an empty table says.
 *
 * Two different facts wearing the same shape, and they need different
 * sentences: a department with nothing behind it has been told good news, and a
 * reader whose filter matches nothing has been told to widen it. Most
 * departments are the first case, so the tab's own sentence is the default and
 * the instruction is the exception.
 */
function emptyText(t: Translate, narrowed: boolean, own: MessageKey): string {
  return narrowed ? t("team.nothingMatches") : t(own)
}

export function LateTable({
  rows,
  many,
  narrowed,
}: {
  rows: TeamRow[]
  many: boolean
  narrowed: boolean
}) {
  const t = useT()
  const locale = useLocale()

  return (
    <DataTable
      columns={lateColumns(t, locale, many)}
      data={rows}
      empty={emptyText(t, narrowed, "team.nothingLate")}
      paginate={false}
    />
  )
}

export function DueTodayTable({
  rows,
  many,
  narrowed,
}: {
  rows: TeamRow[]
  many: boolean
  narrowed: boolean
}) {
  const t = useT()

  return (
    <DataTable
      columns={dueTodayColumns(t, many)}
      data={rows}
      empty={emptyText(t, narrowed, "tasks.nothingDueToday")}
      paginate={false}
    />
  )
}

export function WaitingTable({
  rows,
  many,
  narrowed,
}: {
  rows: TeamRow[]
  many: boolean
  narrowed: boolean
}) {
  const t = useT()
  const locale = useLocale()

  return (
    <DataTable
      columns={waitingColumns(t, locale, many)}
      data={rows}
      empty={emptyText(t, narrowed, "team.nothingWaitingCheck")}
      paginate={false}
    />
  )
}

/**
 * Late, grouped, which is what the tab opens on.
 *
 * Not a `DataTable`. The rows come in two shapes, a duty and a date under it,
 * and TanStack would need one column set covering both: a Due column that is
 * blank on every group row and a count column that is blank on every date. The
 * markup is a plain table instead, with the same `Table` primitives every other
 * list uses, so it reads as the same furniture.
 *
 * Which groups are open is local state and not the url. It is where the reader
 * happens to be looking rather than what they are looking at, and putting it in
 * the address would put fifteen keys in a link somebody sends.
 */
export function LateGroupedTable({
  groups,
  many,
  narrowed,
}: {
  groups: LateGroup<TeamRow>[]
  many: boolean
  narrowed: boolean
}) {
  const t = useT()
  const locale = useLocale()
  const [open, setOpen] = React.useState<ReadonlySet<string>>(new Set())

  const toggle = (key: string) => {
    setOpen((was) => {
      const next = new Set(was)
      if (!next.delete(key)) next.add(key)
      return next
    })
  }

  // The duty cell, the optional department, the count and the window.
  const columns = many ? 4 : 3

  return (
    <div className="overflow-hidden rounded-xl ring-1 ring-foreground/10">
      <Table>
        <TableHeader>
          <TableRow>
            <TableHead>{t("team.duty")}</TableHead>
            {many ? <TableHead>{t("team.department")}</TableHead> : null}
            <TableHead>{t("tasks.due")}</TableHead>
            {/*
              "Late" and not "Status" over the count. Every row in this bucket
              is behind by definition, and the pill says how many dates rather
              than which of the five statuses they are in: those are on the
              lines inside, where they differ from one date to the next.
            */}
            <TableHead className="text-right">{t("team.late")}</TableHead>
          </TableRow>
        </TableHeader>
        <TableBody>
          {groups.length === 0 ? (
            <TableRow>
              <TableCell colSpan={columns} className="py-10 text-center">
                {emptyText(t, narrowed, "team.nothingLate")}
              </TableCell>
            </TableRow>
          ) : (
            groups.map((group, index) => {
              const shown = open.has(group.key)
              /*
                The position in the list and not the group's own key. The key
                carries a person's name, and `aria-controls` is a
                space-separated list of ids: a Thai name with a space in it
                makes the browser look for two elements, neither of which
                exists. 183 of the 193 names have one.
              */
              const panel = `late-panel-${index}`
              return (
                <React.Fragment key={group.key}>
                  <TableRow>
                    <TableCell>
                      <button
                        type="button"
                        onClick={() => toggle(group.key)}
                        aria-expanded={shown}
                        aria-controls={panel}
                        aria-label={
                          shown ? t("team.hideDates") : t("team.showDates")
                        }
                        className="-m-1 flex items-start gap-2 rounded-lg p-1 text-left outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
                      >
                        <IconChevronRight
                          className={`mt-0.5 size-4 shrink-0 transition-transform ${
                            shown ? "rotate-90" : ""
                          }`}
                        />
                        <span className="flex flex-col">
                          {/*
                            The duty in normal weight and the person under it.
                            Bold on the duty would say the duty is the news; the
                            news is that this pair has a run of missed days
                            behind it, and the count says that.
                          */}
                          <span>{group.description}</span>
                          <span
                            className={
                              group.vacant
                                ? "text-sm text-foreground/50"
                                : "text-sm text-foreground/70"
                            }
                          >
                            {group.person}
                          </span>
                        </span>
                      </button>
                    </TableCell>
                    {many ? <TableCell>{group.department}</TableCell> : null}
                    <TableCell className="tabular-nums">
                      {runWindow(group, locale)}
                    </TableCell>
                    <TableCell className="text-right">
                      <Badge variant="destructive">
                        {group.rows.length === 1
                          ? t("team.dateOne")
                          : t("team.dates", { count: group.rows.length })}
                      </Badge>
                    </TableCell>
                  </TableRow>
                  {shown ? (
                    <TableRow id={panel}>
                      <TableCell colSpan={columns} className="bg-muted/30">
                        <ul className="flex flex-col gap-1 ps-8">
                          {group.rows.map((row) => (
                            <li
                              key={row.id}
                              className="flex items-center gap-3"
                            >
                              <Link
                                href={`/tasks/${row.id}`}
                                aria-label={t("team.openTask")}
                                className="tabular-nums hover:underline"
                              >
                                {shortDay(row.dueDate, locale)}
                              </Link>
                              {statusBadge(t, row)}
                            </li>
                          ))}
                        </ul>
                      </TableCell>
                    </TableRow>
                  ) : null}
                </React.Fragment>
              )
            })
          )}
        </TableBody>
      </Table>
    </div>
  )
}

/**
 * The run, as one phrase. A group of one is one date and not an arrow between a
 * day and itself.
 */
function runWindow(group: LateGroup<TeamRow>, locale: Locale): string {
  const from = dayAndMonth(group.from, locale)
  if (group.from === group.to) return from
  return `${from} → ${dayAndMonth(group.to, locale)}`
}
