import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
import { HlmSpinnerImports } from '@spartan-ng/helm/spinner';
import { HlmTableImports } from '@spartan-ng/helm/table';
import {
  FlexRenderDirective,
  type Cell,
  type Header,
  type Row,
  type Table,
} from '@tanstack/angular-table';
import { isElasticColumn, isRigidColumn, type DataTableColumnDef } from '../types';

/**
 * Presentational component for a TanStack `<table>`. Owns the scroll wrapper,
 * sticky header (vertical), sticky columns (horizontal), table layout, and
 * the empty/loading fallback. Hosts compose the toolbar (search + filters +
 * actions) and the paginator (`<au-table-pagination>`) as siblings — this
 * component is ONLY the table.
 *
 * Layout: the underlying `<table>` uses `table-layout: fixed` plus a
 * computed `width` and `min-width`. Rigid columns (declaring `size` only)
 * receive triple-locked width (`width` = `min-width` = `max-width` = `size`)
 * to prevent the browser from rescaling them when `table-layout: fixed` has
 * leftover space. Elastic columns (declaring both `size` and `minSize` with
 * `minSize > 20`) only get `min-width: minSize` so they absorb the surplus.
 *
 * Sticky columns (column pinning): the table is built with TanStack's
 * `enableColumnPinning: true`. Pinning state is seeded from each column's
 * declarative `sticky: 'left' | 'right'` (see `derivePinningState` in
 * `lib/column-helpers.ts`) but lives in TanStack thereafter. The component
 * reads `column.getIsPinned()` and `column.getStart('left')` /
 * `column.getAfter('right')` to position pinned cells. TanStack guarantees
 * pinned columns form a contiguous group on each edge, even if the column
 * order is changed at runtime.
 *
 * Truncation: every `<td>` clips overflowing text with `truncate` plus an
 * inner `<div class="truncate">` so `text-overflow: ellipsis` actually
 * applies (a `<td>` is `display: table-cell` and ellipsis is unreliable on
 * it directly). Tooltip via `[attr.title]` is set ONLY for elastic columns
 * with primitive values — rigid columns rarely overflow and a tooltip would
 * be noise on hover.
 *
 * Surface: the `surface` input drives the background class applied to
 * sticky body cells so they MATCH the surrounding container's color when
 * scrolling — without this, sticky cells would show `bg-background` (body
 * dark) while the table sits inside a lighter `bg-card` or `bg-popover`,
 * producing a visible mismatch under horizontal scroll.
 *
 * Loading semantics: rows from `table().getRowModel().rows` render whenever
 * present. The `loading` input only affects the `@empty` fallback (spinner
 * vs empty message). During refetch with existing rows, the OLD rows stay
 * visible until the new rows arrive — this prevents TBODY collapse and
 * page-scroll yanks (see `grid-multi-render-as-shell-section` archive memo).
 *
 * Row-click is opt-in via `[rowClickable]` predicate + `(rowClick)` output.
 */
@Component({
  selector: 'au-data-table',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [FlexRenderDirective, HlmSpinnerImports, HlmTableImports],
  host: { class: 'block' },
  template: `
    <div
      class="w-full overflow-auto rounded-md border"
      [style.max-height]="maxHeight()"
    >
      <table
        [style.table-layout]="'fixed'"
        [style.width]="tableWidth()"
        [style.min-width.px]="tableMinWidth()"
        hlmTable
      >
        <thead
          class="bg-muted sticky top-0 z-20"
          hlmTHead
        >
          @for (headerGroup of table().getHeaderGroups(); track headerGroup.id) {
            <tr hlmTr>
              @for (header of headerGroup.headers; track header.id) {
                <th
                  class="overflow-hidden"
                  [style.width.px]="headerWidthFor(header)"
                  [style.min-width.px]="headerMinWidthFor(header)"
                  [style.max-width.px]="headerMaxWidthFor(header)"
                  [style.left.px]="headerStickyLeftFor(header)"
                  [style.right.px]="headerStickyRightFor(header)"
                  [attr.colSpan]="header.colSpan"
                  [class.sticky]="isHeaderSticky(header)"
                  [class.bg-muted]="isHeaderSticky(header)"
                  [class.z-30]="isHeaderSticky(header)"
                  hlmTh
                >
                  @if (!header.isPlaceholder) {
                    <ng-container
                      *flexRender="
                        header.column.columnDef.header;
                        props: header.getContext();
                        let headerText
                      "
                    >
                      <div [innerHTML]="headerText"></div>
                    </ng-container>
                  }
                </th>
              }
            </tr>
          }
        </thead>
        <tbody hlmTBody>
          @for (row of table().getRowModel().rows; track row.id) {
            <tr
              [class.cursor-pointer]="rowClickable()(row)"
              [class.hover:bg-muted]="rowClickable()(row)"
              hlmTr
              (click)="onRowClick(row)"
            >
              @for (cell of row.getVisibleCells(); track cell.id) {
                <td
                  class="px-2 py-2"
                  [style.left.px]="cellStickyLeftFor(cell)"
                  [style.right.px]="cellStickyRightFor(cell)"
                  [attr.title]="cellTooltipFor(cell)"
                  [class.sticky]="isCellSticky(cell)"
                  [class.bg-background]="isCellSticky(cell) && surface() === 'background'"
                  [class.bg-card]="isCellSticky(cell) && surface() === 'card'"
                  [class.bg-popover]="isCellSticky(cell) && surface() === 'popover'"
                  [class.bg-muted]="isCellSticky(cell) && surface() === 'muted'"
                  [class.z-10]="isCellSticky(cell)"
                  [class.truncate]="isCellTruncatable(cell)"
                  [class.overflow-visible]="isCellOverflowVisible(cell)"
                  hlmTd
                >
                  <ng-container
                    *flexRender="cell.column.columnDef.cell; props: cell.getContext(); let cellText"
                  >
                    <div
                      [class.truncate]="isCellTruncatable(cell)"
                      [innerHTML]="cellText"
                    ></div>
                  </ng-container>
                </td>
              }
            </tr>
          } @empty {
            <tr hlmTr>
              <td
                class="h-24 text-center"
                [attr.colspan]="columnCount()"
                hlmTd
              >
                @if (loading()) {
                  <hlm-spinner class="text-sm" />
                } @else {
                  {{ emptyMessage() }}
                }
              </td>
            </tr>
          }
        </tbody>
      </table>
    </div>
  `,
})
export class DataTable<T> {
  readonly table = input.required<Table<T>>();
  readonly loading = input<boolean>(false);
  readonly emptyMessage = input<string>('No items');
  readonly rowClickable = input<(row: Row<T>) => boolean>(() => false);
  /** CSS max-height for the scroll wrapper. Defaults to `700px` for full-page
   * lists; dialog hosts pass `480px` to fit inside the modal. */
  readonly maxHeight = input<string>('700px');
  /**
   * Surface the table is rendered ON. Drives the background class applied to
   * sticky body cells so they MATCH the surrounding container's color when
   * scrolling.
   *
   * - `'background'` (default) → `bg-background`. For full-page lists.
   * - `'card'` → `bg-card`. For tables inside a card panel.
   * - `'popover'` → `bg-popover`. For tables inside a dialog/popover.
   * - `'muted'` → `bg-muted`. For tables on muted surfaces.
   */
  readonly surface = input<'background' | 'card' | 'popover' | 'muted'>('background');

  readonly rowClick = output<T>();

  protected readonly columnCount = computed(() => {
    const groups = this.table().getHeaderGroups();
    return groups.length === 0 ? 1 : groups[groups.length - 1].headers.length;
  });

  protected readonly hasElasticVisible = computed(() => {
    const columns = this.table().getVisibleLeafColumns();
    return columns.some((column) => isElasticColumn(column.columnDef as DataTableColumnDef<T>));
  });

  protected readonly tableMinWidth = computed(() => {
    const headers = this.table().getVisibleLeafColumns();
    let total = 0;
    for (const column of headers) {
      const colDef = column.columnDef as DataTableColumnDef<T>;
      const size = colDef.size ?? column.getSize();
      total += isElasticColumn(colDef) ? (colDef.minSize ?? size) : size;
    }
    return total;
  });

  protected readonly tableWidth = computed(() => {
    if (this.hasElasticVisible()) return '100%';
    return `${this.tableMinWidth()}px`;
  });

  protected onRowClick(row: Row<T>): void {
    if (!this.rowClickable()(row)) return;
    this.rowClick.emit(row.original);
  }

  protected isHeaderSticky(header: Header<T, unknown>): boolean {
    return header.column.getIsPinned() !== false;
  }

  protected headerStickyLeftFor(header: Header<T, unknown>): number | null {
    return header.column.getIsPinned() === 'left' ? header.column.getStart('left') : null;
  }

  protected headerStickyRightFor(header: Header<T, unknown>): number | null {
    return header.column.getIsPinned() === 'right' ? header.column.getAfter('right') : null;
  }

  protected headerWidthFor(header: Header<T, unknown>): number | null {
    const colDef = header.column.columnDef as DataTableColumnDef<T>;
    return isRigidColumn(colDef) ? (colDef.size ?? null) : null;
  }

  protected headerMinWidthFor(header: Header<T, unknown>): number | null {
    const colDef = header.column.columnDef as DataTableColumnDef<T>;
    if (isRigidColumn(colDef)) return colDef.size ?? null;
    if (isElasticColumn(colDef)) return colDef.minSize ?? null;
    return null;
  }

  protected headerMaxWidthFor(header: Header<T, unknown>): number | null {
    const colDef = header.column.columnDef as DataTableColumnDef<T>;
    return isRigidColumn(colDef) ? (colDef.size ?? null) : null;
  }

  protected isCellSticky(cell: Cell<T, unknown>): boolean {
    return cell.column.getIsPinned() !== false;
  }

  protected cellStickyLeftFor(cell: Cell<T, unknown>): number | null {
    return cell.column.getIsPinned() === 'left' ? cell.column.getStart('left') : null;
  }

  protected cellStickyRightFor(cell: Cell<T, unknown>): number | null {
    return cell.column.getIsPinned() === 'right' ? cell.column.getAfter('right') : null;
  }

  protected isCellTruncatable(cell: Cell<T, unknown>): boolean {
    return !this.isCellOverflowVisible(cell);
  }

  protected isCellOverflowVisible(cell: Cell<T, unknown>): boolean {
    const colDef = cell.column.columnDef as DataTableColumnDef<T>;
    const meta = colDef.meta as { overflow?: string } | undefined;
    return meta?.overflow === 'visible';
  }

  protected cellTooltipFor(cell: Cell<T, unknown>): string | null {
    if (this.isCellOverflowVisible(cell)) return null;
    const colDef = cell.column.columnDef as DataTableColumnDef<T>;
    if (!isElasticColumn(colDef)) return null;
    const value = cell.getValue();
    if (typeof value === 'string' && value.length > 0) return value;
    if (typeof value === 'number') return String(value);
    return null;
  }
}
