"""
Knowledge Graph Builder Module

This module provides comprehensive knowledge graph construction capabilities
from extracted entities and relationships, with full support for temporal
knowledge graphs and advanced graph operations.

Key Features:
    - Build knowledge graphs from entities and relationships
    - Temporal knowledge graph support with time-aware edges
    - Entity resolution and deduplication
    - Conflict detection and resolution
    - Temporal snapshots and versioning
    - Neo4j integration for graph storage

Example Usage:
    >>> from semantica.kg import GraphBuilder
    >>> builder = GraphBuilder(merge_entities=True, resolve_conflicts=True)
    >>> graph = builder.build(sources=[{"entities": [...], "relationships": [...]}])

Author: Semantica Contributors
License: MIT
"""

from datetime import datetime
from typing import Any, Dict, List, Optional, Union


class GraphBuilder:
    """
    Knowledge graph builder with temporal support.

    • Constructs knowledge graphs from entities and relationships
    • Supports temporal knowledge graphs with time-aware edges
    • Manages node and edge creation with temporal annotations
    • Handles graph structure optimization
    • Supports incremental graph building
    • Enables temporal versioning and snapshots

    Attributes:
        • merge_entities: Whether to merge duplicate entities
        • entity_resolution_strategy: Strategy for entity resolution
        • resolve_conflicts: Whether to resolve conflicts
        • enable_temporal: Enable temporal knowledge graph features
        • temporal_granularity: Time granularity (second, minute, hour, day, etc.)
        • track_history: Track historical changes
        • version_snapshots: Create version snapshots

    Methods:
        • build(): Build knowledge graph from sources
        • add_temporal_edge(): Add edge with temporal validity
        • create_temporal_snapshot(): Create temporal snapshot
        • query_temporal(): Query graph at specific time point
    """

    def __init__(
        self,
        merge_entities=True,
        entity_resolution_strategy="fuzzy",
        resolve_conflicts=True,
        enable_temporal=False,
        temporal_granularity="day",
        track_history=False,
        version_snapshots=False,
        **kwargs,
    ):
        """
        Initialize graph builder.

        Args:
            merge_entities: Whether to merge duplicate entities
            entity_resolution_strategy: Strategy for entity resolution ("fuzzy", "exact", "ml-based")
            resolve_conflicts: Whether to resolve conflicts
            enable_temporal: Enable temporal knowledge graph features
            temporal_granularity: Time granularity ("second", "minute", "hour", "day", "week", "month", "year")
            track_history: Track historical changes to entities/relationships
            version_snapshots: Create version snapshots at intervals
            **kwargs: Additional configuration options
        """
        self.merge_entities = merge_entities
        self.entity_resolution_strategy = entity_resolution_strategy
        self.resolve_conflicts = resolve_conflicts
        self.enable_temporal = enable_temporal
        self.temporal_granularity = temporal_granularity
        self.track_history = track_history
        self.version_snapshots = version_snapshots

        # Initialize logging
        from ..utils.logging import get_logger
        from ..utils.progress_tracker import get_progress_tracker

        self.logger = get_logger("graph_builder")
        self.progress_tracker = get_progress_tracker()

        # Initialize entity resolver if entity merging is enabled
        # This helps deduplicate and merge similar entities
        if self.merge_entities:
            from .entity_resolver import EntityResolver

            entity_resolution_config = kwargs.get("entity_resolution", {})
            self.entity_resolver = EntityResolver(
                strategy=self.entity_resolution_strategy, **entity_resolution_config
            )
            self.logger.debug(
                f"Entity resolver initialized with strategy: {self.entity_resolution_strategy}"
            )
        else:
            self.entity_resolver = None
            self.logger.debug("Entity merging disabled, skipping entity resolver")

        # Initialize conflict detector if conflict resolution is enabled
        # This helps detect and resolve conflicting information in the graph
        if self.resolve_conflicts:
            from ..conflicts.conflict_detector import ConflictDetector

            conflict_detection_config = kwargs.get("conflict_detection", {})
            self.conflict_detector = ConflictDetector(**conflict_detection_config)
            self.logger.debug("Conflict detector initialized")
        else:
            self.conflict_detector = None
            self.logger.debug("Conflict resolution disabled")

    def build(
        self,
        sources: Union[List[Any], Any],
        entity_resolver: Optional[Any] = None,
        **options,
    ) -> Dict[str, Any]:
        """
        Build knowledge graph from sources.

        This method processes various source formats and extracts entities and
        relationships to construct a knowledge graph. It handles entity resolution
        and conflict detection if enabled.

        Args:
            sources: List of sources in various formats:
                - Dict with "entities" and/or "relationships" keys
                - Dict with entity-like structure (has "id" or "entity_id")
                - Dict with relationship structure (has "source" and "target")
                - List of entity/relationship dicts
            entity_resolver: Optional custom entity resolver (overrides default)
            **options: Additional build options

        Returns:
            Dictionary containing:
                - entities: List of resolved entities
                - relationships: List of relationships
                - metadata: Graph metadata including counts and timestamps

        Example:
            >>> sources = [
            ...     {"entities": [{"id": "1", "name": "Alice"}],
            ...      "relationships": [{"source": "1", "target": "2", "type": "knows"}]}
            ... ]
            >>> graph = builder.build(sources)
        """
        # Normalize sources to list
        if not isinstance(sources, list):
            sources = [sources]

        # Track graph building
        tracking_id = self.progress_tracker.start_tracking(
            module="kg",
            submodule="GraphBuilder",
            message=f"Knowledge graph from {len(sources)} source(s)",
        )

        try:
            self.logger.info(f"Building knowledge graph from {len(sources)} source(s)")

            # Use provided resolver or default instance resolver
            resolver_to_use = entity_resolver or self.entity_resolver

            # Extract entities and relationships from all sources
            all_entities = []
            all_relationships = []

            for source in sources:
                if isinstance(source, dict):
                    # Source is a dictionary - extract entities and relationships
                    if "entities" in source:
                        # Explicit entities list
                        all_entities.extend(source["entities"])
                    elif "id" in source or "entity_id" in source:
                        # Single entity object
                        all_entities.append(source)

                    if "relationships" in source:
                        # Explicit relationships list
                        all_relationships.extend(source["relationships"])
                    elif "source" in source and "target" in source:
                        # Single relationship object
                        all_relationships.append(source)

                elif isinstance(source, list):
                    # Source is a list - process each item
                    for item in source:
                        if isinstance(item, dict):
                            # Determine if item is a relationship or entity
                            if "source" in item and "target" in item:
                                all_relationships.append(item)
                            else:
                                all_entities.append(item)

            self.logger.debug(
                f"Extracted {len(all_entities)} entities and "
                f"{len(all_relationships)} relationships from sources"
            )

            # Resolve entities (deduplicate and merge) if resolver is available
            resolved_entities = all_entities
            if resolver_to_use and all_entities:
                self.logger.info(
                    f"Resolving {len(all_entities)} entities using {self.entity_resolution_strategy} strategy"
                )
                resolved_entities = resolver_to_use.resolve_entities(all_entities)
                self.logger.info(
                    f"Entity resolution complete: {len(all_entities)} -> {len(resolved_entities)} unique entities"
                )

            # Build graph structure
            graph = {
                "entities": resolved_entities,
                "relationships": all_relationships,
                "metadata": {
                    "num_entities": len(resolved_entities),
                    "num_relationships": len(all_relationships),
                    "temporal_enabled": self.enable_temporal,
                    "timestamp": self._get_timestamp(),
                    "entity_resolution_applied": resolver_to_use is not None,
                },
            }

            # Detect and resolve conflicts if conflict detector is available
            if self.conflict_detector:
                self.logger.debug("Detecting conflicts in graph")
                # Pass only entities to detect_conflicts as it expects List[Dict]
                detected_conflicts = self.conflict_detector.detect_conflicts(graph["entities"])

                if detected_conflicts:
                    conflict_count = len(detected_conflicts)
                    self.logger.warning(
                        f"Detected {conflict_count} conflict(s) in graph"
                    )

                    # Attempt to resolve conflicts
                    resolution_result = self.conflict_detector.resolve_conflicts(
                        detected_conflicts
                    )
                    resolved_count = resolution_result.get("resolved_count", 0)

                    if resolved_count > 0:
                        self.logger.info(
                            f"Successfully resolved {resolved_count} out of {conflict_count} conflict(s)"
                        )
                    else:
                        self.logger.warning("No conflicts were automatically resolved")

            # Log final graph statistics
            self.logger.info(
                f"Knowledge graph built successfully: "
                f"{len(resolved_entities)} entities, {len(all_relationships)} relationships"
            )

            self.progress_tracker.stop_tracking(
                tracking_id,
                status="completed",
                message=f"Built graph with {len(resolved_entities)} entities, {len(all_relationships)} relationships",
            )
            return graph

        except Exception as e:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message=str(e)
            )
            raise

    def add_temporal_edge(
        self,
        graph,
        source,
        target,
        relationship,
        valid_from=None,
        valid_until=None,
        temporal_metadata=None,
        **kwargs,
    ):
        """
        Add edge with temporal validity information.

        Args:
            graph: Knowledge graph to add edge to
            source: Source entity/node
            target: Target entity/node
            relationship: Relationship type
            valid_from: Start time for relationship validity (datetime, timestamp, or ISO string)
            valid_until: End time for relationship validity (None for ongoing)
            temporal_metadata: Additional temporal metadata (timezone, precision, etc.)
            **kwargs: Additional edge properties

        Returns:
            Edge object with temporal annotations
        """
        tracking_id = self.progress_tracker.start_tracking(
            module="kg",
            submodule="GraphBuilder",
            message=f"Adding temporal edge: {source} -{relationship}-> {target}",
        )

        try:
            self.logger.info(
                f"Adding temporal edge: {source} -{relationship}-> {target}"
            )

            # Parse temporal information
            valid_from = self._parse_time(valid_from) or self._get_timestamp()
            valid_until = self._parse_time(valid_until) if valid_until else None

            # Create edge with temporal information
            edge = {
                "source": source,
                "target": target,
                "type": relationship,
                "valid_from": valid_from,
                "valid_until": valid_until,
                "temporal_metadata": temporal_metadata or {},
                **kwargs,
            }

            # Add to graph
            if "relationships" not in graph:
                graph["relationships"] = []
            graph["relationships"].append(edge)

            self.progress_tracker.stop_tracking(
                tracking_id,
                status="completed",
                message=f"Added temporal edge: {source} -{relationship}-> {target}",
            )
            return edge

        except Exception as e:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message=str(e)
            )
            raise

    def create_temporal_snapshot(
        self, graph, timestamp=None, snapshot_name=None, **options
    ):
        """
        Create temporal snapshot of graph at specific time point.

        Args:
            graph: Knowledge graph to snapshot
            timestamp: Time point for snapshot (None for current time)
            snapshot_name: Optional name for snapshot
            **options: Additional snapshot options

        Returns:
            Temporal snapshot object
        """
        tracking_id = self.progress_tracker.start_tracking(
            module="kg",
            submodule="GraphBuilder",
            message=f"Creating temporal snapshot: {snapshot_name or 'unnamed'}",
        )

        try:
            self.logger.info(
                f"Creating temporal snapshot: {snapshot_name or 'unnamed'}"
            )

            snapshot_time = self._parse_time(timestamp) or self._get_timestamp()

            self.progress_tracker.update_tracking(
                tracking_id, message="Filtering entities and relationships..."
            )

            # Filter entities and relationships valid at snapshot time
            entities = []
            relationships = []

            # Get all entities
            if "entities" in graph:
                entities = graph["entities"].copy()

            # Filter relationships valid at snapshot time
            if "relationships" in graph:
                for rel in graph["relationships"]:
                    valid_from = self._parse_time(rel.get("valid_from"))
                    valid_until = self._parse_time(rel.get("valid_until"))

                    # Check if relationship is valid at snapshot time
                    if (
                        valid_from
                        and self._compare_times(snapshot_time, valid_from) < 0
                    ):
                        continue
                    if (
                        valid_until
                        and self._compare_times(snapshot_time, valid_until) > 0
                    ):
                        continue

                    relationships.append(rel)

            snapshot = {
                "name": snapshot_name or f"snapshot_{snapshot_time}",
                "timestamp": snapshot_time,
                "entities": entities,
                "relationships": relationships,
                "metadata": {
                    "num_entities": len(entities),
                    "num_relationships": len(relationships),
                    "snapshot_time": snapshot_time,
                },
            }

            self.progress_tracker.stop_tracking(
                tracking_id,
                status="completed",
                message=f"Created snapshot with {len(entities)} entities, {len(relationships)} relationships",
            )
            return snapshot

        except Exception as e:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message=str(e)
            )
            raise

    def query_temporal(
        self,
        graph,
        query,
        at_time=None,
        time_range=None,
        temporal_window=None,
        **options,
    ):
        """
        Query graph at specific time point or time range.

        Args:
            graph: Knowledge graph to query
            query: Query (Cypher, SPARQL, or natural language)
            at_time: Query at specific time point
            time_range: Query within time range (start, end)
            temporal_window: Temporal window size
            **options: Additional query options

        Returns:
            Query results with temporal context
        """
        tracking_id = self.progress_tracker.start_tracking(
            module="kg",
            submodule="GraphBuilder",
            message=f"Executing temporal query: {query[:50]}...",
        )

        try:
            self.logger.info(f"Executing temporal query: {query[:50]}...")

            self.progress_tracker.update_tracking(
                tracking_id, message="Creating temporal snapshot for query..."
            )

            # Create snapshot for query time
            if at_time:
                snapshot = self.create_temporal_snapshot(graph, timestamp=at_time)
            elif time_range:
                start_time, end_time = time_range
                # Query at end time
                snapshot = self.create_temporal_snapshot(graph, timestamp=end_time)
            else:
                # Use current graph
                snapshot = graph

            self.progress_tracker.update_tracking(
                tracking_id, message="Executing query..."
            )

            # Basic query execution (simplified)
            # In a real implementation, this would use a proper query engine
            results = {
                "query": query,
                "timestamp": at_time or (time_range[1] if time_range else None),
                "entities": snapshot.get("entities", []),
                "relationships": snapshot.get("relationships", []),
                "metadata": snapshot.get("metadata", {}),
            }

            self.progress_tracker.stop_tracking(
                tracking_id,
                status="completed",
                message=f"Query executed: {len(results.get('entities', []))} entities, {len(results.get('relationships', []))} relationships",
            )
            return results

        except Exception as e:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message=str(e)
            )
            raise

    def load_from_neo4j(
        self,
        uri="bolt://localhost:7687",
        username="neo4j",
        password="password",
        database="neo4j",
        enable_temporal=False,
        temporal_property="valid_time",
        **kwargs,
    ):
        """
        Load graph from Neo4j database.

        Args:
            uri: Neo4j connection URI
            username: Neo4j username
            password: Neo4j password
            database: Neo4j database name
            enable_temporal: Enable temporal features for loaded graph
            temporal_property: Property name for temporal data
            **kwargs: Additional connection options

        Returns:
            Knowledge graph loaded from Neo4j
        """
        tracking_id = self.progress_tracker.start_tracking(
            module="kg",
            submodule="GraphBuilder",
            message=f"Loading graph from Neo4j: {uri}",
        )

        try:
            self.logger.info(f"Loading graph from Neo4j: {uri}")

            from neo4j import GraphDatabase

            self.progress_tracker.update_tracking(
                tracking_id, message="Connecting to Neo4j..."
            )
            driver = GraphDatabase.driver(uri, auth=(username, password))

            with driver.session(database=database) as session:
                # Load nodes
                self.progress_tracker.update_tracking(
                    tracking_id, message="Loading nodes from Neo4j..."
                )
                nodes_result = session.run("MATCH (n) RETURN n")
                entities = []
                for record in nodes_result:
                    node = record["n"]
                    entity = {
                        "id": str(node.id),
                        "type": list(node.labels)[0] if node.labels else "Entity",
                        "properties": dict(node),
                    }
                    entities.append(entity)

                # Load relationships
                self.progress_tracker.update_tracking(
                    tracking_id, message="Loading relationships from Neo4j..."
                )
                rels_result = session.run("MATCH (a)-[r]->(b) RETURN a, r, b")
                relationships = []
                for record in rels_result:
                    source = record["a"]
                    rel = record["r"]
                    target = record["b"]

                    relationship = {
                        "source": str(source.id),
                        "target": str(target.id),
                        "type": rel.type,
                        "properties": dict(rel),
                    }

                    # Add temporal information if enabled
                    if enable_temporal and temporal_property in rel:
                        relationship["valid_from"] = rel[temporal_property]

                    relationships.append(relationship)

            driver.close()

            graph = {
                "entities": entities,
                "relationships": relationships,
                "metadata": {
                    "source": "neo4j",
                    "uri": uri,
                    "database": database,
                    "temporal_enabled": enable_temporal,
                },
            }

            self.logger.info(
                f"Loaded {len(entities)} entities and {len(relationships)} relationships from Neo4j"
            )
            self.progress_tracker.stop_tracking(
                tracking_id,
                status="completed",
                message=f"Loaded {len(entities)} entities and {len(relationships)} relationships from Neo4j",
            )
            return graph

        except ImportError:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message="neo4j library not available"
            )
            raise ImportError(
                "neo4j library not available. Install with: pip install neo4j"
            )
        except Exception as e:
            self.progress_tracker.stop_tracking(
                tracking_id, status="failed", message=str(e)
            )
            self.logger.error(f"Error loading from Neo4j: {e}")
            raise

    def _get_timestamp(self) -> str:
        """
        Get current timestamp in ISO format.

        Returns:
            ISO format timestamp string (e.g., "2024-01-15T10:30:00")
        """
        return datetime.now().isoformat()

    def _parse_time(
        self, time_value: Optional[Union[str, datetime, Any]]
    ) -> Optional[str]:
        """
        Parse time value to ISO format string.

        This method handles various time input formats and converts them
        to a standardized ISO format string for temporal operations.

        Args:
            time_value: Time value in various formats:
                - None: Returns None
                - str: ISO format string (returned as-is)
                - datetime: Converted to ISO string
                - Other: Converted to string

        Returns:
            ISO format timestamp string or None
        """
        if time_value is None:
            return None

        # Already a string - assume it's in correct format
        if isinstance(time_value, str):
            return time_value

        # datetime object - convert to ISO string
        if isinstance(time_value, datetime):
            return time_value.isoformat()

        # Other types - convert to string
        return str(time_value)

    def _compare_times(self, time1: Optional[str], time2: Optional[str]) -> int:
        """
        Compare two time strings.

        This method performs lexicographic comparison of ISO format time strings.
        Returns -1 if time1 < time2, 0 if equal, 1 if time1 > time2.

        Args:
            time1: First time string (ISO format)
            time2: Second time string (ISO format)

        Returns:
            Comparison result: -1, 0, or 1
        """
        # Handle None values
        if time1 is None or time2 is None:
            return 0

        # Simple lexicographic comparison works for ISO format strings
        # ISO format: YYYY-MM-DDTHH:MM:SS ensures correct string comparison
        if time1 < time2:
            return -1
        elif time1 > time2:
            return 1
        else:
            return 0
