import type { CSSProperties } from 'react';
import { Fragment, useCallback, useMemo, useState } from 'react';
import type { DataTableColumn } from '../types';
import './datatable.css';

export type DataTableProps<T extends Record<string, unknown> = Record<string, unknown>> = {
  columns: DataTableColumn<T>[];
  rows: T[];
  striped?: boolean;
  bordered?: boolean;
  compact?: boolean;
  clientSort?: boolean;
  groupBy?: string;
  groupCollapsible?: boolean;
  renderGroupHeader?: (ctx: { groupKey: string; groupValue: unknown; rows: T[] }) => React.ReactNode;
  onSortChange?: (payload: { id: string; direction: 'asc' | 'desc' | '' }) => void;
  onGroupToggle?: (payload: { groupKey: string; expanded: boolean }) => void;
  /** Enables a leading checkbox column + "select all". */
  rowSelection?: boolean;
  /** Required for stable selection. Defaults to row index (not stable across sorts). */
  getRowId?: (row: T, index: number) => string;
  /** Controlled selection ids. */
  selectedRowIds?: string[];
  /** Uncontrolled initial selection ids. */
  defaultSelectedRowIds?: string[];
  onSelectedRowIdsChange?: (ids: string[]) => void;
};

function getClass<T extends Record<string, unknown>>(col: DataTableColumn<T>, row: T): string {
  return typeof col.className === 'function' ? col.className(row) : col.className || '';
}

function getStyle<T extends Record<string, unknown>>(
  col: DataTableColumn<T>,
  row: T
): CSSProperties {
  return typeof col.style === 'function' ? col.style(row) : col.style || {};
}

function headerBorderClasses<T extends Record<string, unknown>>(col: DataTableColumn<T>): string {
  if (!col.borders) return '';
  const c: string[] = [];
  if (col.borders.left) c.push('ccl-datatable__border-left');
  if (col.borders.right) c.push('ccl-datatable__border-right');
  if (col.borders.top) c.push('ccl-datatable__border-top');
  if (col.borders.bottom) c.push('ccl-datatable__border-bottom');
  return c.join(' ');
}

function cellBorderClasses<T extends Record<string, unknown>>(col: DataTableColumn<T>): string {
  if (!col.borders) return '';
  const c: string[] = [];
  if (col.borders.left) c.push('ccl-datatable__cell-border-left');
  if (col.borders.right) c.push('ccl-datatable__cell-border-right');
  if (col.borders.top) c.push('ccl-datatable__cell-border-top');
  if (col.borders.bottom) c.push('ccl-datatable__cell-border-bottom');
  return c.join(' ');
}

function compareValues(a: unknown, b: unknown): number {
  if (a == null && b == null) return 0;
  if (a == null) return 1;
  if (b == null) return -1;

  if (typeof a === 'number' && typeof b === 'number') return a - b;
  if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
  if (typeof a === 'boolean' && typeof b === 'boolean') return Number(a) - Number(b);

  const sa = String(a);
  const sb = String(b);
  return sa.localeCompare(sb, undefined, { numeric: true, sensitivity: 'base' });
}

export function DataTable<T extends Record<string, unknown> = Record<string, unknown>>({
  columns,
  rows,
  striped = false,
  bordered = false,
  compact = false,
  clientSort = true,
  groupBy,
  groupCollapsible = true,
  renderGroupHeader,
  onSortChange,
  onGroupToggle,
  rowSelection = false,
  getRowId,
  selectedRowIds,
  defaultSelectedRowIds,
  onSelectedRowIdsChange,
}: DataTableProps<T>) {
  const [sortId, setSortId] = useState('');
  const [sortDirection, setSortDirection] = useState<'asc' | 'desc' | ''>('');
  const [expandedGroups, setExpandedGroups] = useState(() => new Set<string>());
  const [uncontrolledSelected, setUncontrolledSelected] = useState<Set<string>>(
    () => new Set(defaultSelectedRowIds ?? [])
  );

  const selectedSet = useMemo(() => {
    return selectedRowIds ? new Set(selectedRowIds) : uncontrolledSelected;
  }, [selectedRowIds, uncontrolledSelected]);

  const parentHeaders = useMemo(() => {
    const parentMap = new Map<string, { colspan: number; startIndex: number }>();
    columns.forEach((col, index) => {
      if (col.parentHeader) {
        if (!parentMap.has(col.parentHeader)) {
          parentMap.set(col.parentHeader, { colspan: 0, startIndex: index });
        }
        parentMap.get(col.parentHeader)!.colspan++;
      }
    });
    return Array.from(parentMap.entries()).map(([label, info]) => ({
      label,
      colspan: info.colspan,
      startIndex: info.startIndex,
    }));
  }, [columns]);

  const getParentHeaderAt = useCallback(
    (index: number) => parentHeaders.find((p) => p.startIndex === index),
    [parentHeaders]
  );

  const onHeaderClick = useCallback(
    (col: DataTableColumn<T>) => {
      if (!col.sortable) return;
      let nextDir: 'asc' | 'desc' | '' = 'asc';
      let nextId = col.id;
      if (sortId !== col.id) {
        nextDir = 'asc';
      } else {
        nextDir = sortDirection === 'asc' ? 'desc' : sortDirection === 'desc' ? '' : 'asc';
        if (nextDir === '') nextId = '';
      }
      setSortId(nextId);
      setSortDirection(nextDir);
      onSortChange?.({ id: nextId, direction: nextDir });
    },
    [onSortChange, sortDirection, sortId]
  );

  const rowsToRender = useMemo(() => {
    if (!clientSort || !sortId || !sortDirection) return rows;
    const col = columns.find((c) => c.id === sortId);
    if (!col) return rows;
    const accessor = col.accessor ?? ((row: T) => row[col.id as keyof T]);
    const sorted = [...rows].sort((a, b) => {
      const va = accessor(a);
      const vb = accessor(b);
      return compareValues(va, vb);
    });
    return sortDirection === 'asc' ? sorted : sorted.reverse();
  }, [clientSort, columns, rows, sortDirection, sortId]);

  type GroupItem =
    | { isGroup: true; groupKey: string; groupValue: unknown; groupRows: T[] }
    | { isGroup: false; row: T };

  const groupedRows = useMemo((): GroupItem[] => {
    if (!groupBy) return rowsToRender.map((row) => ({ isGroup: false, row }));
    const groups = new Map<string, T[]>();
    rowsToRender.forEach((row) => {
      const groupKey = String(row[groupBy] ?? 'Unknown');
      if (!groups.has(groupKey)) groups.set(groupKey, []);
      groups.get(groupKey)!.push(row);
    });
    const result: GroupItem[] = [];
    Array.from(groups.entries())
      .sort(([a], [b]) => a.localeCompare(b))
      .forEach(([groupKey, groupRows]) => {
        result.push({
          isGroup: true,
          groupKey,
          groupValue: groupRows[0][groupBy],
          groupRows,
        });
      });
    return result;
  }, [groupBy, rowsToRender]);

  const isGroupExpanded = useCallback(
    (groupKey: string) => !groupCollapsible || expandedGroups.has(groupKey),
    [expandedGroups, groupCollapsible]
  );

  const rowIndexByRef = useMemo(() => {
    const m = new Map<T, number>();
    rowsToRender.forEach((r, i) => m.set(r, i));
    return m;
  }, [rowsToRender]);

  const visibleRows = useMemo(() => {
    const out: T[] = [];
    groupedRows.forEach((item) => {
      if (item.isGroup === false) out.push(item.row);
      else if (isGroupExpanded(item.groupKey)) out.push(...item.groupRows);
    });
    return out;
  }, [groupedRows, isGroupExpanded]);

  const resolveRowId = useCallback(
    (row: T) => {
      const idx = rowIndexByRef.get(row) ?? -1;
      return getRowId ? getRowId(row, idx) : String(idx);
    },
    [getRowId, rowIndexByRef]
  );

  const emitSelection = useCallback(
    (next: Set<string>) => {
      const ids = Array.from(next);
      if (selectedRowIds) onSelectedRowIdsChange?.(ids);
      else {
        setUncontrolledSelected(next);
        onSelectedRowIdsChange?.(ids);
      }
    },
    [onSelectedRowIdsChange, selectedRowIds]
  );

  const allVisibleIds = useMemo(() => {
    if (!rowSelection) return [];
    return visibleRows.map((r) => resolveRowId(r));
  }, [resolveRowId, rowSelection, visibleRows]);

  const allVisibleSelected = useMemo(() => {
    if (!rowSelection || allVisibleIds.length === 0) return false;
    return allVisibleIds.every((id) => selectedSet.has(id));
  }, [allVisibleIds, rowSelection, selectedSet]);

  const someVisibleSelected = useMemo(() => {
    if (!rowSelection || allVisibleIds.length === 0) return false;
    return allVisibleIds.some((id) => selectedSet.has(id)) && !allVisibleSelected;
  }, [allVisibleIds, allVisibleSelected, rowSelection, selectedSet]);

  const toggleAllVisible = useCallback(() => {
    const next = new Set(selectedSet);
    if (allVisibleSelected) allVisibleIds.forEach((id) => next.delete(id));
    else allVisibleIds.forEach((id) => next.add(id));
    emitSelection(next);
  }, [allVisibleIds, allVisibleSelected, emitSelection, selectedSet]);

  const toggleGroup = useCallback(
    (groupKey: string) => {
      if (!groupCollapsible) return;
      setExpandedGroups((prev) => {
        const next = new Set(prev);
        const was = next.has(groupKey);
        if (was) next.delete(groupKey);
        else next.add(groupKey);
        onGroupToggle?.({ groupKey, expanded: !was });
        return next;
      });
    },
    [groupCollapsible, onGroupToggle]
  );

  const handleClick = useCallback((col: DataTableColumn<T>, row: T, value: unknown) => {
    if (col.onClick) col.onClick(row, value);
    else if (col.link) {
      const url = col.link(row);
      if (url) window.location.href = url;
    }
  }, []);

  const tableClass = [
    'ccl-datatable',
    striped ? 'ccl-datatable--striped' : '',
    bordered ? 'ccl-datatable--bordered' : '',
    compact ? 'ccl-datatable--compact' : '',
  ]
    .filter(Boolean)
    .join(' ');

  const renderCellContent = (row: T, index: number) => {
    const col = columns[index];
    if (!col) return null;
    if (col.renderCell) return col.renderCell(row, index);
    return row[col.id as keyof T] == null ? '' : String(row[col.id as keyof T]);
  };

  const hasParent = parentHeaders.length > 0;
  const colSpan = columns.length + (rowSelection ? 1 : 0);

  return (
    <div className="ccl-datatable-wrapper">
      <table className={tableClass} role="table">
        <thead>
          {hasParent ? (
            <>
              <tr role="row" className="ccl-datatable__parent-header">
                {rowSelection ? (
                  <th
                    role="columnheader"
                    scope="colgroup"
                    rowSpan={2}
                    className="ccl-datatable__select-cell"
                  >
                    <input
                      type="checkbox"
                      className="ccl-datatable__checkbox"
                      checked={allVisibleSelected}
                      ref={(el) => {
                        if (el) el.indeterminate = someVisibleSelected;
                      }}
                      aria-label="Select all rows"
                      onChange={() => toggleAllVisible()}
                      onClick={(e) => e.stopPropagation()}
                    />
                  </th>
                ) : null}
                {columns.map((col, i) => {
                  if (col.parentHeader) {
                    const ph = getParentHeaderAt(i);
                    if (!ph) return null;
                    return (
                      <th
                        key={`p-${col.id}-${i}`}
                        role="columnheader"
                        scope="colgroup"
                        colSpan={ph.colspan}
                        className={`ccl-datatable__parent-header-cell ccl-datatable__align-${col.align ?? 'center'} ${headerBorderClasses(col)}`}
                      >
                        {ph.label}
                      </th>
                    );
                  }
                  return (
                    <th
                      key={`p-${col.id}-${i}`}
                      role="columnheader"
                      scope="colgroup"
                      rowSpan={2}
                      aria-sort={
                        col.sortable && sortId === col.id
                          ? sortDirection === 'asc'
                            ? 'ascending'
                            : sortDirection === 'desc'
                              ? 'descending'
                              : 'none'
                          : undefined
                      }
                      className={`ccl-datatable__parent-header-cell${col.sortable ? ' ccl-datatable__sortable' : ''} ccl-datatable__align-${col.align ?? 'center'} ${headerBorderClasses(col)}`}
                      style={{ width: col.width || 'auto' }}
                      onClick={() => onHeaderClick(col)}
                    >
                      <span className="ccl-datatable__header">
                        <span className="ccl-datatable__header-label">{col.label}</span>
                        {col.sortable ? (
                          <span className="ccl-datatable__sort-indicator" aria-hidden>
                            <span
                              className={`ccl-datatable__arrow ccl-datatable__arrow--up${sortId === col.id && sortDirection === 'asc' ? ' active' : ''}`}
                            >
                              ▲
                            </span>
                            <span
                              className={`ccl-datatable__arrow ccl-datatable__arrow--down${sortId === col.id && sortDirection === 'desc' ? ' active' : ''}`}
                            >
                              ▼
                            </span>
                          </span>
                        ) : null}
                      </span>
                    </th>
                  );
                })}
              </tr>
              <tr role="row">
                {columns.map((col) => {
                  if (!col.parentHeader) return null;
                  return (
                    <th
                      key={`c-${col.id}`}
                      role="columnheader"
                      scope="col"
                      aria-sort={
                        col.sortable && sortId === col.id
                          ? sortDirection === 'asc'
                            ? 'ascending'
                            : sortDirection === 'desc'
                              ? 'descending'
                              : 'none'
                          : undefined
                      }
                      style={{ width: col.width || 'auto' }}
                      className={`${col.sortable ? 'ccl-datatable__sortable' : ''} ccl-datatable__align-${col.align ?? 'center'} ${headerBorderClasses(col)}`}
                      onClick={() => onHeaderClick(col)}
                    >
                      <span className="ccl-datatable__header">
                        <span className="ccl-datatable__header-label">{col.label}</span>
                        {col.sortable ? (
                          <span className="ccl-datatable__sort-indicator" aria-hidden>
                            <span
                              className={`ccl-datatable__arrow ccl-datatable__arrow--up${sortId === col.id && sortDirection === 'asc' ? ' active' : ''}`}
                            >
                              ▲
                            </span>
                            <span
                              className={`ccl-datatable__arrow ccl-datatable__arrow--down${sortId === col.id && sortDirection === 'desc' ? ' active' : ''}`}
                            >
                              ▼
                            </span>
                          </span>
                        ) : null}
                      </span>
                    </th>
                  );
                })}
              </tr>
            </>
          ) : (
            <tr role="row">
              {rowSelection ? (
                <th role="columnheader" scope="col" className="ccl-datatable__select-cell">
                  <input
                    type="checkbox"
                    className="ccl-datatable__checkbox"
                    checked={allVisibleSelected}
                    ref={(el) => {
                      if (el) el.indeterminate = someVisibleSelected;
                    }}
                    aria-label="Select all rows"
                    onChange={() => toggleAllVisible()}
                    onClick={(e) => e.stopPropagation()}
                  />
                </th>
              ) : null}
              {columns.map((col) => (
                <th
                  key={col.id}
                  role="columnheader"
                  scope="col"
                  style={{ width: col.width || 'auto' }}
                  aria-sort={
                    col.sortable && sortId === col.id
                      ? sortDirection === 'asc'
                        ? 'ascending'
                        : sortDirection === 'desc'
                          ? 'descending'
                          : 'none'
                      : undefined
                  }
                  className={`${col.sortable ? 'ccl-datatable__sortable' : ''} ccl-datatable__align-${col.align ?? 'center'} ${headerBorderClasses(col)}`}
                  onClick={() => onHeaderClick(col)}
                >
                  <span className="ccl-datatable__header">
                    <span className="ccl-datatable__header-label">{col.label}</span>
                    {col.sortable ? (
                      <span className="ccl-datatable__sort-indicator" aria-hidden>
                        <span
                          className={`ccl-datatable__arrow ccl-datatable__arrow--up${sortId === col.id && sortDirection === 'asc' ? ' active' : ''}`}
                        >
                          ▲
                        </span>
                        <span
                          className={`ccl-datatable__arrow ccl-datatable__arrow--down${sortId === col.id && sortDirection === 'desc' ? ' active' : ''}`}
                        >
                          ▼
                        </span>
                      </span>
                    ) : null}
                  </span>
                </th>
              ))}
            </tr>
          )}
        </thead>
        <tbody>
          {!rows || rows.length === 0 ? (
            <tr>
              <td colSpan={colSpan} className="ccl-datatable__empty">
                No data available.
              </td>
            </tr>
          ) : null}
          {groupedRows.map((item, idx) => {
            if (item.isGroup === false) {
              const row = item.row;
              const rid = rowSelection ? resolveRowId(row) : '';
              return (
                <tr key={`r-${idx}`} role="row">
                  {rowSelection ? (
                    <td className="ccl-datatable__select-cell" role="cell" onClick={(e) => e.stopPropagation()}>
                      <input
                        type="checkbox"
                        className="ccl-datatable__checkbox"
                        checked={selectedSet.has(rid)}
                        aria-label="Select row"
                        onChange={() => {
                          const next = new Set(selectedSet);
                          if (next.has(rid)) next.delete(rid);
                          else next.add(rid);
                          emitSelection(next);
                        }}
                      />
                    </td>
                  ) : null}
                  {columns.map((col, index) => (
                    <td
                      key={col.id}
                      role="cell"
                      className={`${getClass(col, row)} ${cellBorderClasses(col)}`.trim()}
                      style={getStyle(col, row)}
                      onClick={() => handleClick(col, row, row[col.id as keyof T])}
                    >
                      {renderCellContent(row, index)}
                    </td>
                  ))}
                </tr>
              );
            }
            const { groupKey, groupValue, groupRows } = item;
            return (
              <Fragment key={`g-${groupKey}`}>
                <tr className={`ccl-datatable__group-header${groupCollapsible ? ' ccl-datatable__group-collapsible' : ''}`}>
                  <td colSpan={colSpan} className="ccl-datatable__group-cell">
                    {renderGroupHeader ? (
                      renderGroupHeader({ groupKey, groupValue, rows: groupRows })
                    ) : (
                      <div className="ccl-datatable__group-content">
                        {groupCollapsible ? (
                          <button
                            type="button"
                            className="ccl-datatable__group-toggle"
                            onClick={() => toggleGroup(groupKey)}
                            aria-expanded={isGroupExpanded(groupKey)}
                          >
                            <span className={`ccl-datatable__group-icon${isGroupExpanded(groupKey) ? ' expanded' : ''}`}>
                              ▶
                            </span>
                          </button>
                        ) : null}
                        <span className="ccl-datatable__group-label">
                          {String(groupValue)} ({groupRows.length} items)
                        </span>
                      </div>
                    )}
                  </td>
                </tr>
                {isGroupExpanded(groupKey)
                  ? groupRows.map((row, ri) => (
                      <tr key={`${groupKey}-${ri}`} role="row" className="ccl-datatable__group-row">
                        {rowSelection ? (
                          <td className="ccl-datatable__select-cell" role="cell" onClick={(e) => e.stopPropagation()}>
                            <input
                              type="checkbox"
                              className="ccl-datatable__checkbox"
                              checked={selectedSet.has(resolveRowId(row))}
                              aria-label="Select row"
                              onChange={() => {
                                const rid = resolveRowId(row);
                                const next = new Set(selectedSet);
                                if (next.has(rid)) next.delete(rid);
                                else next.add(rid);
                                emitSelection(next);
                              }}
                            />
                          </td>
                        ) : null}
                        {columns.map((col, index) => (
                          <td
                            key={col.id}
                            role="cell"
                            className={`${getClass(col, row)} ${cellBorderClasses(col)}`.trim()}
                            style={getStyle(col, row)}
                            onClick={() => handleClick(col, row, row[col.id as keyof T])}
                          >
                            {renderCellContent(row, index)}
                          </td>
                        ))}
                      </tr>
                    ))
                  : null}
              </Fragment>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}
