#!/usr/bin/env python3
from __future__ import annotations
"""
Novyx Core - GraphQL Schema
============================
Strawberry GraphQL schema definitions for Novyx Core API.

This module defines GraphQL types and resolvers that provide a unified
query interface to the Novyx knowledge graph, reusing existing tools
(factory, ingestor, sentinel, graph, query).

Usage:
    from tools.schema import schema
    # Mount in FastAPI: app.add_route("/graphql", GraphQLRouter(schema=schema))
"""

import strawberry
from strawberry.types import Info
from typing import List, Optional, Dict, Any
from datetime import datetime, timezone
from pathlib import Path
import json
import uuid as uuid_lib

# Import Novyx tools for resolvers
from tools.factory import create_artifact, persist_artifact
from tools.sentinel import NovyxSentinel
from tools.graph import scan_artifacts, build_index, find_orphaned_links, get_primary_ids
from tools.artifacts import get_artifact_config, list_artifact_types
from tools.util import calculate_hash_for_new_artifact


# ============================================================================
# GraphQL Types (Data Models)
# ============================================================================

@strawberry.type
class Artifact:
    """
    Base artifact type representing any JSON-LD artifact in Novyx Core.
    """
    uuid: str
    type: str
    title: Optional[str] = None
    created_at: str
    updated_at: Optional[str] = None
    category: Optional[str] = None
    integrity_hash: str
    
    @strawberry.field
    def primary_id(self) -> str:
        """Primary identifier (UUID URN)."""
        return self.uuid
    
    @strawberry.field
    def artifact_type(self) -> str:
        """Artifact type classification."""
        return self.type


@strawberry.type
class Decision(Artifact):
    """
    Decision artifact with causal reasoning fields.
    """
    context: str
    options_considered: List[str]
    chosen_option: str
    reasoning: str
    confidence: float
    expected_outcome: str
    constraints: List[str] = strawberry.field(default_factory=list)
    assumptions: List[str] = strawberry.field(default_factory=list)


@strawberry.type
class SearchResult:
    """
    Result from semantic search query.
    """
    artifact_id: str
    title: Optional[str]
    similarity_score: float
    category: Optional[str]
    created_at: str
    snippet: Optional[str] = None


@strawberry.type
class GraphStats:
    """
    Knowledge graph statistics.
    """
    total_artifacts: int
    total_ids: int
    total_links: int
    orphaned_links: int
    artifacts_by_type: str  # JSON string
    artifacts_by_category: str  # JSON string
    latest_timestamp: Optional[str]


@strawberry.type
class ValidationResult:
    """
    Result from Sentinel validation.
    """
    total_artifacts: int
    passed: int
    failed: int
    unique_ids: int
    cross_links: int
    success: bool
    errors: List[str] = strawberry.field(default_factory=list)


@strawberry.type
class RepairResult:
    """
    Result from graph repair operation.
    """
    orphans_found: int
    links_removed: int
    stubs_created: int
    files_modified: int
    success: bool
    message: str


@strawberry.type
class ArtifactCreationResult:
    """
    Result from artifact creation operation.
    """
    success: bool
    artifact_id: Optional[str]
    file_path: Optional[str]
    message: str


# ============================================================================
# Input Types (for Mutations)
# ============================================================================

@strawberry.input
class CreateDecisionInput:
    """
    Input for creating a Decision artifact.
    """
    title: str
    context: str
    options_considered: List[str]
    chosen_option: str
    reasoning: str
    confidence: float
    expected_outcome: str
    category: str = "graphql"
    constraints: List[str] = strawberry.field(default_factory=list)
    assumptions: List[str] = strawberry.field(default_factory=list)
    external_links: List[str] = strawberry.field(default_factory=list)


@strawberry.input
class IngestTextInput:
    """
    Input for ingesting raw text.
    """
    text: str
    category: str = "graphql"
    external_links: List[str] = strawberry.field(default_factory=list)


@strawberry.input
class CreateArtifactInput:
    """
    Generic input for creating any artifact type.
    """
    artifact_type: str
    payload: str  # JSON string of artifact fields
    external_links: List[str] = strawberry.field(default_factory=list)


@strawberry.input
class RepairInput:
    """
    Input for graph repair operation.
    """
    directory: str = "memory"
    dry_run: bool = False
    create_stubs: bool = False


# ============================================================================
# Query Type (Read Operations)
# ============================================================================

@strawberry.type
class Query:
    """
    GraphQL Query root type.
    Provides read-only access to the Novyx knowledge graph.
    """
    
    @strawberry.field
    def hello(self) -> str:
        """Health check query."""
        return "Welcome to Novyx Core GraphQL API"
    
    @strawberry.field
    def artifact_types(self) -> List[str]:
        """
        List all registered artifact types.
        """
        return list_artifact_types()
    
    @strawberry.field
    def search_artifacts(
        self,
        search_term: str,
        threshold: float = 0.5,
        directory: str = "memory"
    ) -> List[SearchResult]:
        """
        Perform simple text search across artifacts.
        Note: For full semantic search with embeddings, use the CLI query tool.
        
        Args:
            search_term: Text to search for
            threshold: Minimum similarity score (0.0-1.0) - not used in simple search
            directory: Directory to search in
        
        Returns:
            List of matching artifacts with similarity scores
        """
        try:
            artifacts = scan_artifacts(Path(directory))
            search_results = []
            
            search_lower = search_term.lower()
            
            for artifact in artifacts:
                # Simple text matching in title, description, context
                score = 0.0
                text_fields = []
                
                title = artifact.get("title", "")
                if title:
                    text_fields.append(title)
                if search_lower in title.lower():
                    score += 0.5
                
                desc = artifact.get("description", "") or artifact.get("context", "")
                if desc and search_lower in desc.lower():
                    score += 0.3
                    text_fields.append(desc[:200])
                
                body = artifact.get("articleBody", "")
                if body and search_lower in body.lower():
                    score += 0.2
                
                if score > 0:
                    search_results.append(SearchResult(
                        artifact_id=artifact.get("uuid", "unknown"),
                        title=artifact.get("title"),
                        similarity_score=min(score, 1.0),
                        category=artifact.get("category"),
                        created_at=artifact.get("createdAt", ""),
                        snippet=" ".join(text_fields)[:300] if text_fields else None
                    ))
            
            # Sort by score descending
            search_results.sort(key=lambda x: x.similarity_score, reverse=True)
            
            return search_results
        except Exception as e:
            return []
    
    @strawberry.field
    def graph_stats(self, directory: str = "memory") -> GraphStats:
        """
        Get knowledge graph statistics.
        
        Args:
            directory: Directory to analyze
        
        Returns:
            Graph statistics including artifact counts and orphan links
        """
        try:
            artifacts = scan_artifacts(Path(directory))
            index = build_index(artifacts)
            orphaned = find_orphaned_links(index)
            
            # Count by type and category
            by_type = {}
            by_category = {}
            latest_ts = None
            
            for artifact in artifacts:
                # Type counting
                art_type = artifact.get("@type", "Unknown")
                if isinstance(art_type, list):
                    art_type = ", ".join(art_type)
                by_type[art_type] = by_type.get(art_type, 0) + 1
                
                # Category counting
                category = artifact.get("category", "uncategorized")
                by_category[category] = by_category.get(category, 0) + 1
                
                # Latest timestamp
                ts = artifact.get("createdAt") or artifact.get("Temporal_Marker")
                if ts:
                    if not latest_ts or ts > latest_ts:
                        latest_ts = ts
            
            return GraphStats(
                total_artifacts=len(artifacts),
                total_ids=len(index.get("id_to_file", {})),
                total_links=sum(len(links) for links in index.get("file_to_links", {}).values()),
                orphaned_links=len(orphaned),
                artifacts_by_type=json.dumps(by_type),
                artifacts_by_category=json.dumps(by_category),
                latest_timestamp=latest_ts
            )
        except Exception as e:
            return GraphStats(
                total_artifacts=0,
                total_ids=0,
                total_links=0,
                orphaned_links=0,
                artifacts_by_type="{}",
                artifacts_by_category="{}",
                latest_timestamp=None
            )
    
    @strawberry.field
    def validate_graph(
        self,
        directory: str = "memory",
        enforce_graph: bool = False
    ) -> ValidationResult:
        """
        Run Sentinel integrity validation on artifacts.
        
        Args:
            directory: Directory to validate
            enforce_graph: Enable graph-wide link enforcement
        
        Returns:
            Validation results with pass/fail counts
        """
        try:
            sentinel = NovyxSentinel(directory=Path(directory))
            
            # Run file-level validation
            file_results = []
            for artifact_file in Path(directory).rglob("*.jsonld"):
                if ".git" in str(artifact_file):
                    continue
                result = sentinel.validate_artifact(artifact_file)
                file_results.append(result)
            
            passed = sum(1 for r in file_results if r.get("valid", False))
            failed = len(file_results) - passed
            
            # Count unique IDs
            artifacts = scan_artifacts(Path(directory))
            unique_ids = set()
            for artifact in artifacts:
                unique_ids.update(get_primary_ids(artifact))
            
            # Count cross-links
            index = build_index(artifacts)
            cross_links = sum(len(links) for links in index.get("file_to_links", {}).values())
            
            errors = []
            if enforce_graph:
                orphaned = find_orphaned_links(index)
                if orphaned:
                    errors.append(f"Found {len(orphaned)} orphaned internal links")
            
            return ValidationResult(
                total_artifacts=len(file_results),
                passed=passed,
                failed=failed,
                unique_ids=len(unique_ids),
                cross_links=cross_links,
                success=(failed == 0 and len(errors) == 0),
                errors=errors
            )
        except Exception as e:
            return ValidationResult(
                total_artifacts=0,
                passed=0,
                failed=0,
                unique_ids=0,
                cross_links=0,
                success=False,
                errors=[str(e)]
            )


# ============================================================================
# Mutation Type (Write Operations)
# ============================================================================

@strawberry.type
class Mutation:
    """
    GraphQL Mutation root type.
    Provides write operations for creating and repairing artifacts.
    """
    
    @strawberry.mutation
    def create_decision(self, input: CreateDecisionInput) -> ArtifactCreationResult:
        """
        Create a new Decision artifact.
        
        Args:
            input: Decision creation input with all required fields
        
        Returns:
            Creation result with artifact ID and file path
        """
        try:
            payload = {
                "title": input.title,
                "context": input.context,
                "options_considered": input.options_considered,
                "chosen_option": input.chosen_option,
                "reasoning": input.reasoning,
                "confidence": input.confidence,
                "expected_outcome": input.expected_outcome,
                "category": input.category,
                "constraints": input.constraints,
                "assumptions": input.assumptions
            }
            
            artifact = create_artifact("decision", payload, input.external_links or None)
            
            # Persist artifact
            config = get_artifact_config("decision")
            output_dir = Path(config["output_dir"])
            output_dir.mkdir(parents=True, exist_ok=True)
            
            file_path = persist_artifact(artifact, output_dir / f"graphql_decision_{artifact['uuid'][-8:]}")
            
            return ArtifactCreationResult(
                success=True,
                artifact_id=artifact.get("uuid"),
                file_path=str(file_path),
                message=f"Decision artifact created successfully"
            )
        except Exception as e:
            return ArtifactCreationResult(
                success=False,
                artifact_id=None,
                file_path=None,
                message=f"Failed to create decision: {str(e)}"
            )
    
    @strawberry.mutation
    def ingest_text(self, input: IngestTextInput) -> ArtifactCreationResult:
        """
        Ingest raw text as a DigitalDocument artifact.
        
        Args:
            input: Text ingestion input
        
        Returns:
            Creation result with artifact ID
        """
        try:
            # Create a DigitalDocument artifact using factory
            # Note: For full semantic features (embeddings), use ingestor.py CLI
            payload = {
                "title": f"Text ingested via GraphQL",
                "articleBody": input.text,
                "category": input.category,
                "description": f"Ingested text ({len(input.text)} characters)"
            }
            
            # Create artifact (factory handles this generically but DigitalDocument
            # isn't registered, so we'll just use the generic approach)
            artifact = {
                "@context": "https://novyx.ai/CORE_SCHEMA.jsonld",
                "@type": ["schema:DigitalDocument", "novyx:Artifact"],
                "uuid": f"urn:uuid:{uuid_lib.uuid4()}",
                "createdAt": datetime.now(timezone.utc).isoformat(),
                "updatedAt": datetime.now(timezone.utc).isoformat(),
                **payload,
                "sameAs": input.external_links if input.external_links else []
            }
            
            # Add integrity hash
            artifact["novyx:integrityHash"] = calculate_hash_for_new_artifact(artifact)
            artifact["Integrity_Hash"] = artifact["novyx:integrityHash"]
            
            # Persist to memory/research or inbox
            output_dir = Path("memory") / input.category
            output_dir.mkdir(parents=True, exist_ok=True)
            
            file_path = persist_artifact(artifact, output_dir / f"graphql_text_{artifact['uuid'][-8:]}")
            
            return ArtifactCreationResult(
                success=True,
                artifact_id=artifact.get("uuid"),
                file_path=str(file_path),
                message="Text ingested successfully"
            )
        except Exception as e:
            return ArtifactCreationResult(
                success=False,
                artifact_id=None,
                file_path=None,
                message=f"Failed to ingest text: {str(e)}"
            )
    
    @strawberry.mutation
    def create_artifact(self, input: CreateArtifactInput) -> ArtifactCreationResult:
        """
        Create a generic artifact of any registered type.
        
        Args:
            input: Generic artifact creation input
        
        Returns:
            Creation result
        """
        try:
            # Parse JSON payload
            payload = json.loads(input.payload)
            
            artifact = create_artifact(
                input.artifact_type,
                payload,
                input.external_links or None
            )
            
            # Persist artifact
            config = get_artifact_config(input.artifact_type)
            if not config:
                raise ValueError(f"Unknown artifact type: {input.artifact_type}")
            
            output_dir = Path(config["output_dir"])
            output_dir.mkdir(parents=True, exist_ok=True)
            
            file_path = persist_artifact(
                artifact,
                output_dir / f"graphql_{input.artifact_type}_{artifact['uuid'][-8:]}"
            )
            
            return ArtifactCreationResult(
                success=True,
                artifact_id=artifact.get("uuid"),
                file_path=str(file_path),
                message=f"{input.artifact_type.capitalize()} artifact created successfully"
            )
        except Exception as e:
            return ArtifactCreationResult(
                success=False,
                artifact_id=None,
                file_path=None,
                message=f"Failed to create artifact: {str(e)}"
            )
    
    @strawberry.mutation
    def repair_graph(self, input: RepairInput) -> RepairResult:
        """
        Repair orphaned links in the knowledge graph.
        
        Args:
            input: Repair operation input
        
        Returns:
            Repair result with counts of changes made
        """
        try:
            directory = Path(input.directory)
            artifacts = scan_artifacts(directory)
            index = build_index(artifacts)
            orphaned = find_orphaned_links(index)
            
            if not orphaned:
                return RepairResult(
                    orphans_found=0,
                    links_removed=0,
                    stubs_created=0,
                    files_modified=0,
                    success=True,
                    message="No orphaned links found. Graph is healthy."
                )
            
            if input.dry_run:
                return RepairResult(
                    orphans_found=len(orphaned),
                    links_removed=0,
                    stubs_created=0,
                    files_modified=0,
                    success=True,
                    message=f"Dry run: Would repair {len(orphaned)} orphaned links"
                )
            
            # Actual repair logic would go here
            # For now, return placeholder
            return RepairResult(
                orphans_found=len(orphaned),
                links_removed=0,
                stubs_created=0,
                files_modified=0,
                success=True,
                message="Repair operation completed (placeholder)"
            )
        except Exception as e:
            return RepairResult(
                orphans_found=0,
                links_removed=0,
                stubs_created=0,
                files_modified=0,
                success=False,
                message=f"Repair failed: {str(e)}"
            )


# ============================================================================
# Schema Definition
# ============================================================================

schema = strawberry.Schema(
    query=Query,
    mutation=Mutation,
    config=strawberry.schema.config.StrawberryConfig(
        auto_camel_case=True  # Convert snake_case to camelCase for GraphQL
    )
)


if __name__ == "__main__":
    # Print schema SDL for inspection
    print("=== Novyx Core GraphQL Schema ===")
    print(schema.as_str())
