import { useCallback, useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import * as db from '../../lib/db';
import { callLLM } from '../../lib/llm';
import { EMIT_GRAPH_ACTIONS_TOOL } from '../../lib/llmTools';
import { Network, Table2 } from 'lucide-react';
import { useGraphData } from '../../hooks/useGraphData';
import { filterAiActions } from '../../lib/graphGuard';
import GraphControls from './graph/GraphControls';
import NodeDetailPanel from './graph/NodeDetailPanel';
import GraphTable from './graph/GraphTable';

// ── Edge visual style per relation type ────────────────────────────────────────
// stroke — line colour
// dash   — SVG stroke-dasharray (null = solid)
const EDGE_STYLE = {
  related_to:  { stroke: '#94A3B8', dash: '6,3'  },   // slate, dashed
  depends_on:  { stroke: '#F97316', dash: null    },   // orange, solid
  part_of:     { stroke: '#10B981', dash: null    },   // emerald, solid
  executed_by: { stroke: '#C084FC', dash: '3,3'  },   // violet, dotted
};
const NODE_RADIUS = 22; // circle r (20) + white stroke buffer

// ── System prompts per analysis scope ─────────────────────────────────────────
const SYSTEM_PROMPTS = {
  full: `You are a strict Data Quality AI maintaining a Knowledge Graph for Respellion.
Evaluate the provided topics and relations and emit the actions to take via the emit_graph_actions tool.

PROTECTED TOPICS — NEVER include these in merges (as deleteId) or in deletions:
  • Topics with learning_relevance="exclude" are REFERENCE MATERIAL, kept on purpose
    so R42 can still answer questions about them. They are intentionally outside
    the theme-learning flow. Do not propose deleting or merging them away.
  • Topics with relevance_locked=true are admin-pinned. Do not change their
    learning_relevance and do not delete or merge them.
These topics may appear as the keepId in a merge (others fold into them), and
may appear as source/target of newRelations, but their own row must survive.

Rules:
1. Identify topics that mean exactly the same thing. Choose one to keep, one to delete (merges).
2. Identify topics that are too vague, irrelevant, or malformed (deletions).
3. Identify missing logical relations (depends_on, part_of, related_to, executed_by) between conceptually linked topics (newRelations).
4. Evaluate learning_relevance. Mark purely operational topics (printer guides, etc.) as "exclude"; low-priority as "peripheral" (relevanceUpdates).
   Skip topics where relevance_locked=true — omit them from relevanceUpdates entirely.

Do not return the entire graph — only the actions to take.`,

  relations: `You are analyzing a knowledge graph for Respellion's learning platform.
Your ONLY task: identify missing logical relations between existing topics.
Suggest new relations using: depends_on, part_of, related_to, executed_by.
Only suggest connections that are clearly implied — do not invent loose associations.
Emit ONLY newRelations. Set merges=[], deletions=[], relevanceUpdates=[].`,

  relevance: `You are analyzing a knowledge graph for Respellion's learning platform.
Your ONLY task: re-score the learning_relevance for each topic.
Relevance levels:
  core       — foundational knowledge every employee must master
  standard   — important, covered in the normal learning flow
  peripheral — supplementary or nice-to-know, lower priority
  exclude    — operational/administrative, not relevant to learning (e.g. printer guides, HR forms)
Do NOT change topics where relevance_locked=true — omit them from relevanceUpdates entirely.
Emit ONLY relevanceUpdates. Set merges=[], deletions=[], newRelations=[].`,
};

const SCOPE_CONFIRM = {
  full:      'Send the entire graph to the AI (Opus) for a full quality pass — merges, deletions, missing relations, and relevance scoring. This may take a minute.',
  relations: 'Ask the AI (Sonnet) to suggest missing logical relations between existing topics.',
  relevance: 'Ask the AI (Sonnet) to re-score learning relevance for all unlocked topics.',
};

/**
 * Knowledge-graph admin view.
 *
 * Orchestrator: owns the D3 canvas, selected-node cursor, and AI analysis flow.
 * Data lives in useGraphData; panel UI lives in GraphControls / NodeDetailPanel.
 */
const KnowledgeGraph = () => {
  const svgRef     = useRef(null);
  const wrapperRef = useRef(null);
  const zoomRef    = useRef(null);   // D3 zoom behaviour — shared with panToNode
  const nodesRef   = useRef([]);     // live node positions (mutated by D3 in-place)

  const [dimensions, setDimensions] = useState({ width: 800, height: 600 });
  const [selectedNode, setSelectedNode] = useState(null);
  const [showExcludeNodes, setShowExcludeNodes] = useState(false);
  const [isAnalyzing, setIsAnalyzing] = useState(false);
  const [analyzeError, setAnalyzeError] = useState(null);
  const [analyzeNotice, setAnalyzeNotice] = useState(null); // info-level message from last analyze
  const [isRestoring, setIsRestoring] = useState(false);
  const [viewMode, setViewMode] = useState('graph'); // 'graph' | 'table'

  const {
    topics, relations, snapshotMeta,
    reload, updateTopic, bulkUpdateTopics, deleteTopic, addRelation, removeRelation, bulkSave, restoreSnapshot,
  } = useGraphData();

  // ── Canvas sizing ────────────────────────────────────────────────────────────

  useEffect(() => {
    if (!wrapperRef.current) return;
    const measure = () => {
      const { width, height } = wrapperRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    };
    measure();
    window.addEventListener('resize', measure);
    return () => window.removeEventListener('resize', measure);
  }, []);

  // ── D3 force graph ───────────────────────────────────────────────────────────

  useEffect(() => {
    if (viewMode !== 'graph') return;
    if (!svgRef.current || topics.length === 0) return;

    const { width, height } = dimensions;
    d3.select(svgRef.current).selectAll('*').remove();

    const filteredTopics = showExcludeNodes
      ? topics
      : topics.filter(t => t.learning_relevance !== 'exclude');
    const filteredIds = new Set(filteredTopics.map(t => t.id));
    const filteredRelations = relations.filter(
      r => filteredIds.has(r.source) && filteredIds.has(r.target),
    );

    // Spread every datum so D3 simulation mutations don't touch React state.
    const nodes = filteredTopics.map(d => ({ ...d }));
    const links = filteredRelations.map(d => ({ ...d }));
    nodesRef.current = nodes; // D3 mutates x/y in-place — ref always has latest positions

    const svg = d3
      .select(svgRef.current)
      .attr('viewBox', [0, 0, width, height])
      .attr('width', width)
      .attr('height', height);

    // ── Arrowhead markers — one per relation type ──────────────────────────────
    const defs = svg.append('defs');
    Object.entries(EDGE_STYLE).forEach(([type, style]) => {
      defs.append('marker')
        .attr('id', `kb-arrow-${type}`)
        .attr('viewBox', '0 -5 10 10')
        .attr('refX', 10)       // tip of arrow at line endpoint
        .attr('refY', 0)
        .attr('markerWidth', 5)
        .attr('markerHeight', 5)
        .attr('orient', 'auto')
        .append('path')
        .attr('d', 'M0,-5L10,0L0,5Z')
        .attr('fill', style.stroke)
        .attr('opacity', 0.85);
    });

    const g = svg.append('g');

    const zoom = d3.zoom()
      .scaleExtent([0.1, 4])
      .on('zoom', event => g.attr('transform', event.transform));

    svg.call(zoom);
    zoomRef.current = zoom; // expose to panToNode

    const color = d3
      .scaleOrdinal()
      .domain(['concept', 'role', 'process'])
      .range(['#1F5560', '#5C1DD8', '#AFC0FF']);

    const simulation = d3
      .forceSimulation(nodes)
      .force('link', d3.forceLink(links).id(d => d.id).distance(100))
      .force('charge', d3.forceManyBody().strength(-300))
      .force('center', d3.forceCenter(width / 2, height / 2))
      .force('collide', d3.forceCollide().radius(40));

    // ── Edges — coloured and typed ─────────────────────────────────────────────
    const link = g
      .append('g')
      .selectAll('line')
      .data(links)
      .join('line')
      .attr('stroke', d => EDGE_STYLE[d.type]?.stroke ?? '#94A3B8')
      .attr('stroke-opacity', 0.7)
      .attr('stroke-width', 1.5)
      .attr('stroke-dasharray', d => EDGE_STYLE[d.type]?.dash ?? null)
      .attr('marker-end', d => `url(#kb-arrow-${d.type})`);

    // ── Nodes ──────────────────────────────────────────────────────────────────
    const node = g
      .append('g')
      .selectAll('g')
      .data(nodes)
      .join('g')
      .call(
        d3.drag()
          .on('start', event => {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            event.subject.fx = event.subject.x;
            event.subject.fy = event.subject.y;
          })
          .on('drag', event => {
            event.subject.fx = event.x;
            event.subject.fy = event.y;
          })
          .on('end', event => {
            if (!event.active) simulation.alphaTarget(0);
            event.subject.fx = null;
            event.subject.fy = null;
          }),
      );

    node
      .append('circle')
      .attr('r', 20)
      .attr('fill', d => color(d.type) || '#1F5560')
      .attr('stroke', '#fff')
      .attr('stroke-width', 2)
      .attr('stroke-dasharray', d => (d.learning_relevance === 'exclude' ? '4,4' : 'none'))
      .attr('opacity', d => {
        if (d.learning_relevance === 'exclude') return 0.3;
        if (d.learning_relevance === 'peripheral') return 0.5;
        return 1;
      })
      .on('click', (_event, d) => setSelectedNode(d));

    node
      .append('text')
      .text(d => d.label)
      .attr('x', 25)
      .attr('y', 5)
      .attr('font-size', '12px')
      .attr('fill', 'var(--color-fg)')
      .attr('class', 'font-mono select-none pointer-events-none');

    // ── Tick — offset line endpoints to circle edge so arrows land cleanly ─────
    simulation.on('tick', () => {
      link
        .attr('x1', d => d.source.x)
        .attr('y1', d => d.source.y)
        .attr('x2', d => {
          const dx = d.target.x - d.source.x;
          const dy = d.target.y - d.source.y;
          const dist = Math.hypot(dx, dy) || 1;
          return d.target.x - (dx / dist) * NODE_RADIUS;
        })
        .attr('y2', d => {
          const dx = d.target.x - d.source.x;
          const dy = d.target.y - d.source.y;
          const dist = Math.hypot(dx, dy) || 1;
          return d.target.y - (dy / dist) * NODE_RADIUS;
        });
      node.attr('transform', d => `translate(${d.x},${d.y})`);
    });

    return () => simulation.stop();
  }, [dimensions, topics, relations, showExcludeNodes, viewMode]);

  // ── Pan/zoom canvas to a node ────────────────────────────────────────────────

  const panToNode = useCallback((nodeId) => {
    const n = nodesRef.current.find(nd => nd.id === nodeId);
    if (!n || !svgRef.current || !zoomRef.current) return;
    const { width, height } = dimensions;
    const scale = 2;
    d3.select(svgRef.current)
      .transition()
      .duration(500)
      .call(
        zoomRef.current.transform,
        d3.zoomIdentity
          .translate(width / 2 - n.x * scale, height / 2 - n.y * scale)
          .scale(scale),
      );
  }, [dimensions]);

  // ── Node navigation (jump from relation row) ─────────────────────────────────

  const handleJumpToNode = useCallback((nodeId) => {
    panToNode(nodeId);
    const topic = topics.find(t => t.id === nodeId);
    if (topic) setSelectedNode(topic);
  }, [panToNode, topics]);

  // ── Node mutation handlers ───────────────────────────────────────────────────

  const handleNodeSave = useCallback(async (updatedTopic) => {
    await updateTopic(updatedTopic);
    setSelectedNode(updatedTopic);
  }, [updateTopic]);

  const handleNodeDelete = useCallback(async () => {
    if (!selectedNode) return;
    if (confirm(`Are you sure you want to delete "${selectedNode.label}"? This will also remove any relations connected to it.`)) {
      await deleteTopic(selectedNode.id);
      setSelectedNode(null);
    }
  }, [selectedNode, deleteTopic]);

  // ── Snapshot restore ─────────────────────────────────────────────────────────

  const handleRestore = useCallback(async () => {
    if (!confirm(
      `Restore the graph to the snapshot from ${new Date(snapshotMeta?.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}?\n\nThis will undo the last Analyze run. The snapshot will be cleared after restoring.`,
    )) return;
    setIsRestoring(true);
    try {
      await restoreSnapshot();
      setSelectedNode(null);
    } finally {
      setIsRestoring(false);
    }
  }, [restoreSnapshot, snapshotMeta]);

  // ── AI graph analysis (scoped) ───────────────────────────────────────────────

  const analyzeGraph = useCallback(async (scope = 'full') => {
    if (!confirm(SCOPE_CONFIRM[scope])) return;

    setIsAnalyzing(true);
    setAnalyzeError(null);
    setAnalyzeNotice(null);

    try {
      const [currentTopics, currentRelations] = await Promise.all([
        db.getTopics(),
        db.getRelations(),
      ]);

      const tier = scope === 'full' ? 'reasoning' : 'standard';

      // For full + relevance scopes the model needs to see relevance_locked so
      // it can honor the "do not touch protected topics" rule. (relations-scope
      // only emits newRelations, so the flag is irrelevant there.)
      const sendLocked = scope === 'full' || scope === 'relevance';
      const compactTopics = currentTopics.map(t => ({
        id: t.id,
        label: t.label,
        type: t.type,
        learning_relevance: t.learning_relevance,
        ...(sendLocked && t.relevance_locked ? { relevance_locked: true } : {}),
      }));
      const compactRelations = currentRelations.map(({ source, target, type }) => ({
        source, target, type,
      }));

      const llmResult = await callLLM({
        task: `graph.analyze.${scope}`,
        tier,
        system: [{ type: 'text', text: SYSTEM_PROMPTS[scope], cache_control: { type: 'ephemeral' } }],
        user: `Here is the current knowledge graph:\n${JSON.stringify({ topics: compactTopics, relations: compactRelations })}`,
        tools: [EMIT_GRAPH_ACTIONS_TOOL],
        toolChoice: { type: 'tool', name: EMIT_GRAPH_ACTIONS_TOOL.name },
        maxTokens: scope === 'full' ? 4096 : 2048,
      });

      const rawActions = llmResult.toolUses[0]?.input;
      if (!rawActions) throw new Error('Graph analysis did not emit a tool result.');

      // ── Safety guard ─────────────────────────────────────────────────────
      // Strip merges/deletions that target excluded or locked topics. Excluded
      // topics are reference material kept on purpose for R42; locked topics
      // are admin-pinned. The AI may suggest removing them anyway — we drop
      // those suggestions before they reach bulkSave.
      const { filtered: actions, dropped } = filterAiActions(currentTopics, rawActions);
      const droppedCount = dropped.deletions.length + dropped.merges.length;
      if (droppedCount > 0) {
        // Visible feedback so the admin knows the AI tried to touch protected rows.
        console.warn(
          `[graph guard] Blocked ${droppedCount} destructive action(s) on protected topics`,
          dropped,
        );
        setAnalyzeNotice(
          `Guard blocked ${droppedCount} destructive AI action${droppedCount === 1 ? '' : 's'} on excluded or locked topics. ${dropped.deletions.length} deletion${dropped.deletions.length === 1 ? '' : 's'}, ${dropped.merges.length} merge${dropped.merges.length === 1 ? '' : 's'}.`,
        );
      }

      let updatedTopics = [...currentTopics];
      let updatedRelations = [...currentRelations];

      // Full scope: merges + deletions
      if (scope === 'full') {
        for (const merge of actions.merges ?? []) {
          updatedTopics = updatedTopics.filter(t => t.id !== merge.deleteId);
          updatedRelations = updatedRelations.map(r => ({
            ...r,
            source: r.source === merge.deleteId ? merge.keepId : r.source,
            target: r.target === merge.deleteId ? merge.keepId : r.target,
          }));
        }
        if (actions.deletions?.length) {
          const toDelete = new Set(actions.deletions);
          updatedTopics = updatedTopics.filter(t => !toDelete.has(t.id));
          updatedRelations = updatedRelations.filter(
            r => !toDelete.has(r.source) && !toDelete.has(r.target),
          );
        }
      }

      // Full + relations scope: new relations
      if (scope === 'full' || scope === 'relations') {
        for (const rel of actions.newRelations ?? []) {
          const dup = updatedRelations.some(
            r => r.source === rel.source && r.target === rel.target && r.type === rel.type,
          );
          if (!dup) updatedRelations.push(rel);
        }
      }

      // Full + relevance scope: relevance updates (respect locked topics)
      if (scope === 'full' || scope === 'relevance') {
        for (const update of actions.relevanceUpdates ?? []) {
          const idx = updatedTopics.findIndex(t => t.id === update.id);
          if (idx !== -1 && !updatedTopics[idx].relevance_locked) {
            updatedTopics[idx] = { ...updatedTopics[idx], learning_relevance: update.learning_relevance };
          }
        }
      }

      // Drop edges that now reference non-existent nodes or are self-loops.
      const finalIds = new Set(updatedTopics.map(t => t.id));
      updatedRelations = updatedRelations.filter(
        r => r.source !== r.target && finalIds.has(r.source) && finalIds.has(r.target),
      );

      await bulkSave(updatedTopics, updatedRelations);
      setSelectedNode(null);
    } catch (e) {
      setAnalyzeError(e.message || 'Analysis failed. Please try again.');
    } finally {
      setIsAnalyzing(false);
    }
  }, [bulkSave]);

  // ── Render ───────────────────────────────────────────────────────────────────

  return (
    <div className="relative w-full h-full flex flex-col md:flex-row">
      {/* Main view: graph canvas or table */}
      <div className="flex-1 flex flex-col border-r border-bg-warm min-w-0">
        {/* View-mode toggle */}
        <div className="flex items-center gap-1 px-3 py-2 border-b border-bg-warm bg-paper">
          <button
            onClick={() => setViewMode('graph')}
            className={`flex items-center gap-1.5 px-3 py-1 text-xs rounded-[var(--r-sm)] transition-colors ${
              viewMode === 'graph' ? 'bg-bg-warm text-teal font-medium' : 'text-fg-muted hover:bg-bg-warm/50'
            }`}
            title="Force-directed graph view"
          >
            <Network size={13} /> Graph
          </button>
          <button
            onClick={() => setViewMode('table')}
            className={`flex items-center gap-1.5 px-3 py-1 text-xs rounded-[var(--r-sm)] transition-colors ${
              viewMode === 'table' ? 'bg-bg-warm text-teal font-medium' : 'text-fg-muted hover:bg-bg-warm/50'
            }`}
            title="Sortable table with bulk edit"
          >
            <Table2 size={13} /> Table
          </button>
        </div>

        {viewMode === 'graph' ? (
          <div
            ref={wrapperRef}
            className="flex-1 h-[400px] md:h-full cursor-grab active:cursor-grabbing"
          >
            {topics.length === 0 ? (
              <div className="h-full flex items-center justify-center text-fg-muted p-8 text-center">
                No knowledge graph data yet. Upload source material in the Sources tab first.
              </div>
            ) : (
              <svg ref={svgRef} className="w-full h-full" />
            )}
          </div>
        ) : (
          <div className="flex-1 min-h-[400px] overflow-hidden">
            {topics.length === 0 ? (
              <div className="h-full flex items-center justify-center text-fg-muted p-8 text-center">
                No knowledge graph data yet. Upload source material in the Sources tab first.
              </div>
            ) : (
              <GraphTable
                topics={topics}
                relations={relations}
                onBulkUpdate={bulkUpdateTopics}
                onSelectNode={setSelectedNode}
              />
            )}
          </div>
        )}
      </div>

      {/* Sidebar */}
      <div className="w-full md:w-80 bg-paper p-6 overflow-y-auto border-t md:border-t-0 border-bg-warm flex flex-col">
        <GraphControls
          showExcludeNodes={showExcludeNodes}
          onShowExcludeChange={setShowExcludeNodes}
          excludedCount={topics.filter(t => t.learning_relevance === 'exclude').length}
          onAnalyze={analyzeGraph}
          isAnalyzing={isAnalyzing}
          analyzeError={analyzeError}
          analyzeNotice={analyzeNotice}
          disabled={topics.length === 0}
          onApplied={reload}
          snapshotMeta={snapshotMeta}
          onRestore={handleRestore}
          isRestoring={isRestoring}
        />
        <NodeDetailPanel
          selectedNode={selectedNode}
          topics={topics}
          relations={relations}
          onSave={handleNodeSave}
          onDelete={handleNodeDelete}
          onAddRelation={addRelation}
          onRemoveRelation={removeRelation}
          onJumpToNode={handleJumpToNode}
        />
      </div>
    </div>
  );
};

export default KnowledgeGraph;
