import { useState, useEffect, useCallback, useRef } from 'react';
import * as db from '../lib/db';

/**
 * Centralises all knowledge-graph data fetching and mutation.
 *
 * Returns stable callback refs so callers can safely list them in
 * useEffect dependency arrays without causing infinite loops.
 *
 * Relation shape contract: source and target are always plain strings.
 * The normalization in db.getRelations() enforces this at the DB boundary;
 * the filter here drops any orphaned edges whose endpoints no longer exist.
 *
 * Snapshot contract:
 *   bulkSave() writes a snapshot of the CURRENT state to PocketBase before
 *   overwriting it — so the admin can always roll back one step. snapshotMeta
 *   is non-null whenever a rollback point exists; restoreSnapshot() applies it
 *   and then clears it so it cannot be applied twice.
 */
export function useGraphData() {
  const [topics, setTopics] = useState([]);
  const [relations, setRelations] = useState([]);

  // { ts: number, topicCount: number, relationCount: number }
  const [snapshotMeta, setSnapshotMeta] = useState(null);

  // Refs that always hold the latest state — lets mutation callbacks avoid
  // stale-closure bugs without including state in their dependency arrays.
  const topicsRef = useRef([]);
  const relationsRef = useRef([]);

  useEffect(() => { topicsRef.current = topics; }, [topics]);
  useEffect(() => { relationsRef.current = relations; }, [relations]);

  // ── Load ────────────────────────────────────────────────────────────────────

  const reload = useCallback(async () => {
    const [allTopics, allRelations] = await Promise.all([
      db.getTopics(),
      db.getRelations(), // already normalised to string IDs
    ]);
    const topicIds = new Set(allTopics.map(t => t.id));
    setTopics(allTopics);
    setRelations(allRelations.filter(r => topicIds.has(r.source) && topicIds.has(r.target)));
  }, []);

  useEffect(() => {
    reload();
    window.addEventListener('respellion:kb-updated', reload);
    return () => window.removeEventListener('respellion:kb-updated', reload);
  }, [reload]);

  // On mount, check whether a snapshot from a previous session exists.
  useEffect(() => {
    const snap = db.getGraphSnapshot();
    if (snap?.ts) {
      setSnapshotMeta({ ts: snap.ts, topicCount: snap.topicCount, relationCount: snap.relationCount });
    }
  }, []);

  // ── Single-topic mutations ───────────────────────────────────────────────────

  /**
   * Persist a topic update and mirror it into local state.
   * Returns the saved topic object.
   */
  const updateTopic = useCallback(async (topic) => {
    await db.upsertTopic(topic);
    setTopics(prev => prev.map(t => (t.id === topic.id ? topic : t)));
    return topic;
  }, []);

  /**
   * Delete a topic plus all relations that reference it.
   * Uses the ref so the callback is stable even as state changes.
   */
  const deleteTopic = useCallback(async (topicId) => {
    const updatedTopics = topicsRef.current.filter(t => t.id !== topicId);
    const updatedRelations = relationsRef.current.filter(
      r => r.source !== topicId && r.target !== topicId,
    );
    await db.saveTopics(updatedTopics);
    await db.saveRelations(updatedRelations);
    await db.deleteContent(topicId);
    setTopics(updatedTopics);
    setRelations(updatedRelations);
  }, []);

  // ── Relation mutations ───────────────────────────────────────────────────────

  /**
   * Add a relation if not already present.
   * Returns true if added, false if it was a duplicate.
   */
  const addRelation = useCallback(async (relation) => {
    const isDup = relationsRef.current.some(
      r => r.source === relation.source &&
           r.target === relation.target &&
           r.type === relation.type,
    );
    if (isDup) return false;
    await db.addRelation(relation);
    setRelations(prev => [...prev, relation]);
    return true;
  }, []);

  /**
   * Apply the same patch to many topics in one call. `ids` is the set of
   * topic IDs to update; `patch` is the partial-topic to merge into each one
   * (e.g. `{ type: 'process' }` or `{ learning_relevance: 'core', relevance_locked: true }`).
   *
   * Persists each topic via db.upsertTopic in parallel and mirrors into state.
   * Returns the number of topics actually written (selected ∩ existing).
   */
  const bulkUpdateTopics = useCallback(async (ids, patch) => {
    const idSet = new Set(ids);
    const targets = topicsRef.current.filter(t => idSet.has(t.id));
    const updated = targets.map(t => ({ ...t, ...patch }));
    await Promise.all(updated.map(t => db.upsertTopic(t)));
    setTopics(prev => prev.map(t => (idSet.has(t.id) ? { ...t, ...patch } : t)));
    return updated.length;
  }, []);

  /** Remove a specific (source, target, type) relation triple. */
  const removeRelation = useCallback(async (source, target, type) => {
    await db.removeRelation(source, target, type);
    setRelations(prev =>
      prev.filter(r => !(r.source === source && r.target === target && r.type === type)),
    );
  }, []);

  // ── Bulk mutations (used by AI analysis) ────────────────────────────────────

  /**
   * Snapshot the current graph then replace everything in PocketBase and local
   * state. The snapshot is written BEFORE the destructive delete-all/create-all
   * so it survives a mid-operation failure.
   *
   * Called by analyzeGraph after all AI actions have been applied in memory.
   */
  const bulkSave = useCallback(async (newTopics, newRelations) => {
    // Persist a rollback point of the state we are about to overwrite.
    db.saveGraphSnapshot(topicsRef.current, relationsRef.current);
    setSnapshotMeta({
      ts: Date.now(),
      topicCount: topicsRef.current.length,
      relationCount: relationsRef.current.length,
    });

    await db.saveTopics(newTopics);
    await db.saveRelations(newRelations);
    setTopics(newTopics);
    setRelations(newRelations);
  }, []);

  /**
   * Load the latest snapshot from PocketBase and apply it as the current graph.
   * Clears the snapshot afterwards so the admin cannot restore the same point
   * twice (which would silently re-apply the AI changes they just rolled back).
   *
   * Returns true on success, false if no snapshot exists.
   */
  const restoreSnapshot = useCallback(async () => {
    const snap = db.getGraphSnapshot();
    if (!snap?.topics) return false;

    // Apply without taking a new snapshot — we don't want to overwrite
    // the only rollback point we have.
    await db.saveTopics(snap.topics);
    await db.saveRelations(snap.relations);
    setTopics(snap.topics);
    setRelations(snap.relations);

    db.clearGraphSnapshot();
    setSnapshotMeta(null);
    return true;
  }, []);

  return {
    topics,
    relations,
    snapshotMeta,
    reload,
    updateTopic,
    bulkUpdateTopics,
    deleteTopic,
    addRelation,
    removeRelation,
    bulkSave,
    restoreSnapshot,
  };
}
