import { NgTemplateOutlet } from '@angular/common';
import { Component, DestroyRef, HostListener, computed, contentChildren, effect, inject, input, output, signal } from '@angular/core';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DataTableCellDirective } from '../../directives/data-table-cell.directive';
import { CdkConnectedOverlay, CdkOverlayOrigin, ConnectedPosition } from '@angular/cdk/overlay';

import { DataTableAction, DataTableActionEvent, DataTableCellContext, DataTableColumn, DataTablePageEvent, DataTableRecord, DataTableSortEvent } from './data-table.types';

import { Button } from '../button/button';
import { contentChild } from '@angular/core';

import { DataTableToolbarDirective } from '../../directives/data-table-toolbar/data-table-toolbar.directive';

@Component({
  selector: 'app-data-table',
  imports: [NgTemplateOutlet,
    MatPaginatorModule,
    CdkOverlayOrigin,
    CdkConnectedOverlay,
    Button],
  templateUrl: './data-table.html',
  styleUrl: './data-table.scss',
  standalone: true
})
export class DataTable<T extends DataTableRecord = DataTableRecord> {

  private readonly destroyRef = inject(DestroyRef);

  private readonly searchTerms$ = new Subject<string>();

  readonly cellTemplates = contentChildren(DataTableCellDirective);
  readonly toolbarTemplate = contentChild(DataTableToolbarDirective);

  private readonly defaultRowClasses = [
    'table-primary',
    'table-secondary',
    'table-success',
    'table-danger',
    'table-warning',
    'table-info',
    'table-light'
  ];

  readonly actionMenuPositions: ConnectedPosition[] = [
    // Open below and align right
    {
      originX: 'end',
      originY: 'bottom',
      overlayX: 'end',
      overlayY: 'top',
      offsetY: 4
    },

    // Open above and align right
    {
      originX: 'end',
      originY: 'top',
      overlayX: 'end',
      overlayY: 'bottom',
      offsetY: -4
    },

    // Mobile fallback: open below towards the right
    {
      originX: 'start',
      originY: 'bottom',
      overlayX: 'start',
      overlayY: 'top',
      offsetY: 4
    },

    // Mobile fallback: open above towards the right
    {
      originX: 'start',
      originY: 'top',
      overlayX: 'start',
      overlayY: 'bottom',
      offsetY: -4
    }
  ];

  openActionMenuUpward = signal(false);
  columns = input<DataTableColumn<T>[]>([]);
  rows = input<T[]>([]);
  actions = input<DataTableAction<T>[]>([]);
  loading = input(false);

  /* --------- pagination inputs ---- */

  totalRecords = input(0);
  pageIndex = input(1);
  pageSize = input(15);
  pageSizeOptions = input<number[]>([10, 15, 20, 50]);
  showPaginator = input(true);

  /*---------------------------*/

  tableTitle = input<string>('');
  toolTip = input<string>('');
  buttonTitle = input<string>('');

  /* --------- Search inputs ---- */
  showSearch = input<boolean>(false);
  showformSelect = input<boolean>(false);
  showAddButton = input<boolean>(false);
  searchPlaceholder = input('Search...');
  searchDebounceTime = input(300);
  emptyMessage = input('No records found');
  emptyDescription = input('There is currently no data to display.');
  /*---------------------------*/

  /* --------- Permission inputs ---- */

  allowedPermissions = input<string[]>([]);

  /*------------outputs ---------------*/

  searchChanged = output<string>();
  pageChanged = output<DataTablePageEvent>();
  sortChanged = output<DataTableSortEvent>();
  actionClicked = output<DataTableActionEvent<T>>();
  rowClicked = output<T>();

  sortColumn = signal('');
  sortDirection = signal<'asc' | 'desc'>('asc');
  openActionRowId = signal<string | number | null>(null);

  tableClass = input<string>('table table-bordered whitespace-nowrap min-w-full');
  tableHeadClass = input<string>('');
  tableBodyClass = input<string>('');
  trHeadClass = input<string>('border-b border-defaultborder bg-primary/10 dark:bg-primary/15 dark:border-defaultborder/10');
  trBodyClass = input<string>('border-b border-defaultborder hover:bg-light cursor-pointer');
  rowClass = input<string | ((row: T, index: number) => string)>('');
  rowColorMode = input<'none' | 'status' | 'cycle'>('status');

  defaultThClass = input<string>('text-defaulttextcolor dark:text-white font-semibold text-start');
  defaultTdClass = input<string>('');
  actionHeaderClass = input<string>('!text-center');
  actionCellClass = input<string>('!text-center');
  addClicked = output<void>();

  totalVisibleColumns = computed(() => this.columns().length + (this.actions().length > 0 ? 1 : 0));

  allowedPermissionSet = computed(() => new Set(this.allowedPermissions()));

  cellTemplateMap = computed(() => {
    const templateMap = new Map<string, DataTableCellDirective>();

    for (const template of this.cellTemplates() as DataTableCellDirective[]) {
      templateMap.set(template.appDataTableCell(), template);
    }

    return templateMap;
  });

  totalPages = computed(() => {
    return Math.ceil(this.totalRecords() / this.pageSize()) || 1;
  });

  startRecord = computed(() => {
    if (!this.totalRecords()) return 0;
    return (this.pageIndex() - 1) * this.pageSize() + 1;
  });

  endRecord = computed(() => {
    return Math.min(this.pageIndex() * this.pageSize(), this.totalRecords());
  });

  visiblePages = computed(() => {
    const currentPage = this.pageIndex();
    const total = this.totalPages();
    const maxVisiblePages = 5;

    let startPage = Math.max(
      1,
      currentPage - Math.floor(maxVisiblePages / 2)
    );

    let endPage = Math.min(
      total,
      startPage + maxVisiblePages - 1
    );

    if (endPage - startPage + 1 < maxVisiblePages) {
      startPage = Math.max(
        1,
        endPage - maxVisiblePages + 1
      );
    }

    return Array.from(
      { length: endPage - startPage + 1 },
      (_, index) => startPage + index
    );
  });
  goToPage(page: number): void {
    if (
      page < 1 ||
      page > this.totalPages() ||
      page === this.pageIndex()
    ) {
      return;
    }

    this.onPageChange({
      pageIndex: page - 1,
      pageSize: this.pageSize(),
      length: this.totalRecords()
    });
  }

  onPageSizeChange(event: Event): void {
    const pageSize = Number(
      (event.target as HTMLSelectElement).value
    );

    if (!pageSize || pageSize === this.pageSize()) {
      return;
    }

    this.onPageChange({
      pageIndex: 0,
      pageSize,
      length: this.totalRecords()
    });
  }

  constructor() {
    effect((onCleanup) => {
      const debounce = this.searchDebounceTime();
      const subscription = this.searchTerms$
        .pipe(debounceTime(debounce), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
        .subscribe((value) => {
          this.searchChanged.emit(value);
        });

      onCleanup(() => subscription.unsubscribe());
    });
  }

  getCellValue(row: T, key: string): unknown {
    return row[key] ?? null;
  }

  onSearch(value: string): void {
    this.searchTerms$.next(value.trim());
  }

  onSort(column: DataTableColumn<T>): void {
    if (!column.sortable) return;

    const key = String(column.key);

    if (this.sortColumn() === key) {
      this.sortDirection.update(value => value === 'asc' ? 'desc' : 'asc');
    } else {
      this.sortColumn.set(key);
      this.sortDirection.set('asc');
    }

    this.sortChanged.emit({
      column: this.sortColumn(),
      direction: this.sortDirection()
    });

    this.closeActionMenu();
  }

  onPageChange(event: PageEvent): void {
    this.pageChanged.emit({
      pageIndex: event.pageIndex + 1,
      pageSize: event.pageSize
    });

    this.closeActionMenu();
  }

  onActionClick(event: MouseEvent, action: DataTableAction<T>, row: T): void {
    event.stopPropagation();

    if (this.isActionDisabled(action, row)) {
      return;
    }

    this.actionClicked.emit({
      action,
      row
    });
  }

  onAddClick(event: MouseEvent): void {
    event.preventDefault();
    event.stopPropagation();

    this.closeActionMenu();
    this.addClicked.emit();
  }

  toggleActionMenu(event: MouseEvent, row: T): void {
    event.stopPropagation();

    const rowId = this.getRowId(row);

    if (rowId === null) {
      return;
    }

    this.openActionRowId.update(currentRowId =>
      currentRowId === rowId ? null : rowId
    );
  }

  closeActionMenu(): void {
    this.openActionRowId.set(null);
  }
  onDropdownActionClick(
    event: MouseEvent,
    action: DataTableAction<T>,
    row: T
  ): void {
    event.stopPropagation();

    this.onActionClick(event, action, row);
    this.closeActionMenu();
  }

  isActionMenuOpen(row: T): boolean {
    const rowId = row['id'];

    return (
      (typeof rowId === 'string' ||
        typeof rowId === 'number') &&
      this.openActionRowId() === rowId
    );
  }

  getActionMenuId(row: T): string {
    const rowId = this.getRowId(row);
    return rowId === null ? 'row-actions-menu' : `row-actions-menu-${String(rowId)}`;
  }

  onActionMenuOutsideClick(event: MouseEvent): void {
    const target = event.target;

    if (!(target instanceof HTMLElement)) {
      this.closeActionMenu();
      return;
    }

    if (target.closest('[data-action-menu-trigger]')) {
      return;
    }

    this.closeActionMenu();
  }

  isActionVisible(action: DataTableAction<T>, row: T): boolean {
    if (action.permission && !this.allowedPermissionSet().has(action.permission)) {
      return false;
    }

    return action.visible ? action.visible(row) : true;
  }

  isActionDisabled(action: DataTableAction<T>, row: T): boolean {
    return action.disabled ? action.disabled(row) : false;
  }

  getActionLabel(action: DataTableAction<T>): string {
    return action.tooltip || action.label;
  }

  getColumnKey(column: DataTableColumn<T>): string {
    return String(column.key);
  }

  getSortIconClass(column: DataTableColumn<T>): string {
    if (!column.sortable) {
      return '';
    }

    const key = this.getColumnKey(column);

    if (this.sortColumn() !== key) {
      return 'ri-arrow-up-down-line';
    }

    return this.sortDirection() === 'asc' ? 'ri-sort-asc' : 'ri-sort-desc';
  }

  getHeaderClass(column: DataTableColumn<T>): string {
    return this.composeClass(this.defaultThClass(), this.getAlignClass(column.headerAlign), column.headerClass);
  }

  getCellClass(column: DataTableColumn<T>): string {
    return this.composeClass(this.defaultTdClass(), this.getAlignClass(column.align), column.cellClass);
  }

  getBadgeClass(column: DataTableColumn<T>, row: T): string {
    const value = this.getCellValue(row, this.getColumnKey(column));
    return this.composeClass(column.badgeClass ? column.badgeClass(value, row) : this.getDefaultBadgeClass(value));
  }

  getDisplayValue(column: DataTableColumn<T>, row: T): string | number {
    const value = this.getCellValue(row, this.getColumnKey(column));

    if (column.formatter) {
      return column.formatter(value, row);
    }

    if (value === null || value === undefined) {
      return '-';
    }

    return typeof value === 'string' || typeof value === 'number' ? value : String(value);
  }

  getCellTemplate(column: DataTableColumn<T>): DataTableCellDirective | undefined {
    return this.cellTemplateMap().get(this.getColumnKey(column));
  }

  getCellTemplateContext(column: DataTableColumn<T>, row: T): DataTableCellContext<T> {
    return {
      $implicit: row,
      row,
      value: this.getCellValue(row, this.getColumnKey(column))
    };
  }

  getActionButtonClass(action: DataTableAction<T>): string {
    return this.composeClass(
      'ti-btn ti-btn-icon ti-btn-sm',
      action.className || this.getDefaultActionClass(action.type)
    );
  }

  onRowClick(row: T): void {
    this.rowClicked.emit(row);
  }

  @HostListener('document:click')
  onDocumentClick(): void {
    this.closeActionMenu();
  }

  @HostListener('document:keydown.escape')
  onEscapeKey(): void {
    this.closeActionMenu();
  }

  private getAlignClass(align?: DataTableColumn<T>['align']): string {
    switch (align) {
      case 'left':
        return 'text-start';
      case 'right':
        return 'text-end';
      default:
        return '!text-center';
    }
  }

  composeClass(...classes: Array<string | undefined>): string {
    return classes.filter(Boolean).join(' ');
  }

  private getRowId(row: T): string | number | null {
    const rowId = row['id'];

    return typeof rowId === 'string' || typeof rowId === 'number'
      ? rowId
      : null;
  }

  private getDefaultActionClass(actionType: string): string {
    switch (actionType) {
      case 'view':
        return 'ti-btn-info-full';
      case 'edit':
        return 'ti-btn-primary-full';
      case 'delete':
        return 'ti-btn-danger-full';
      default:
        return 'ti-btn-light';
    }
  }

  private getDefaultBadgeClass(value: unknown): string {
    if (typeof value === 'string') {
      if (value.toLowerCase() === 'active') {
        return 'badge bg-success/10 text-success';
      }

      if (value.toLowerCase() === 'inactive') {
        return 'badge bg-danger/10 text-danger';
      }
    }

    return 'badge bg-light text-defaulttextcolor';
  }

  getRowClass(row: T, index: number): string {
    const baseClass = this.trBodyClass();

    // 1. Parent override
    const rowClassValue = this.rowClass();

    if (rowClassValue) {
      const customClass =
        typeof rowClassValue === 'function'
          ? rowClassValue(row, index)
          : rowClassValue;

      return this.composeClass(baseClass, customClass);
    }

    // 2. Automatic status detection
    const status =
      row['status'] ??
      row['state'] ??
      row['approvalStatus'] ??
      row['workflowStatus'];

    if (typeof status === 'string') {
      switch (status.toLowerCase()) {
        case 'active':
        case 'approved':
        case 'success':
          return this.composeClass(baseClass, 'table-success');

        case 'inactive':
        case 'rejected':
        case 'failed':
          return this.composeClass(baseClass, 'table-danger');

        case 'pending':
          return this.composeClass(baseClass, 'table-warning');

        case 'draft':
          return this.composeClass(baseClass, 'table-info');
      }
    }

    // 3. Boolean status
    if (typeof row['isActive'] === 'boolean') {
      return this.composeClass(
        baseClass,
        row['isActive'] ? '' : 'table-danger'
      );
    }

    // 4. Cycle through Ynex colors
    const colorClass =
      this.defaultRowClasses[index % this.defaultRowClasses.length];

    return this.composeClass(baseClass, '');
  }

}

export { DataTableCellDirective } from '../../directives/data-table-cell.directive';
