import type { QueryClient } from '@tanstack/react-query'
import { useQueryClient } from '@tanstack/react-query'
import { useEventListener } from 'ahooks'
import isDeepEqual from 'fast-deep-equal'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from '@/app/components/base/ui/toast'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { extractToolConfigIds } from '@/app/components/workflow/utils'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { consoleQuery } from '@/service/client'
import { useUpdateAppAssetFileContent } from '@/service/use-app-asset'
import { skillCollaborationManager } from '../../collaboration/skills/skill-collaboration-manager'
import { START_TAB_ID } from '../constants'
import { SkillSaveContext } from './skill-save-context'

type SaveSnapshot = {
  content: string
  metadata?: Record<string, unknown>
  hasDraftContent: boolean
  hasMetadataDirty: boolean
}

type CachedFileContent = {
  content?: string
  metadata?: Record<string, unknown>
  [key: string]: unknown
}

export type SaveFileOptions = {
  fallbackContent?: string
  fallbackMetadata?: Record<string, unknown>
}

export type SaveResult = {
  saved: boolean
  error?: unknown
}

export type FallbackEntry = {
  content: string
  metadata?: Record<string, unknown>
}

type SkillSaveProviderProps = {
  appId: string
  children: React.ReactNode
}

type CollaborativeSaveWaiter = {
  promise: Promise<boolean>
  cancel: () => void
}

const COLLABORATION_SYNC_TIMEOUT_MS = 1500

const normalizeMetadata = (
  rawMetadata: Record<string, unknown> | undefined,
  content: string,
): Record<string, unknown> | undefined => {
  if (!rawMetadata || typeof rawMetadata !== 'object' || !('tools' in rawMetadata))
    return rawMetadata

  const toolIds = extractToolConfigIds(content)
  const rawTools = (rawMetadata as Record<string, unknown>).tools
  if (!rawTools || typeof rawTools !== 'object')
    return rawMetadata

  const entries = Object.entries(rawTools as Record<string, unknown>)
  const nextTools = entries.reduce<Record<string, unknown>>((acc, [id, value]) => {
    if (toolIds.has(id))
      acc[id] = value
    return acc
  }, {})
  const nextMetadata = { ...(rawMetadata as Record<string, unknown>) }
  if (Object.keys(nextTools).length > 0)
    nextMetadata.tools = nextTools
  else
    delete nextMetadata.tools
  return nextMetadata
}

const patchFileContentCache = (
  qc: QueryClient,
  queryKey: readonly unknown[],
  serialized: string,
) => {
  qc.setQueryData<CachedFileContent>(queryKey, (existing) => {
    if (!existing || typeof existing !== 'object')
      return { content: serialized }
    return { ...existing, content: serialized }
  })
}

export const SkillSaveProvider = ({
  appId,
  children,
}: SkillSaveProviderProps) => {
  const { t } = useTranslation()
  const storeApi = useWorkflowStore()
  const queryClient = useQueryClient()
  const { mutateAsync: updateFileContent } = useUpdateAppAssetFileContent()
  const isCollaborationEnabled = useGlobalPublicStore(s => s.systemFeatures.enable_collaboration_mode)
  const queueRef = useRef<Map<string, Promise<SaveResult>>>(new Map())
  const fallbackRegistryRef = useRef<Map<string, FallbackEntry>>(new Map())

  const getCachedContent = useCallback((fileId: string): string | undefined => {
    if (!appId)
      return undefined

    const cached = queryClient.getQueryData<CachedFileContent>(
      consoleQuery.appAsset.getFileContent.queryKey({
        input: { params: { appId, nodeId: fileId } },
      }),
    )

    const rawContent = cached?.content
    if (!rawContent)
      return undefined

    try {
      const parsed = JSON.parse(rawContent) as { content?: unknown }
      if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string')
        return parsed.content
    }
    catch {
      // Fall back to raw content when it's not a JSON wrapper.
    }

    return rawContent
  }, [appId, queryClient])

  const buildSnapshot = useCallback((
    fileId: string,
    fallbackContent?: string,
    fallbackMetadata?: Record<string, unknown>,
  ): SaveSnapshot | null => {
    const state = storeApi.getState()
    const draftContent = state.dirtyContents.get(fileId)
    const isMetadataDirty = state.dirtyMetadataIds.has(fileId)

    if (draftContent === undefined && !isMetadataDirty)
      return null

    const registryEntry = fallbackRegistryRef.current.get(fileId)
    const rawMetadata = state.fileMetadata.get(fileId) ?? fallbackMetadata ?? registryEntry?.metadata
    const content = draftContent ?? getCachedContent(fileId) ?? fallbackContent ?? registryEntry?.content

    if (content === undefined)
      return null

    const metadata = normalizeMetadata(rawMetadata, content)

    return {
      content,
      metadata,
      hasDraftContent: draftContent !== undefined,
      hasMetadataDirty: isMetadataDirty,
    }
  }, [getCachedContent, storeApi])

  const updateCachedContent = useCallback((fileId: string, snapshot: SaveSnapshot) => {
    if (!appId)
      return

    const queryKey = consoleQuery.appAsset.getFileContent.queryKey({
      input: { params: { appId, nodeId: fileId } },
    })
    const serialized = JSON.stringify({
      content: snapshot.content,
      ...(snapshot.metadata ? { metadata: snapshot.metadata } : {}),
    })
    patchFileContentCache(queryClient, queryKey, serialized)
  }, [appId, queryClient])

  const createCollaborativeSaveWaiter = useCallback((fileId: string): CollaborativeSaveWaiter => {
    let settled = false
    let unsubscribe: (() => void) | null = null
    let timeoutId: ReturnType<typeof setTimeout> | null = null

    const promise = new Promise<boolean>((resolve) => {
      const finish = (saved: boolean) => {
        if (settled)
          return
        settled = true
        if (timeoutId !== null)
          clearTimeout(timeoutId)
        unsubscribe?.()
        resolve(saved)
      }

      unsubscribe = skillCollaborationManager.onAnyFileSaved((payload) => {
        if (!payload || payload.file_id !== fileId)
          return
        finish(true)
      })

      timeoutId = setTimeout(() => finish(false), COLLABORATION_SYNC_TIMEOUT_MS)
    })

    return {
      promise,
      cancel: () => {
        if (settled)
          return
        settled = true
        if (timeoutId !== null)
          clearTimeout(timeoutId)
        unsubscribe?.()
      },
    }
  }, [])

  const performSave = useCallback(async (
    fileId: string,
    options?: SaveFileOptions,
  ): Promise<SaveResult> => {
    if (!appId || !fileId || fileId === START_TAB_ID)
      return { saved: false }

    const isCollaborativeFollower = isCollaborationEnabled
      && skillCollaborationManager.isFileCollaborative(fileId)
      && !skillCollaborationManager.isLeader(fileId)

    if (isCollaborativeFollower) {
      const delegatedSaveWaiter = createCollaborativeSaveWaiter(fileId)
      const didRequestSync = skillCollaborationManager.requestSync(fileId)
      if (didRequestSync) {
        const wasSavedByLeader = await delegatedSaveWaiter.promise
        if (wasSavedByLeader)
          return { saved: true }
      }
      else {
        delegatedSaveWaiter.cancel()
      }
    }

    const snapshot = buildSnapshot(fileId, options?.fallbackContent, options?.fallbackMetadata)
    if (!snapshot) {
      return { saved: false }
    }

    try {
      await updateFileContent({
        appId,
        nodeId: fileId,
        payload: {
          content: snapshot.content,
          ...(snapshot.metadata ? { metadata: snapshot.metadata } : {}),
        },
      })

      updateCachedContent(fileId, snapshot)

      const latestState = storeApi.getState()
      if (snapshot.hasDraftContent) {
        const latestDraft = latestState.dirtyContents.get(fileId)
        if (latestDraft === snapshot.content)
          latestState.clearDraftContent(fileId)
      }

      if (snapshot.hasMetadataDirty) {
        const latestMetadata = latestState.fileMetadata.get(fileId)
        const normalizedLatest = normalizeMetadata(latestMetadata, snapshot.content)
        if (isDeepEqual(normalizedLatest, snapshot.metadata))
          latestState.clearDraftMetadata(fileId)
      }

      if (isCollaborationEnabled && skillCollaborationManager.isFileCollaborative(fileId)) {
        skillCollaborationManager.emitFileSaved(fileId, snapshot.content, snapshot.metadata)
      }

      return { saved: true }
    }
    catch (error) {
      return { saved: false, error }
    }
  }, [appId, buildSnapshot, createCollaborativeSaveWaiter, isCollaborationEnabled, storeApi, updateCachedContent, updateFileContent])

  const saveFile = useCallback(async (
    fileId: string,
    options?: SaveFileOptions,
  ): Promise<SaveResult> => {
    if (!fileId || fileId === START_TAB_ID)
      return { saved: false }

    const previous = queueRef.current.get(fileId) || Promise.resolve({ saved: false })
    const next = previous.then(() => performSave(fileId, options))
    queueRef.current.set(fileId, next)
    return next.finally(() => {
      if (queueRef.current.get(fileId) === next)
        queueRef.current.delete(fileId)
    })
  }, [performSave])

  const saveAllDirty = useCallback(() => {
    if (!appId)
      return

    const { dirtyContents, dirtyMetadataIds } = storeApi.getState()
    if (dirtyContents.size === 0 && dirtyMetadataIds.size === 0)
      return

    const dirtyIds = new Set<string>()
    dirtyContents.forEach((_value, fileId) => {
      dirtyIds.add(fileId)
    })
    dirtyMetadataIds.forEach((fileId) => {
      dirtyIds.add(fileId)
    })

    const tasks = Array.from(dirtyIds)
      .filter(fileId => fileId !== START_TAB_ID)
      .map(fileId => saveFile(fileId))

    if (tasks.length === 0)
      return

    void Promise.allSettled(tasks)
  }, [appId, saveFile, storeApi])

  const registerFallback = useCallback((fileId: string, entry: FallbackEntry) => {
    fallbackRegistryRef.current.set(fileId, entry)
  }, [])

  const unregisterFallback = useCallback((fileId: string) => {
    fallbackRegistryRef.current.delete(fileId)
  }, [])

  useEventListener('keydown', (e: KeyboardEvent) => {
    if ((e.ctrlKey || e.metaKey) && e.key === 's') {
      e.preventDefault()
      const { activeTabId } = storeApi.getState()
      if (!activeTabId || activeTabId === START_TAB_ID)
        return

      const fallback = fallbackRegistryRef.current.get(activeTabId)
      void saveFile(activeTabId, {
        fallbackContent: fallback?.content,
        fallbackMetadata: fallback?.metadata,
      }).then((result) => {
        if (result.error) {
          const errorMessage = result.error instanceof Error
            ? result.error.message
            : String(result.error)
          toast.error(errorMessage)
        }
        else if (result.saved) {
          toast.success(t('api.saved', { ns: 'common' }))
        }
      })
    }
  }, { target: typeof window !== 'undefined' ? window : undefined })

  const value = useMemo(() => ({
    saveFile,
    saveAllDirty,
    registerFallback,
    unregisterFallback,
  }), [saveAllDirty, saveFile, registerFallback, unregisterFallback])

  useEffect(() => {
    if (!appId || !isCollaborationEnabled)
      return

    return skillCollaborationManager.onAnyFileSaved((payload) => {
      if (!payload?.file_id || typeof payload.content !== 'string')
        return

      const fileId = payload.file_id
      const queryKey = consoleQuery.appAsset.getFileContent.queryKey({
        input: { params: { appId, nodeId: fileId } },
      })
      const serialized = JSON.stringify({
        content: payload.content,
        ...(payload.metadata ? { metadata: payload.metadata } : {}),
      })
      patchFileContentCache(queryClient, queryKey, serialized)

      const state = storeApi.getState()
      state.clearDraftContent(fileId)

      const latestMetadata = state.fileMetadata.get(fileId)
      const normalizedLatest = normalizeMetadata(latestMetadata, payload.content)
      if (payload.metadata === undefined || isDeepEqual(normalizedLatest, payload.metadata))
        state.clearDraftMetadata(fileId)
    })
  }, [appId, isCollaborationEnabled, queryClient, storeApi])

  return (
    <SkillSaveContext.Provider value={value}>
      {children}
    </SkillSaveContext.Provider>
  )
}
