import { action, computed, makeObservable, observable } from 'mobx';
import type { Cell, CellBorder } from './types';
import type { FiltersMap } from '@wix/bex-core';
import type { EditableTableState } from './EditableTableState';
import { cellKey, parseCellKey, noBorder } from './utils';
import { EditTrigger } from '../../components/EditableTable/types';

type Direction = 'up' | 'down' | 'left' | 'right';
type CommitHandler = (editingValue: any, cell: Cell) => void;

export class CellInteractionState<T, F extends FiltersMap> {
  focusedCell: Cell | null = null;
  editingCell: Cell | null = null;
  editingValue: any = undefined;
  editTrigger: EditTrigger | null = null;
  selectedCells = new Set<string>();
  selectionAnchor: Cell | null = null;
  focusedCellElement: HTMLElement | null | undefined = null;
  /** True while the user is performing a mouse sweep selection. */
  isSelecting = false;
  private readonly _parent: EditableTableState<T, F>;
  private readonly _commitHandler: CommitHandler;

  constructor(commitHandler: CommitHandler, parent: EditableTableState<T, F>) {
    this._commitHandler = commitHandler;
    this._parent = parent;
    makeObservable(this, {
      focusedCell: observable.ref,
      editingCell: observable.ref,
      editingValue: observable.ref,
      editTrigger: observable.ref,
      selectedCells: observable,
      selectionAnchor: observable.ref,
      isSelecting: observable.ref,
      isEditing: computed,
      selectedCellsList: computed,
      focusCell: action,
      clearFocus: action,
      startEdit: action,
      commitEdit: action,
      cancelEdit: action,
      setEditingValue: action,
      selectRange: action,
      selectAll: action,
      moveFocus: action,
      moveToNextCell: action,
      moveToPreviousCell: action,
      startSweep: action,
      sweepTo: action,
      stopSweep: action,
    });
  }

  get isEditing(): boolean {
    return this.editingCell !== null;
  }

  focusCell(rowKey: string, columnId: string) {
    if (this.editingCell) {
      this.commitEdit();
    }
    this.focusedCell = { rowKey, columnId };
    this.selectedCells.clear();
    this.selectedCells.add(cellKey(rowKey, columnId));
    this.selectionAnchor = { rowKey, columnId };
  }

  clearFocus() {
    this.focusedCell = null;
    this.selectedCells.clear();
    this.selectionAnchor = null;
    this.cancelEdit();
  }

  startEdit(
    rowKey?: string,
    columnId?: string,
    trigger: EditTrigger = 'enter',
  ) {
    const rk = rowKey ?? this.focusedCell?.rowKey;
    const cid = columnId ?? this.focusedCell?.columnId;
    if (rk != null && cid != null) {
      this.editingValue = undefined;
      this.editTrigger = trigger;
      this.editingCell = { rowKey: rk, columnId: cid };
      this.focusedCell = { rowKey: rk, columnId: cid };
      // Collapse any multi-cell range selection down to the editing cell.
      // Without this, range-selected cells remain visually selected alongside
      // the editing cell, producing an invalid "two cells selected" state.
      this.selectedCells.clear();
      this.selectedCells.add(cellKey(rk, cid));
      this.selectionAnchor = { rowKey: rk, columnId: cid };
    }
  }

  setEditingValue(value: any) {
    this.editingValue = value;
  }

  commitEdit() {
    try {
      if (this.editingCell) {
        this._commitHandler(this.editingValue, this.editingCell);
      }
    } finally {
      this.editingValue = undefined;
      this.editTrigger = null;
      this.editingCell = null;
    }
  }

  cancelEdit() {
    this.editingValue = undefined;
    this.editTrigger = null;
    this.editingCell = null;
  }

  selectRange(from: Cell, to: Cell) {
    const fromPos = this._parent._toIndices(from);
    const toPos = this._parent._toIndices(to);
    if (!fromPos || !toPos) {
      return;
    }

    const { keyedItems, editableColumns } = this._parent;
    const minRow = Math.min(fromPos.row, toPos.row);
    const maxRow = Math.max(fromPos.row, toPos.row);
    const minCol = Math.min(fromPos.col, toPos.col);
    const maxCol = Math.max(fromPos.col, toPos.col);

    const newKeys = new Set<string>();
    for (let r = minRow; r <= maxRow; r++) {
      for (let c = minCol; c <= maxCol; c++) {
        newKeys.add(cellKey(keyedItems[r].key, editableColumns[c].id));
      }
    }

    this._diffSelection(newKeys);
    this.focusedCell = to;
  }

  selectAll() {
    const { keyedItems, editableColumns } = this._parent;
    const newKeys = new Set<string>();
    for (const ki of keyedItems) {
      for (const col of editableColumns) {
        newKeys.add(cellKey(ki.key, col.id));
      }
    }

    this._diffSelection(newKeys);

    if (keyedItems.length > 0 && editableColumns.length > 0) {
      this.selectionAnchor = this._parent._toPosition(0, 0)!;
      this.focusedCell = this._parent._toPosition(
        keyedItems.length - 1,
        editableColumns.length - 1,
      )!;
    }
  }

  moveFocus(direction: Direction, extend = false) {
    const pos = this.focusedCell && this._parent._toIndices(this.focusedCell);
    if (!pos) {
      return;
    }

    const { keyedItems, editableColumns } = this._parent;
    let { row, col } = pos;

    switch (direction) {
      case 'up':
        row = Math.max(0, row - 1);
        break;
      case 'down':
        row = Math.min(keyedItems.length - 1, row + 1);
        break;
      case 'left':
        col = Math.max(0, col - 1);
        break;
      case 'right':
        col = Math.min(editableColumns.length - 1, col + 1);
        break;
    }

    const target = this._parent._toPosition(row, col)!;
    if (extend && this.selectionAnchor) {
      this.selectRange(this.selectionAnchor, target);
    } else {
      this.focusCell(target.rowKey, target.columnId);
    }
  }

  moveToNextCell() {
    const pos = this.focusedCell && this._parent._toIndices(this.focusedCell);
    if (!pos) {
      return;
    }

    const { keyedItems, editableColumns } = this._parent;
    let { row, col } = pos;

    col++;
    if (col >= editableColumns.length) {
      col = 0;
      row++;
      if (row >= keyedItems.length) {
        row = keyedItems.length - 1;
        col = editableColumns.length - 1;
      }
    }
    this.focusCell(keyedItems[row].key, editableColumns[col].id);
  }

  moveToPreviousCell() {
    const pos = this.focusedCell && this._parent._toIndices(this.focusedCell);
    if (!pos) {
      return;
    }

    const { keyedItems, editableColumns } = this._parent;
    let { row, col } = pos;

    col--;
    if (col < 0) {
      col = editableColumns.length - 1;
      row--;
      if (row < 0) {
        row = 0;
        col = 0;
      }
    }
    this.focusCell(keyedItems[row].key, editableColumns[col].id);
  }

  startSweep(rowKey: string, columnId: string) {
    this.isSelecting = true;
    this.focusCell(rowKey, columnId);
  }

  sweepTo(rowKey: string, columnId: string) {
    if (!this.isSelecting || !this.selectionAnchor) {
      return;
    }
    this.selectRange(this.selectionAnchor, { rowKey, columnId });
  }

  stopSweep() {
    this.isSelecting = false;
  }

  private _diffSelection(newKeys: Set<string>) {
    for (const key of this.selectedCells) {
      if (!newKeys.has(key)) {
        this.selectedCells.delete(key);
      }
    }
    for (const key of newKeys) {
      if (!this.selectedCells.has(key)) {
        this.selectedCells.add(key);
      }
    }
  }

  isCellFocused(rowKey: string, columnId: string): boolean {
    return (
      this.focusedCell?.rowKey === rowKey &&
      this.focusedCell?.columnId === columnId
    );
  }

  isCellEditing(rowKey: string, columnId: string): boolean {
    return (
      this.editingCell?.rowKey === rowKey &&
      this.editingCell?.columnId === columnId
    );
  }

  isCellSelected(rowKey: string, columnId: string): boolean {
    return this.selectedCells.has(cellKey(rowKey, columnId));
  }

  get selectedCellsList(): Cell[] {
    const result: Cell[] = [];
    for (const key of this.selectedCells) {
      const pos = parseCellKey(key);
      if (pos) {
        result.push(pos);
      }
    }
    return result;
  }

  scrollFocusedCellIntoView() {
    if (typeof requestAnimationFrame === 'undefined') {
      return;
    }
    requestAnimationFrame(() => {
      const cellEl = this.focusedCellElement;
      if (!cellEl) {
        return;
      }

      cellEl.scrollIntoView({ block: 'nearest', inline: 'nearest' });
      this._adjustScrollForStickyElements(cellEl);
    });
  }

  /**
   * After scrollIntoView, the cell may be hidden behind sticky columns
   * or the sticky header. If so, scroll a bit more to fully reveal it.
   */
  private _adjustScrollForStickyElements(cellEl: HTMLElement) {
    const td = cellEl.parentElement;
    if (!td) {
      return;
    }

    // Horizontal: check if cell is behind a sticky column.
    // Sticky columns stay in place while other columns scroll, so their
    // bounding rect's right edge can overlap the focused cell's left edge.
    const row = td.parentElement;
    const cellLeft = td.getBoundingClientRect().left;
    if (row) {
      let maxPrevRight = 0;
      for (const child of Array.from(row.children)) {
        if (child === td) {
          break;
        }
        const right = child.getBoundingClientRect().right;
        if (right > maxPrevRight) {
          maxPrevRight = right;
        }
      }
      if (cellLeft < maxPrevRight) {
        const scrollParent = cellEl.closest('table')?.parentElement;
        if (scrollParent) {
          scrollParent.scrollLeft -= maxPrevRight - cellLeft;
        }
      }
    }

    // Vertical: same pattern — check if cell is behind the sticky header
    // or below the visible area of the page-level scroll container.
    const headerEl = this._parent.tableState.headerElement;
    const scrollEl = this._parent.collection.scrollableContent;
    if (scrollEl) {
      const cellRect = td.getBoundingClientRect();
      const scrollRect = scrollEl.getBoundingClientRect();
      const effectiveTop = headerEl
        ? headerEl.getBoundingClientRect().bottom
        : scrollRect.top;

      if (cellRect.top < effectiveTop) {
        scrollEl.scrollTop -= effectiveTop - cellRect.top + 2;
      } else if (cellRect.bottom > scrollRect.bottom) {
        scrollEl.scrollTop += cellRect.bottom - scrollRect.bottom + 2;
      }
    }
  }

  getSelectionBorder(rowKey: string, columnId: string): CellBorder {
    if (!this.isCellSelected(rowKey, columnId)) {
      return noBorder;
    }

    const pos = this._parent._toIndices({ rowKey, columnId });
    if (!pos) {
      return noBorder;
    }

    const isNeighborSelected = (row: number, col: number) => {
      const neighbor = this._parent._toPosition(row, col);
      return (
        neighbor !== null &&
        this.selectedCells.has(cellKey(neighbor.rowKey, neighbor.columnId))
      );
    };

    return {
      top: !isNeighborSelected(pos.row - 1, pos.col),
      bottom: !isNeighborSelected(pos.row + 1, pos.col),
      left: !isNeighborSelected(pos.row, pos.col - 1),
      right: !isNeighborSelected(pos.row, pos.col + 1),
    };
  }
}
