import {
  DEFAULT_FUNCTIONS,
  ExcelFunction,
  VOLATILE_FUNCTIONS,
} from './functions'

// Ограничиваем поиск ячеек диапазоном A1-Z120
const QUALIFIED_REF_RE = /\b[A-Za-z0-9_]+![A-Z]([1-9]|[1-8][0-9]|150)\b/g
const LOCAL_REF_RE = /\b[A-Z]([1-9]|[1-8][0-9]|150)\b/g
const RANGE_RE = /\b[A-Z]([1-9]|[1-8][0-9]|150):[A-Z]([1-9]|[1-8][0-9]|150)\b/g
const QUALIFIED_RANGE_RE =
  /\b[A-Za-z0-9_]+![A-Z]([1-9]|[1-8][0-9]|150):[A-Z]([1-9]|[1-8][0-9]|150)\b/g

// Типы для значений ячеек
export type CellValue = string | number | boolean | null | undefined

// Утилитная функция для устранения машинной ошибки в числах с плавающей точкой
// Применяется только при финальном выводе значений
function fixFloatingPointError(value: CellValue): CellValue {
  if (typeof value === 'number' && isFinite(value)) {
    // Проверяем, является ли число "почти целым" или "почти простой дробью"
    const tolerance = 1e-10

    // Проверка на целое число
    if (Math.abs(value - Math.round(value)) < tolerance) {
      return Math.round(value)
    }

    // Проверка на простые дроби (до 6 знаков после запятой)
    for (let precision = 1; precision <= 6; precision++) {
      const factor = Math.pow(10, precision)
      const rounded = Math.round(value * factor) / factor
      if (Math.abs(value - rounded) < tolerance) {
        return rounded
      }
    }

    // Для остальных случаев ограничиваем до разумного количества знаков
    return Math.round(value * 1e10) / 1e10
  }
  return value
}

export class Cell {
  sheet: Sheet
  name: string
  raw: CellValue = null
  value: CellValue = null
  precedents: Set<string> = new Set()
  dependents: Set<string> = new Set()
  isVolatile: boolean = false

  constructor(sheet: Sheet, name: string) {
    this.sheet = sheet
    this.name = name
  }

  get qualifiedName(): string {
    return `${this.sheet.name}!${this.name}`
  }

  set(raw: CellValue): void {
    // Удаляем старые зависимости
    for (const ref of this.precedents) {
      this.sheet.workbook.getCell(ref).dependents.delete(this.qualifiedName)
    }

    // Убираем ячейку из списка волатильных если она там была
    if (this.isVolatile) {
      this.sheet.workbook.removeVolatileCell(this.qualifiedName)
      this.isVolatile = false
    }

    this.raw = raw
    this.precedents.clear()

    // Помечаем себя и всех зависимых ячеек "грязными"
    this.sheet.workbook.markDirty(this.qualifiedName)
  }

  evaluate(): CellValue {
    // Если не формула — просто возвращаем значение
    if (typeof this.raw !== 'string' || !this.raw.startsWith('=')) {
      this.value = this.raw
      return this.value
    }

    // Убираем знак равенства
    const expr = this.raw.substring(1)

    // Проверяем, содержит ли формула волатильные функции
    this.checkVolatileFunctions(expr)

    // Регистрируем зависимости на другие ячейки
    // Сначала обрабатываем квалифицированные ссылки (SheetName!A1)
    const qualifiedMatches = expr.match(QUALIFIED_REF_RE)
    if (qualifiedMatches) {
      for (const ref of qualifiedMatches) {
        this.precedents.add(ref)
        const cell = this.sheet.workbook.getCell(ref)
        cell.dependents.add(this.qualifiedName)
      }
    }

    // Затем обрабатываем локальные ссылки (A1, B2)
    const localMatches = expr.match(LOCAL_REF_RE)
    if (localMatches) {
      for (const ref of localMatches) {
        // Избегаем конфликтов с функциями (например, SUM, MAX)
        if (!this.isFunction(ref)) {
          const qualifiedRef = `${this.sheet.name}!${ref}`
          this.precedents.add(qualifiedRef)
          const cell = this.sheet.workbook.getCell(qualifiedRef)
          cell.dependents.add(this.qualifiedName)
        }
      }
    }

    try {
      // Заменяем ссылки на вычисленные значения
      let code = expr

      // Обрабатываем ЕПУСТО до замены ссылок на значения,
      // чтобы проверить raw-значение ячейки напрямую
      code = code.replace(
        /ЕПУСТО\s*\(\s*(?:([A-Za-z0-9_]+)!)?([A-Za-z](?:[1-9]|[1-8][0-9]|150))\s*\)/gi,
        (_match, sheetName, cellRef) => {
          const qualifiedRef = sheetName
            ? `${sheetName}!${cellRef}`
            : `${this.sheet.name}!${cellRef}`
          try {
            const cell = this.sheet.workbook.getCell(qualifiedRef)
            const raw = cell.raw
            return (raw === null || raw === undefined || raw === '') ? 'true' : 'false'
          } catch {
            return 'true'
          }
        }
      )

      // ВАЖНО: Сначала заменяем диапазоны ячеек на массивы значений,
      // затем отдельные ячейки, чтобы избежать конфликтов
      code = code.replace(QUALIFIED_RANGE_RE, match => {
        const [sheetName, range] = match.split('!')
        const values = this.expandRange(range, sheetName)
        return `[${values.join(',')}]`
      })

      code = code.replace(RANGE_RE, match => {
        const values = this.expandRange(match)
        return `[${values.join(',')}]`
      })

      // Заменяем квалифицированные ссылки на ячейки
      code = code.replace(QUALIFIED_REF_RE, match => {
        try {
          const [sheetName] = match.split('!')
          const sheet = this.sheet.workbook.getSheet(sheetName)
          if (!sheet) {
            return '"#REF!"'
          }
          const cellValue = this.sheet.workbook.getValue(match)
          // Только строки оборачиваем в кавычки, числа и булевы значения - нет
          if (typeof cellValue === 'string') {
            return `"${cellValue}"`
          } else if (
            typeof cellValue === 'number' ||
            typeof cellValue === 'boolean'
          ) {
            return String(cellValue)
          } else {
            return '0'
          }
        } catch (error) {
          return '"#REF!"'
        }
      })

      // Заменяем локальные ссылки на ячейки
      code = code.replace(LOCAL_REF_RE, match => {
        // Избегаем замены функций
        if (this.isFunction(match)) {
          return match
        }
        try {
          const qualifiedRef = `${this.sheet.name}!${match}`
          const cellValue = this.sheet.workbook.getValue(qualifiedRef)
          // Только строки оборачиваем в кавычки, числа и булевы значения - нет
          if (typeof cellValue === 'string') {
            return `"${cellValue}"`
          } else if (
            typeof cellValue === 'number' ||
            typeof cellValue === 'boolean'
          ) {
            return String(cellValue)
          } else {
            return '0'
          }
        } catch (error) {
          return '"#REF!"'
        }
      })

      // Заменяем функции Excel
      const functions = this.sheet.workbook.getFunctions()

      // Сортируем функции по длине имени (от длинных к коротким),
      // чтобы ЕСЛИ заменилось раньше И, и не было частичных замен
      const sortedFuncNames = Object.keys(functions).sort(
        (a, b) => b.length - a.length
      )

      for (const funcName of sortedFuncNames) {
        // Экранируем специальные символы в названии функции для регулярного выражения
        const escapedFuncName = funcName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
        // Используем юникодные границы: перед функцией не должно быть буквы/цифры (включая кириллицу)
        const funcRegex = new RegExp(
          `(?:^|[^\\wА-Яа-яЁё])(${escapedFuncName})\\s*\\(`,
          'gi'
        )
        const before = code
        code = code.replace(funcRegex, (match, p1) => {
          // p1 - это название функции, нужно сохранить символ перед функцией если он есть
          const prefix = match.substring(0, match.indexOf(p1))
          return `${prefix}__functions__.${funcName}(`
        })

        if (before !== code && expr.includes('ТЕСТ')) {
          // console.log(`Replaced ${funcName}:`, before, '->', code)
        }
      }

      // Создаём контекст для выполнения
      const context = {
        __functions__: functions,
        wb: this.sheet.workbook,
      }

      // Выполняем код в безопасном контексте
      this.value = this.evaluateExpression(code, context)
      return this.value
    } catch (error) {
      console.error(`Error evaluating formula in ${this.qualifiedName}:`, error)
      this.value = '#ERROR!'
      return this.value
    }
  }

  private checkVolatileFunctions(expr: string): void {
    // Проверяем, содержит ли формула волатильные функции
    const functions = this.sheet.workbook.getFunctions()
    let hasVolatileFunction = false

    for (const funcName of Object.keys(functions)) {
      if (VOLATILE_FUNCTIONS.has(funcName)) {
        // Экранируем специальные символы для регулярного выражения
        const escapedFuncName = funcName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
        // Для кириллических символов используем более универсальное решение
        const funcRegex = new RegExp(
          `(?:^|[^\\w])${escapedFuncName}\\s*\\(`,
          'gi'
        )
        if (funcRegex.test(expr)) {
          hasVolatileFunction = true
          break
        }
      }
    }

    if (hasVolatileFunction && !this.isVolatile) {
      this.isVolatile = true
      this.sheet.workbook.addVolatileCell(this.qualifiedName)
    } else if (!hasVolatileFunction && this.isVolatile) {
      this.isVolatile = false
      this.sheet.workbook.removeVolatileCell(this.qualifiedName)
    }
  }

  private isFunction(ref: string): boolean {
    const functions = this.sheet.workbook.getFunctions()
    return Object.keys(functions).includes(ref.toUpperCase())
  }

  private evaluateExpression(expr: string, context: any): CellValue {
    try {
      // Создаём функцию с контекстом
      const func = new Function(...Object.keys(context), `return ${expr}`)

      const result = func(...Object.values(context))

      return result
    } catch (error) {
      throw new Error(`Invalid expression: ${expr}`)
    }
  }

  // Функция для развертывания диапазона в массив значений
  private expandRange(range: string, sheetName?: string): number[] {
    const parts = range.split(':')
    if (parts.length !== 2) return []

    const startCell = parts[0]
    const endCell = parts[1]

    const startCol = startCell.match(/[A-Z]+/)?.[0] || ''
    const startRow = parseInt(startCell.match(/[0-9]+/)?.[0] || '0')
    const endCol = endCell.match(/[A-Z]+/)?.[0] || ''
    const endRow = parseInt(endCell.match(/[0-9]+/)?.[0] || '0')

    const startColIndex = startCol.charCodeAt(0) - 65
    const endColIndex = endCol.charCodeAt(0) - 65

    const values: number[] = []

    for (let row = startRow; row <= endRow; row++) {
      for (let col = startColIndex; col <= endColIndex; col++) {
        const cellName = String.fromCharCode(65 + col) + row
        const qualifiedName = sheetName
          ? `${sheetName}!${cellName}`
          : `${this.sheet.name}!${cellName}`
        const cellValue = this.sheet.workbook.getValue(qualifiedName)
        const numValue =
          typeof cellValue === 'number'
            ? cellValue
            : parseFloat(String(cellValue)) || 0
        values.push(numValue)
      }
    }

    return values
  }
}

export class Sheet {
  name: string
  workbook: Workbook
  cells: Map<string, Cell> = new Map()

  constructor(name: string, workbook: Workbook) {
    this.name = name
    this.workbook = workbook
  }

  getCell(name: string): Cell {
    if (!this.cells.has(name)) {
      this.cells.set(name, new Cell(this, name))
    }
    return this.cells.get(name)!
  }

  setCell(name: string, raw: CellValue): void {
    const cell = this.getCell(name)
    cell.set(raw)
  }

  getCellValue(name: string): CellValue {
    return this.getCell(name).value
  }

  getAllCells(): Map<string, Cell> {
    return this.cells
  }

  toJSON(): Record<string, CellValue> {
    const result: Record<string, CellValue> = {}
    for (const [name, cell] of this.cells) {
      if (cell.raw !== null && cell.raw !== undefined && cell.raw !== '') {
        result[name] = cell.raw
      }
    }
    return result
  }

  fromJSON(data: Record<string, CellValue>): void {
    for (const [name, value] of Object.entries(data)) {
      this.setCell(name, value)
    }
  }
}

export class Workbook {
  sheets: Map<string, Sheet> = new Map()
  private _dirty: Set<string> = new Set()
  private _functions: Record<string, ExcelFunction> = {}

  constructor() {
    // Инициализируем функции через метод addFunction, чтобы обеспечить единообразие
    Object.entries(DEFAULT_FUNCTIONS).forEach(([name, func]) => {
      this.addFunction(name, func)
    })
  }
  private _volatileCells: Set<string> = new Set() // Список волатильных ячеек

  addSheet(name: string): Sheet {
    const sheet = new Sheet(name, this)
    this.sheets.set(name, sheet)
    return sheet
  }

  getSheet(name: string): Sheet | undefined {
    return this.sheets.get(name)
  }

  getCell(qualifiedName: string): Cell {
    const [sheetName, cellName] = qualifiedName.split('!')
    const sheet = this.sheets.get(sheetName)
    if (!sheet) {
      throw new Error(`Sheet "${sheetName}" not found`)
    }
    return sheet.getCell(cellName)
  }

  setCell(qualifiedName: string, raw: CellValue): void {
    const cell = this.getCell(qualifiedName)
    cell.set(raw)
  }

  markDirty(qualifiedName: string): void {
    const toVisit = new Set([qualifiedName])

    while (toVisit.size > 0) {
      const current = toVisit.values().next().value!
      toVisit.delete(current)

      if (!this._dirty.has(current)) {
        this._dirty.add(current)
        // Добавляем всех зависимых ячеек
        for (const dep of this.getCell(current).dependents) {
          toVisit.add(dep)
        }
      }
    }

    // Также помечаем все волатильные ячейки как "грязные"
    for (const volatileCell of this._volatileCells) {
      if (!this._dirty.has(volatileCell)) {
        this._dirty.add(volatileCell)
        // Добавляем всех зависимых от волатильных ячеек
        for (const dep of this.getCell(volatileCell).dependents) {
          toVisit.add(dep)
        }
      }
    }

    // Обрабатываем зависимых от волатильных ячеек
    while (toVisit.size > 0) {
      const current = toVisit.values().next().value!
      toVisit.delete(current)

      if (!this._dirty.has(current)) {
        this._dirty.add(current)
        // Добавляем всех зависимых ячеек
        for (const dep of this.getCell(current).dependents) {
          toVisit.add(dep)
        }
      }
    }
  }

  private topologicalSort(cells: Set<string>): string[] {
    const visited = new Set<string>()
    const visiting = new Set<string>()
    const result: string[] = []
    const hasCycle = new Set<string>()

    const visit = (cellName: string): boolean => {
      if (visiting.has(cellName)) {
        // Обнаружена циклическая зависимость
        hasCycle.add(cellName)
        return false
      }

      if (visited.has(cellName)) {
        return true
      }

      visiting.add(cellName)

      try {
        const cell = this.getCell(cellName)

        // Посещаем всех предшественников (ячейки, от которых зависит текущая)
        for (const precedent of cell.precedents) {
          if (cells.has(precedent)) {
            if (!visit(precedent)) {
              hasCycle.add(cellName)
            }
          }
        }
      } catch (error) {
        // Ячейка не существует или другая ошибка - игнорируем
      }

      visiting.delete(cellName)
      visited.add(cellName)
      result.push(cellName)

      return true
    }

    // Посещаем все ячейки
    for (const cellName of cells) {
      if (!visited.has(cellName)) {
        visit(cellName)
      }
    }

    // Если обнаружены циклы, выводим предупреждение
    if (hasCycle.size > 0) {
      console.warn(
        'Обнаружены циклические зависимости в ячейках:',
        Array.from(hasCycle)
      )
    }

    return result
  }

  private detectCycle(startCell: string): boolean {
    const visited = new Set<string>()
    const visiting = new Set<string>()

    const visit = (cellName: string): boolean => {
      if (visiting.has(cellName)) {
        return true // Цикл найден
      }

      if (visited.has(cellName)) {
        return false
      }

      visiting.add(cellName)

      try {
        const cell = this.getCell(cellName)
        for (const precedent of cell.precedents) {
          if (visit(precedent)) {
            return true
          }
        }
      } catch (error) {
        // Ячейка не существует - игнорируем
      }

      visiting.delete(cellName)
      visited.add(cellName)
      return false
    }

    return visit(startCell)
  }

  recalc(): void {
    if (this._dirty.size === 0) {
      return
    }

    // Выполняем топологическую сортировку для определения правильного порядка пересчета
    const sortedCells = this.topologicalSort(this._dirty)

    // Очищаем множество "грязных" ячеек
    this._dirty.clear()

    // Пересчитываем ячейки в правильном порядке
    for (const cellName of sortedCells) {
      try {
        const cell = this.getCell(cellName)

        // Проверяем на циклические зависимости перед вычислением
        if (this.detectCycle(cellName)) {
          console.error(
            `Циклическая зависимость обнаружена для ячейки ${cellName}`
          )
          cell.value = '#CYCLE!'
        } else {
          cell.evaluate()
        }
      } catch (error) {
        console.error(`Ошибка при пересчете ячейки ${cellName}:`, error)
        try {
          const cell = this.getCell(cellName)
          cell.value = '#ERROR!'
        } catch (getError) {
          // Ячейка не существует - игнорируем
        }
      }
    }
  }

  getValue(qualifiedName: string): CellValue {
    // Исправление: пересчитываем если ВООБЩЕ есть грязные ячейки,
    // а не только если конкретная запрашиваемая ячейка грязная
    if (this._dirty.size > 0) {
      this.recalc()
    }
    const value = this.getCell(qualifiedName).value

    return fixFloatingPointError(value)
  }

  // Управление волатильными ячейками
  addVolatileCell(qualifiedName: string): void {
    this._volatileCells.add(qualifiedName)
  }

  removeVolatileCell(qualifiedName: string): void {
    this._volatileCells.delete(qualifiedName)
  }

  getVolatileCells(): Set<string> {
    return new Set(this._volatileCells)
  }

  // Управление функциями
  addFunction(name: string, func: ExcelFunction): void {
    this._functions[name.toUpperCase()] = func

    // Если добавляем волатильную функцию, обновляем список
    if (VOLATILE_FUNCTIONS.has(name.toUpperCase())) {
      this.markAllVolatileCellsDirty()
    }
  }

  getFunctions(): Record<string, ExcelFunction> {
    return this._functions
  }

  // Помечаем все волатильные ячейки как "грязные"
  private markAllVolatileCellsDirty(): void {
    for (const volatileCell of this._volatileCells) {
      this.markDirty(volatileCell)
    }
  }

  // Сериализация
  toJSON(): Record<string, Record<string, CellValue>> {
    const result: Record<string, Record<string, CellValue>> = {}
    for (const [name, sheet] of this.sheets) {
      result[name] = sheet.toJSON()
    }
    return result
  }

  fromJSON(data: Record<string, Record<string, CellValue>>): void {
    for (const [sheetName, sheetData] of Object.entries(data)) {
      if (!this.sheets.has(sheetName)) {
        this.addSheet(sheetName)
      }
      this.sheets.get(sheetName)!.fromJSON(sheetData)
    }
  }

  // Очистка всех данных
  clear(): void {
    this.sheets.clear()
    this._dirty.clear()
    this._volatileCells.clear()
  }
}

// Утилиты для работы с координатами ячеек
export class CellUtils {
  static indexToColumn(index: number): string {
    let result = ''
    while (index >= 0) {
      result = String.fromCharCode(65 + (index % 26)) + result
      index = Math.floor(index / 26) - 1
    }
    return result
  }

  static createCellName(column: string | number, row: number): string {
    const col =
      typeof column === 'number' ? CellUtils.indexToColumn(column) : column
    return `${col}${row}`
  }
}

// Экспорт для удобства использования
export { DEFAULT_FUNCTIONS, VOLATILE_FUNCTIONS }
