import { computed, ref, shallowRef, watch, type Ref } from 'vue';

import { createFillRangeCommand, iterateRange, iterateRanges, normalizeRange, rangesContainCell } from '../commands/FillRangeCommand';
import { createPasteRangeCommand } from '../commands/PasteRangeCommand';
import { createSetCellValueCommand } from '../commands/SetCellValueCommand';
import {
  computeFixedWidth,
  getContentSize,
  getFixedColIndices,
  renderSelectionImage,
} from '../canvas/SpreadsheetRenderer';
import { applyViewState, isColumnFilterActive } from './filterSort';
import {
  getRowHeight,
  hitTestCell,
  hitTestHeaderCol,
  hitTestRowNumber,
  hitTestHeaderArea,
  measureColumnWidths,
} from '../composables/useAutoMeasure';
import { useClipboard } from '../composables/useClipboard';
import { useEditHistory } from '../composables/useEditHistory';
import { useSelection } from '../composables/useSelection';
import { useUndoRedo } from '../composables/useUndoRedo';
import type {
  CellEditContext,
  CellRange,
  SpreadsheetBatchCellChange,
  SpreadsheetColumnConfig,
  SpreadsheetEngineExpose,
  SpreadsheetHeaderConfig,
  SpreadsheetHistoryEntry,
  SpreadsheetTableConfig,
  SpreadsheetViewState,
  SortDirection,
} from '../types';
import {
  createDefaultViewState,
  DEFAULT_COL_WIDTH,
  DEFAULT_ROW_HEIGHT,
  HEADER_HEIGHT,
  ROW_NUMBER_WIDTH,
} from '../types';
import {
  evaluateFormula,
  formatFormulaResult,
  type CellValueReader,
  type FormulaResult,
} from '../formula/evaluator';
import {
  formatCellWithSuffix,
  resolveConditionalStyle,
} from '../composables/cellDisplay';

export function useSpreadsheetEngine(options: {
  tableConfig: Ref<SpreadsheetTableConfig>;
  mergedHeaders: Ref<SpreadsheetHeaderConfig[]>;
  columns: Ref<SpreadsheetColumnConfig[]>;
  data: Ref<Record<string, unknown>[]>;
  rowKeyField: Ref<string>;
  layoutColWidths: Ref<Record<number, number>>;
  scrollViewportHeight: Ref<number>;
  isDark: Ref<boolean>;
  onChange: (data: Record<string, unknown>[]) => void;
}) {
  const sortedColumns = computed(() =>
    [...options.columns.value].sort((a, b) => a.col - b.col),
  );

  const colWidths = ref<number[]>([]);
  const rowHeight = ref(DEFAULT_ROW_HEIGHT);
  const scrollTop = ref(0);
  const scrollLeft = ref(0);
  const viewportWidth = ref(800);
  const viewportHeight = ref(600);
  const filterValues = ref<Record<number, string>>({});
  const viewState = ref<SpreadsheetViewState>(createDefaultViewState());
  const initialSnapshot = shallowRef<Map<string, unknown>>(new Map());
  const dirtyKeys = ref<Set<string>>(new Set());
  const editing = ref<{ row: number; col: number } | null>(null);
  const measureCanvas = ref<HTMLCanvasElement | null>(null);

  const selectionApi = useSelection();
  const undoRedo = useUndoRedo();
  const clipboard = useClipboard();
  const history = useEditHistory(
    options.tableConfig.value.tableName,
    options.tableConfig.value.sheetKey,
    options.tableConfig.value.enableHistory !== false,
  );

  const filteredRowIndices = computed(() =>
    applyViewState(
      options.data.value,
      sortedColumns.value,
      viewState.value,
      (dataRowIndex, colCfg, row) =>
        resolveDisplayValueForDataRow(dataRowIndex, colCfg, row),
    ),
  );

  function resolveDisplayValueForDataRow(
    dataRowIndex: number,
    colCfg: SpreadsheetColumnConfig,
    row?: Record<string, unknown>,
  ): unknown {
    if (colCfg.cellType === 'formula' && colCfg.formula) {
      const result = evaluateFormula(
        colCfg.formula,
        dataRowIndex,
        (viewRow, col) => {
          const cfg = colConfig(col);
          if (!cfg || cfg.cellType === 'formula') return 0;
          const dr = options.data.value[viewRow];
          return dr ? getCellValue(viewRow, cfg.fieldId) : 0;
        },
      );
      return formatFormulaResult(result);
    }
    const rowData = row ?? options.data.value[dataRowIndex];
    if (!rowData) return undefined;
    return rowData[colCfg.fieldId];
  }

  const fixedCols = computed(() =>
    getFixedColIndices(options.mergedHeaders.value),
  );

  const fixedWidth = computed(() =>
    computeFixedWidth(colWidths.value, fixedCols.value),
  );

  const displayRowCount = computed(() => {
    const minRows = Math.max(
      1,
      Math.ceil(options.scrollViewportHeight.value / rowHeight.value),
    );
    return Math.max(filteredRowIndices.value.length, minRows);
  });

  const colCount = computed(
    () => Math.max(...sortedColumns.value.map((c) => c.col), 0) + 1,
  );

  const defaultColWidth = computed(
    () => options.tableConfig.value.defaultColWidth ?? DEFAULT_COL_WIDTH,
  );

  const dataContentWidth = computed(
    () => ROW_NUMBER_WIDTH + colWidths.value.reduce((s, w) => s + w, 0),
  );

  const displayContentWidth = computed(() =>
    Math.max(dataContentWidth.value, viewportWidth.value),
  );

  const fillerColCount = computed(() => {
    const remaining = displayContentWidth.value - dataContentWidth.value;
    if (remaining <= 0) return 0;
    return Math.ceil(remaining / defaultColWidth.value);
  });

  const displayColCount = computed(
    () => colCount.value + fillerColCount.value,
  );

  const paddedColWidths = computed(() => {
    const defaultW = defaultColWidth.value;
    const result: number[] = [];
    for (let c = 0; c < displayColCount.value; c += 1) {
      result.push(colWidths.value[c] ?? defaultW);
    }
    return result;
  });

  function colConfig(col: number) {
    return sortedColumns.value.find((c) => c.col === col);
  }

  function dataRowFromViewRow(viewRow: number) {
    return filteredRowIndices.value[viewRow] ?? viewRow;
  }

  function viewRowFromDataRow(dataRow: number) {
    return filteredRowIndices.value.indexOf(dataRow);
  }

  function isEditable(colCfg?: SpreadsheetColumnConfig) {
    if (!colCfg) return false;
    if (colCfg.cellType === 'readonly' || colCfg.cellType === 'formula') {
      return false;
    }
    return colCfg.editable !== false;
  }

  const readCellForFormula: CellValueReader = (viewRow, col) => {
    const cfg = colConfig(col);
    if (!cfg || cfg.cellType === 'formula') return 0;
    const dataRow = dataRowFromViewRow(viewRow);
    return getCellValue(dataRow, cfg.fieldId);
  };

  function resolveNumericValue(
    viewRow: number,
    colCfg: SpreadsheetColumnConfig,
    row?: Record<string, unknown>,
  ): number | null {
    if (colCfg.cellType === 'formula' && colCfg.formula) {
      const result: FormulaResult = evaluateFormula(
        colCfg.formula,
        viewRow,
        readCellForFormula,
      );
      if (result === 'DIV_ZERO' || result == null) return null;
      return result;
    }
    if (colCfg.cellType === 'number' && row) {
      const n = Number(row[colCfg.fieldId]);
      return Number.isFinite(n) ? n : null;
    }
    return null;
  }

  function resolveCellStyle(
    viewRow: number,
    colCfg: SpreadsheetColumnConfig,
    row?: Record<string, unknown>,
  ) {
    return resolveConditionalStyle(
      colCfg,
      resolveNumericValue(viewRow, colCfg, row),
    );
  }

  function resolveDisplayValue(
    viewRow: number,
    colCfg: SpreadsheetColumnConfig,
    row?: Record<string, unknown>,
  ): unknown {
    if (colCfg.cellType === 'formula' && colCfg.formula) {
      const result = evaluateFormula(
        colCfg.formula,
        viewRow,
        readCellForFormula,
      );
      return formatFormulaResult(result);
    }
    if (!row) return undefined;
    return row[colCfg.fieldId];
  }

  function getCellValue(dataRow: number, fieldId: string) {
    return options.data.value[dataRow]?.[fieldId];
  }

  function buildCellContext(viewRow: number, col: number): CellEditContext | null {
    const colCfg = colConfig(col);
    if (!colCfg) return null;
    const dataRow = dataRowFromViewRow(viewRow);
    const rowData = options.data.value[dataRow];
    if (!rowData) return null;
    return {
      fieldId: colCfg.fieldId,
      fieldValue: rowData[colCfg.fieldId],
      rowIndex: viewRow,
      colIndex: col,
      rowKey: rowData[options.rowKeyField.value] as string | number,
      rowData,
      editParams: colCfg.editParams ?? {},
      selection: selectionApi.getPrimaryRange() ?? undefined,
    };
  }

  function snapshotKey(dataRow: number, fieldId: string) {
    const row = options.data.value[dataRow];
    const rowKey = row?.[options.rowKeyField.value];
    return `${rowKey}:${fieldId}`;
  }

  function markDirty(dataRow: number, fieldId: string, value: unknown) {
    const key = snapshotKey(dataRow, fieldId);
    const initial = initialSnapshot.value.get(key);
    const next = new Set(dirtyKeys.value);
    if (initial !== undefined && initial !== value) {
      next.add(`${dataRow}:${fieldId}`);
    } else {
      next.delete(`${dataRow}:${fieldId}`);
    }
    dirtyKeys.value = next;
  }

  function applyLocalValue(dataRow: number, fieldId: string, value: unknown) {
    const rows = options.data.value.map((r, i) =>
      i === dataRow ? { ...r, [fieldId]: value } : r,
    );
    options.onChange(rows);
    markDirty(dataRow, fieldId, value);
  }

  async function persistChanges(
    items: Array<{ viewRow: number; col: number; value: unknown }>,
    opts: {
      recordHistory?: boolean;
      action?: SpreadsheetHistoryEntry['action'];
    } = {},
  ) {
    const batchChanges: SpreadsheetBatchCellChange[] = [];

    for (const item of items) {
      const colCfg = colConfig(item.col);
      if (!colCfg || !isEditable(colCfg)) continue;

      const dataRow = dataRowFromViewRow(item.viewRow);
      const ctx = buildCellContext(item.viewRow, item.col);
      if (!ctx) continue;

      const before = getCellValue(dataRow, colCfg.fieldId);
      applyLocalValue(dataRow, colCfg.fieldId, item.value);
      batchChanges.push({
        ctx: { ...ctx, fieldValue: item.value },
        before,
        after: item.value,
        col: item.col,
        viewRow: item.viewRow,
      });
    }

    if (batchChanges.length === 0) return;

    const onBatchEdit = options.tableConfig.value.onBatchEdit;
    if (onBatchEdit) {
      await onBatchEdit(batchChanges);
    } else {
      for (const ch of batchChanges) {
        const cfg = colConfig(ch.col);
        if (cfg?.onEdit) {
          await cfg.onEdit({ ...ch.ctx, fieldValue: ch.after });
        }
      }
    }

    if (
      opts.recordHistory !== false &&
      options.tableConfig.value.enableHistory !== false
    ) {
      for (const ch of batchChanges) {
        await history.recordChange({
          action: opts.action ?? 'edit',
          rowKey: ch.ctx.rowKey,
          fieldId: ch.ctx.fieldId,
          beforeSnapshot: ch.before,
          afterSnapshot: ch.after,
        });
      }
    }
  }

  function toPersistItems(
    batch: Array<{ row: number; col: number; value: unknown }>,
  ) {
    return batch.map((ch) => ({
      viewRow: ch.row,
      col: ch.col,
      value: ch.value,
    }));
  }

  async function commitCellChange(
    viewRow: number,
    col: number,
    newValue: unknown,
    recordHistory = true,
    action: SpreadsheetHistoryEntry['action'] = 'edit',
  ) {
    const colCfg = colConfig(col);
    if (!colCfg || !isEditable(colCfg)) return;

    const dataRow = dataRowFromViewRow(viewRow);
    const before = getCellValue(dataRow, colCfg.fieldId);
    if (before === newValue) return;

    await persistChanges([{ viewRow, col, value: newValue }], {
      recordHistory,
      action,
    });

    undoRedo.push(
      createSetCellValueCommand(
        () =>
          persistChanges([{ viewRow, col, value: before }], {
            recordHistory: false,
          }),
        () =>
          persistChanges([{ viewRow, col, value: newValue }], {
            recordHistory: false,
          }),
      ),
    );
  }

  async function setCellValue(
    viewRow: number,
    col: number,
    value: unknown,
    recordHistory = true,
    action: SpreadsheetHistoryEntry['action'] = 'edit',
  ) {
    await commitCellChange(viewRow, col, value, recordHistory, action);
  }

  async function fillRange(source: CellRange, target: CellRange) {
    const src = normalizeRange(source);
    const tgt = normalizeRange(target);
    const before: Array<{ row: number; col: number; value: unknown }> = [];
    const after: Array<{ row: number; col: number; value: unknown }> = [];

    const srcRows = src.endRow - src.startRow + 1;
    const srcCols = src.endCol - src.startCol + 1;

    iterateRange(tgt, (row, col) => {
      const colCfg = colConfig(col);
      if (!colCfg || !isEditable(colCfg)) return;
      const relRow = (row - tgt.startRow) % srcRows;
      const relCol = (col - tgt.startCol) % srcCols;
      const srcRow = src.startRow + relRow;
      const srcCol = src.startCol + relCol;
      const srcColCfg = colConfig(srcCol);
      if (!srcColCfg) return;

      const dataRow = dataRowFromViewRow(row);
      const beforeVal = getCellValue(dataRow, colCfg.fieldId);

      const afterVal = getCellValue(
        dataRowFromViewRow(srcRow),
        srcColCfg.fieldId,
      );

      before.push({ row, col, value: beforeVal });
      after.push({ row, col, value: afterVal });
    });

    await persistChanges(toPersistItems(after), {
      recordHistory: true,
      action: 'fill',
    });

    undoRedo.push(
      createFillRangeCommand(
        (batch) => persistChanges(toPersistItems(batch), { recordHistory: false }),
        before,
        after,
      ),
    );
  }

  function copy(selectionOverride?: CellRange | CellRange[]) {
    const selRanges = Array.isArray(selectionOverride)
      ? selectionOverride.map((r) => normalizeRange(r))
      : selectionOverride
        ? [normalizeRange(selectionOverride)]
        : selectionApi.getSelectionRanges();
    if (!selRanges.length) return;

    const rowSet = new Set<number>();
    const colSet = new Set<number>();
    iterateRanges(selRanges, (viewRow, col) => {
      rowSet.add(viewRow);
      colSet.add(col);
    });

    const uniqueRows = [...rowSet].sort((a, b) => a - b);
    const uniqueCols = [...colSet].sort((a, b) => a - b);
    if (!uniqueRows.length || !uniqueCols.length) return;

    const rows: unknown[][] = uniqueRows.map((viewRow) =>
      uniqueCols.map((col) => {
        if (!rangesContainCell(selRanges, viewRow, col)) return '';
        const colCfg = colConfig(col);
        if (!colCfg) return '';
        const dataRow = dataRowFromViewRow(viewRow);
        const row = options.data.value[dataRow];
        const raw = resolveDisplayValue(viewRow, colCfg, row);
        return formatCellWithSuffix(raw, colCfg);
      }),
    );

    const matrix = {
      rows,
      startRow: uniqueRows[0]!,
      startCol: uniqueCols[0]!,
    };
    clipboard.setMatrix(matrix);
    void clipboard.writeSystemClipboard(matrix);
  }

  async function copyAsImage(selectionOverride?: CellRange | CellRange[]) {
    const selRanges = Array.isArray(selectionOverride)
      ? selectionOverride.map((r) => normalizeRange(r))
      : selectionOverride
        ? [normalizeRange(selectionOverride)]
        : selectionApi.getSelectionRanges();
    if (!selRanges.length) return;
    const blob = await renderSelectionImage({
      selectionRanges: selRanges,
      headers: options.mergedHeaders.value,
      columns: sortedColumns.value,
      colWidths: paddedColWidths.value,
      rowHeight: rowHeight.value,
      defaultColWidth: defaultColWidth.value,
      defaultTextAlign: options.tableConfig.value.defaultTextAlign ?? 'left',
      isDark: options.isDark.value,
      filteredRowIndices: filteredRowIndices.value,
      rows: options.data.value,
      dirtyKeys: dirtyKeys.value,
      resolveDisplayValue,
      resolveCellStyle,
    });
    await navigator.clipboard.write([
      new ClipboardItem({ 'image/png': blob }),
    ]);
  }

  async function paste() {
    let matrix = clipboard.getMatrix();
    if (!matrix) {
      const text = await clipboard.readSystemClipboard();
      if (!text) return;
      matrix = {
        rows: clipboard.tsvToMatrix(text),
        startRow: 0,
        startCol: 0,
      };
    }

    const start = selectionApi.activeCell.value ?? {
      row: 0,
      col: sortedColumns.value[0]?.col ?? 0,
    };

    const before: Array<{ row: number; col: number; value: unknown }> = [];
    const after: Array<{ row: number; col: number; value: unknown }> = [];

    matrix.rows.forEach((rowVals, rOff) => {
      rowVals.forEach((val, cOff) => {
        const viewRow = start.row + rOff;
        const col = start.col + cOff;
        const colCfg = colConfig(col);
        if (!colCfg || !isEditable(colCfg)) return;
        const dataRow = dataRowFromViewRow(viewRow);
        if (dataRow >= options.data.value.length) return;
        before.push({
          row: viewRow,
          col,
          value: getCellValue(dataRow, colCfg.fieldId),
        });
        after.push({ row: viewRow, col, value: val });
      });
    });

    const applyBatch = (
      changes: Array<{ row: number; col: number; value: unknown }>,
    ) => persistChanges(toPersistItems(changes), { recordHistory: false });

    await persistChanges(toPersistItems(after), {
      recordHistory: true,
      action: 'paste',
    });

    undoRedo.push(createPasteRangeCommand(applyBatch, before, after));
  }

  function remeasure() {
    if (!measureCanvas.value) return;
    const defaultW =
      options.tableConfig.value.defaultColWidth ?? DEFAULT_COL_WIDTH;
    const measured = measureColumnWidths(
      measureCanvas.value,
      options.mergedHeaders.value,
      sortedColumns.value,
      options.data.value,
      defaultW,
      (dataRowIdx, colCfg, row) =>
        resolveDisplayValue(dataRowIdx, colCfg, row),
      formatCellWithSuffix,
    );
    const overrides = options.layoutColWidths.value;
    colWidths.value = measured.map((w, i) => overrides[i] ?? w);
    const firstType = sortedColumns.value[0]?.cellType ?? 'text';
    rowHeight.value = getRowHeight(
      firstType,
      options.tableConfig.value.rowHeight,
    );
  }

  function initSnapshot() {
    const map = new Map<string, unknown>();
    options.data.value.forEach((row, dataRow) => {
      sortedColumns.value.forEach((col) => {
        map.set(snapshotKey(dataRow, col.fieldId), row[col.fieldId]);
      });
    });
    initialSnapshot.value = map;
    dirtyKeys.value = new Set();
  }

  watch(
    () => options.data.value,
    () => {
      if (initialSnapshot.value.size === 0) initSnapshot();
      remeasure();
    },
    { deep: true, immediate: true },
  );

  watch(
    () => options.mergedHeaders.value,
    () => remeasure(),
    { deep: true },
  );

  watch(
    () => options.layoutColWidths.value,
    () => remeasure(),
    { deep: true },
  );

  const contentSize = computed(() =>
    getContentSize(
      colWidths.value,
      displayRowCount.value,
      rowHeight.value,
      displayContentWidth.value,
    ),
  );

  function setColumnFilter(col: number, checkedValues: string[] | undefined) {
    const filters = { ...viewState.value.filters };
    if (checkedValues === undefined) {
      delete filters[col];
    } else {
      filters[col] = { checkedValues };
    }
    viewState.value = { ...viewState.value, filters };
  }

  function clearColumnFilter(col: number) {
    const next = { ...viewState.value.filters };
    delete next[col];
    viewState.value = { ...viewState.value, filters: next };
  }

  function setSort(col: number, direction: SortDirection) {
    viewState.value = { ...viewState.value, sort: { col, direction } };
  }

  function clearSort() {
    viewState.value = { ...viewState.value, sort: null };
  }

  const engine: SpreadsheetEngineExpose = {
    copy,
    copyAsImage,
    paste,
    undo: undoRedo.undo,
    redo: undoRedo.redo,
    canUndo: undoRedo.canUndo,
    canRedo: undoRedo.canRedo,
    canCopy: () => selectionApi.getSelectionRanges().length > 0,
    canCopyAsImage: () => selectionApi.getSelectionRanges().length > 0,
    canPaste: () => clipboard.hasContent() || true,
    canEdit: (_row, col) => {
      const cfg = colConfig(col);
      return !!cfg && isEditable(cfg);
    },
    getSelection: selectionApi.getSelectionRanges,
    getCellEditContext: (row, col) => buildCellContext(row, col),
    getHeaderContext: (col) => {
      const header =
        options.mergedHeaders.value.find((h) => h.col === col) ?? {
          title: '',
          col,
        };
      return {
        colIndex: col,
        fieldId: header.fieldId ?? colConfig(col)?.fieldId,
        headerTitle: header.title,
        filterValue: isColumnFilterActive(viewState.value.filters[col])
          ? 'active'
          : '',
      };
    },
    setCellValue,
    fillRange,
    getColumnConfig: colConfig,
    getFilterValue: (col) =>
      isColumnFilterActive(viewState.value.filters[col]) ? 'active' : '',
  };

  return {
    engine,
    sortedColumns,
    colWidths,
    rowHeight,
    scrollTop,
    scrollLeft,
    viewportWidth,
    viewportHeight,
    filterValues,
    viewState,
    setColumnFilter,
    clearColumnFilter,
    setSort,
    clearSort,
    dirtyKeys,
    editing,
    measureCanvas,
    filteredRowIndices,
    contentSize,
    displayRowCount,
    displayColCount,
    displayContentWidth,
    defaultColWidth,
    paddedColWidths,
    fixedCols,
    fixedWidth,
    colCount,
    selectionApi,
    undoRedo,
    dataRowFromViewRow,
    viewRowFromDataRow,
    colConfig,
    isEditable,
    resolveDisplayValue,
    resolveCellStyle,
    buildCellContext,
    commitCellChange,
    remeasure,
    initSnapshot,
    hitTestCell: (x: number, y: number) =>
      hitTestCell(
        x,
        y,
        scrollTop.value,
        scrollLeft.value,
        paddedColWidths.value,
        rowHeight.value,
        displayRowCount.value,
        colCount.value,
        fixedCols.value,
        fixedWidth.value,
      ),
    hitTestHeaderCol: (x: number) =>
      hitTestHeaderCol(
        x,
        scrollLeft.value,
        paddedColWidths.value,
        displayColCount.value,
        fixedCols.value,
        fixedWidth.value,
      ),
    hitTestRowNumber: (x: number, y: number) =>
      hitTestRowNumber(
        x,
        y,
        scrollTop.value,
        rowHeight.value,
        displayRowCount.value,
      ),
    hitTestHeaderArea: (x: number, y: number) =>
      hitTestHeaderArea(
        x,
        y,
        scrollLeft.value,
        paddedColWidths.value,
        displayColCount.value,
        fixedCols.value,
        fixedWidth.value,
      ),
    HEADER_HEIGHT,
    ROW_NUMBER_WIDTH,
  };
}

export type SpreadsheetEngine = ReturnType<typeof useSpreadsheetEngine>;
