#!/usr/bin/env python3
"""
Novyx Core - Semantic Pulse Engine v1.1.0 (tools/ingestor.py)
-------------------------------------------------------------
Responsibility:
1. Ingest raw text artifacts.
2. Generate SHA-256 cryptographic signatures.
3. Generate 384-dimensional embeddings (all-MiniLM-L6-v2).
4. Identify top 3 semantic links from existing knowledge graph.
5. Support Schema v1.1.0 (External Authority Linking).
"""

import os
import sys
import json
import uuid
from tools.util import calculate_hash
import time
import argparse
from datetime import datetime, timezone
from pathlib import Path
from typing import List, Dict, Tuple

# ML Dependency Handling with Fallback
try:
    from sentence_transformers import SentenceTransformer, util
    import torch
    ML_AVAILABLE = True
except ImportError:
    ML_AVAILABLE = False

# Configuration
MEMORY_DIR = Path("memory")
DOMAINS = ["health", "research", "decisions", "inbox"]
MODEL_NAME = 'all-MiniLM-L6-v2'
SIMILARITY_THRESHOLD = 0.2


class SemanticPulse:
    def __init__(self, strict: bool = False, force_no_embeddings: bool = False):
        self.model = None
        self.strict = strict
        self.force_no_embeddings = force_no_embeddings
        if self.force_no_embeddings:
            print("SEMANTIC MODE: OFF (--no-embeddings requested)")
        elif ML_AVAILABLE:
            print("SEMANTIC MODE: ON (embeddings enabled)")
            print(f"🧠 Loading Semantic Model: {MODEL_NAME}...")
            # Load model optimized for CPU if CUDA not available
            device = 'cuda' if torch.cuda.is_available() else 'cpu'
            self.model = SentenceTransformer(MODEL_NAME, device=device)
            print(f"   Model loaded on {device.upper()}. Pulse active.")
        else:
            print(
                "SEMANTIC MODE: OFF (missing 'sentence-transformers' or 'torch'). "
                "Install with: pip install sentence-transformers torch"
            )

    def generate_uuid(self) -> str:
        """Generates a persistent URN:UUID."""
        return f"urn:uuid:{str(uuid.uuid4())}"

    def compute_hash(self, content: str) -> str:
        """Generates SHA-256 hash for cryptographic integrity (raw content)."""
        return calculate_hash(content)

    def compute_file_hash(self, file_path: Path) -> str:
        """Calculates SHA-256 hash of JSON-LD file (excluding hash line).

        Matches Sentinel's validation logic by filtering out the hash field line.
        """
        return calculate_hash(file_path)

    def get_existing_artifacts(self) -> List[Dict]:
        """Scans memory/ directory for existing JSON-LD artifacts."""
        artifacts = []
        failed = 0
        if not MEMORY_DIR.exists():
            return artifacts

        for path in MEMORY_DIR.rglob("*.jsonld"):
            try:
                with open(path, 'r') as f:
                    data = json.load(f)
                    if 'novyx:embedding' in data and data['novyx:embedding']:
                        artifacts.append({
                            'id': data['uuid'],
                            'content': data.get('articleBody', ''),
                            'embedding': data['novyx:embedding'],
                            'path': str(path)
                        })
            except Exception as e:
                failed += 1
                message = f"⚠️  Warning: Failed to load artifact {path}: {type(e).__name__}: {e}"
                print(message, file=sys.stderr)
                if self.strict:
                    raise
                continue
        if failed:
            print(f"⚠️  Warning: Skipped {failed} artifact(s) due to load errors.", file=sys.stderr)
        return artifacts

    def find_semantic_links(self, current_embedding, existing_artifacts) -> List[Dict]:
        """Finds Top 3 related artifacts using cosine similarity."""
        if not ML_AVAILABLE or not existing_artifacts:
            return []

        links = []
        # Convert list of embeddings to tensor
        corpus_embeddings = torch.tensor(
            [a['embedding'] for a in existing_artifacts])

        # Compute cosine similarities
        hits = util.semantic_search(
            current_embedding, corpus_embeddings, top_k=3)

        # Process results
        for hit in hits[0]:
            score = hit['score']
            if score > SIMILARITY_THRESHOLD:
                artifact = existing_artifacts[hit['corpus_id']]
                links.append({
                    "targetId": artifact['id'],
                    "confidenceScore": round(score, 4),
                    "rationale": f"Semantic match ({round(score * 100, 1)}%) based on vector similarity."
                })

        return links

    def _safe_list(self, value) -> List:
        """Normalize list-like inputs; split comma/newline strings."""
        if value is None:
            return []
        if isinstance(value, list):
            return value
        if isinstance(value, str):
            parts = [p.strip() for p in value.replace("\n", ",").split(",")]
            return [p for p in parts if p]
        return [value]

    def _persist_artifact(self, artifact: Dict, output_dir: Path, base_filename: str) -> Tuple[Path, str]:
        """Write artifact with hash calculation, cleaning temp files on failure."""
        output_dir.mkdir(parents=True, exist_ok=True)
        temp_path = output_dir / f"{base_filename}_temp.jsonld"
        output_path = None
        try:
            with open(temp_path, 'w', encoding='utf-8') as f:
                json.dump(artifact, f, indent=2)

            file_hash = self.compute_file_hash(temp_path)
            artifact["novyx:integrityHash"] = file_hash
            artifact["Integrity_Hash"] = file_hash

            output_filename = f"{base_filename}_{file_hash[:8]}.jsonld"
            output_path = output_dir / output_filename

            with open(output_path, 'w', encoding='utf-8') as f:
                json.dump(artifact, f, indent=2)

            return output_path, file_hash
        finally:
            if temp_path.exists():
                temp_path.unlink(missing_ok=True)

    def ingest_decision(self, decision_data: Dict, external_links: List[str] = None):
        """Create a Decision artifact with hashing, embeddings, and semantic links."""
        start_time = time.time()
        artifact_uuid = self.generate_uuid()
        timestamp = datetime.now(timezone.utc).isoformat()
        category = decision_data.get("category", "decisions") or "decisions"

        title = decision_data.get("title") or "Decision"
        context = decision_data.get("context", "")
        options_considered = self._safe_list(decision_data.get("options_considered"))
        chosen_option = decision_data.get("chosen_option", "")
        reasoning = decision_data.get("reasoning", "")
        constraints = self._safe_list(decision_data.get("constraints"))
        assumptions = self._safe_list(decision_data.get("assumptions"))
        confidence_raw = decision_data.get("confidence", "")
        expected_outcome = decision_data.get("expected_outcome", "")

        confidence = confidence_raw
        try:
            if isinstance(confidence_raw, str) and confidence_raw.strip():
                confidence_val = float(confidence_raw)
                confidence = round(confidence_val, 3)
        except ValueError:
            confidence = confidence_raw

        content_for_embedding = " ".join([
            title,
            context,
            reasoning,
            expected_outcome,
            " ".join(options_considered),
            " ".join(constraints),
            " ".join(assumptions)
        ]).strip()

        embedding_list = []
        semantic_links = []

        if self.model and content_for_embedding:
            embedding = self.model.encode(content_for_embedding, convert_to_tensor=True)
            embedding_list = embedding.tolist()
            existing = self.get_existing_artifacts()
            if existing:
                semantic_links = self.find_semantic_links(embedding, existing)

        artifact = {
            "@context": {
                "@vocab": "https://schema.org/",
                "novyx": "https://novyx.ai/ns/core#"
            },
            "@type": ["Decision", "DigitalDocument"],
            "uuid": artifact_uuid,
            "id": artifact_uuid,
            "name": title,
            "title": title,
            "articleBody": content_for_embedding or reasoning or context,
            "context": context,
            "createdAt": timestamp,
            "updatedAt": timestamp,
            "options_considered": options_considered,
            "chosen_option": chosen_option,
            "reasoning": reasoning,
            "constraints": constraints,
            "assumptions": assumptions,
            "confidence": confidence,
            "expected_outcome": expected_outcome,
            "category": category,
            "novyx:integrityHash": "pending_calculation",
            "Integrity_Hash": "pending_calculation",
            "novyx:ingestedAt": timestamp,
            "novyx:updatedAt": timestamp,
            "novyx:embedding": embedding_list,
            "novyx:semanticLinks": semantic_links,
            "sameAs": external_links if external_links else []
        }

        output_dir = MEMORY_DIR / "decisions"
        safe_title = title.lower().replace(" ", "_") or "decision"
        output_path, file_hash = self._persist_artifact(artifact, output_dir, safe_title)

        duration = time.time() - start_time
        print(f"✅ Decision recorded: {output_path}")
        print(f"⏱  Time: {duration:.2f}s")

        return {
            "uuid": artifact_uuid,
            "hash": file_hash,
            "path": str(output_path),
            "links": semantic_links
        }

    def ingest(self, file_path: str, category: str = "inbox", external_links: List[str] = None):
        """Main ingestion workflow."""
        start_time = time.time()
        path = Path(file_path)

        if not path.exists():
            print(f"❌ Error: File {file_path} not found.")
            return

        with open(path, 'r', encoding='utf-8') as f:
            content = f.read().strip()

        print(f"📥 Processing: {path.name}...")

        # 1. Identity
        artifact_uuid = self.generate_uuid()
        timestamp = datetime.now(timezone.utc).isoformat()

        # 2. Semantic Processing
        embedding_list = []
        semantic_links = []

        if self.model:
            # Generate embedding
            embedding = self.model.encode(content, convert_to_tensor=True)
            embedding_list = embedding.tolist()  # JSON serializable

            # Find connections
            existing = self.get_existing_artifacts()
            if existing:
                print(
                    f"   Scanning {len(existing)} existing artifacts for connections...")
                semantic_links = self.find_semantic_links(embedding, existing)

        # 3. Construct JSON-LD (Schema v1.1.0 Compliant) - without hash initially
        artifact = {
            "@context": {
                "@vocab": "https://schema.org/",
                "novyx": "https://novyx.ai/ns/core#"
            },
            "@type": "DigitalDocument",
            "uuid": artifact_uuid,
            "name": path.stem.replace("_", " ").title(),
            "articleBody": content,
            "novyx:integrityHash": "pending_calculation",
            "novyx:ingestedAt": timestamp,
            "novyx:embedding": embedding_list,
            "novyx:semanticLinks": semantic_links,
            "sameAs": external_links if external_links else []
        }

        # 4. Persistence (initial write)
        output_dir = MEMORY_DIR / category
        output_dir.mkdir(parents=True, exist_ok=True)

        # Temporary filename
        temp_filename = f"{path.stem}_temp.jsonld"
        temp_path = output_dir / temp_filename

        with open(temp_path, 'w', encoding='utf-8') as f:
            json.dump(artifact, f, indent=2)

        # 5. Calculate integrity hash from actual file
        file_hash = self.compute_file_hash(temp_path)
        artifact["novyx:integrityHash"] = file_hash

        # 6. Final write with correct hash and filename
        output_filename = f"{path.stem}_{file_hash[:8]}.jsonld"
        output_path = output_dir / output_filename

        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(artifact, f, indent=2)

        # Clean up temp file
        temp_path.unlink()

        duration = time.time() - start_time
        print(f"✅ Ingested to {output_path}")
        if semantic_links:
            print(f"🔗 Semantic Links found: {len(semantic_links)}")
        if external_links:
            print(f"🌐 External Authorities linked: {len(external_links)}")
        print(f"⏱  Time: {duration:.2f}s")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Novyx Core Semantic Ingestor v1.1.0")
    parser.add_argument("--file", help="Path to text file to ingest")
    parser.add_argument("--batch", help="Path to directory to batch ingest")
    parser.add_argument("--category", default="inbox",
                        help="Target category (health, research, decisions)")
    parser.add_argument("--link", action="append",
                        help="Add external authority URL (can use multiple times)")
    parser.add_argument("--strict", action="store_true",
                        help="Fail fast on artifact load errors")
    parser.add_argument("--no-embeddings", action="store_true",
                        help="Disable embeddings even if dependencies are installed")
    parser.add_argument("--decision", action="store_true",
                        help="Create an interactive Decision artifact (Layer 1: Causal Memory)")
    parser.add_argument("--decision-file",
                        help="Path to JSON file containing decision fields (title, context, options_considered, etc.)")

    args = parser.parse_args()

    engine = SemanticPulse(strict=args.strict, force_no_embeddings=args.no_embeddings)

    def prompt_decision_inputs() -> Dict:
        print("🧭 Decision Capture (Layer 1: Causal Memory)")
        title = input("Title: ").strip()
        context = input("Context: ").strip()
        options = input("Options considered (comma-separated): ").strip()
        chosen_option = input("Chosen option: ").strip()
        reasoning = input("Reasoning: ").strip()
        constraints = input("Constraints (comma-separated, optional): ").strip()
        assumptions = input("Assumptions (comma-separated, optional): ").strip()
        confidence = input("Confidence (0-1 or enum, optional): ").strip()
        expected_outcome = input("Expected outcome: ").strip()
        category = input("Category (default: decisions): ").strip() or "decisions"

        return {
            "title": title,
            "context": context,
            "options_considered": options,
            "chosen_option": chosen_option,
            "reasoning": reasoning,
            "constraints": constraints,
            "assumptions": assumptions,
            "confidence": confidence,
            "expected_outcome": expected_outcome,
            "category": category
        }

    if args.decision or args.decision_file:
        if args.decision_file:
            with open(args.decision_file, "r", encoding="utf-8") as f:
                decision_payload = json.load(f)
        else:
            decision_payload = prompt_decision_inputs()

        engine.ingest_decision(decision_payload, args.link)

    elif args.file:
        engine.ingest(args.file, args.category, args.link)
    elif args.batch:
        batch_path = Path(args.batch)
        if batch_path.is_dir():
            for txt_file in batch_path.glob("*.txt"):
                engine.ingest(str(txt_file), args.category, args.link)
    else:
        parser.print_help()
