import { readBeacons } from '../../beacon/api/beaconsService'
import type { BeaconVibrationSample } from '../../beacon/model/types'
import { mockBeaconVibrationSamples } from '../../beacon/mock/mockBeaconVibrationSamples'
import { mockMachineCalibrationRules } from '../mock/mockMachineCalibrationRules'
import { mockMachineStateDefinitions } from '../mock/mockMachineStateDefinitions'
import type {
  MachineCalibrationPeriod,
  MachineCalibrationRule,
  MachineStateDefinition,
  RecognizedMachineStateInterval,
} from '../model/types'

export type MachineStateDefinitionPayload = {
  name: string
  icon: string
  color: string
  description: string
}

export type MachineCalibrationRulePayload = {
  stateDefinitionId: number
  vibrationMin: number
  vibrationMax: number
  minDurationSeconds?: number
  priority?: number
}

type PeriodRange = {
  start: number
  end: number
}

type DraftRecognizedInterval = Omit<RecognizedMachineStateInterval, 'id'>

const STATE_DEFINITIONS_STORAGE_KEY =
  'fleet-ble-monitor.machineStateDefinitions'
const CALIBRATION_RULES_STORAGE_KEY =
  'fleet-ble-monitor.machineCalibrationRules'
const aggregateRuleGap = 0.02
const noDataDefinitionId = 4
const noDataGapMs = 30 * 60 * 1000
const mockMachineVibrationMacByMachineId: Record<number, string> = {
  11: 'E4:5F:01:AA:10:10',
}

function cloneStateDefinition(
  definition: MachineStateDefinition,
): MachineStateDefinition {
  return { ...definition }
}

function cloneCalibrationRule(
  rule: MachineCalibrationRule,
): MachineCalibrationRule {
  return { ...rule }
}

function cloneVibrationSample(
  sample: BeaconVibrationSample,
): BeaconVibrationSample {
  return { ...sample }
}

function cloneRecognizedInterval(
  interval: RecognizedMachineStateInterval,
): RecognizedMachineStateInterval {
  return { ...interval }
}

function readStateDefinitions(): MachineStateDefinition[] {
  if (typeof window === 'undefined') {
    return mockMachineStateDefinitions.map(cloneStateDefinition)
  }

  const storedDefinitions = window.localStorage.getItem(
    STATE_DEFINITIONS_STORAGE_KEY,
  )

  if (!storedDefinitions) {
    const initialDefinitions = mockMachineStateDefinitions.map(cloneStateDefinition)
    window.localStorage.setItem(
      STATE_DEFINITIONS_STORAGE_KEY,
      JSON.stringify(initialDefinitions),
    )

    return initialDefinitions
  }

  try {
    return (JSON.parse(storedDefinitions) as MachineStateDefinition[])
      .map(cloneStateDefinition)
  } catch {
    const initialDefinitions = mockMachineStateDefinitions.map(cloneStateDefinition)
    window.localStorage.setItem(
      STATE_DEFINITIONS_STORAGE_KEY,
      JSON.stringify(initialDefinitions),
    )

    return initialDefinitions
  }
}

function writeStateDefinitions(definitions: MachineStateDefinition[]) {
  if (typeof window === 'undefined') return

  window.localStorage.setItem(
    STATE_DEFINITIONS_STORAGE_KEY,
    JSON.stringify(definitions),
  )
  window.dispatchEvent(new Event('machine-calibration:change'))
}

function readCalibrationRules(): MachineCalibrationRule[] {
  if (typeof window === 'undefined') {
    return cloneCalibrationRulesWithAggregation(mockMachineCalibrationRules)
  }

  const storedRules = window.localStorage.getItem(CALIBRATION_RULES_STORAGE_KEY)

  if (!storedRules) {
    const initialRules = mockMachineCalibrationRules.map(cloneCalibrationRule)
    window.localStorage.setItem(
      CALIBRATION_RULES_STORAGE_KEY,
      JSON.stringify(initialRules),
    )

    return cloneCalibrationRulesWithAggregation(initialRules)
  }

  try {
    return cloneCalibrationRulesWithAggregation(
      JSON.parse(storedRules) as MachineCalibrationRule[],
    )
  } catch {
    const initialRules = mockMachineCalibrationRules.map(cloneCalibrationRule)
    window.localStorage.setItem(
      CALIBRATION_RULES_STORAGE_KEY,
      JSON.stringify(initialRules),
    )

    return cloneCalibrationRulesWithAggregation(initialRules)
  }
}

function writeCalibrationRules(rules: MachineCalibrationRule[]) {
  if (typeof window === 'undefined') return

  window.localStorage.setItem(
    CALIBRATION_RULES_STORAGE_KEY,
    JSON.stringify(rules),
  )
  window.dispatchEvent(new Event('machine-calibration:change'))
}

function normalizeStateDefinitionPayload(payload: MachineStateDefinitionPayload) {
  return {
    name: payload.name.trim(),
    icon: payload.icon.trim() || 'Activity',
    color: payload.color.trim() || '#2f80ed',
    description: payload.description.trim(),
  }
}

function normalizeCalibrationRulePayload(
  payload: MachineCalibrationRulePayload,
  fallback?: Pick<MachineCalibrationRule, 'minDurationSeconds' | 'priority'>,
) {
  const vibrationMin = Number(payload.vibrationMin)
  const vibrationMax = Number(payload.vibrationMax)

  return {
    stateDefinitionId: Number(payload.stateDefinitionId),
    vibrationMin: Math.min(vibrationMin, vibrationMax),
    vibrationMax: Math.max(vibrationMin, vibrationMax),
    minDurationSeconds: Math.max(
      0,
      Number(payload.minDurationSeconds ?? fallback?.minDurationSeconds ?? 0),
    ),
    priority: Math.max(0, Number(payload.priority ?? fallback?.priority ?? 0)),
  }
}

function canAggregateRules(
  first: Pick<MachineCalibrationRule, 'vibrationMin' | 'vibrationMax'>,
  second: Pick<MachineCalibrationRule, 'vibrationMin' | 'vibrationMax'>,
) {
  return (
    first.vibrationMin <= second.vibrationMax + aggregateRuleGap &&
    second.vibrationMin <= first.vibrationMax + aggregateRuleGap
  )
}

function hasRuleRangeIntersection(
  first: Pick<MachineCalibrationRule, 'vibrationMin' | 'vibrationMax'>,
  second: Pick<MachineCalibrationRule, 'vibrationMin' | 'vibrationMax'>,
) {
  return (
    first.vibrationMin <= second.vibrationMax &&
    second.vibrationMin <= first.vibrationMax
  )
}

function findConflictingCalibrationRule(
  rule: MachineCalibrationRule,
  rules: MachineCalibrationRule[],
) {
  return rules.find((item) => (
    item.id !== rule.id &&
    item.machineId === rule.machineId &&
    item.stateDefinitionId !== rule.stateDefinitionId &&
    hasRuleRangeIntersection(item, rule)
  )) ?? null
}

function assertCalibrationRuleCanBeSaved(
  rule: MachineCalibrationRule,
  rules: MachineCalibrationRule[],
) {
  if (rule.stateDefinitionId === noDataDefinitionId) {
    throw new Error('Состояние "Нет данных" назначается автоматически')
  }

  const conflictRule = findConflictingCalibrationRule(rule, rules)

  if (conflictRule) {
    throw new Error(
      `Диапазон пересекается с другим состоянием: ${conflictRule.vibrationMin.toFixed(3)} — ${conflictRule.vibrationMax.toFixed(3)}`,
    )
  }
}

function aggregateCalibrationRule(
  baseRule: MachineCalibrationRule,
  rules: MachineCalibrationRule[],
) {
  let aggregatedRule = { ...baseRule }
  let restRules = rules.filter((rule) => rule.id !== aggregatedRule.id)
  let hasAggregatedRule = true

  while (hasAggregatedRule) {
    hasAggregatedRule = false

    restRules = restRules.filter((rule) => {
      if (
        rule.machineId !== aggregatedRule.machineId ||
        rule.stateDefinitionId !== aggregatedRule.stateDefinitionId ||
        !canAggregateRules(rule, aggregatedRule)
      ) {
        return true
      }

      aggregatedRule = {
        ...aggregatedRule,
        createdAt: rule.createdAt < aggregatedRule.createdAt
          ? rule.createdAt
          : aggregatedRule.createdAt,
        minDurationSeconds: Math.min(
          aggregatedRule.minDurationSeconds,
          rule.minDurationSeconds,
        ),
        priority: Math.min(aggregatedRule.priority, rule.priority),
        updatedAt: new Date().toISOString(),
        vibrationMax: Math.max(aggregatedRule.vibrationMax, rule.vibrationMax),
        vibrationMin: Math.min(aggregatedRule.vibrationMin, rule.vibrationMin),
      }
      hasAggregatedRule = true

      return false
    })
  }

  return {
    rule: aggregatedRule,
    rules: [...restRules, aggregatedRule],
  }
}

function aggregateCalibrationRulesList(rules: MachineCalibrationRule[]) {
  return [...rules]
    .sort((first, second) => (
      first.machineId - second.machineId ||
      first.stateDefinitionId - second.stateDefinitionId ||
      first.vibrationMin - second.vibrationMin ||
      first.priority - second.priority
    ))
    .reduce<MachineCalibrationRule[]>((result, rule) => {
      const aggregateIndex = result.findIndex((item) => (
        item.machineId === rule.machineId &&
        item.stateDefinitionId === rule.stateDefinitionId &&
        canAggregateRules(item, rule)
      ))

      if (aggregateIndex === -1) {
        result.push({ ...rule })
        return result
      }

      const aggregateRule = result[aggregateIndex]

      result[aggregateIndex] = {
        ...aggregateRule,
        createdAt: rule.createdAt < aggregateRule.createdAt
          ? rule.createdAt
          : aggregateRule.createdAt,
        minDurationSeconds: Math.min(
          aggregateRule.minDurationSeconds,
          rule.minDurationSeconds,
        ),
        priority: Math.min(aggregateRule.priority, rule.priority),
        updatedAt: rule.updatedAt > aggregateRule.updatedAt
          ? rule.updatedAt
          : aggregateRule.updatedAt,
        vibrationMax: Math.max(aggregateRule.vibrationMax, rule.vibrationMax),
        vibrationMin: Math.min(aggregateRule.vibrationMin, rule.vibrationMin),
      }

      return result
    }, [])
}

function cloneCalibrationRulesWithAggregation(rules: MachineCalibrationRule[]) {
  return aggregateCalibrationRulesList(rules).map(cloneCalibrationRule)
}

function getMachineMacAddress(machineId: number) {
  return readBeacons().find((beacon) => beacon.boundMachineId === machineId)
    ?.macAddress ?? mockMachineVibrationMacByMachineId[machineId] ?? null
}

function getPeriodRange(
  period: MachineCalibrationPeriod,
): PeriodRange {
  return {
    start: period.periodStart.getTime(),
    end: period.periodEnd.getTime(),
  }
}

function getSamplesForMachine(
  machineId: number,
  period: MachineCalibrationPeriod,
) {
  const macAddress = getMachineMacAddress(machineId)

  if (!macAddress) return []

  const range = getPeriodRange(period)

  return mockBeaconVibrationSamples
    .filter((sample) => (
      sample.mac === macAddress &&
      sample.timeMillis >= range.start &&
      sample.timeMillis <= range.end
    ))
    .sort((first, second) => first.timeMillis - second.timeMillis)
}

function findMatchingRule(
  sample: BeaconVibrationSample,
  rules: MachineCalibrationRule[],
) {
  return [...rules]
    .sort((first, second) => first.priority - second.priority)
    .find((rule) => (
      sample.vibration >= rule.vibrationMin &&
      sample.vibration <= rule.vibrationMax
    )) ?? null
}

function createSampleInterval(
  machineId: number,
  sample: BeaconVibrationSample,
  rule: MachineCalibrationRule | null,
): DraftRecognizedInterval {
  return {
    machineId,
    stateDefinitionId: rule?.stateDefinitionId ?? null,
    startedAt: new Date(sample.timeMillis).toISOString(),
    endedAt: new Date(sample.timeMillis + sample.duration * 1000).toISOString(),
    durationSeconds: sample.duration,
    vibrationMin: sample.vibration,
    vibrationMax: sample.vibration,
    vibrationAvg: sample.vibration,
    sampleCount: 1,
    isNoData: false,
  }
}

function createNoDataInterval(
  machineId: number,
  startedAt: number,
  endedAt: number,
): DraftRecognizedInterval {
  return {
    machineId,
    stateDefinitionId: noDataDefinitionId,
    startedAt: new Date(startedAt).toISOString(),
    endedAt: new Date(endedAt).toISOString(),
    durationSeconds: Math.floor((endedAt - startedAt) / 1000),
    vibrationMin: null,
    vibrationMax: null,
    vibrationAvg: null,
    sampleCount: 0,
    isNoData: true,
  }
}

function canMergeIntervals(
  current: DraftRecognizedInterval,
  next: DraftRecognizedInterval,
) {
  const currentEnd = new Date(current.endedAt).getTime()
  const nextStart = new Date(next.startedAt).getTime()

  return (
    current.stateDefinitionId === next.stateDefinitionId &&
    current.isNoData === next.isNoData &&
    nextStart - currentEnd <= noDataGapMs
  )
}

function mergeIntervals(
  current: DraftRecognizedInterval,
  next: DraftRecognizedInterval,
): DraftRecognizedInterval {
  const totalSamples = current.sampleCount + next.sampleCount
  const currentWeighted = (current.vibrationAvg ?? 0) * current.sampleCount
  const nextWeighted = (next.vibrationAvg ?? 0) * next.sampleCount

  return {
    ...current,
    endedAt: next.endedAt,
    durationSeconds: Math.floor((
      new Date(next.endedAt).getTime() -
      new Date(current.startedAt).getTime()
    ) / 1000),
    vibrationMin:
      current.vibrationMin === null || next.vibrationMin === null
        ? current.vibrationMin ?? next.vibrationMin
        : Math.min(current.vibrationMin, next.vibrationMin),
    vibrationMax:
      current.vibrationMax === null || next.vibrationMax === null
        ? current.vibrationMax ?? next.vibrationMax
        : Math.max(current.vibrationMax, next.vibrationMax),
    vibrationAvg: totalSamples > 0
      ? Number(((currentWeighted + nextWeighted) / totalSamples).toFixed(3))
      : null,
    sampleCount: totalSamples,
  }
}

function buildRecognizedIntervals(
  machineId: number,
  samples: BeaconVibrationSample[],
  rules: MachineCalibrationRule[],
) {
  const intervals: DraftRecognizedInterval[] = []

  samples.forEach((sample, index) => {
    if (index > 0) {
      const previousSample = samples[index - 1]
      const previousEnd = previousSample.timeMillis + previousSample.duration * 1000
      const gap = sample.timeMillis - previousEnd

      if (gap > noDataGapMs) {
        intervals.push(createNoDataInterval(machineId, previousEnd, sample.timeMillis))
      }
    }

    intervals.push(createSampleInterval(
      machineId,
      sample,
      findMatchingRule(sample, rules),
    ))
  })

  return intervals.reduce<RecognizedMachineStateInterval[]>(
    (result, interval) => {
      const previous = result.at(-1)

      if (previous && canMergeIntervals(previous, interval)) {
        result[result.length - 1] = {
          ...mergeIntervals(previous, interval),
          id: previous.id,
        }
        return result
      }

      result.push({
        ...interval,
        id: result.length + 1,
      })

      return result
    },
    [],
  )
}

export const machineCalibrationService = {
  async getMachineStateDefinitions(): Promise<MachineStateDefinition[]> {
    return readStateDefinitions().map(cloneStateDefinition)
  },

  async getMachineStateDefinitionsSnapshot(): Promise<MachineStateDefinition[]> {
    return readStateDefinitions().map(cloneStateDefinition)
  },

  async restoreMachineStateDefinitions(
    definitions: MachineStateDefinition[],
  ): Promise<MachineStateDefinition[]> {
    const nextDefinitions = definitions.map(cloneStateDefinition)

    writeStateDefinitions(nextDefinitions)

    return nextDefinitions.map(cloneStateDefinition)
  },

  async createMachineStateDefinition(
    payload: MachineStateDefinitionPayload,
  ): Promise<MachineStateDefinition> {
    const definitions = readStateDefinitions()
    const now = new Date().toISOString()
    const normalizedPayload = normalizeStateDefinitionPayload(payload)

    if (!normalizedPayload.name) {
      throw new Error('Название состояния обязательно')
    }

    const definition: MachineStateDefinition = {
      id: Math.max(0, ...definitions.map((item) => item.id)) + 1,
      ...normalizedPayload,
      isSystem: false,
      createdAt: now,
      updatedAt: now,
    }

    writeStateDefinitions([...definitions, definition])

    return cloneStateDefinition(definition)
  },

  async updateMachineStateDefinition(
    id: number | string,
    payload: MachineStateDefinitionPayload,
  ): Promise<MachineStateDefinition> {
    const numericId = Number(id)
    const definitions = readStateDefinitions()
    const definitionIndex = definitions.findIndex((item) => item.id === numericId)
    const normalizedPayload = normalizeStateDefinitionPayload(payload)

    if (!normalizedPayload.name) {
      throw new Error('Название состояния обязательно')
    }

    if (definitionIndex === -1) {
      throw new Error('Состояние не найдено')
    }

    if (numericId === noDataDefinitionId) {
      throw new Error('Системное состояние "Нет данных" нельзя изменить')
    }

    const nextDefinition: MachineStateDefinition = {
      ...definitions[definitionIndex],
      ...normalizedPayload,
      updatedAt: new Date().toISOString(),
    }
    const nextDefinitions = [...definitions]
    nextDefinitions[definitionIndex] = nextDefinition

    writeStateDefinitions(nextDefinitions)

    return cloneStateDefinition(nextDefinition)
  },

  async getMachineCalibrationRules(
    machineId: number | string,
  ): Promise<MachineCalibrationRule[]> {
    const numericMachineId = Number(machineId)

    return readCalibrationRules()
      .filter((rule) => rule.machineId === numericMachineId)
      .sort((first, second) => first.priority - second.priority)
      .map(cloneCalibrationRule)
  },

  async getCalibrationRulesSnapshot(): Promise<MachineCalibrationRule[]> {
    return readCalibrationRules().map(cloneCalibrationRule)
  },

  async restoreCalibrationRules(
    rules: MachineCalibrationRule[],
  ): Promise<MachineCalibrationRule[]> {
    const nextRules = rules.map(cloneCalibrationRule)

    writeCalibrationRules(nextRules)

    return nextRules.map(cloneCalibrationRule)
  },

  async createMachineCalibrationRule(
    machineId: number | string,
    payload: MachineCalibrationRulePayload,
  ): Promise<MachineCalibrationRule> {
    const numericMachineId = Number(machineId)
    const rules = readCalibrationRules()
    const nextPriority = rules
      .filter((rule) => rule.machineId === numericMachineId)
      .reduce((maxPriority, rule) => Math.max(maxPriority, rule.priority), 10) + 10
    const normalizedPayload = normalizeCalibrationRulePayload(payload, {
      minDurationSeconds: 0,
      priority: nextPriority,
    })
    const now = new Date().toISOString()

    const rule: MachineCalibrationRule = {
      id: Math.max(0, ...rules.map((item) => item.id)) + 1,
      machineId: numericMachineId,
      ...normalizedPayload,
      createdAt: now,
      updatedAt: now,
    }
    const aggregation = aggregateCalibrationRule(rule, rules)

    assertCalibrationRuleCanBeSaved(aggregation.rule, aggregation.rules)

    writeCalibrationRules(aggregation.rules)

    return cloneCalibrationRule(aggregation.rule)
  },

  async updateMachineCalibrationRule(
    id: number | string,
    payload: MachineCalibrationRulePayload,
  ): Promise<MachineCalibrationRule> {
    const numericId = Number(id)
    const rules = readCalibrationRules()
    const ruleIndex = rules.findIndex((item) => item.id === numericId)

    if (ruleIndex === -1) {
      throw new Error('Правило калибровки не найдено')
    }

    const normalizedPayload = normalizeCalibrationRulePayload(
      payload,
      rules[ruleIndex],
    )

    const nextRule: MachineCalibrationRule = {
      ...rules[ruleIndex],
      ...normalizedPayload,
      updatedAt: new Date().toISOString(),
    }
    const nextRules = [...rules]
    nextRules[ruleIndex] = nextRule
    const aggregation = aggregateCalibrationRule(nextRule, nextRules)

    assertCalibrationRuleCanBeSaved(aggregation.rule, aggregation.rules)

    writeCalibrationRules(aggregation.rules)

    return cloneCalibrationRule(aggregation.rule)
  },

  async deleteMachineCalibrationRule(id: number | string): Promise<boolean> {
    const numericId = Number(id)
    const rules = readCalibrationRules()
    const nextRules = rules.filter((rule) => rule.id !== numericId)

    if (nextRules.length === rules.length) {
      throw new Error('Правило калибровки не найдено')
    }

    writeCalibrationRules(nextRules)

    return true
  },

  async getMachineVibrationSamples(
    machineId: number | string,
    period: MachineCalibrationPeriod,
  ): Promise<BeaconVibrationSample[]> {
    return getSamplesForMachine(Number(machineId), period)
      .map(cloneVibrationSample)
  },

  async getRecognizedStateIntervals(
    machineId: number | string,
    period: MachineCalibrationPeriod,
  ): Promise<RecognizedMachineStateInterval[]> {
    const numericMachineId = Number(machineId)
    const [samples, rules] = await Promise.all([
      this.getMachineVibrationSamples(numericMachineId, period),
      this.getMachineCalibrationRules(numericMachineId),
    ])

    return buildRecognizedIntervals(numericMachineId, samples, rules)
      .map(cloneRecognizedInterval)
  },
}
