import { action, makeObservable, observable } from 'mobx';
import type { CellType, Cell, ValidationResult } from './types';
import type { FiltersMap } from '@wix/bex-core';
import type { EditableTableState } from './EditableTableState';
import {
  builtInCellTypes,
  textCellType,
} from '../../components/EditableTable/cellTypes';
import { runCellValidation, validResult } from './utils';

const builtInCellTypeMap = new Map<string, CellType>(
  builtInCellTypes.map((ct) => [ct.type, ct]),
);

export class CellValueState<T, F extends FiltersMap> {
  /** Pending invalid cell values, keyed by `${itemId}:${colId}`. */
  readonly _pendingCells = new Map<string, { value: any; errors: string[] }>();

  private readonly _parent: EditableTableState<T, F>;
  private readonly _extraCellTypes: Map<string, CellType>;
  private _cellTypeMap: Map<string, CellType> = builtInCellTypeMap;

  constructor(
    parent: EditableTableState<T, F>,
    extraCellTypes: Map<string, CellType> = new Map(),
  ) {
    this._parent = parent;
    this._extraCellTypes = extraCellTypes;

    makeObservable(this, {
      _pendingCells: observable,
      clearCells: action,
      toggleCell: action,
      commitHandler: action,
      applyBulkRawValues: action,
      applyBulkValues: action,
    });
  }

  _buildCellTypeMap() {
    const map = new Map<string, CellType>();
    for (const col of this._parent._allColumns) {
      if (
        col.cellType === 'custom' &&
        col.typeConfig &&
        typeof col.typeConfig === 'object' &&
        'ViewComponent' in col.typeConfig
      ) {
        map.set(col.id, {
          type: 'custom',
          ...(col.typeConfig as Omit<CellType, 'type'>),
        });
      } else {
        map.set(
          col.id,
          builtInCellTypeMap.get(col.cellType) ??
            this._extraCellTypes.get(col.cellType) ??
            textCellType,
        );
      }
    }
    this._cellTypeMap = map;
  }

  resolveCellTypeByColumnId(columnId: string): CellType {
    return this._cellTypeMap.get(columnId) ?? textCellType;
  }

  getCellDisplayValue(rowKey: string, columnId: string): any {
    const pending = this._getPendingCell(rowKey, columnId);
    if (pending) {
      return pending.value;
    }
    const column = this._parent._columnMap.get(columnId);
    const keyedItem = this._parent._getKeyedItem(rowKey);
    return column && keyedItem ? column.getValue(keyedItem.item) : undefined;
  }

  getCellValidation(rowKey: string, columnId: string): ValidationResult {
    const pending = this._getPendingCell(rowKey, columnId);
    if (pending) {
      return { valid: false, errors: pending.errors };
    }
    return validResult;
  }

  isCellEditable(rowKey: string, columnId: string): boolean {
    const column = this._parent._columnMap.get(columnId);
    if (!column) {
      return false;
    }

    const resolved = this.resolveCellTypeByColumnId(column.id);
    if (resolved.editable === false || !resolved.EditComponent) {
      return false;
    }
    if (!column.setValue) {
      return false;
    }

    if (typeof column.editable === 'function') {
      const keyedItem = this._parent._getKeyedItem(rowKey);
      return keyedItem ? column.editable(keyedItem.item) : false;
    }
    return column.editable !== false;
  }

  /** Serialize a cell value to a string (for clipboard copy/cut). */
  serializeCellValue(rowKey: string, columnId: string): string {
    const column = this._parent._columnMap.get(columnId);
    const keyedItem = this._parent._getKeyedItem(rowKey);
    if (!column || !keyedItem) {
      return '';
    }
    const resolved = this.resolveCellTypeByColumnId(column.id);
    const pending = this._getPendingCell(rowKey, columnId);
    const value = pending ? pending.value : column.getValue(keyedItem.item);
    return resolved.serialize ? resolved.serialize(value) : String(value ?? '');
  }

  /** Clear selected cells (Delete/Backspace). Respects cellType.clearable. */
  clearCells(cells: Cell[]) {
    const changes: { rowKey: string; columnId: string; value: any }[] = [];

    for (const cell of cells) {
      const column = this._parent._columnMap.get(cell.columnId);
      if (!column?.setValue) {
        continue;
      }
      const resolved = this.resolveCellTypeByColumnId(column.id);
      if (resolved.clearable === false) {
        continue;
      }

      const pendingKey = this._pendingCellKey(cell.rowKey, cell.columnId);
      if (pendingKey) {
        this._pendingCells.delete(pendingKey);
      }
      const value = resolved.deserialize ? resolved.deserialize('') : '';
      changes.push({ rowKey: cell.rowKey, columnId: cell.columnId, value });
    }

    if (changes.length > 0) {
      this.applyBulkValues(changes);
    }
  }

  toggleCell(rowKey: string, columnId: string) {
    const column = this._parent._columnMap.get(columnId);
    const keyedItem = this._parent._getKeyedItem(rowKey);
    if (!column?.setValue || !keyedItem) {
      return;
    }

    const resolved = this.resolveCellTypeByColumnId(column.id);
    if (!resolved.toggleValue) {
      return;
    }

    const value = column.getValue(keyedItem.item);
    const newValue = resolved.toggleValue(value);
    const updatedItem = column.setValue(keyedItem.item, newValue);
    this._parent.mutation.updateItems([updatedItem]);
  }

  /** Commit handler — used as callback for CellInteractionState. */
  commitHandler(editingValue: any, cell: Cell) {
    if (editingValue !== undefined) {
      const resolved = this.resolveCellTypeByColumnId(cell.columnId);
      const normalized = resolved.normalizeOnCommit
        ? resolved.normalizeOnCommit(editingValue)
        : editingValue;
      this._applyValue(cell.rowKey, cell.columnId, normalized);
    }
  }

  private _getPendingCell(
    rowKey: string,
    columnId: string,
  ): { value: any; errors: string[] } | undefined {
    const key = this._pendingCellKey(rowKey, columnId);
    return key ? this._pendingCells.get(key) : undefined;
  }

  private _pendingCellKey(
    rowKey: string,
    columnId: string,
  ): string | undefined {
    const keyedItem = this._parent._getKeyedItem(rowKey);
    const column = this._parent._columnMap.get(columnId);
    if (!keyedItem || !column) {
      return undefined;
    }
    return `${keyedItem.key}:${column.id}`;
  }

  /**
   * Deserialize a raw string to a typed value. Returns an error message
   * if the conversion fails
   */
  tryDeserialize(
    columnId: string,
    rawValue: string,
  ): { value: any; error?: string } {
    const column = this._parent._columnMap.get(columnId);
    if (!column) {
      return { value: rawValue };
    }
    const resolved = this.resolveCellTypeByColumnId(column.id);
    if (!resolved.deserialize) {
      return { value: rawValue };
    }
    try {
      return { value: resolved.deserialize(rawValue) };
    } catch {
      return { value: rawValue, error: 'Value is not compatible' };
    }
  }

  /**
   * Deserialize and apply multiple raw string values in bulk.
   * Handles conversion failures by storing them as pending cells.
   */
  applyBulkRawValues(
    rawChanges: { rowKey: string; columnId: string; rawValue: string }[],
  ) {
    const changes: { rowKey: string; columnId: string; value: any }[] = [];
    for (const { rowKey, columnId, rawValue } of rawChanges) {
      const { value, error } = this.tryDeserialize(columnId, rawValue);
      if (error) {
        const pendingKey = this._pendingCellKey(rowKey, columnId);
        if (pendingKey) {
          this._pendingCells.set(pendingKey, { value, errors: [error] });
        }
      } else {
        changes.push({ rowKey, columnId, value });
      }
    }
    this.applyBulkValues(changes);
  }

  /**
   * Apply multiple cell values in bulk. Validates per cell, accumulates
   * changes per row, and submits all modified items in a single update.
   */
  applyBulkValues(changes: { rowKey: string; columnId: string; value: any }[]) {
    const rowItems = new Map<string, T>();

    for (const { rowKey, columnId, value } of changes) {
      const column = this._parent._columnMap.get(columnId);
      const keyedItem = this._parent._getKeyedItem(rowKey);
      if (!column?.setValue || !keyedItem) {
        continue;
      }

      const resolved = this.resolveCellTypeByColumnId(column.id);
      const result = runCellValidation(
        value,
        column.validation,
        resolved.validate,
      );
      const pendingKey = this._pendingCellKey(rowKey, columnId);

      if (result.valid) {
        if (pendingKey) {
          this._pendingCells.delete(pendingKey);
        }
        const currentItem = rowItems.get(rowKey) ?? keyedItem.item;
        rowItems.set(rowKey, column.setValue(currentItem, value));
      } else if (pendingKey) {
        this._pendingCells.set(pendingKey, {
          value,
          errors: result.errors ?? [],
        });
      }
    }

    this._parent.mutation.updateItems(Array.from(rowItems.values()));
  }

  private _applyValue(rowKey: string, columnId: string, value: any) {
    const column = this._parent._columnMap.get(columnId);
    const keyedItem = this._parent._getKeyedItem(rowKey);
    if (!column?.setValue || !keyedItem) {
      return;
    }

    const resolved = this.resolveCellTypeByColumnId(column.id);
    const result = runCellValidation(
      value,
      column.validation,
      resolved.validate,
    );
    const pendingKey = this._pendingCellKey(rowKey, columnId);

    if (result.valid) {
      if (pendingKey) {
        this._pendingCells.delete(pendingKey);
      }
      const updatedItem = column.setValue(keyedItem.item, value);
      this._parent.mutation.updateItems([updatedItem]);
    } else if (pendingKey) {
      this._pendingCells.set(pendingKey, {
        value,
        errors: result.errors ?? [],
      });
    }
  }
}
