import { convertQuantity, evaluateQuantityExpression, getUnit, quantityFromNumber, quantityFromUnit, type Quantity } from './units'

export type SpreadsheetCellInput = number | string | null
export type SpreadsheetErrorCode = 'INVALID_ADDRESS' | 'UNKNOWN_REFERENCE' | 'DEPENDENCY_CYCLE' | 'EXPRESSION_ERROR'
export type SpreadsheetCellStyle = { background?: string; color?: string; bold?: boolean; italic?: boolean; numberFormat?: string }

export type SpreadsheetCell = {
  address: string
  input: SpreadsheetCellInput
  alias?: string
  unit?: string
  style?: SpreadsheetCellStyle
  value?: Quantity
  display?: string
  error?: { code: SpreadsheetErrorCode; message: string }
}

export type SpreadsheetBinding = { alias: string; objectId: string; propertyName: string }
export type SpreadsheetNamedRange = { name: string; range: string }

export type SpreadsheetDependency = { source: string; target: string; reference: string }

export type SpreadsheetEvaluation = {
  cells: Record<string, SpreadsheetCell>
  dependencies: SpreadsheetDependency[]
  cycles: string[][]
  errors: Array<{ address: string; code: SpreadsheetErrorCode; message: string }>
}

export type SpreadsheetSnapshot = {
  id: string
  label: string
  cells: SpreadsheetCell[]
  bindings: SpreadsheetBinding[]
  namedRanges: SpreadsheetNamedRange[]
  mergedRanges: string[]
  version: number
}

export type SpreadsheetApi = {
  snapshot(): SpreadsheetSnapshot
  setCell(address: string, input: SpreadsheetCellInput, options?: { alias?: string; unit?: string }): SpreadsheetCell
  setAlias(address: string, alias: string): SpreadsheetCell
  setStyle(range: string, style: SpreadsheetCellStyle): SpreadsheetCell[]
  mergeCells(range: string): SpreadsheetSnapshot
  setNamedRange(name: string, range: string): SpreadsheetNamedRange
  insertRows(at: number, count?: number): SpreadsheetSnapshot
  insertColumns(at: number, count?: number): SpreadsheetSnapshot
  bindProperty(alias: string, objectId: string, propertyName: string): SpreadsheetBinding
  evaluate(): SpreadsheetEvaluation
  exportCsv(): string
}

type CellState = SpreadsheetCell & { address: string }

const addressPattern = /^([A-Z]+)([1-9]\d*)$/

const normalizeAddress = (address: string) => {
  const normalized = address.trim().replace(/\$/g, '').toUpperCase()
  if (!addressPattern.test(normalized)) throw new RangeError(`Invalid spreadsheet cell address: ${address}`)
  return normalized
}

const parseAddress = (address: string) => {
  const match = addressPattern.exec(address)
  if (!match) throw new RangeError(`Invalid spreadsheet cell address: ${address}`)
  let column = 0
  for (const character of match[1]) column = column * 26 + character.charCodeAt(0) - 64
  return { column, row: Number(match[2]) }
}

const columnName = (column: number) => { let value = ''; for (let current = column; current > 0; current = Math.floor((current - 1) / 26)) value = String.fromCharCode(65 + ((current - 1) % 26)) + value; return value }
const formatAddress = (column: number, row: number) => `${columnName(column)}${row}`
const parseRange = (range: string) => {
  const [start, end = start] = range.split(':').map(normalizeAddress)
  const first = parseAddress(start); const last = parseAddress(end)
  return { start, end, minColumn: Math.min(first.column, last.column), maxColumn: Math.max(first.column, last.column), minRow: Math.min(first.row, last.row), maxRow: Math.max(first.row, last.row) }
}
const rangeAddress = (range: ReturnType<typeof parseRange>) => `${formatAddress(range.minColumn, range.minRow)}:${formatAddress(range.maxColumn, range.maxRow)}`

const isFormula = (input: SpreadsheetCellInput): input is string => typeof input === 'string' && input.trim().startsWith('=')

const formulaBody = (input: string) => input.trim().replace(/^=/, '').trim()

const referencePattern = /\b(?:[A-Za-z_][A-Za-z0-9_]*\.)?[A-Za-z]{1,3}[1-9]\d*\b|\b[A-Za-z_][A-Za-z0-9_]*\b/g
const functionNames = new Set(['abs', 'sin', 'cos', 'tan', 'atan2', 'min', 'max', 'clamp', 'round', 'pow', 'pi'])

const referencesFor = (expression: string) => [...new Set((expression.match(referencePattern) ?? []).filter((reference) => !functionNames.has(reference.toLowerCase()) && !getUnit(reference)))]

const cloneCell = (cell: SpreadsheetCell): SpreadsheetCell => ({ ...cell, value: cell.value ? { ...cell.value } : undefined, style: cell.style ? { ...cell.style } : undefined, error: cell.error ? { ...cell.error } : undefined })

const cloneSnapshot = (snapshot: SpreadsheetSnapshot): SpreadsheetSnapshot => ({ ...snapshot, cells: snapshot.cells.map(cloneCell), bindings: snapshot.bindings.map((binding) => ({ ...binding })), namedRanges: snapshot.namedRanges.map((namedRange) => ({ ...namedRange })), mergedRanges: [...snapshot.mergedRanges] })

const quantityForLiteral = (input: SpreadsheetCellInput, unit?: string): Quantity | undefined => {
  if (typeof input === 'number') return unit ? quantityFromUnit(input, unit) : quantityFromNumber(input)
  if (typeof input !== 'string' || !input.trim() || isFormula(input)) return undefined
  const evaluated = evaluateQuantityExpression(input)
  if (!unit) return evaluated.value
  const definition = getUnit(unit)
  if (!definition || evaluated.value.dimension !== definition.dimension) throw new TypeError(`Cell unit ${unit} does not match its value dimension.`)
  return quantityFromUnit(evaluated.value.value / definition.factor, unit)
}

export const createSpreadsheet = (id = 'spreadsheet', label = 'Spreadsheet'): SpreadsheetApi => {
  const cells = new Map<string, CellState>()
  const bindings = new Map<string, SpreadsheetBinding>()
  const namedRanges = new Map<string, SpreadsheetNamedRange>()
  const mergedRanges = new Set<string>()
  let version = 0

  const setCell = (address: string, input: SpreadsheetCellInput, options: { alias?: string; unit?: string } = {}) => {
    const normalized = normalizeAddress(address)
    if (options.alias) {
      const alias = options.alias.trim()
      if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) throw new RangeError(`Invalid spreadsheet alias: ${options.alias}`)
      const existing = [...cells.values()].find((cell) => cell.alias === alias && cell.address !== normalized)
      if (existing) throw new RangeError(`Spreadsheet alias is already used: ${alias}`)
    }
    const previous = cells.get(normalized)
    const cell: CellState = { address: normalized, input, ...(options.alias ? { alias: options.alias.trim() } : previous?.alias ? { alias: previous.alias } : {}), ...(options.unit ? { unit: options.unit } : previous?.unit ? { unit: previous.unit } : {}), ...(previous?.style ? { style: { ...previous.style } } : {}) }
    cells.set(normalized, cell)
    version += 1
    return cloneCell(cell)
  }

  const setAlias = (address: string, alias: string) => {
    const normalized = normalizeAddress(address)
    const cell = cells.get(normalized)
    if (!cell) throw new RangeError(`Spreadsheet cell does not exist: ${normalized}`)
    return setCell(normalized, cell.input, { alias, unit: cell.unit })
  }

  const setStyle = (range: string, style: SpreadsheetCellStyle) => {
    const parsed = parseRange(range)
    const validColor = (value: string | undefined) => value === undefined || /^#[0-9a-f]{6}$/i.test(value)
    if (!validColor(style.background) || !validColor(style.color) || (style.numberFormat !== undefined && !style.numberFormat.trim())) throw new RangeError('Spreadsheet style contains an invalid color or number format.')
    const updated: SpreadsheetCell[] = []
    for (let row = parsed.minRow; row <= parsed.maxRow; row += 1) for (let column = parsed.minColumn; column <= parsed.maxColumn; column += 1) {
      const address = formatAddress(column, row)
      const current = cells.get(address) ?? { address, input: null }
      const next = { ...current, style: { ...current.style, ...style } }
      cells.set(address, next); updated.push(cloneCell(next))
    }
    version += 1
    return updated
  }

  const mergeCells = (range: string) => { const parsed = parseRange(range); const normalized = rangeAddress(parsed); mergedRanges.add(normalized); version += 1; return snapshot() }

  const setNamedRange = (name: string, range: string) => {
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new RangeError(`Invalid spreadsheet named range: ${name}`)
    const normalized = rangeAddress(parseRange(range))
    const namedRange = { name, range: normalized }
    namedRanges.set(name, namedRange); version += 1
    return { ...namedRange }
  }

  const shiftRange = (range: string, axis: 'row' | 'column', at: number, count: number) => {
    const parsed = parseRange(range)
    if (axis === 'row' && parsed.maxRow < at) return rangeAddress(parsed)
    if (axis === 'column' && parsed.maxColumn < at) return rangeAddress(parsed)
    const delta = (value: number) => value >= at ? value + count : value
    return axis === 'row' ? `${formatAddress(parsed.minColumn, delta(parsed.minRow))}:${formatAddress(parsed.maxColumn, delta(parsed.maxRow))}` : `${formatAddress(delta(parsed.minColumn), parsed.minRow)}:${formatAddress(delta(parsed.maxColumn), parsed.maxRow)}`
  }

  const insert = (axis: 'row' | 'column', at: number, count: number) => {
    if (!Number.isInteger(at) || at < 1 || !Number.isInteger(count) || count < 1 || count > 1000) throw new RangeError(`Spreadsheet ${axis} insertion range is invalid.`)
    const moved = new Map<string, CellState>()
    for (const cell of cells.values()) {
      const parsed = parseAddress(cell.address)
      const shifted = axis === 'row' ? { column: parsed.column, row: parsed.row >= at ? parsed.row + count : parsed.row } : { column: parsed.column >= at ? parsed.column + count : parsed.column, row: parsed.row }
      moved.set(formatAddress(shifted.column, shifted.row), { ...cell, address: formatAddress(shifted.column, shifted.row) })
    }
    cells.clear(); moved.forEach((cell, address) => cells.set(address, cell))
    for (const [name, namedRange] of namedRanges) namedRanges.set(name, { ...namedRange, range: shiftRange(namedRange.range, axis, at, count) })
    const shiftedMerges = [...mergedRanges].map((range) => shiftRange(range, axis, at, count)); mergedRanges.clear(); shiftedMerges.forEach((range) => mergedRanges.add(range))
    version += 1
    return snapshot()
  }

  const insertRows = (at: number, count = 1) => insert('row', at, count)
  const insertColumns = (at: number, count = 1) => insert('column', at, count)

  const bindProperty = (alias: string, objectId: string, propertyName: string) => {
    const cell = [...cells.values()].find((candidate) => candidate.alias === alias)
    if (!cell) throw new ReferenceError(`Spreadsheet alias does not exist: ${alias}`)
    const binding = { alias, objectId, propertyName }
    bindings.set(alias, binding)
    version += 1
    return { ...binding }
  }

  const evaluate = (): SpreadsheetEvaluation => {
    const result = new Map<string, SpreadsheetCell>()
    const dependencies: SpreadsheetDependency[] = []
    const stack: string[] = []
    const cycles: string[][] = []
    const errors: Array<{ address: string; code: SpreadsheetErrorCode; message: string }> = []
    const aliasToAddress = new Map([...cells.values()].filter((cell) => cell.alias).map((cell) => [cell.alias as string, cell.address]))
    const resolve = (reference: string, sourceAddress?: string): Quantity => {
      const address = cells.has(reference) ? reference : aliasToAddress.get(reference)
      if (!address) throw Object.assign(new ReferenceError(`Unknown spreadsheet reference: ${reference}`), { code: 'UNKNOWN_REFERENCE' as const })
      if (sourceAddress && sourceAddress !== address) dependencies.push({ source: sourceAddress, target: address, reference })
      if (stack.includes(address)) {
        const cycle = [...stack.slice(stack.indexOf(address)), address]
        if (!cycles.some((candidate) => candidate.join('>') === cycle.join('>'))) cycles.push(cycle)
        throw Object.assign(new Error(`Spreadsheet dependency cycle: ${cycle.join(' -> ')}`), { code: 'DEPENDENCY_CYCLE' as const })
      }
      const previous = result.get(address)
      if (previous?.value) return previous.value
      const cell = cells.get(address)
      if (!cell) throw new ReferenceError(`Unknown spreadsheet cell: ${address}`)
      stack.push(address)
      try {
        const references = isFormula(cell.input) ? referencesFor(formulaBody(cell.input)) : []
        const variables = new Map<string, Quantity>()
        for (const reference of references) variables.set(reference, resolve(reference, address))
        const literal = isFormula(cell.input) ? undefined : quantityForLiteral(cell.input, cell.unit)
        if (!isFormula(cell.input) && !literal) throw new TypeError(`Spreadsheet cell ${address} is not a quantity.`)
        const evaluated = isFormula(cell.input) ? evaluateQuantityExpression(formulaBody(cell.input), variables) : { value: literal as Quantity }
        const displayValue = cell.unit ? convertQuantity(evaluated.value, cell.unit) : evaluated.value.value
        const updated = { ...cloneCell(cell), value: evaluated.value, display: `${displayValue} ${cell.unit ?? evaluated.value.dimension}`, error: undefined }
        result.set(address, updated)
        return evaluated.value
      } catch (error) {
        const code = (error as { code?: SpreadsheetErrorCode }).code ?? (error instanceof ReferenceError ? 'UNKNOWN_REFERENCE' : 'EXPRESSION_ERROR')
        const message = error instanceof Error ? error.message : String(error)
        const updated = { ...cloneCell(cell), error: { code, message } }
        result.set(address, updated)
        errors.push({ address, code, message })
        throw error
      } finally { stack.pop() }
    }
    for (const cell of cells.values()) { try { resolve(cell.address) } catch { /* diagnostics are returned with the cell */ } }
    for (const cycle of cycles) for (const address of cycle) {
      const cell = result.get(address) ?? cells.get(address)
      if (cell) result.set(address, { ...cloneCell(cell), error: { code: 'DEPENDENCY_CYCLE', message: `Spreadsheet dependency cycle: ${cycle.join(' -> ')}` } })
    }
    return { cells: Object.fromEntries([...cells.keys()].map((address) => [address, cloneCell(result.get(address) ?? cells.get(address)!)])), dependencies, cycles, errors }
  }

  const snapshot = (): SpreadsheetSnapshot => cloneSnapshot({ id, label, cells: [...cells.values()].map(cloneCell), bindings: [...bindings.values()].map((binding) => ({ ...binding })), namedRanges: [...namedRanges.values()].map((namedRange) => ({ ...namedRange })), mergedRanges: [...mergedRanges], version })

  const exportCsv = () => {
    const evaluated = evaluate()
    if (cells.size === 0) return ''
    const coordinates = [...cells.keys()].map(parseAddress)
    const maxRow = Math.max(...coordinates.map((coordinate) => coordinate.row))
    const maxColumn = Math.max(...coordinates.map((coordinate) => coordinate.column))
    return Array.from({ length: maxRow }, (_, rowIndex) => Array.from({ length: maxColumn }, (_, columnIndex) => {
      const cell = evaluated.cells[formatAddress(columnIndex + 1, rowIndex + 1)]
      const value = cell?.display ?? (cell?.input == null ? '' : String(cell.input))
      return /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value
    }).join(',')).join('\n')
  }

  return { snapshot, setCell, setAlias, setStyle, mergeCells, setNamedRange, insertRows, insertColumns, bindProperty, evaluate, exportCsv }
}
