/* eslint-disable @typescript-eslint/no-unnecessary-condition */
"use client";

import { useCallback, useEffect, useState } from "react";
import { useSession } from "@/lib/auth-client";
import { handleSessionExpired, isSessionExpiring } from "@/lib/api";
import { API_BASE_URL } from "@/lib/config";

// API Response types
interface TagDto {
  slug: string;
  name: string;
}

interface EntryDto {
  id: string;
  slug: string;
  title: string;
  content: string;
  summary: string;
  status: string;
  visibility: string;
  tags: TagDto[];
  createdBy: string;
  updatedBy: string;
  createdAt: string;
  updatedAt: string;
}

interface EntriesResponse {
  data: EntryDto[];
}

interface BacklinksResponse {
  backlinks: { id: string }[];
}

interface CreateEntryDto {
  title: string;
  content: string;
  summary: string;
  tags: string[];
  status: string;
  visibility: string;
}

interface UpdateEntryDto {
  title?: string;
  content?: string | null;
  summary?: string;
  tags?: string[];
}

export interface KnowledgeNode {
  id: string;
  title: string;
  node_type: string;
  content: string | null;
  tags: string[];
  domain: string | null;
  metadata: Record<string, unknown>;
  created_at: string;
  updated_at: string;
}

/** Input type for creating a new node (without server-generated fields) */
export type NodeCreateInput = Omit<KnowledgeNode, "id" | "created_at" | "updated_at">;

/** Input type for creating a new edge (without server-generated fields) */
export type EdgeCreateInput = Omit<KnowledgeEdge, "created_at">;

export interface KnowledgeEdge {
  source_id: string;
  target_id: string;
  relation_type: string;
  weight: number;
  metadata: Record<string, unknown>;
  created_at: string;
}

export interface GraphData {
  nodes: KnowledgeNode[];
  edges: KnowledgeEdge[];
}

export interface MermaidData {
  diagram: string;
  style: string;
}

export interface GraphStatistics {
  node_count: number;
  edge_count: number;
  nodes_by_type: Record<string, number>;
  edges_by_type: Record<string, number>;
}

interface UseGraphDataOptions {
  rootId?: string;
  maxDepth?: number;
  autoFetch?: boolean;
}

interface UseGraphDataResult {
  graph: GraphData | null;
  mermaid: MermaidData | null;
  statistics: GraphStatistics | null;
  isLoading: boolean;
  error: string | null;
  fetchGraph: () => Promise<void>;
  fetchMermaid: (style?: "flowchart" | "mindmap") => Promise<void>;
  fetchStatistics: () => Promise<void>;
  createNode: (
    node: Omit<KnowledgeNode, "id" | "created_at" | "updated_at">
  ) => Promise<KnowledgeNode | null>;
  updateNode: (id: string, updates: Partial<KnowledgeNode>) => Promise<KnowledgeNode | null>;
  deleteNode: (id: string) => Promise<boolean>;
  createEdge: (edge: Omit<KnowledgeEdge, "created_at">) => Promise<KnowledgeEdge | null>;
  deleteEdge: (sourceId: string, targetId: string, relationType: string) => Promise<boolean>;
  searchNodes: (query: string) => Promise<KnowledgeNode[]>;
}

const API_BASE = API_BASE_URL;

/**
 * Sanitize labels for Mermaid diagrams to prevent XSS
 * Removes HTML tags, dangerous protocols, and special characters
 */
function sanitizeMermaidLabel(label: string): string {
  if (!label) return "";

  // Remove HTML tags
  let sanitized = label.replace(/<[^>]*>/g, "");

  // Remove dangerous protocols
  sanitized = sanitized.replace(/javascript:/gi, "");
  sanitized = sanitized.replace(/data:/gi, "");
  sanitized = sanitized.replace(/vbscript:/gi, "");

  // Remove control characters
  // eslint-disable-next-line no-control-regex
  sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, "");

  // Escape Mermaid special characters that could break syntax
  sanitized = sanitized.replace(/["\n\r]/g, " ");
  sanitized = sanitized.replace(/[[\](){}]/g, "");

  // Truncate to prevent DoS
  sanitized = sanitized.slice(0, 200);

  return sanitized.trim();
}

async function apiFetch<T>(
  endpoint: string,
  accessToken: string | null,
  options: RequestInit = {}
): Promise<T> {
  // Skip request if session is already expiring (prevents request storms)
  if (isSessionExpiring()) {
    throw new Error("Session expired");
  }

  const headers: Record<string, string> = {
    "Content-Type": "application/json",
    ...(options.headers as Record<string, string>),
  };

  // Add Authorization header if we have a token
  if (accessToken) {
    headers.Authorization = `Bearer ${accessToken}`;
  }

  const response = await fetch(`${API_BASE}/api/knowledge${endpoint}`, {
    ...options,
    credentials: "include",
    headers,
  });

  if (!response.ok) {
    // Handle session expiration
    if (response.status === 401) {
      handleSessionExpired();
      throw new Error("Session expired");
    }
    const error = (await response
      .json()
      .catch((): { detail?: string; message?: string } => ({ detail: response.statusText }))) as {
      detail?: string;
      message?: string;
    };
    throw new Error(error.detail ?? error.message ?? "API request failed");
  }

  if (response.status === 204) {
    return undefined as T;
  }

  return response.json() as Promise<T>;
}

// Transform Knowledge Entry to Graph Node
function entryToNode(entry: EntryDto): KnowledgeNode {
  const tags = entry.tags;
  return {
    id: entry.id,
    title: entry.title,
    node_type: tags[0]?.slug ?? "concept", // Use first tag as node type, fallback to 'concept'
    content: entry.content ?? entry.summary ?? null,
    tags: tags.map((t): string => t.slug),
    domain: tags.length > 0 ? (tags[0]?.name ?? null) : null,
    metadata: {
      slug: entry.slug,
      status: entry.status,
      visibility: entry.visibility,
      createdBy: entry.createdBy,
      updatedBy: entry.updatedBy,
    },
    created_at: entry.createdAt,
    updated_at: entry.updatedAt,
  };
}

// Transform Node to Entry Create DTO
function nodeToCreateDto(
  node: Omit<KnowledgeNode, "id" | "created_at" | "updated_at">
): CreateEntryDto {
  return {
    title: node.title,
    content: node.content ?? "",
    summary: node.content?.slice(0, 200) ?? "",
    tags: node.tags.length > 0 ? node.tags : [node.node_type],
    status: "PUBLISHED",
    visibility: "WORKSPACE",
  };
}

// Transform Node update to Entry Update DTO
function nodeToUpdateDto(updates: Partial<KnowledgeNode>): UpdateEntryDto {
  const dto: UpdateEntryDto = {};

  if (updates.title !== undefined) dto.title = updates.title;
  if (updates.content !== undefined) {
    dto.content = updates.content;
    dto.summary = updates.content?.slice(0, 200) ?? "";
  }
  if (updates.tags !== undefined) dto.tags = updates.tags;

  return dto;
}

export function useGraphData(options: UseGraphDataOptions = {}): UseGraphDataResult {
  const { autoFetch = true } = options;

  // Get access token from BetterAuth session
  const { data: sessionData } = useSession();
  const accessToken =
    (sessionData?.user as { accessToken?: string } | undefined)?.accessToken ?? null;

  const [graph, setGraph] = useState<GraphData | null>(null);
  const [mermaid, setMermaid] = useState<MermaidData | null>(null);
  const [statistics, setStatistics] = useState<GraphStatistics | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchGraph = useCallback(async (): Promise<void> => {
    if (!accessToken) {
      setError("Not authenticated");
      return;
    }
    setIsLoading(true);
    setError(null);
    try {
      // Fetch all entries
      const response = await apiFetch<EntriesResponse>("/entries?limit=100", accessToken);
      const entries = response.data;

      // Transform entries to nodes
      const nodes: KnowledgeNode[] = entries.map(entryToNode);

      // Fetch backlinks for all entries to build edges
      const edges: KnowledgeEdge[] = [];
      const edgeSet = new Set<string>(); // To avoid duplicates

      for (const entry of entries) {
        try {
          const backlinksResponse = await apiFetch<BacklinksResponse>(
            `/entries/${entry.slug}/backlinks`,
            accessToken
          );

          for (const backlink of backlinksResponse.backlinks) {
            const edgeId = `${backlink.id}-${entry.id}`;
            if (!edgeSet.has(edgeId)) {
              edges.push({
                source_id: backlink.id,
                target_id: entry.id,
                relation_type: "relates_to",
                weight: 1.0,
                metadata: {},
                created_at: new Date().toISOString(),
              });
              edgeSet.add(edgeId);
            }
          }
        } catch {
          // Silently skip backlink errors for individual entries
          // Logging suppressed to avoid console pollution in production
        }
      }

      setGraph({ nodes, edges });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to fetch graph");
    } finally {
      setIsLoading(false);
    }
  }, [accessToken]);

  const fetchMermaid = useCallback(
    (style: "flowchart" | "mindmap" = "flowchart"): Promise<void> => {
      if (!graph) {
        setError("No graph data available");
        return Promise.resolve();
      }

      setIsLoading(true);
      setError(null);
      try {
        // Generate Mermaid diagram from graph data
        let diagram = "";

        if (style === "mindmap") {
          diagram = "mindmap\n  root((Knowledge))\n";

          // Group nodes by type
          const nodesByType: Record<string, KnowledgeNode[]> = {};
          graph.nodes.forEach((node): void => {
            const nodeType = node.node_type;
            nodesByType[nodeType] ??= [];
            nodesByType[nodeType].push(node);
          });

          // Add nodes by type with sanitized labels
          Object.entries(nodesByType).forEach(([type, nodes]): void => {
            diagram += `    ${sanitizeMermaidLabel(type)}\n`;
            nodes.forEach((node): void => {
              diagram += `      ${sanitizeMermaidLabel(node.title)}\n`;
            });
          });
        } else {
          diagram = "graph TD\n";

          // Add all edges with sanitized labels
          graph.edges.forEach((edge): void => {
            const source = graph.nodes.find((n): boolean => n.id === edge.source_id);
            const target = graph.nodes.find((n): boolean => n.id === edge.target_id);

            if (source && target) {
              const sourceLabel = sanitizeMermaidLabel(source.title);
              const targetLabel = sanitizeMermaidLabel(target.title);
              diagram += `  ${edge.source_id}["${sourceLabel}"] --> ${edge.target_id}["${targetLabel}"]\n`;
            }
          });

          // Add standalone nodes (no edges) with sanitized labels
          graph.nodes.forEach((node): void => {
            const hasEdge = graph.edges.some(
              (e): boolean => e.source_id === node.id || e.target_id === node.id
            );
            if (!hasEdge) {
              const label = sanitizeMermaidLabel(node.title);
              diagram += `  ${node.id}["${label}"]\n`;
            }
          });
        }

        setMermaid({
          diagram,
          style: style,
        });
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to generate diagram");
      } finally {
        setIsLoading(false);
      }
      return Promise.resolve();
    },
    [graph]
  );

  const fetchStatistics = useCallback((): Promise<void> => {
    if (!graph) return Promise.resolve();

    try {
      const nodesByType: Record<string, number> = {};
      const edgesByType: Record<string, number> = {};

      graph.nodes.forEach((node): void => {
        nodesByType[node.node_type] = (nodesByType[node.node_type] ?? 0) + 1;
      });

      graph.edges.forEach((edge): void => {
        edgesByType[edge.relation_type] = (edgesByType[edge.relation_type] ?? 0) + 1;
      });

      setStatistics({
        node_count: graph.nodes.length,
        edge_count: graph.edges.length,
        nodes_by_type: nodesByType,
        edges_by_type: edgesByType,
      });
    } catch {
      // Silently fail - statistics are non-critical
    }
    return Promise.resolve();
  }, [graph]);

  const createNode = useCallback(
    async (
      node: Omit<KnowledgeNode, "id" | "created_at" | "updated_at">
    ): Promise<KnowledgeNode | null> => {
      if (!accessToken) {
        setError("Not authenticated");
        return null;
      }
      try {
        const createDto = nodeToCreateDto(node);
        const created = await apiFetch<EntryDto>("/entries", accessToken, {
          method: "POST",
          body: JSON.stringify(createDto),
        });
        await fetchGraph();
        return entryToNode(created);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to create node");
        return null;
      }
    },
    [fetchGraph, accessToken]
  );

  const updateNode = useCallback(
    async (id: string, updates: Partial<KnowledgeNode>): Promise<KnowledgeNode | null> => {
      if (!accessToken) {
        setError("Not authenticated");
        return null;
      }
      try {
        // Find the node to get its slug
        const node = graph?.nodes.find((n) => n.id === id);
        if (!node) {
          throw new Error("Node not found");
        }

        const slug = node.metadata.slug as string;
        const updateDto = nodeToUpdateDto(updates);

        const updated = await apiFetch<EntryDto>(`/entries/${slug}`, accessToken, {
          method: "PUT",
          body: JSON.stringify(updateDto),
        });
        await fetchGraph();
        return entryToNode(updated);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to update node");
        return null;
      }
    },
    [fetchGraph, accessToken, graph]
  );

  const deleteNode = useCallback(
    async (id: string): Promise<boolean> => {
      if (!accessToken) {
        setError("Not authenticated");
        return false;
      }
      try {
        // Find the node to get its slug
        const node = graph?.nodes.find((n) => n.id === id);
        if (!node) {
          throw new Error("Node not found");
        }

        const slug = node.metadata.slug as string;
        await apiFetch(`/entries/${slug}`, accessToken, { method: "DELETE" });
        await fetchGraph();
        return true;
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to delete node");
        return false;
      }
    },
    [fetchGraph, accessToken, graph]
  );

  const createEdge = useCallback(
    async (edge: Omit<KnowledgeEdge, "created_at">): Promise<KnowledgeEdge | null> => {
      if (!accessToken) {
        setError("Not authenticated");
        return null;
      }
      try {
        // For now, we'll store the edge in local state only
        // The Knowledge API uses backlinks which are automatically created from wiki-links in content
        // To properly create a link, we'd need to update the source entry's content to include a wiki-link

        // Find source and target nodes
        const sourceNode = graph?.nodes.find((n): boolean => n.id === edge.source_id);
        const targetNode = graph?.nodes.find((n): boolean => n.id === edge.target_id);

        if (!sourceNode || !targetNode) {
          throw new Error("Source or target node not found");
        }

        // Update source node content to include a link to target
        const targetSlug = targetNode.metadata.slug as string;
        const wikiLink = `[[${targetSlug}|${targetNode.title}]]`;
        const updatedContent = sourceNode.content
          ? `${sourceNode.content}\n\n${wikiLink}`
          : wikiLink;

        const slug = sourceNode.metadata.slug as string;
        await apiFetch(`/entries/${slug}`, accessToken, {
          method: "PUT",
          body: JSON.stringify({
            content: updatedContent,
          }),
        });

        // Refresh graph to get updated backlinks
        await fetchGraph();

        return {
          ...edge,
          created_at: new Date().toISOString(),
        };
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to create edge");
        return null;
      }
    },
    [fetchGraph, accessToken, graph]
  );

  const deleteEdge = useCallback(
    async (sourceId: string, targetId: string, _relationType: string): Promise<boolean> => {
      if (!accessToken) {
        setError("Not authenticated");
        return false;
      }
      try {
        // To delete an edge, we need to remove the wiki-link from the source content
        const sourceNode = graph?.nodes.find((n): boolean => n.id === sourceId);
        const targetNode = graph?.nodes.find((n): boolean => n.id === targetId);

        if (!sourceNode || !targetNode) {
          throw new Error("Source or target node not found");
        }

        const targetSlug = targetNode.metadata.slug as string;
        // eslint-disable-next-line security/detect-non-literal-regexp
        const wikiLinkPattern = new RegExp(`\\[\\[${targetSlug}(?:\\|[^\\]]+)?\\]\\]`, "g");
        const updatedContent = sourceNode.content?.replace(wikiLinkPattern, "") ?? "";

        const slug = sourceNode.metadata.slug as string;
        await apiFetch(`/entries/${slug}`, accessToken, {
          method: "PUT",
          body: JSON.stringify({
            content: updatedContent,
          }),
        });

        await fetchGraph();
        return true;
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to delete edge");
        return false;
      }
    },
    [fetchGraph, accessToken, graph]
  );

  const searchNodes = useCallback(
    async (query: string): Promise<KnowledgeNode[]> => {
      if (!accessToken) {
        setError("Not authenticated");
        return [];
      }
      try {
        const params = new URLSearchParams({ q: query, limit: "50" });
        const response = await apiFetch<EntriesResponse>(`/search?${params}`, accessToken);
        const results = response.data;
        return results.map(entryToNode);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to search");
        return [];
      }
    },
    [accessToken]
  );

  // Initial data fetch - only run when autoFetch is true and we have an access token
  useEffect(() => {
    if (autoFetch && accessToken) {
      void fetchGraph();
    }
  }, [autoFetch, accessToken, fetchGraph]);

  // Update statistics when graph changes
  useEffect((): void => {
    if (graph) {
      void fetchStatistics();
    }
  }, [graph, fetchStatistics]);

  return {
    graph,
    mermaid,
    statistics,
    isLoading,
    error,
    fetchGraph,
    fetchMermaid,
    fetchStatistics,
    createNode,
    updateNode,
    deleteNode,
    createEdge,
    deleteEdge,
    searchNodes,
  };
}
