'use client'

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { GraphCanvas, GraphCanvasRef, type Theme, type GraphNode as ReagraphNode, type GraphEdge as ReagraphEdge, type InternalGraphNode } from 'reagraph'
import { useMissionControl } from '@/store'

// --- Data interfaces (match API response) ---

interface AgentFileInfo {
  path: string
  chunks: number
  textSize: number
}

interface AgentGraphData {
  name: string
  dbSize: number
  totalChunks: number
  totalFiles: number
  files: AgentFileInfo[]
}

// --- Obsidian-inspired palette (muted purples, warm grays) ---

const AGENT_COLORS = [
  '#b4befe', // lavender
  '#cba6f7', // mauve
  '#f5c2e7', // pink
  '#89b4fa', // blue
  '#74c7ec', // sapphire
  '#89dceb', // sky
  '#94e2d5', // teal
  '#a6e3a1', // green
  '#f9e2af', // yellow
  '#fab387', // peach
  '#eba0ac', // maroon
  '#f38ba8', // red
  '#cdd6f4', // text
  '#bac2de', // subtext1
  '#a6adc8', // subtext0
  '#b4befe', // lavender2
  '#cba6f7', // mauve2
]

function getFileColor(filePath: string): string {
  if (filePath.startsWith('sessions/') || filePath.includes('/sessions/')) return '#89dceb'
  if (filePath.startsWith('memory/') || filePath.includes('/memory/')) return '#94e2d5'
  if (filePath.startsWith('knowledge') || filePath.includes('/knowledge')) return '#b4befe'
  if (filePath.endsWith('.md')) return '#f9e2af'
  if (filePath.endsWith('.json') || filePath.endsWith('.jsonl')) return '#cba6f7'
  return '#89b4fa'
}

function formatBytes(bytes: number): string {
  if (bytes === 0) return '0 B'
  const k = 1024
  const sizes = ['B', 'KB', 'MB', 'GB']
  const i = Math.floor(Math.log(bytes) / Math.log(k))
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
}

// --- Obsidian graph theme ---

const obsidianTheme: Theme = {
  canvas: {
    background: '#11111b',
    fog: '#11111b',
  },
  node: {
    fill: '#6c7086',
    activeFill: '#cba6f7',
    opacity: 1,
    selectedOpacity: 1,
    inactiveOpacity: 0.1,
    label: {
      color: '#cdd6f4',
      stroke: '#11111b',
      activeColor: '#f5f5f7',
    },
  },
  ring: {
    fill: '#6c7086',
    activeFill: '#cba6f7',
  },
  edge: {
    fill: '#45475a',
    activeFill: '#cba6f7',
    opacity: 0.15,
    selectedOpacity: 0.5,
    inactiveOpacity: 0.03,
    label: {
      color: '#6c7086',
      activeColor: '#cdd6f4',
    },
  },
  arrow: {
    fill: '#45475a',
    activeFill: '#cba6f7',
  },
  lasso: {
    background: 'rgba(203, 166, 247, 0.08)',
    border: 'rgba(203, 166, 247, 0.25)',
  },
}

// --- Component ---

export function MemoryGraph() {
  const { memoryGraphAgents, setMemoryGraphAgents } = useMissionControl()
  const agents = memoryGraphAgents || []
  const [selectedAgent, setSelectedAgent] = useState<string>('all')
  const [isLoading, setIsLoading] = useState(memoryGraphAgents === null)
  const [error, setError] = useState<string | null>(null)
  const [searchQuery, setSearchQuery] = useState('')
  const [selectedFile, setSelectedFile] = useState<AgentFileInfo | null>(null)
  const [actives, setActives] = useState<string[]>([])
  const [hoveredNode, setHoveredNode] = useState<{ label: string; sub?: string } | null>(null)

  const graphRef = useRef<GraphCanvasRef | null>(null)

  // Fetch data
  const fetchData = useCallback(async () => {
    setIsLoading(true)
    setError(null)
    try {
      const res = await fetch('/api/memory/graph?agent=all')
      if (!res.ok) {
        const data = await res.json().catch(() => ({}))
        throw new Error(data.error || `HTTP ${res.status}`)
      }
      const data = await res.json()
      setMemoryGraphAgents(data.agents || [])
    } catch (err: unknown) {
      setError(err instanceof Error ? err.message : 'Failed to load')
    } finally {
      setIsLoading(false)
    }
  }, [setMemoryGraphAgents])

  useEffect(() => {
    if (memoryGraphAgents !== null) return
    fetchData()
  }, [fetchData, memoryGraphAgents])

  // Stats
  const stats = useMemo(() => {
    const totalAgents = agents.length
    const totalFiles = agents.reduce((s, a) => s + a.totalFiles, 0)
    const totalChunks = agents.reduce((s, a) => s + a.totalChunks, 0)
    const totalSize = agents.reduce((s, a) => s + a.dbSize, 0)
    return { totalAgents, totalFiles, totalChunks, totalSize }
  }, [agents])

  // Build reagraph nodes/edges from API data
  const { graphNodes, graphEdges } = useMemo(() => {
    if (!agents.length) return { graphNodes: [], graphEdges: [] }

    const nodes: ReagraphNode[] = []
    const edges: ReagraphEdge[] = []

    if (selectedAgent === 'all') {
      agents.forEach((agent, i) => {
        const color = AGENT_COLORS[i % AGENT_COLORS.length]
        const hubSize = Math.max(5, Math.min(15, 4 + Math.sqrt(agent.totalChunks) * 0.8))

        nodes.push({
          id: `hub-${agent.name}`,
          label: agent.name,
          fill: color,
          size: hubSize,
        })

        const maxFiles = 25
        const files = agent.files.slice(0, maxFiles)
        files.forEach((file, fi) => {
          const fileSize = Math.max(1.5, Math.min(5, 1 + Math.sqrt(file.chunks) * 0.6))
          const fileColor = getFileColor(file.path)
          const nodeId = `file-${agent.name}-${fi}`

          nodes.push({
            id: nodeId,
            label: '',
            fill: fileColor,
            size: fileSize,
            data: { filePath: file.path, chunks: file.chunks, textSize: file.textSize, agentName: agent.name },
          })

          edges.push({
            id: `edge-hub-${agent.name}-${nodeId}`,
            source: `hub-${agent.name}`,
            target: nodeId,
            fill: color,
          })
        })
      })
    } else {
      const agent = agents.find((a) => a.name === selectedAgent)
      if (!agent) return { graphNodes: [], graphEdges: [] }

      const agentIdx = agents.indexOf(agent)
      const color = AGENT_COLORS[agentIdx % AGENT_COLORS.length]
      const hubSize = Math.max(6, Math.min(18, 5 + Math.sqrt(agent.totalChunks) * 0.8))

      nodes.push({
        id: `hub-${agent.name}`,
        label: agent.name,
        fill: color,
        size: hubSize,
      })

      let files = agent.files
      if (searchQuery) {
        const q = searchQuery.toLowerCase()
        files = files.filter((f) => f.path.toLowerCase().includes(q))
      }

      const maxFiles = 120
      const displayFiles = files.slice(0, maxFiles)

      displayFiles.forEach((file, fi) => {
        const fileSize = Math.max(2, Math.min(8, 2 + Math.sqrt(file.chunks) * 0.8))
        const fileColor = getFileColor(file.path)
        const nodeId = `file-${agent.name}-${fi}`

        nodes.push({
          id: nodeId,
          label: file.path.split('/').pop() || file.path,
          fill: fileColor,
          size: fileSize,
          data: { filePath: file.path, chunks: file.chunks, textSize: file.textSize, agentName: agent.name },
        })

        edges.push({
          id: `edge-hub-${agent.name}-${nodeId}`,
          source: `hub-${agent.name}`,
          target: nodeId,
          fill: color,
        })
      })

      // Weak inter-file edges for same-directory clustering
      const dirMap = new Map<string, string[]>()
      displayFiles.forEach((file, fi) => {
        const dir = file.path.split('/').slice(0, -1).join('/')
        if (!dir) return
        const nodeId = `file-${agent.name}-${fi}`
        if (!dirMap.has(dir)) dirMap.set(dir, [])
        dirMap.get(dir)!.push(nodeId)
      })
      for (const ids of dirMap.values()) {
        for (let i = 0; i < ids.length - 1 && i < 5; i++) {
          edges.push({
            id: `edge-dir-${ids[i]}-${ids[i + 1]}`,
            source: ids[i],
            target: ids[i + 1],
          })
        }
      }
    }

    return { graphNodes: nodes, graphEdges: edges }
  }, [agents, selectedAgent, searchQuery])

  // Auto-fit the graph after layout settles (nodes change)
  useEffect(() => {
    if (!graphNodes.length) return
    // reagraph force layout needs time to settle before fitNodesInView works
    const t1 = setTimeout(() => graphRef.current?.fitNodesInView(), 800)
    const t2 = setTimeout(() => graphRef.current?.fitNodesInView(), 2000)
    return () => { clearTimeout(t1); clearTimeout(t2) }
  }, [graphNodes.length, selectedAgent])

  // Navigation helpers
  const goBack = useCallback(() => {
    setSelectedAgent('all')
    setSelectedFile(null)
    setSearchQuery('')
    setActives([])
    setHoveredNode(null)
  }, [])

  const drillInto = useCallback((agentName: string) => {
    setSelectedAgent(agentName)
    setSelectedFile(null)
    setSearchQuery('')
    setActives([])
    setHoveredNode(null)
  }, [])

  // Interaction handlers
  const handleNodeClick = useCallback((node: InternalGraphNode) => {
    const id = node.id
    if (id.startsWith('hub-') && selectedAgent === 'all') {
      drillInto(id.replace('hub-', ''))
    } else if (id.startsWith('hub-') && selectedAgent !== 'all') {
      // clicking the hub in drilled-in view goes back
      goBack()
    } else if (id.startsWith('file-') && node.data) {
      const { filePath, chunks, textSize } = node.data as { filePath: string; chunks: number; textSize: number }
      setSelectedFile({ path: filePath, chunks, textSize })
    }
  }, [selectedAgent, drillInto, goBack])

  const handleNodeHover = useCallback((node: InternalGraphNode) => {
    setActives([node.id])
    if (node.data) {
      const d = node.data as { filePath: string; chunks: number; textSize: number; agentName: string }
      setHoveredNode({ label: d.filePath, sub: `${d.chunks} chunks / ${formatBytes(d.textSize)}` })
    } else if (node.id.startsWith('hub-')) {
      const name = node.id.replace('hub-', '')
      const agent = agents.find(a => a.name === name)
      if (agent) {
        setHoveredNode({ label: agent.name, sub: `${agent.totalChunks} chunks / ${agent.totalFiles} files / ${formatBytes(agent.dbSize)}` })
      }
    }
  }, [agents])

  const handleNodeUnhover = useCallback(() => {
    setActives([])
    setHoveredNode(null)
  }, [])

  const handleCanvasClick = useCallback(() => {
    setActives([])
    setSelectedFile(null)
    setHoveredNode(null)
  }, [])

  // --- Render ---

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-full" style={{ background: '#11111b' }}>
        <div className="flex flex-col items-center gap-3">
          <div className="w-8 h-8 rounded-full border-2 border-[#cba6f7]/30 border-t-[#cba6f7] animate-spin" />
          <span className="text-[#6c7086] text-sm font-mono">Loading graph...</span>
        </div>
      </div>
    )
  }

  if (error) {
    return (
      <div className="flex flex-col items-center justify-center h-full gap-3" style={{ background: '#11111b' }}>
        <span className="text-[#f38ba8] text-sm">{error}</span>
        <button onClick={fetchData} className="px-3 py-1.5 text-xs rounded-md bg-[#1e1e2e] border border-[#45475a] text-[#cdd6f4] hover:border-[#cba6f7]/50 transition-colors">
          Retry
        </button>
      </div>
    )
  }

  if (!agents.length) {
    return (
      <div className="flex flex-col items-center justify-center h-full gap-2" style={{ background: '#11111b' }}>
        <span className="text-[#6c7086] text-sm">No memory databases found</span>
        <span className="text-[#45475a] text-xs">OpenClaw memory SQLite files not detected</span>
      </div>
    )
  }

  const activeAgent = selectedAgent !== 'all' ? agents.find(a => a.name === selectedAgent) : null

  return (
    <div className="relative h-full w-full overflow-hidden" style={{ background: '#11111b' }}>
      {/* Full-bleed graph canvas */}
      <GraphCanvas
        ref={graphRef}
        nodes={graphNodes}
        edges={graphEdges}
        theme={obsidianTheme}
        layoutType="forceDirected2d"
        layoutOverrides={{
          linkDistance: selectedAgent === 'all' ? 80 : 100,
          nodeStrength: selectedAgent === 'all' ? -60 : -80,
        }}
        labelType={selectedAgent === 'all' ? 'auto' : 'auto'}
        edgeArrowPosition="none"
        animated={true}
        draggable={true}
        defaultNodeSize={4}
        minNodeSize={1.5}
        maxNodeSize={15}
        cameraMode="pan"
        actives={actives}
        onNodeClick={handleNodeClick}
        onNodePointerOver={handleNodeHover}
        onNodePointerOut={handleNodeUnhover}
        onCanvasClick={handleCanvasClick}
      />

      {/* Floating breadcrumb / navigation bar (top-left) */}
      <div className="absolute top-3 left-3 flex items-center gap-1.5 z-10">
        <button
          onClick={goBack}
          className={`px-2.5 py-1 text-[11px] font-mono rounded-md backdrop-blur-xl transition-all ${
            selectedAgent === 'all'
              ? 'bg-[#cba6f7]/15 text-[#cba6f7] border border-[#cba6f7]/25'
              : 'bg-[#1e1e2e]/80 text-[#6c7086] border border-[#45475a]/50 hover:text-[#cdd6f4] hover:border-[#cba6f7]/30'
          }`}
        >
          all agents
        </button>
        {activeAgent && (
          <>
            <span className="text-[#45475a] text-[10px]">/</span>
            <span className="px-2.5 py-1 text-[11px] font-mono rounded-md bg-[#cba6f7]/15 text-[#cba6f7] border border-[#cba6f7]/25">
              {activeAgent.name}
            </span>
          </>
        )}
      </div>

      {/* Floating stats (top-right) */}
      <div className="absolute top-3 right-3 flex items-center gap-3 z-10">
        {selectedAgent !== 'all' && (
          <input
            type="text"
            value={searchQuery}
            onChange={(e) => setSearchQuery(e.target.value)}
            placeholder="filter files..."
            className="px-2.5 py-1 text-[11px] font-mono rounded-md bg-[#1e1e2e]/80 backdrop-blur-xl border border-[#45475a]/50 text-[#cdd6f4] placeholder-[#45475a] focus:outline-none focus:border-[#cba6f7]/40 w-36 transition-colors"
          />
        )}
        <div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-[#1e1e2e]/80 backdrop-blur-xl border border-[#45475a]/30">
          <StatChip label="agents" value={stats.totalAgents} />
          <Sep />
          <StatChip label="files" value={stats.totalFiles} />
          <Sep />
          <StatChip label="chunks" value={stats.totalChunks} />
          <Sep />
          <StatChip label="size" value={formatBytes(stats.totalSize)} />
        </div>
      </div>

      {/* Hover tooltip (bottom-center) */}
      {hoveredNode && (
        <div className="absolute bottom-16 left-1/2 -translate-x-1/2 z-10 pointer-events-none">
          <div className="px-3 py-2 rounded-lg bg-[#1e1e2e]/90 backdrop-blur-xl border border-[#45475a]/40 shadow-2xl shadow-black/40 max-w-md">
            <div className="text-[11px] font-mono text-[#cdd6f4] truncate">{hoveredNode.label}</div>
            {hoveredNode.sub && (
              <div className="text-[10px] font-mono text-[#6c7086] mt-0.5">{hoveredNode.sub}</div>
            )}
          </div>
        </div>
      )}

      {/* Selected file detail panel (bottom-left) */}
      {selectedFile && (
        <div className="absolute bottom-3 left-3 z-10 max-w-sm">
          <div className="px-4 py-3 rounded-lg bg-[#1e1e2e]/90 backdrop-blur-xl border border-[#45475a]/40 shadow-2xl shadow-black/40">
            <div className="flex items-center justify-between gap-4 mb-2">
              <h3 className="text-[11px] font-mono text-[#cdd6f4] truncate">{selectedFile.path}</h3>
              <button
                onClick={() => setSelectedFile(null)}
                className="text-[#6c7086] hover:text-[#cdd6f4] text-xs transition-colors shrink-0"
              >
                x
              </button>
            </div>
            <div className="flex items-center gap-4 text-[10px] font-mono text-[#6c7086]">
              <span><span className="text-[#cba6f7]">{selectedFile.chunks}</span> chunks</span>
              <span><span className="text-[#89b4fa]">{formatBytes(selectedFile.textSize)}</span> text</span>
            </div>
          </div>
        </div>
      )}

      {/* Color legend (bottom-right) */}
      <div className="absolute bottom-3 right-3 z-10">
        <div className="px-3 py-2 rounded-lg bg-[#1e1e2e]/80 backdrop-blur-xl border border-[#45475a]/30">
          <div className="flex items-center gap-3 text-[9px] font-mono text-[#585b70]">
            <span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-[#89dceb]" />sessions</span>
            <span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-[#94e2d5]" />memory</span>
            <span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-[#b4befe]" />knowledge</span>
            <span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-[#f9e2af]" />.md</span>
            <span className="flex items-center gap-1"><span className="w-1.5 h-1.5 rounded-full bg-[#cba6f7]" />.json</span>
          </div>
        </div>
      </div>

      {/* Keyboard hint */}
      <div className="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 text-[9px] font-mono text-[#313244] pointer-events-none select-none">
        scroll to zoom / drag to pan / click node to drill in
      </div>
    </div>
  )
}

function StatChip({ label, value }: { label: string; value: number | string }) {
  const display = typeof value === 'number' ? value.toLocaleString() : value
  return (
    <span className="text-[10px] font-mono">
      <span className="text-[#cdd6f4]">{display}</span>
      <span className="text-[#585b70] ml-1">{label}</span>
    </span>
  )
}

function Sep() {
  return <span className="text-[#313244]">|</span>
}
