"""Service layer for querying graph database and computing verification results"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/services/verify.ipynb.

# %% auto #0
__all__ = ['DEBUG_VERIFY_SERVICE', 'VerifyService']

# %% ../../nbs/services/verify.ipynb #verify-service-imports
from typing import Optional, List, Set
import asyncio

from cjm_plugin_system.core.manager import PluginManager
from cjm_graph_plugin_system.core import GraphContext, GraphNode, GraphEdge
from cjm_graph_domains.domains.relations import StructureRelations

from ..models import VerificationResult, SegmentSample
from ..utils import truncate_text

# %% ../../nbs/services/verify.ipynb #debug-flag
DEBUG_VERIFY_SERVICE = False  # Enable for verbose graph query logging

# %% ../../nbs/services/verify.ipynb #verify-service-class
class VerifyService:
    """Service for querying graph database and computing verification results."""
    
    def __init__(
        self,
        plugin_manager: PluginManager,  # Plugin manager for accessing graph plugin
        plugin_name: str = "cjm-graph-plugin-sqlite",  # Name of the graph plugin
    ):
        """Initialize with plugin manager."""
        self._manager = plugin_manager
        self._plugin_name = plugin_name
    
    def is_available(self) -> bool:  # True if plugin is loaded and ready
        """Check if the graph plugin is available."""
        return self._manager.get_plugin(self._plugin_name) is not None
    
    async def _get_context_async(
        self,
        node_id: str,  # UUID of the node to query
        depth: int = 1,  # Traversal depth
    ) -> Optional[GraphContext]:  # GraphContext or None if error
        """Get context from graph plugin asynchronously."""
        try:
            result = await self._manager.execute_plugin_async(
                self._plugin_name,
                action="get_context",
                node_id=node_id,
                depth=depth,
            )
            return GraphContext.from_dict(result)
        except Exception as e:
            if DEBUG_VERIFY_SERVICE:
                print(f"[VerifyService] Plugin error during get_context: {e}")
            return None
    
    def _get_context(
        self,
        node_id: str,  # UUID of the node to query
        depth: int = 1,  # Traversal depth
    ) -> Optional[GraphContext]:  # GraphContext or None if error
        """Get context from graph plugin synchronously."""
        return asyncio.get_event_loop().run_until_complete(
            self._get_context_async(node_id, depth)
        )
    
    async def verify_document_async(
        self,
        document_id: str,  # UUID of the Document node to verify
    ) -> Optional[VerificationResult]:  # Verification results or None if error/not found
        """Query graph and compute verification results for a document."""
        if DEBUG_VERIFY_SERVICE:
            print(f"[VerifyService] verify_document called with id: {document_id}")
        
        if not self.is_available():
            if DEBUG_VERIFY_SERVICE:
                print(f"[VerifyService] Plugin {self._plugin_name} not available")
            return None
        
        # Get context with depth 2 to get Document + Segments + NEXT edges between segments
        # depth=1 only finds edges directly connected to the Document (STARTS_WITH, PART_OF)
        # depth=2 also finds edges between the discovered Segments (NEXT)
        context = await self._get_context_async(document_id, depth=2)
        
        if context is None:
            return None
        
        if DEBUG_VERIFY_SERVICE:
            print(f"[VerifyService] Context has {len(context.nodes)} nodes, {len(context.edges)} edges")
        
        # Handle empty context (document not found)
        if not context.nodes:
            if DEBUG_VERIFY_SERVICE:
                print(f"[VerifyService] Empty context returned - document not found")
            return None
        
        # Extract Document node
        document_node = self._find_document_node(context, document_id)
        if document_node is None:
            if DEBUG_VERIFY_SERVICE:
                print(f"[VerifyService] Document node not found in context")
            return None
        
        # Extract Segment nodes and sort by index
        segment_nodes = self._get_segment_nodes(context)
        segment_nodes.sort(key=lambda n: n.properties.get('index', 0))
        
        if DEBUG_VERIFY_SERVICE:
            print(f"[VerifyService] Found {len(segment_nodes)} segments")
        
        # Compute edge counts by type
        edge_counts = self._count_edges_by_type(context.edges)
        
        if DEBUG_VERIFY_SERVICE:
            print(f"[VerifyService] Edge counts: {edge_counts}")
        
        # Compute timing stats
        timing_stats = self._compute_timing_stats(segment_nodes)
        
        # Compute source stats
        source_stats = self._compute_source_stats(segment_nodes)
        
        # Extract sample segments
        first_samples = self._extract_samples(segment_nodes[:3])
        last_samples = self._extract_samples(segment_nodes[-3:]) if len(segment_nodes) > 3 else []
        
        # Build result
        segment_count = len(segment_nodes)
        
        return VerificationResult(
            # Document info
            document_id=document_id,
            document_title=document_node.properties.get('title', 'Untitled'),
            document_media_type=document_node.properties.get('media_type', 'unknown'),
            
            # Segment stats
            segment_count=segment_count,
            total_duration=timing_stats['total_duration'],
            avg_segment_duration=timing_stats['avg_duration'],
            
            # STARTS_WITH check
            has_starts_with=edge_counts['STARTS_WITH'] >= 1,
            starts_with_count=edge_counts['STARTS_WITH'],
            
            # NEXT chain check
            next_chain_complete=edge_counts['NEXT'] == max(0, segment_count - 1),
            next_count=edge_counts['NEXT'],
            
            # PART_OF check
            part_of_complete=edge_counts['PART_OF'] == segment_count,
            part_of_count=edge_counts['PART_OF'],
            
            # Timing check
            all_have_timing=timing_stats['missing_count'] == 0,
            segments_missing_timing=timing_stats['missing_count'],
            
            # Sources check
            all_have_sources=source_stats['missing_count'] == 0,
            segments_missing_sources=source_stats['missing_count'],
            
            # Source info
            source_plugins=source_stats['plugins'],
            
            # Sample segments
            first_segments=first_samples,
            last_segments=last_samples,
        )
    
    def verify_document(
        self,
        document_id: str,  # UUID of the Document node to verify
    ) -> Optional[VerificationResult]:  # Verification results or None if error/not found
        """Query graph and compute verification results for a document synchronously."""
        return asyncio.get_event_loop().run_until_complete(
            self.verify_document_async(document_id)
        )
    
    async def get_segment_by_index_async(
        self,
        document_id: str,  # UUID of the Document node
        index: int,  # Segment index to fetch
    ) -> Optional[SegmentSample]:  # Segment sample or None if error/not found
        """Fetch a single segment by index for jump-to-index feature."""
        if DEBUG_VERIFY_SERVICE:
            print(f"[VerifyService] get_segment_by_index: document={document_id}, index={index}")
        
        # Validate index is non-negative
        if index < 0:
            if DEBUG_VERIFY_SERVICE:
                print(f"[VerifyService] Invalid index (negative): {index}")
            return None
        
        if not self.is_available():
            return None
        
        # Get context to find all segments
        context = await self._get_context_async(document_id, depth=1)
        
        if context is None:
            return None
        
        # Find segment with matching index
        for node in context.nodes:
            if node.label == 'Segment' and node.properties.get('index') == index:
                return self._node_to_sample(node)
        
        if DEBUG_VERIFY_SERVICE:
            print(f"[VerifyService] Segment with index {index} not found")
        return None
    
    def get_segment_by_index(
        self,
        document_id: str,  # UUID of the Document node
        index: int,  # Segment index to fetch
    ) -> Optional[SegmentSample]:  # Segment sample or None if error/not found
        """Fetch a single segment by index for jump-to-index feature synchronously."""
        return asyncio.get_event_loop().run_until_complete(
            self.get_segment_by_index_async(document_id, index)
        )
    
    async def get_segment_count_async(
        self,
        document_id: str,  # UUID of the Document node
    ) -> int:  # Number of segments or 0 if error
        """Get total segment count for index validation."""
        if not self.is_available():
            return 0
        
        context = await self._get_context_async(document_id, depth=1)
        if context is None:
            return 0
        
        return len([n for n in context.nodes if n.label == 'Segment'])
    
    def get_segment_count(
        self,
        document_id: str,  # UUID of the Document node
    ) -> int:  # Number of segments or 0 if error
        """Get total segment count for index validation synchronously."""
        return asyncio.get_event_loop().run_until_complete(
            self.get_segment_count_async(document_id)
        )
    
    def _find_document_node(
        self,
        context: GraphContext,  # Graph context from query
        document_id: str,  # Expected document UUID
    ) -> Optional[GraphNode]:  # Document node or None
        """Find the Document node in context."""
        for node in context.nodes:
            if node.label == 'Document' and node.id == document_id:
                return node
        return None
    
    def _get_segment_nodes(
        self,
        context: GraphContext,  # Graph context from query
    ) -> List[GraphNode]:  # List of Segment nodes
        """Extract all Segment nodes from context."""
        return [n for n in context.nodes if n.label == 'Segment']
    
    def _count_edges_by_type(
        self,
        edges: List[GraphEdge],  # Edges from graph context
    ) -> dict:  # Counts by relation type
        """Count edges by relation type."""
        counts = {
            'STARTS_WITH': 0,
            'NEXT': 0,
            'PART_OF': 0,
        }
        for edge in edges:
            if edge.relation_type == StructureRelations.STARTS_WITH:
                counts['STARTS_WITH'] += 1
            elif edge.relation_type == StructureRelations.NEXT:
                counts['NEXT'] += 1
            elif edge.relation_type == StructureRelations.PART_OF:
                counts['PART_OF'] += 1
        return counts
    
    def _compute_timing_stats(
        self,
        segment_nodes: List[GraphNode],  # Sorted segment nodes
    ) -> dict:  # Timing statistics
        """Compute timing statistics from segment nodes."""
        total_duration = 0.0
        missing_count = 0
        valid_count = 0
        
        for node in segment_nodes:
            start = node.properties.get('start_time')
            end = node.properties.get('end_time')
            
            if start is not None and end is not None:
                total_duration += (end - start)
                valid_count += 1
            else:
                missing_count += 1
        
        avg_duration = total_duration / valid_count if valid_count > 0 else 0.0
        
        return {
            'total_duration': total_duration,
            'avg_duration': avg_duration,
            'missing_count': missing_count,
        }
    
    def _compute_source_stats(
        self,
        segment_nodes: List[GraphNode],  # Segment nodes
    ) -> dict:  # Source statistics
        """Compute source statistics from segment nodes."""
        missing_count = 0
        plugins: Set[str] = set()
        
        for node in segment_nodes:
            sources = node.sources
            if not sources:
                missing_count += 1
            else:
                for source in sources:
                    # SourceRef has plugin_name attribute
                    if hasattr(source, 'plugin_name'):
                        plugins.add(source.plugin_name)
                    elif isinstance(source, dict) and 'plugin_name' in source:
                        plugins.add(source['plugin_name'])
        
        return {
            'missing_count': missing_count,
            'plugins': sorted(list(plugins)),
        }
    
    def _extract_samples(
        self,
        nodes: List[GraphNode],  # Segment nodes to convert to samples
    ) -> List[SegmentSample]:  # Sample list
        """Convert segment nodes to sample objects."""
        return [self._node_to_sample(n) for n in nodes]
    
    def _node_to_sample(
        self,
        node: GraphNode,  # Segment node to convert
    ) -> SegmentSample:  # Sample object
        """Convert a single segment node to a sample object."""
        return SegmentSample(
            index=node.properties.get('index', 0),
            text=truncate_text(node.properties.get('text', '')),
            start_time=node.properties.get('start_time'),
            end_time=node.properties.get('end_time'),
        )
