import { useMemo } from 'react'

import { cn } from '@lib/utils'
import { type Color, toCss } from '@lib/color'

import { evaluate, isErr, type Value } from './engine'
import { applyExcelFormat, applyNormalizedFormat, ptToRem } from './format'
import defaultModel from './model.json'
import { HorizontalAlign, VerticalAlign, type Cell, type DataBlocks, type Model } from './types'
import { ExhaustiveSwitchCheck } from '@postxl/utils'

// SPREADSHEET -----------------------------------------------------------------

export type SpreadsheetProps = {
  model?: Model
  data?: DataBlocks
} & React.HTMLAttributes<HTMLDivElement>

export function Spreadsheet({ model = defaultModel as Model, data, className, style, ...props }: SpreadsheetProps) {
  const cells = useMemo(() => {
    const values = evaluate(model, data)
    const covered = new Set<string>()
    const spans = (model.merged ?? []).reduce((map, { r0, c0, r1, c1 }) => {
      const rowSpan = r1 - r0 + 1
      const colSpan = c1 - c0 + 1

      map.set(`${r0},${c0}`, { rowSpan, colSpan })

      return map
    }, new Map<string, { rowSpan: number; colSpan: number }>())

    const config = model.cells.reduce((map, cell) => {
      map.set(`${cell.r},${cell.c}`, cell)
      return map
    }, new Map<string, Cell>())

    const cells: React.ReactNode[] = []

    for (let r = 0; r < model.rows; r++) {
      for (let c = 0; c < model.cols; c++) {
        const key = `${r},${c}`

        if (covered.has(key)) {
          continue
        }

        // Base cell info (may be blank)
        const cell = config.get(key)

        // Determine span from (1) explicit per-cell, else (2) merged map, else (3) 1x1
        const rowSpan = cell?.rowSpan ?? spans.get(key)?.rowSpan ?? 1
        const colSpan = cell?.colSpan ?? spans.get(key)?.colSpan ?? 1

        // Mark covered cells (excluding the anchor itself)
        if (rowSpan > 1 || colSpan > 1) {
          for (let rr = r; rr < r + rowSpan; rr++) {
            for (let cc = c; cc < c + colSpan; cc++) {
              if (rr === r && cc === c) {
                continue
              }

              covered.add(`${rr},${cc}`)
            }
          }
        }

        cells.push(cell ? <Cell key={key} config={cell} value={values.get(key) ?? cell.value} /> : <div key={key} />)
      }
    }

    return cells
  }, [model, data])

  const gridTemplateColumns = useMemo(() => {
    const widths = model.colWidths
      ? Array.from({ length: Math.max(model.colWidths.length, model.cols) }, (_, i) => model.colWidths?.[i] ?? 96)
      : new Array(model.cols).fill(96)

    return widths.map((w) => `${w}px`).join(' ')
  }, [model])

  const gridTemplateRows = useMemo(() => {
    const heights = model.rowHeights
      ? Array.from({ length: Math.max(model.rowHeights.length, model.rows) }, (_, i) => model.rowHeights?.[i] ?? 24)
      : new Array(model.rows).fill(24)

    return heights.map((h) => `${h}px`).join(' ')
  }, [model])

  return (
    <div
      className={cn(className, 'grid bg-white w-max h-max')}
      style={{ gridTemplateColumns, gridTemplateRows, ...style }}
      {...props}
    >
      {cells}
    </div>
  )
}

// CELL ------------------------------------------------------------------------

type CellProps = Readonly<{
  config: Cell
  value: Value
}>

function toJustify(input: HorizontalAlign | null | undefined, value: Value): React.CSSProperties['justifyContent'] {
  switch (input) {
    case 'left':
      return 'flex-start'
    case 'right':
      return 'flex-end'
    case 'center':
      return 'center'
    case undefined:
    case null:
      return typeof value === 'number' ? 'flex-end' : 'flex-start'
    default:
      throw new ExhaustiveSwitchCheck(input)
  }
}

function toAlign(input: VerticalAlign | null | undefined): React.CSSProperties['alignItems'] {
  switch (input) {
    case 'top':
      return 'flex-start'
    case 'bottom':
      return 'flex-end'
    case 'center':
      return 'center'
    case undefined:
    case null:
      return 'center' // default Excel is baseline-ish; center looks nicer for a grid preview
    default:
      throw new ExhaustiveSwitchCheck(input)
  }
}
function Cell({ config, value }: CellProps) {
  const whiteSpace = config.align?.wrapText ? 'normal' : 'nowrap'

  const style: React.CSSProperties = {
    gridColumn: `${config.c + 1} / span ${config.colSpan ?? 1}`,
    gridRow: `${config.r + 1} / span ${config.rowSpan ?? 1}`,
    display: 'flex',
    justifyContent: toJustify(config.align?.horizontal, value),
    alignItems: toAlign(config.align?.vertical),
    padding: '2px 6px',
    whiteSpace,
    overflow: 'hidden',
    textOverflow: 'ellipsis',
    borderTop: config.borders?.top ? toCellBorder(config.borders.top) : undefined,
    borderLeft: config.borders?.left ? toCellBorder(config.borders.left) : undefined,
    borderRight: config.borders?.right ? toCellBorder(config.borders.right) : undefined,
    borderBottom: config.borders?.bottom ? toCellBorder(config.borders.bottom) : undefined,
    fontFamily: config.font?.name,
    fontSize: config.font?.size ? `${ptToRem(config.font.size)}rem` : '1rem',
    fontWeight: config.font?.bold ? 700 : undefined,
    fontStyle: config.font?.italic ? 'italic' : undefined,
    textDecoration: config.font?.underline ? 'underline' : undefined,
    color: config.font?.color ? toCss(config.font.color) : undefined,
    background: config.fill?.color ? toCss(config.fill.color) : undefined,
  }

  const text = useMemo(() => {
    switch (typeof value) {
      case 'number': {
        if (config.fmt?.normalized) {
          return applyNormalizedFormat(value, config.fmt.normalized)
        } else if (config.fmt?.numberFormat) {
          return applyExcelFormat(value, config.fmt.numberFormat)
        } else if (isErr(value)) {
          return value.reason
        } else if (value === null) {
          return ''
        } else {
          return String(value)
        }
      }

      case 'string':
        return value

      default:
        return ''
    }
  }, [config.fmt, value])

  return (
    <div style={style} title={text}>
      {text}
    </div>
  )
}

function toCellBorder(side: { style?: 'solid' | 'dashed' | 'dotted'; color?: Color }) {
  const style = side.style || 'solid'
  const color = side.color ? toCss(side.color) : '#DDE1E6'

  return `1px ${style} ${color}`
}
