import { Injectable, NotFoundException } from "@nestjs/common";
import { PrismaService } from "../../prisma/prisma.service";
import type {
  EntryGraphResponse,
  GraphNode,
  GraphEdge,
  FullGraphResponse,
  GraphStatsResponse,
} from "../entities/graph.entity";
import { KnowledgeCacheService } from "./cache.service";
import { Prisma } from "@prisma/client";

interface GraphFilterOptions {
  tags?: string[];
  status?: string;
  limit?: number;
}

/**
 * Service for knowledge graph operations
 */
@Injectable()
export class GraphService {
  constructor(
    private readonly prisma: PrismaService,
    private readonly cache: KnowledgeCacheService
  ) {}

  /**
   * Get entry-centered graph view by slug
   * Helper method that looks up the entry ID first
   */
  async getEntryGraphBySlug(
    workspaceId: string,
    slug: string,
    maxDepth = 1
  ): Promise<EntryGraphResponse> {
    // Find entry by slug
    const entry = await this.prisma.knowledgeEntry.findUnique({
      where: {
        workspaceId_slug: {
          workspaceId,
          slug,
        },
      },
    });

    if (!entry) {
      throw new NotFoundException("Entry not found");
    }

    return this.getEntryGraph(workspaceId, entry.id, maxDepth);
  }

  /**
   * Get entry-centered graph view
   * Returns the entry and all connected nodes up to specified depth
   */
  async getEntryGraph(
    workspaceId: string,
    entryId: string,
    maxDepth = 1
  ): Promise<EntryGraphResponse> {
    // Check cache first
    const cached = await this.cache.getGraph<EntryGraphResponse>(workspaceId, entryId, maxDepth);
    if (cached) {
      return cached;
    }

    // Verify entry exists
    const centerEntry = await this.prisma.knowledgeEntry.findUnique({
      where: { id: entryId },
      include: {
        tags: {
          include: {
            tag: true,
          },
        },
      },
    });

    if (!centerEntry || centerEntry.workspaceId !== workspaceId) {
      throw new NotFoundException("Entry not found");
    }

    // Build graph using BFS
    const visitedNodes = new Set<string>();
    const nodes: GraphNode[] = [];
    const edges: GraphEdge[] = [];
    const nodeDepths = new Map<string, number>();

    // Queue: [entryId, depth]
    const queue: [string, number][] = [[entryId, 0]];
    visitedNodes.add(entryId);
    nodeDepths.set(entryId, 0);

    while (queue.length > 0) {
      const item = queue.shift();
      if (!item) break; // Should never happen, but satisfy TypeScript
      const [currentId, depth] = item;

      // Fetch current entry with related data
      const currentEntry = await this.prisma.knowledgeEntry.findUnique({
        where: { id: currentId },
        include: {
          tags: {
            include: {
              tag: true,
            },
          },
          outgoingLinks: {
            include: {
              target: {
                select: {
                  id: true,
                  slug: true,
                  title: true,
                  summary: true,
                },
              },
            },
          },
          incomingLinks: {
            include: {
              source: {
                select: {
                  id: true,
                  slug: true,
                  title: true,
                  summary: true,
                },
              },
            },
          },
        },
      });

      if (!currentEntry) continue;

      // Add current node
      const graphNode: GraphNode = {
        id: currentEntry.id,
        slug: currentEntry.slug,
        title: currentEntry.title,
        summary: currentEntry.summary,
        tags: currentEntry.tags.map((et) => ({
          id: et.tag.id,
          name: et.tag.name,
          slug: et.tag.slug,
          color: et.tag.color,
        })),
        depth,
      };
      nodes.push(graphNode);

      // Continue BFS if not at max depth
      if (depth < maxDepth) {
        // Process outgoing links (only resolved ones)
        for (const link of currentEntry.outgoingLinks) {
          // Skip unresolved links
          if (!link.targetId || !link.resolved) continue;

          // Add edge
          edges.push({
            id: link.id,
            sourceId: link.sourceId,
            targetId: link.targetId,
            linkText: link.linkText,
          });

          // Add target to queue if not visited
          if (!visitedNodes.has(link.targetId)) {
            visitedNodes.add(link.targetId);
            nodeDepths.set(link.targetId, depth + 1);
            queue.push([link.targetId, depth + 1]);
          }
        }

        // Process incoming links (only resolved ones)
        for (const link of currentEntry.incomingLinks) {
          // Skip unresolved links
          if (!link.targetId || !link.resolved) continue;

          // Add edge
          const edgeExists = edges.some(
            (e) => e.sourceId === link.sourceId && e.targetId === link.targetId
          );
          if (!edgeExists) {
            edges.push({
              id: link.id,
              sourceId: link.sourceId,
              targetId: link.targetId,
              linkText: link.linkText,
            });
          }

          // Add source to queue if not visited
          if (!visitedNodes.has(link.sourceId)) {
            visitedNodes.add(link.sourceId);
            nodeDepths.set(link.sourceId, depth + 1);
            queue.push([link.sourceId, depth + 1]);
          }
        }
      }
    }

    // Find center node
    const centerNode = nodes.find((n) => n.id === entryId);
    if (!centerNode) {
      throw new Error(`Center node ${entryId} not found in graph`);
    }

    const result: EntryGraphResponse = {
      centerNode,
      nodes,
      edges,
      stats: {
        totalNodes: nodes.length,
        totalEdges: edges.length,
        maxDepth,
      },
    };

    // Cache the result
    await this.cache.setGraph(workspaceId, entryId, maxDepth, result);

    return result;
  }

  /**
   * Get full knowledge graph with optional filtering
   * Returns all entries and links in the workspace
   */
  async getFullGraph(
    workspaceId: string,
    filters?: GraphFilterOptions
  ): Promise<FullGraphResponse> {
    // Build where clause for entries
    const where: Prisma.KnowledgeEntryWhereInput = {
      workspaceId,
    };

    if (filters?.status) {
      where.status = filters.status as Prisma.EnumEntryStatusFilter;
    }

    if (filters?.tags && filters.tags.length > 0) {
      where.tags = {
        some: {
          tag: {
            slug: {
              in: filters.tags,
            },
          },
        },
      };
    }

    // Build query options
    const queryOptions: {
      where: Prisma.KnowledgeEntryWhereInput;
      include: {
        tags: {
          include: {
            tag: true;
          };
        };
      };
      take?: number;
      orderBy: {
        updatedAt: "desc";
      };
    } = {
      where,
      include: {
        tags: {
          include: {
            tag: true,
          },
        },
      },
      orderBy: {
        updatedAt: "desc",
      },
    };

    if (filters?.limit !== undefined) {
      queryOptions.take = filters.limit;
    }

    // Fetch entries
    const entries = await this.prisma.knowledgeEntry.findMany(queryOptions);

    // Get entry IDs for link filtering
    const entryIds = entries.map((e) => e.id);

    // Fetch all links between these entries
    const links = await this.prisma.knowledgeLink.findMany({
      where: {
        sourceId: { in: entryIds },
        targetId: { in: entryIds },
        resolved: true,
      },
    });

    // Build nodes
    const nodes: GraphNode[] = entries.map((entry) => ({
      id: entry.id,
      slug: entry.slug,
      title: entry.title,
      summary: entry.summary,
      status: entry.status,
      tags: entry.tags.map(
        (et: { tag: { id: string; name: string; slug: string; color: string | null } }) => ({
          id: et.tag.id,
          name: et.tag.name,
          slug: et.tag.slug,
          color: et.tag.color,
        })
      ),
      depth: 0, // Full graph has no depth concept
      isOrphan: false, // Will be calculated next
    }));

    // Build edges
    const edges: GraphEdge[] = links.map((link) => ({
      id: link.id,
      sourceId: link.sourceId,
      targetId: link.targetId,
      linkText: link.linkText,
    }));

    // Detect orphans (entries with no incoming or outgoing links)
    const connectedIds = new Set<string>();
    for (const edge of edges) {
      connectedIds.add(edge.sourceId);
      connectedIds.add(edge.targetId);
    }

    let orphanCount = 0;
    for (const node of nodes) {
      if (!connectedIds.has(node.id)) {
        node.isOrphan = true;
        orphanCount++;
      }
    }

    return {
      nodes,
      edges,
      stats: {
        totalNodes: nodes.length,
        totalEdges: edges.length,
        orphanCount,
      },
    };
  }

  /**
   * Get graph statistics including orphan detection
   */
  async getGraphStats(workspaceId: string): Promise<GraphStatsResponse> {
    // Get total counts
    const [totalEntries, totalLinks] = await Promise.all([
      this.prisma.knowledgeEntry.count({
        where: { workspaceId },
      }),
      this.prisma.knowledgeLink.count({
        where: {
          source: { workspaceId },
          resolved: true,
        },
      }),
    ]);

    // Calculate average links per entry
    const averageLinks = totalEntries > 0 ? totalLinks / totalEntries : 0;

    // Find most connected entries using raw query for better performance
    const mostConnected = await this.prisma.$queryRaw<
      {
        id: string;
        slug: string;
        title: string;
        link_count: string;
      }[]
    >`
      SELECT
        e.id,
        e.slug,
        e.title,
        COUNT(DISTINCT l.id) as link_count
      FROM knowledge_entries e
      LEFT JOIN knowledge_links l ON (l.source_id = e.id OR l.target_id = e.id)
      WHERE e.workspace_id = ${workspaceId}::uuid
        AND (l.resolved = true OR l.id IS NULL)
      GROUP BY e.id, e.slug, e.title
      ORDER BY link_count DESC
      LIMIT 10
    `;

    const mostConnectedEntries = mostConnected.map((entry) => ({
      id: entry.id,
      slug: entry.slug,
      title: entry.title,
      linkCount: parseInt(entry.link_count, 10),
    }));

    // Find orphan entries (entries with no links)
    const orphanEntries = await this.prisma.knowledgeEntry.findMany({
      where: {
        workspaceId,
        AND: [
          {
            outgoingLinks: {
              none: {
                resolved: true,
              },
            },
          },
          {
            incomingLinks: {
              none: {
                resolved: true,
              },
            },
          },
        ],
      },
      select: {
        id: true,
      },
    });

    // Get tag distribution
    const tagGroups = await this.prisma.$queryRaw<
      {
        tag_id: string;
        tag_name: string;
        entry_count: string;
      }[]
    >`
      SELECT
        t.id as tag_id,
        t.name as tag_name,
        COUNT(DISTINCT et.entry_id) as entry_count
      FROM knowledge_tags t
      LEFT JOIN knowledge_entry_tags et ON et.tag_id = t.id
      WHERE t.workspace_id = ${workspaceId}::uuid
      GROUP BY t.id, t.name
      ORDER BY entry_count DESC
      LIMIT 20
    `;

    const tagDistribution = tagGroups.map((tag) => ({
      tagId: tag.tag_id,
      tagName: tag.tag_name,
      entryCount: parseInt(tag.entry_count, 10),
    }));

    return {
      totalEntries,
      totalLinks,
      orphanEntries: orphanEntries.length,
      averageLinks,
      mostConnectedEntries,
      tagDistribution,
    };
  }
}
