import Handsontable from 'handsontable'
import { MenuItemConfig } from 'handsontable/plugins/contextMenu'

export type HotExportFormat = 'csv' | 'xlsx'

/**
 * Cell types whose values must be exported as raw stored strings rather than
 * converted to native Excel date/time cells. SAS raw data is the canonical
 * representation, so dates/times are exported exactly as the grid stores them.
 * `numeric` is intentionally NOT listed — it stays a native Excel number.
 */
const RAW_EXPORT_TYPES = new Set([
  'date',
  'time',
  'datetime',
  'intl-date',
  'intl-time',
  'intl-datetime'
])

/**
 * Client-side export of a Handsontable grid's current view via the built-in
 * `exportFile` plugin. Exports the current selection if one exists, otherwise
 * the whole grid. This is a "what you see" convenience export and is distinct
 * from any server-side (SAS) download.
 *
 * @param hot Live Handsontable instance.
 * @param format `'csv'` or `'xlsx'`.
 * @param filename File name (without extension).
 * @param skipLeadingCols Number of leading UI/housekeeping columns to exclude
 *   (e.g. the editor's `Delete?` column at index 0).
 */
export async function exportGrid(
  hot: Handsontable,
  format: HotExportFormat,
  filename: string,
  skipLeadingCols = 0
): Promise<void> {
  const plugin = hot.getPlugin('exportFile')

  const opts: Record<string, any> = {
    filename: filename || 'export',
    colHeaders: true
  }

  // Selection-aware, contiguous range (visual indexes). Hidden columns are
  // excluded automatically (exportHiddenColumns defaults to false).
  const sel = hot.getSelectedRangeLast()
  const lastRow = hot.countRows() - 1
  const lastCol = hot.countCols() - 1

  // Mirror HOT's own export item: only honor a selection that spans more than
  // one cell. A right-click places a single-cell cursor, and the corner click
  // is select-all (negative coords) — both mean "export the whole table".
  const isCornerSelectAll = !!sel && sel.from.row < 0 && sel.from.col < 0
  if (sel && !sel.isSingleCell() && !isCornerSelectAll) {
    const top = Math.max(0, Math.min(sel.from.row, sel.to.row))
    const left = Math.max(0, Math.min(sel.from.col, sel.to.col))
    const bottom = Math.max(sel.from.row, sel.to.row)
    const right = Math.max(sel.from.col, sel.to.col)
    opts['range'] = [top, Math.max(left, skipLeadingCols), bottom, right]
  }

  if (!opts['range'] && skipLeadingCols > 0) {
    opts['range'] = [0, skipLeadingCols, lastRow, lastCol]
  }

  if (format === 'csv') {
    // CSV reads raw stored values (getDataAtCell) — no type conversion needed.
    plugin.downloadFile('csv', opts)
    return
  }

  // xlsx: temporarily override cell meta during the export's single
  // getCellMeta() pass, then restore (HOT returns cached meta objects).
  //  - date/time/datetime types -> 'text' so values export raw, not as native
  //    Excel dates.
  //  - drop color/highlight: clear classNames and readOnly so the exporter
  //    doesn't bake in theme/validation/read-only fills. Cell classes carry no
  //    meaning in the exported file.
  const restore: Array<{
    meta: any
    type: any
    className: any
    readOnly: any
  }> = []
  const seen = new Set<any>()
  const hook = (_row: number, _col: number, cp: any): void => {
    if (seen.has(cp)) return
    seen.add(cp)

    const needsTypeFix = RAW_EXPORT_TYPES.has(cp.type)
    const needsClassFix =
      typeof cp.className === 'string' && cp.className !== ''
    const needsReadOnlyFix = cp.readOnly === true

    if (!needsTypeFix && !needsClassFix && !needsReadOnlyFix) return

    restore.push({
      meta: cp,
      type: cp.type,
      className: cp.className,
      readOnly: cp.readOnly
    })

    if (needsTypeFix) cp.type = 'text'
    if (needsClassFix) cp.className = ''
    if (needsReadOnlyFix) cp.readOnly = false
  }

  hot.addHook('afterGetCellMeta', hook)
  try {
    const mod: any = await import('exceljs')
    const ExcelJS = mod.default ?? mod
    await plugin.downloadFileAsync('xlsx', { ...opts, engine: ExcelJS })
  } finally {
    hot.removeHook('afterGetCellMeta', hook)
    restore.forEach(({ meta, type, className, readOnly }) => {
      meta.type = type
      meta.className = className
      meta.readOnly = readOnly
    })
  }
}

/**
 * Builds a reusable `export_file` context-menu item (CSV / Excel submenu) that
 * exports the given grid via {@link exportGrid}. Add it to any HOT context-menu
 * `items` map under the `export_file` key.
 *
 * @param getFilename Returns the export file name at click time.
 * @param skipLeadingCols Leading UI columns to exclude (see {@link exportGrid}).
 */
export function buildExportMenuItem(
  getFilename: () => string,
  skipLeadingCols = 0
): MenuItemConfig {
  // HOT invokes context-menu callbacks with `this` bound to the clicked grid
  // instance, so the item works uniformly even for components that host
  // multiple grids (e.g. viewboxes).
  const makeCb = (format: HotExportFormat) =>
    function (this: Handsontable): void {
      void exportGrid(this, format, getFilename(), skipLeadingCols)
    }

  return {
    name: 'Export',
    // The exportFile plugin registers a predefined item under the same
    // `export_file` key whose `hidden()` returns true unless an `exportFile`
    // setting exists. HOT shallow-merges our config over it, so we must
    // explicitly override `hidden` to keep the item visible.
    hidden: false,
    submenu: {
      // Submenu keys use the documented "parent_key:sub_key" form. The array
      // form is what HOT's own exportFile plugin uses at runtime; cast to
      // satisfy the (stricter) MenuConfig typing.
      items: [
        {
          key: 'export_file:csv',
          name: 'CSV',
          callback: makeCb('csv')
        },
        {
          key: 'export_file:xlsx',
          name: 'Excel (.xlsx)',
          callback: makeCb('xlsx')
        }
      ] as any
    }
  }
}
