import { computed, markRaw, reactive, ref, shallowRef, watch } from 'vue'
import { defineStore } from 'pinia'
import type {
  ColorGroup,
  MosaicRegion,
  MosaicSettings,
  ProjectFile,
  ToastMessage,
} from '@/types/mosaic'
import { floodFillRegion, getFloodFillReferenceColor } from '@/utils/floodFill'
import { groupRegionsByColor, refreshGroupColors, sortGroupsByLightness } from '@/utils/colorGroups'
import { colorDistance, rgbToLab } from '@/utils/colors'
import {
  buildOccupancy,
  createRegionMask,
  maskContains,
  subtractRegionMask,
  summarizeRegionMask,
} from '@/utils/regionMask'
import { createProjectFile, isProjectFile } from '@/utils/projectExport'
import { clearLastProject, loadLastProject, saveLastProject } from '@/utils/projectStorage'

interface HistorySnapshot {
  regions: MosaicRegion[]
  groups: ColorGroup[]
}

const DEFAULT_SETTINGS: MosaicSettings = {
  floodTolerance: 34,
  colorTolerance: 12,
  darkBoundaryThreshold: 0.045,
  connectivity: 4,
  exportOverlay: false,
  otherGroupsMode: 'dim',
  useUserClickPositionForLabels: false,
}

const cloneRegions = (regions: MosaicRegion[]): MosaicRegion[] => regions.map((region) => markRaw({
  ...region,
  color: { ...region.color },
  labColor: { ...region.labColor },
  boundingBox: { ...region.boundingBox },
  labelPosition: { ...region.labelPosition },
  userClickPosition: region.userClickPosition ? { ...region.userClickPosition } : undefined,
  // Masks are immutable; sharing their typed arrays keeps the 30-step history compact.
  mask: region.mask,
}))

const cloneGroups = (groups: ColorGroup[]): ColorGroup[] => groups.map((group) => ({
  ...group,
  color: { ...group.color },
  labColor: { ...group.labColor },
  regionIds: [...group.regionIds],
}))

export const useMosaicStore = defineStore('mosaic', () => {
  const imageCanvas = shallowRef<HTMLCanvasElement | null>(null)
  const imageData = shallowRef<ImageData | null>(null)
  const imageBlob = shallowRef<Blob | null>(null)
  const imageName = ref('')
  const imageWidth = ref(0)
  const imageHeight = ref(0)
  const regions = shallowRef<MosaicRegion[]>([])
  const groups = ref<ColorGroup[]>([])
  const selectedGroupId = ref<string | null>(null)
  const selectedRegionId = ref<string | null>(null)
  const settings = reactive<MosaicSettings>({ ...DEFAULT_SETTINGS })
  const toasts = ref<ToastMessage[]>([])
  const isAnalyzing = ref(false)
  const hasRecovery = ref(false)
  const recoveryTimestamp = ref('')
  const undoStack = shallowRef<HistorySnapshot[]>([])
  const redoStack = shallowRef<HistorySnapshot[]>([])
  let recoveryProject: ProjectFile | null = null
  let recoveryBlob: Blob | null = null
  let autosaveTimer = 0

  const hasImage = computed(() => Boolean(imageCanvas.value && imageData.value))
  const canUndo = computed(() => undoStack.value.length > 0)
  const canRedo = computed(() => redoStack.value.length > 0)

  function notify(text: string, type: ToastMessage['type'] = 'info'): void {
    const id = crypto.randomUUID()
    toasts.value.push({ id, text, type })
    window.setTimeout(() => dismissToast(id), 5000)
  }

  function dismissToast(id: string): void {
    toasts.value = toasts.value.filter((toast) => toast.id !== id)
  }

  function snapshot(): HistorySnapshot {
    return { regions: cloneRegions(regions.value), groups: cloneGroups(groups.value) }
  }

  function recordHistory(): void {
    undoStack.value = [...undoStack.value.slice(-29), snapshot()]
    redoStack.value = []
  }

  function restoreSnapshot(value: HistorySnapshot): void {
    regions.value = cloneRegions(value.regions)
    groups.value = cloneGroups(value.groups)
    selectedGroupId.value = null
    selectedRegionId.value = null
    scheduleAutosave()
  }

  function undo(): void {
    const previous = undoStack.value.at(-1)
    if (!previous) return
    redoStack.value = [...redoStack.value, snapshot()].slice(-30)
    undoStack.value = undoStack.value.slice(0, -1)
    restoreSnapshot(previous)
  }

  function redo(): void {
    const next = redoStack.value.at(-1)
    if (!next) return
    undoStack.value = [...undoStack.value, snapshot()].slice(-30)
    redoStack.value = redoStack.value.slice(0, -1)
    restoreSnapshot(next)
  }

  async function decodeImage(blob: Blob): Promise<HTMLCanvasElement> {
    const bitmap = await createImageBitmap(blob)
    if (bitmap.width * bitmap.height > 80_000_000) {
      bitmap.close()
      throw new Error('Obrázok je príliš veľký (limit je 80 miliónov pixelov). Skúste menšiu verziu.')
    }
    const canvas = document.createElement('canvas')
    canvas.width = bitmap.width
    canvas.height = bitmap.height
    const context = canvas.getContext('2d', { willReadFrequently: true })
    if (!context) throw new Error('Prehliadač nepodporuje Canvas 2D.')
    context.drawImage(bitmap, 0, 0)
    bitmap.close()
    return canvas
  }

  async function loadImage(file: Blob, filename = 'obrazok'): Promise<void> {
    if (file instanceof File && !['image/png', 'image/jpeg', 'image/webp'].includes(file.type)) {
      notify('Podporované sú iba obrázky PNG, JPEG a WebP.', 'error')
      return
    }
    try {
      const canvas = await decodeImage(file)
      const context = canvas.getContext('2d', { willReadFrequently: true })
      if (!context) throw new Error('Prehliadač nepodporuje Canvas 2D.')
      const pendingImportedProject = regions.value.length > 0 && !imageCanvas.value
      if (pendingImportedProject && (imageWidth.value !== canvas.width || imageHeight.value !== canvas.height)) {
        notify(`Projekt očakáva obrázok ${imageWidth.value} × ${imageHeight.value} px. Vybraný obrázok má ${canvas.width} × ${canvas.height} px.`, 'error')
        return
      }
      const replacingMatchingImage = pendingImportedProject
      imageCanvas.value = markRaw(canvas)
      imageData.value = markRaw(context.getImageData(0, 0, canvas.width, canvas.height))
      imageBlob.value = markRaw(file)
      imageName.value = filename
      imageWidth.value = canvas.width
      imageHeight.value = canvas.height
      if (!replacingMatchingImage) {
        regions.value = []
        groups.value = []
        undoStack.value = []
        redoStack.value = []
      }
      if (canvas.width * canvas.height > 24_000_000) {
        notify('Obrázok je veľmi veľký. Analýza rozsiahlych plôch môže chvíľu trvať.', 'warning')
      } else {
        notify('Obrázok bol načítaný iba lokálne v prehliadači.', 'success')
      }
      scheduleAutosave()
    } catch (error) {
      notify(error instanceof Error ? error.message : 'Obrázok je poškodený alebo ho nemožno načítať.', 'error')
    }
  }

  function addRegionAt(x: number, y: number): void {
    if (!imageData.value || isAnalyzing.value) return
    const floodTolerance = settings.floodTolerance
    const minimumRegionPixelCount = floodTolerance <= 1 ? 1 : 12
    const referenceColor = getFloodFillReferenceColor(imageData.value, x, y, floodTolerance)
    const existingRegion = regions.value.find((region) => maskContains(region.mask, x, y))
    if (existingRegion && colorDistance(existingRegion.color, referenceColor) <= floodTolerance) {
      notify('Táto plocha už bola označená.', 'warning')
      return
    }
    const occupancy = buildOccupancy(
      imageWidth.value,
      imageHeight.value,
      regions.value.filter((region) => region.id !== existingRegion?.id).map((region) => region.mask),
    )
    isAnalyzing.value = true
    window.setTimeout(() => {
      try {
        const result = floodFillRegion(imageData.value as ImageData, x, y, {
          tolerance: floodTolerance,
          darkBoundaryThreshold: settings.darkBoundaryThreshold,
          connectivity: settings.connectivity,
          occupancy,
        })
        if (!result || result.pixelCount < minimumRegionPixelCount) {
          notify('Nájdená plocha je príliš malá. Skúste iný bod alebo vyššiu toleranciu.', 'warning')
          return
        }
        if (result.pixelCount > imageWidth.value * imageHeight.value * 0.92) {
          notify('Výber pokrýva takmer celý obrázok. Znížte toleranciu alebo kliknite dovnútra ohraničenej plochy.', 'warning')
          return
        }
        recordHistory()
        const region: MosaicRegion = markRaw({
          id: crypto.randomUUID(),
          color: result.color,
          labColor: rgbToLab(result.color),
          pixelCount: result.pixelCount,
          boundingBox: result.boundingBox,
          labelPosition: result.labelPosition,
          userClickPosition: { x, y },
          groupId: '',
          mask: result.mask,
        })
        const nextRegions = regions.value.flatMap((item) => {
          if (item.id !== existingRegion?.id) return [item]
          const remainingMask = subtractRegionMask(item.mask, result.mask)
          const summary = remainingMask ? summarizeRegionMask(remainingMask) : null
          if (!remainingMask || !summary || summary.pixelCount < minimumRegionPixelCount) return []
          return [markRaw({
            ...item,
            mask: remainingMask,
            pixelCount: summary.pixelCount,
            boundingBox: summary.boundingBox,
            labelPosition: summary.labelPosition,
            userClickPosition: item.userClickPosition && maskContains(remainingMask, item.userClickPosition.x, item.userClickPosition.y)
              ? item.userClickPosition
              : undefined,
          })]
        })
        regions.value = [...nextRegions, region]
        regroup(false)
        selectedRegionId.value = region.id
        selectedGroupId.value = regions.value.find((item) => item.id === region.id)?.groupId ?? null
        scheduleAutosave()
      } finally {
        isAnalyzing.value = false
      }
    }, 20)
  }

  function regroup(withHistory = true): void {
    if (withHistory && regions.value.length) recordHistory()
    groups.value = groupRegionsByColor(regions.value, settings.colorTolerance)
    const regionToGroup = new Map(groups.value.flatMap((group) => group.regionIds.map((id) => [id, group.id])))
    regions.value = regions.value.map((region) => markRaw({ ...region, groupId: regionToGroup.get(region.id) ?? '' }))
    selectedGroupId.value = null
    scheduleAutosave()
  }

  function setColorTolerance(value: number): void {
    settings.colorTolerance = value
    regroup()
  }

  function removeRegion(id: string): void {
    if (!regions.value.some((region) => region.id === id)) return
    recordHistory()
    regions.value = regions.value.filter((region) => region.id !== id)
    groups.value = refreshGroupColors(groups.value.map((group) => ({ ...group, regionIds: group.regionIds.filter((regionId) => regionId !== id) })), regions.value)
    selectedRegionId.value = null
    scheduleAutosave()
  }

  function moveRegion(regionId: string, targetGroupId: string): void {
    const region = regions.value.find((item) => item.id === regionId)
    if (!region || region.groupId === targetGroupId || !groups.value.some((group) => group.id === targetGroupId)) return
    recordHistory()
    regions.value = regions.value.map((item) => item.id === regionId ? markRaw({ ...item, groupId: targetGroupId }) : item)
    groups.value = refreshGroupColors(groups.value.map((group) => ({
      ...group,
      regionIds: group.id === targetGroupId
        ? [...group.regionIds, regionId]
        : group.regionIds.filter((id) => id !== regionId),
    })), regions.value)
    scheduleAutosave()
  }

  function splitRegion(regionId: string): void {
    const region = regions.value.find((item) => item.id === regionId)
    const oldGroup = groups.value.find((group) => group.id === region?.groupId)
    if (!region || !oldGroup || oldGroup.regionIds.length <= 1) return
    recordHistory()
    const newGroup: ColorGroup = {
      id: crypto.randomUUID(), number: groups.value.length + 1, color: { ...region.color },
      labColor: { ...region.labColor }, regionIds: [region.id],
    }
    regions.value = regions.value.map((item) => item.id === region.id ? markRaw({ ...item, groupId: newGroup.id }) : item)
    groups.value = refreshGroupColors([...groups.value.map((group) => ({ ...group, regionIds: group.regionIds.filter((id) => id !== region.id) })), newGroup], regions.value)
    selectedGroupId.value = newGroup.id
    scheduleAutosave()
  }

  function mergeGroups(sourceId: string, targetId: string): void {
    if (sourceId === targetId) return
    const source = groups.value.find((group) => group.id === sourceId)
    const target = groups.value.find((group) => group.id === targetId)
    if (!source || !target) return
    recordHistory()
    regions.value = regions.value.map((region) => source.regionIds.includes(region.id) ? markRaw({ ...region, groupId: targetId }) : region)
    groups.value = refreshGroupColors(groups.value
      .filter((group) => group.id !== sourceId)
      .map((group) => group.id === targetId ? { ...group, regionIds: [...group.regionIds, ...source.regionIds] } : group), regions.value)
    selectedGroupId.value = targetId
    scheduleAutosave()
  }

  function setGroupColor(groupId: string, hex: string): void {
    const match = /^#([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i.exec(hex)
    if (!match) return
    recordHistory()
    const color = { r: Number.parseInt(match[1] ?? '0', 16), g: Number.parseInt(match[2] ?? '0', 16), b: Number.parseInt(match[3] ?? '0', 16) }
    groups.value = sortGroupsByLightness(groups.value.map((group) => group.id === groupId ? { ...group, color, labColor: rgbToLab(color), customColor: true } : group))
    scheduleAutosave()
  }

  function clearResults(): void {
    if (!regions.value.length) return
    recordHistory()
    regions.value = []
    groups.value = []
    selectedGroupId.value = null
    selectedRegionId.value = null
    scheduleAutosave()
  }

  function serializeProject(includeSmallImage = false): Promise<ProjectFile> {
    return createProjectFile({
      imageName: imageName.value, imageBlob: imageBlob.value,
      dimensions: { width: imageWidth.value, height: imageHeight.value },
      settings: { ...settings }, regions: regions.value, groups: groups.value, includeSmallImage,
    })
  }

  async function applyProject(project: ProjectFile, blob: Blob | null = null): Promise<void> {
    Object.assign(settings, DEFAULT_SETTINGS, project.settings, {
      useUserClickPositionForLabels: project.settings.useUserClickPositionForLabels ?? false,
    })
    imageName.value = project.imageName
    imageWidth.value = project.dimensions.width
    imageHeight.value = project.dimensions.height
    regions.value = project.regions.map((region) => markRaw({
      ...region,
      userClickPosition: region.userClickPosition ? { ...region.userClickPosition } : undefined,
      mask: createRegionMask(region.mask.width, region.mask.height, region.mask.runs),
    }))
    groups.value = cloneGroups(project.groups)
    undoStack.value = []
    redoStack.value = []
    imageCanvas.value = null
    imageData.value = null
    imageBlob.value = null
    if (blob) await loadImage(blob, project.imageName)
    else if (project.imageDataUrl) {
      const response = await fetch(project.imageDataUrl)
      await loadImage(await response.blob(), project.imageName)
    } else notify('Projekt je načítaný. Vyberte pôvodný obrázok s rovnakými rozmermi.', 'warning')
  }

  async function importProjectFile(file: File): Promise<void> {
    try {
      const parsed: unknown = JSON.parse(await file.text())
      if (!isProjectFile(parsed)) throw new Error('Súbor nemá platný formát projektu Mozaic.')
      await applyProject(parsed)
      notify('Projekt bol úspešne importovaný.', 'success')
    } catch (error) {
      notify(error instanceof Error ? error.message : 'JSON projekt je poškodený.', 'error')
    }
  }

  function scheduleAutosave(): void {
    window.clearTimeout(autosaveTimer)
    autosaveTimer = window.setTimeout(async () => {
      if (!imageWidth.value) return
      try { await saveLastProject(await serializeProject(false), imageBlob.value) } catch { /* Private mode may block IndexedDB. */ }
    }, 800)
  }

  async function checkRecovery(): Promise<void> {
    try {
      const stored = await loadLastProject()
      if (!stored) return
      recoveryProject = stored.project
      recoveryBlob = stored.imageBlob
      recoveryTimestamp.value = stored.savedAt
      hasRecovery.value = true
    } catch { /* IndexedDB is optional. */ }
  }

  async function restoreRecovery(): Promise<void> {
    if (!recoveryProject) return
    await applyProject(recoveryProject, recoveryBlob)
    hasRecovery.value = false
    notify('Posledná rozpracovaná práca bola obnovená.', 'success')
  }

  async function dismissRecovery(): Promise<void> {
    hasRecovery.value = false
    recoveryProject = null
    recoveryBlob = null
    await clearLastProject().catch(() => undefined)
  }

  watch(settings, scheduleAutosave)

  return {
    imageCanvas, imageData, imageBlob, imageName, imageWidth, imageHeight, regions, groups,
    selectedGroupId, selectedRegionId, settings, toasts, isAnalyzing, hasRecovery, recoveryTimestamp,
    hasImage, canUndo, canRedo, loadImage, addRegionAt, regroup, setColorTolerance, removeRegion,
    moveRegion, splitRegion, mergeGroups, setGroupColor, clearResults, undo, redo, notify,
    dismissToast, serializeProject, importProjectFile, checkRecovery, restoreRecovery, dismissRecovery,
  }
})
