// frontend/src/components/Tables.tsx

import {
  Fragment,
  type ReactNode,
  useLayoutEffect,
  useRef,
  useState,
} from 'react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import Button from './Button'

export type TableVariant =
  | 'simple'
  | 'card'
  | 'full-width'
  | 'constrained'
  | 'striped'
  | 'uppercase'
  | 'stacked-mobile'
  | 'hidden-mobile'
  | 'avatars'
  | 'sticky-header'
  | 'vertical-lines'
  | 'condensed'
  | 'sortable'
  | 'grouped'
  | 'summary'
  | 'border'

export type TableColumn<T> = {
  key: string
  header: string
  render: (row: T) => ReactNode
  mobileRender?: (row: T) => ReactNode
  isPrimary?: boolean
  sortable?: boolean
  hideOnMobile?: 'sm' | 'md' | 'lg'
  align?: 'left' | 'center' | 'right'
  className?: string
  headerClassName?: string
}

export type TableGroup<T> = {
  id: string | number
  name: string
  rows: T[]
}

export type TableSummaryRow = {
  label: ReactNode
  value: ReactNode
  emphasized?: boolean
  colSpan?: number
}

type SortDirection = 'asc' | 'desc'

type TablesProps<T> = {
  title?: string
  description?: string

  rows?: T[]
  groups?: TableGroup<T>[]

  columns: TableColumn<T>[]
  getRowId: (row: T) => string | number

  variant?: TableVariant
  emptyText?: string

  actionLabel?: string
  onAction?: () => void
  headerAction?: ReactNode

  rowActions?: (row: T) => ReactNode

  selectable?: boolean
  selectedRowIds?: Array<string | number>
  onSelectedRowIdsChange?: (ids: Array<string | number>) => void

  sortKey?: string
  sortDirection?: SortDirection
  onSort?: (column: TableColumn<T>) => void

  summaryRows?: TableSummaryRow[]

  hideHeadings?: boolean
  stickyTopClassName?: string

  className?: string
}

function classNames(...classes: Array<string | false | null | undefined>) {
  return classes.filter(Boolean).join(' ')
}

function getAlignmentClass(align?: TableColumn<unknown>['align']) {
  if (align === 'right') {
    return 'text-right'
  }

  if (align === 'center') {
    return 'text-center'
  }

  return 'text-left'
}

function getHiddenClass(hideOnMobile?: TableColumn<unknown>['hideOnMobile']) {
  if (hideOnMobile === 'sm') {
    return 'hidden sm:table-cell'
  }

  if (hideOnMobile === 'md') {
    return 'hidden md:table-cell'
  }

  if (hideOnMobile === 'lg') {
    return 'hidden lg:table-cell'
  }

  return ''
}

function Checkbox({
  checked,
  onChange,
  inputRef,
  value,
}: {
  checked: boolean
  onChange: (checked: boolean) => void
  inputRef?: React.Ref<HTMLInputElement>
  value?: string
}) {
  return (
    <div className="group absolute top-1/2 left-4 -mt-2 grid size-4 grid-cols-1">
      <input
        ref={inputRef}
        type="checkbox"
        value={value}
        checked={checked}
        onChange={(event) => onChange(event.target.checked)}
        className="col-start-1 row-start-1 appearance-none rounded-sm border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 indeterminate:border-indigo-600 indeterminate:bg-indigo-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 dark:border-white/20 dark:bg-gray-800/50 dark:checked:border-indigo-500 dark:checked:bg-indigo-500 dark:indeterminate:border-indigo-500 dark:indeterminate:bg-indigo-500 dark:focus-visible:outline-indigo-500 dark:disabled:border-white/10 dark:disabled:bg-gray-800 dark:disabled:checked:bg-gray-800 forced-colors:appearance-auto"
      />

      <svg
        viewBox="0 0 14 14"
        fill="none"
        className="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-disabled:stroke-gray-950/25 dark:group-has-disabled:stroke-white/25"
      >
        <path
          d="M3 8L6 11L11 3.5"
          strokeWidth={2}
          strokeLinecap="round"
          strokeLinejoin="round"
          className="opacity-0 group-has-checked:opacity-100"
        />
        <path
          d="M3 7H11"
          strokeWidth={2}
          strokeLinecap="round"
          strokeLinejoin="round"
          className="opacity-0 group-has-indeterminate:opacity-100"
        />
      </svg>
    </div>
  )
}

export default function Tables<T>({
  title,
  description,

  rows = [],
  groups,

  columns,
  getRowId,

  variant = 'simple',
  emptyText = 'Keine Einträge vorhanden.',

  actionLabel,
  onAction,
  headerAction,

  rowActions,

  selectable = false,
  selectedRowIds,
  onSelectedRowIdsChange,

  sortKey,
  sortDirection = 'asc',
  onSort,

  summaryRows,

  hideHeadings = false,
  stickyTopClassName = 'top-0',

  className,
}: TablesProps<T>) {
  const headerCheckboxRef = useRef<HTMLInputElement>(null)
  const [internalSelectedRowIds, setInternalSelectedRowIds] = useState<Array<string | number>>([])

  const rowGroups =
    groups && groups.length > 0
      ? groups
      : [
          {
            id: 'default',
            name: '',
            rows,
          },
        ]

  const allRows = rowGroups.flatMap((group) => group.rows)
  const allRowIds = allRows.map(getRowId)

  const selectedIds = selectedRowIds ?? internalSelectedRowIds
  const selectedSet = new Set(selectedIds)

  const checked = allRowIds.length > 0 && selectedIds.length === allRowIds.length
  const indeterminate = selectedIds.length > 0 && selectedIds.length < allRowIds.length

  const isCard = variant === 'card'
  const isBorder = variant === 'border'
  const isFullWidth = variant === 'full-width'
  const isConstrained = variant === 'constrained'
  const isStriped = variant === 'striped'
  const isUppercase = variant === 'uppercase'
  const isStackedMobile = variant === 'stacked-mobile'
  const isStickyHeader = variant === 'sticky-header'
  const isVerticalLines = variant === 'vertical-lines'
  const isCondensed = variant === 'condensed'
  const isSortable = variant === 'sortable'
  const isGrouped = variant === 'grouped' || Boolean(groups?.length)
  const hasSummary = variant === 'summary' || Boolean(summaryRows?.length)

  const totalColumns =
    columns.length +
    (selectable ? 1 : 0) +
    (rowActions ? 1 : 0)

  useLayoutEffect(() => {
    if (!headerCheckboxRef.current) {
      return
    }

    headerCheckboxRef.current.indeterminate = indeterminate
  }, [indeterminate])

  function updateSelected(nextSelectedIds: Array<string | number>) {
    if (!selectedRowIds) {
      setInternalSelectedRowIds(nextSelectedIds)
    }

    onSelectedRowIdsChange?.(nextSelectedIds)
  }

  function toggleAll(nextChecked: boolean) {
    updateSelected(nextChecked ? allRowIds : [])
  }

  function toggleRow(rowId: string | number, nextChecked: boolean) {
    if (nextChecked) {
      updateSelected(Array.from(new Set([...selectedIds, rowId])))
      return
    }

    updateSelected(selectedIds.filter((id) => id !== rowId))
  }

  function renderHeaderAction() {
    if (headerAction) {
      return headerAction
    }

    if (!actionLabel) {
      return null
    }

    return (
      <Button
        type="button"
        color="indigo"
        size="md"
        onClick={onAction}
      >
        {actionLabel}
      </Button>
    )
  }

  function renderHeader() {
    if (!title && !description && !actionLabel && !headerAction) {
      return null
    }

    return (
      <div
        className={classNames(
          isConstrained && 'mx-auto max-w-7xl',
          !isFullWidth && 'px-4 sm:px-6 lg:px-8',
          isFullWidth && 'px-4 sm:px-6 lg:px-8',
          'sm:flex sm:items-center',
        )}
      >
        <div className="sm:flex-auto">
          {title && (
            <h1 className="text-base font-semibold text-gray-900 dark:text-white">
              {title}
            </h1>
          )}

          {description && (
            <p className="mt-2 text-sm text-gray-700 dark:text-gray-300">
              {description}
            </p>
          )}
        </div>

        {(actionLabel || headerAction) && (
          <div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
            {renderHeaderAction()}
          </div>
        )}
      </div>
    )
  }

  function renderColumnHeader(column: TableColumn<T>) {
    const activeSortColumn = sortKey === column.key
    const showSortIcon = isSortable || column.sortable

    if (!showSortIcon) {
      return column.header
    }

    return (
      <button
        type="button"
        onClick={() => onSort?.(column)}
        className="group inline-flex"
      >
        {column.header}

        <span
          className={classNames(
            activeSortColumn
              ? 'ml-2 flex-none rounded-sm bg-gray-100 text-gray-900 group-hover:bg-gray-200 dark:bg-gray-800 dark:text-white dark:group-hover:bg-gray-700'
              : 'invisible ml-2 flex-none rounded-sm text-gray-400 group-hover:visible group-focus:visible dark:text-gray-500',
          )}
        >
          <ChevronDownIcon
            aria-hidden="true"
            className={classNames(
              'size-5',
              activeSortColumn && sortDirection === 'asc' && 'rotate-180',
            )}
          />
        </span>
      </button>
    )
  }

  function renderTableHead() {
    return (
      <thead
        className={classNames(
          hideHeadings && 'sr-only',
          isCard && 'bg-gray-50 dark:bg-gray-800/75',
          !isCard && 'bg-white dark:bg-gray-900',
        )}
      >
        <tr className={classNames(isVerticalLines && 'divide-x divide-gray-200 dark:divide-white/10')}>
          {selectable && (
            <th scope="col" className="relative px-7 sm:w-12 sm:px-6">
              <Checkbox
                inputRef={headerCheckboxRef}
                checked={checked}
                onChange={toggleAll}
              />
            </th>
          )}

          {columns.map((column, columnIndex) => {
            const isFirst = columnIndex === 0
            const isLast = columnIndex === columns.length - 1 && !rowActions
            const hiddenClass = getHiddenClass(column.hideOnMobile)

            return (
              <th
                key={column.key}
                scope="col"
                className={classNames(
                  isStickyHeader &&
                    `sticky ${stickyTopClassName} z-10 bg-white/75 backdrop-blur-sm backdrop-filter dark:bg-gray-900/75`,
                  isUppercase
                    ? 'py-3 text-xs font-medium tracking-wide text-gray-500 uppercase dark:text-gray-400'
                    : 'py-3.5 text-sm font-semibold text-gray-900 dark:text-white',
                  isCondensed ? 'px-2' : 'px-3',
                  isFirst && !selectable && 'pl-4 sm:pl-0',
                  isCard && isFirst && 'sm:pl-6',
                  isLast && !isCard && 'pr-4 sm:pr-0',
                  isLast && isCard && 'sm:pr-6',
                  getAlignmentClass(column.align),
                  hiddenClass,
                  column.headerClassName,
                )}
              >
                {renderColumnHeader(column)}
              </th>
            )
          })}

          {rowActions && (
            <th
              scope="col"
              className={classNames(
                isStickyHeader &&
                  `sticky ${stickyTopClassName} z-10 bg-white/75 backdrop-blur-sm backdrop-filter dark:bg-gray-900/75`,
                'py-3.5 pr-4 pl-3 sm:pr-0',
                isCard && 'sm:pr-6',
              )}
            >
              <span className="sr-only">Aktionen</span>
            </th>
          )}
        </tr>
      </thead>
    )
  }

  function renderRow(row: T, rowIndex: number) {
    const rowId = getRowId(row)
    const selected = selectedSet.has(rowId)

    return (
      <tr
        key={rowId}
        className={classNames(
          isStriped && 'even:bg-gray-50 dark:even:bg-gray-800/50',
          selected && 'bg-gray-50 dark:bg-gray-800/50',
          isVerticalLines && 'divide-x divide-gray-200 dark:divide-white/10',
        )}
      >
        {selectable && (
          <td className="relative px-7 sm:w-12 sm:px-6">
            {selected && (
              <div className="absolute inset-y-0 left-0 w-0.5 bg-indigo-600 dark:bg-indigo-500" />
            )}

            <Checkbox
              checked={selected}
              value={String(rowId)}
              onChange={(nextChecked) => toggleRow(rowId, nextChecked)}
            />
          </td>
        )}

        {columns.map((column, columnIndex) => {
          const isFirst = columnIndex === 0
          const isLast = columnIndex === columns.length - 1 && !rowActions
          const hiddenClass = getHiddenClass(column.hideOnMobile)

          return (
            <td
              key={column.key}
              className={classNames(
                isCondensed ? 'py-2' : isAvatarVariant(variant) ? 'py-5' : 'py-4',
                isCondensed ? 'px-2' : 'px-3',
                isFirst && !selectable && 'pl-4 sm:pl-0',
                isCard && isFirst && 'sm:pl-6',
                isLast && !isCard && 'pr-4 sm:pr-0',
                isLast && isCard && 'sm:pr-6',
                'text-sm text-gray-500 dark:text-gray-400',
                !isStackedMobile && 'whitespace-nowrap',
                column.isPrimary && 'font-medium text-gray-900 dark:text-white',
                selected && column.isPrimary && 'text-indigo-600 dark:text-indigo-400',
                getAlignmentClass(column.align),
                hiddenClass,
                column.className,
              )}
            >
              {column.render(row)}

              {isStackedMobile && column.isPrimary && column.mobileRender && (
                <dl className="font-normal lg:hidden">
                  {column.mobileRender(row)}
                </dl>
              )}
            </td>
          )
        })}

        {rowActions && (
          <td
            className={classNames(
              isCondensed ? 'py-2' : 'py-4',
              'pr-4 pl-3 text-right text-sm font-medium whitespace-nowrap sm:pr-0',
              isCard && 'sm:pr-6',
            )}
          >
            {rowActions(row)}
          </td>
        )}
      </tr>
    )
  }

  function renderGroupedRows() {
    return rowGroups.map((group) => (
      <Fragment key={group.id}>
        {isGrouped && group.name && (
          <tr className="border-t border-gray-200 dark:border-white/10">
            <th
              scope="colgroup"
              colSpan={totalColumns}
              className="bg-gray-50 py-2 pr-3 pl-4 text-left text-sm font-semibold text-gray-900 sm:pl-3 dark:bg-gray-800/50 dark:text-white"
            >
              {group.name}
            </th>
          </tr>
        )}

        {group.rows.map(renderRow)}
      </Fragment>
    ))
  }

  function renderEmptyRow() {
    if (allRows.length > 0) {
      return null
    }

    return (
      <tr>
        <td
          colSpan={totalColumns}
          className="px-4 py-10 text-center text-sm text-gray-500 dark:text-gray-400"
        >
          {emptyText}
        </td>
      </tr>
    )
  }

  function renderSummaryRows() {
    if (!hasSummary || !summaryRows?.length) {
      return null
    }

    return (
      <tfoot>
        {summaryRows.map((row, index) => {
          const colSpan = row.colSpan ?? Math.max(totalColumns - 1, 1)

          return (
            <tr key={index}>
              <th
                scope="row"
                colSpan={colSpan}
                className={classNames(
                  index === 0 ? 'pt-6' : 'pt-4',
                  'pr-3 pl-4 text-right text-sm sm:pl-0',
                  row.emphasized
                    ? 'font-semibold text-gray-900 dark:text-white'
                    : 'font-normal text-gray-500 dark:text-gray-400',
                )}
              >
                {row.label}
              </th>

              <td
                className={classNames(
                  index === 0 ? 'pt-6' : 'pt-4',
                  'pr-4 pl-3 text-right text-sm sm:pr-0',
                  row.emphasized
                    ? 'font-semibold text-gray-900 dark:text-white'
                    : 'text-gray-500 dark:text-gray-400',
                )}
              >
                {row.value}
              </td>
            </tr>
          )
        })}
      </tfoot>
    )
  }

  function renderTable() {
    return (
      <table
        className={classNames(
          isStickyHeader
            ? 'min-w-full border-separate border-spacing-0'
            : 'relative min-w-full divide-y divide-gray-300 dark:divide-white/15',
          isBorder && 'divide-y divide-gray-300 dark:divide-white/15',
        )}
      >
        {renderTableHead()}

        <tbody
          className={classNames(
            !isStickyHeader && 'divide-y divide-gray-200 dark:divide-white/10',
            isCard
              ? 'bg-white dark:bg-gray-800/50'
              : 'bg-white dark:bg-gray-900',
          )}
        >
          {renderEmptyRow()}
          {renderGroupedRows()}
        </tbody>

        {renderSummaryRows()}
      </table>
    )
  }

  return (
    <div className={className}>
      {renderHeader()}

      <div
        className={classNames(
          title || description || actionLabel || headerAction ? 'mt-8' : '',
          'flow-root',
          isFullWidth && 'overflow-hidden',
        )}
      >
        <div
          className={classNames(
            isConstrained && 'mx-auto max-w-7xl px-4 sm:px-6 lg:px-8',
            !isConstrained && '-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8',
          )}
        >
          <div
            className={classNames(
              isConstrained
                ? ''
                : 'inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8',
              isFullWidth && 'sm:px-0 lg:px-0',
            )}
          >
            {isCard || isBorder ? (
              <div
                className={classNames(
                  isCard &&
                    'overflow-hidden shadow-sm outline-1 outline-black/5 sm:rounded-lg dark:shadow-none dark:-outline-offset-1 dark:outline-white/10',
                  isBorder &&
                    'overflow-hidden ring-1 ring-gray-300 sm:rounded-lg dark:ring-white/15',
                )}
              >
                {renderTable()}
              </div>
            ) : (
              renderTable()
            )}
          </div>
        </div>
      </div>
    </div>
  )
}

function isAvatarVariant(variant: TableVariant) {
  return variant === 'avatars'
}