#!/usr/bin/env python3
"""
Novyx Core - Unified CLI
=========================
Single entrypoint for artifact creation, validation, and querying.

Usage:
    python3 -m tools.cli create decision [--json FILE | interactive] [--link URL ...]
    python3 -m tools.cli validate --directory PATH [--verbose]
    python3 -m tools.cli query --directory PATH

Commands:
    create decision    Create a Decision artifact (interactive or JSON)
    validate          Verify artifact integrity
    query             Show artifact statistics
"""

import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Optional

# Import Novyx Core tools
from tools.factory import create_artifact, persist_artifact
from tools.sentinel import NovyxSentinel
from tools.artifacts import get_artifact_config, list_artifact_types


def create_decision_interactive() -> Dict:
    """Interactively prompt for Decision artifact fields."""
    print("📝 Create Decision Artifact (Interactive Mode)")
    print("=" * 50)
    
    title = input("Title: ").strip()
    if not title:
        print("❌ Title is required")
        sys.exit(1)
    
    context = input("Context: ").strip()
    
    print("\nOptions considered (comma-separated): ")
    options_input = input("> ").strip()
    options_considered = [opt.strip() for opt in options_input.split(",") if opt.strip()]
    
    chosen_option = input("Chosen option: ").strip()
    reasoning = input("Reasoning: ").strip()
    
    print("\nConstraints (comma-separated, optional): ")
    constraints_input = input("> ").strip()
    constraints = [c.strip() for c in constraints_input.split(",") if c.strip()]
    
    print("\nAssumptions (comma-separated, optional): ")
    assumptions_input = input("> ").strip()
    assumptions = [a.strip() for a in assumptions_input.split(",") if a.strip()]
    
    confidence_input = input("Confidence (0.0-1.0): ").strip()
    try:
        confidence = float(confidence_input) if confidence_input else 0.5
    except ValueError:
        confidence = 0.5
    
    expected_outcome = input("Expected outcome: ").strip()
    category = input("Category (default: decisions): ").strip() or "decisions"
    
    return {
        "title": title,
        "context": context,
        "options_considered": options_considered,
        "chosen_option": chosen_option,
        "reasoning": reasoning,
        "constraints": constraints,
        "assumptions": assumptions,
        "confidence": confidence,
        "expected_outcome": expected_outcome,
        "category": category
    }


def load_decision_from_json(json_path: Path) -> Dict:
    """Load Decision payload from JSON file."""
    if not json_path.exists():
        print(f"❌ File not found: {json_path}")
        sys.exit(1)
    
    try:
        with open(json_path, 'r') as f:
            return json.load(f)
    except json.JSONDecodeError as e:
        print(f"❌ Invalid JSON: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"❌ Error reading file: {e}")
        sys.exit(1)


def cmd_create_decision(args):
    """Create a Decision artifact."""
    # Get payload
    if args.json:
        payload = load_decision_from_json(Path(args.json))
    else:
        payload = create_decision_interactive()
    
    # Create artifact using factory
    try:
        print("\n🔨 Creating Decision artifact...")
        artifact = create_artifact("decision", payload, external_links=args.link)
        
        # Get output directory
        config = get_artifact_config("decision")
        output_dir = Path(config["output_dir"])
        
        # Persist artifact
        output_path = persist_artifact(artifact, output_dir)
        
        print(f"✅ Decision created successfully!")
        print(f"   📁 Path: {output_path}")
        print(f"   🆔 UUID: {artifact['uuid']}")
        print(f"   🔒 Hash: {artifact['novyx:integrityHash'][:16]}...")
        
        # Auto-validate the created artifact
        print(f"\n🛡️  Running integrity validation...")
        sentinel = NovyxSentinel(root_dir=output_dir, verbose=args.verbose if hasattr(args, 'verbose') else False)
        result = sentinel.validate_artifact(output_path)
        
        if result.passed:
            print(f"✅ Validation: PASSED")
            print(f"   All integrity checks passed")
            return 0
        else:
            print(f"❌ Validation: FAILED")
            for error in result.errors:
                print(f"   ✗ {error}")
            return 1
    
    except KeyError as e:
        print(f"❌ Missing required fields: {e}")
        return 1
    except Exception as e:
        print(f"❌ Failed to create artifact: {e}")
        import traceback
        if args.verbose if hasattr(args, 'verbose') else False:
            traceback.print_exc()
        return 1


def cmd_validate(args):
    """Validate artifacts in a directory."""
    directory = Path(args.directory)
    
    # Phase 9: Multi-Tenant filtering
    tenant_id = getattr(args, 'tenant', None)
    # Only apply tenant filter if it's an actual string value
    if tenant_id and isinstance(tenant_id, str):
        directory = directory / tenant_id
        print(f"🏢 Validating tenant: {tenant_id}")
    
    if not directory.exists():
        print(f"❌ Directory not found: {directory}")
        if tenant_id:
            print(f"   (Tenant '{tenant_id}' may not have any artifacts)")
        return 1
    
    print(f"🛡️  Validating artifacts in {directory}")
    print()
    
    try:
        sentinel = NovyxSentinel(root_dir=directory, verbose=args.verbose)
        sentinel.scan_directory(directory)
        passed = sentinel.print_report()
        
        return 0 if passed else 1
    
    except Exception as e:
        print(f"❌ Validation failed: {e}")
        import traceback
        if args.verbose:
            traceback.print_exc()
        return 1


def cmd_query(args):
    """Query and show statistics for artifacts in a directory."""
    directory = Path(args.directory)
    
    # Phase 9: Multi-Tenant filtering
    tenant_id = getattr(args, 'tenant', None)
    # Only apply tenant filter if it's an actual string value
    if tenant_id and isinstance(tenant_id, str):
        directory = directory / tenant_id
        print(f"🏢 Filtering by tenant: {tenant_id}")
    
    if not directory.exists():
        print(f"❌ Directory not found: {directory}")
        if tenant_id:
            print(f"   (Tenant '{tenant_id}' may not have any artifacts)")
        return 1
    
    # If --search is provided, use semantic search (Phase 8)
    if hasattr(args, 'search') and args.search:
        try:
            # Import QueryEngine (lazy import to avoid dependency issues)
            sys.path.insert(0, str(Path(__file__).parent.parent))
            from tools.query import QueryEngine
            
            # Create engine and perform search
            engine = QueryEngine(strict=False, force_no_embeddings=False)
            
            # Get search parameters
            min_score = getattr(args, 'min_score', 0.2)
            top_k = getattr(args, 'top_k', 10)
            json_output = getattr(args, 'json', False)
            
            results = engine.search_semantic(
                args.search,
                top_k=top_k,
                min_score=min_score,
                json_output=json_output
            )
            
            if json_output and results:
                print(json.dumps(results, indent=2))
            
            return 0 if results else 1
            
        except Exception as e:
            print(f"❌ Semantic search failed: {e}")
            if getattr(args, 'verbose', False):
                import traceback
                traceback.print_exc()
            return 1
    
    # Otherwise, show statistics
    print(f"📊 Querying artifacts in {directory}")
    print("=" * 50)
    
    try:
        # Scan for .jsonld files
        artifacts = list(directory.rglob("*.jsonld"))
        
        if not artifacts:
            print("No artifacts found.")
            return 0
        
        # Parse and categorize
        by_type = {}
        by_category = {}
        latest_timestamp = None
        total = 0
        
        for artifact_path in artifacts:
            try:
                with open(artifact_path, 'r') as f:
                    data = json.load(f)
                
                # Get type
                artifact_type = data.get("@type")
                if isinstance(artifact_type, list):
                    type_str = ", ".join(artifact_type)
                else:
                    type_str = artifact_type or "Unknown"
                
                by_type[type_str] = by_type.get(type_str, 0) + 1
                
                # Get category (from directory or field)
                category = data.get("category") or artifact_path.parent.name
                by_category[category] = by_category.get(category, 0) + 1
                
                # Get timestamp
                timestamp = (data.get("createdAt") or 
                           data.get("novyx:ingestedAt") or 
                           data.get("Temporal_Marker"))
                
                if timestamp and (latest_timestamp is None or timestamp > latest_timestamp):
                    latest_timestamp = timestamp
                
                total += 1
            
            except Exception as e:
                if args.verbose if hasattr(args, 'verbose') else False:
                    print(f"⚠️  Warning: Failed to parse {artifact_path}: {e}")
        
        # Print summary
        print(f"\n📈 Summary:")
        print(f"   Total artifacts: {total}")
        print()
        
        print(f"📂 By Type:")
        for type_str, count in sorted(by_type.items(), key=lambda x: x[1], reverse=True):
            print(f"   • {type_str}: {count}")
        print()
        
        print(f"📁 By Category:")
        for category, count in sorted(by_category.items(), key=lambda x: x[1], reverse=True):
            print(f"   • {category}: {count}")
        print()
        
        if latest_timestamp:
            print(f"🕐 Latest timestamp: {latest_timestamp}")
        
        print("=" * 50)
        
        return 0
    
    except Exception as e:
        print(f"❌ Query failed: {e}")
        import traceback
        if args.verbose if hasattr(args, 'verbose') else False:
            traceback.print_exc()
        return 1


def cmd_explain(args):
    """Explain an artifact (human-readable context)."""
    from tools.graph import get_primary_ids, extract_internal_links
    from tools.graph_cache import get_index
    
    try:
        # Find artifact by file or ID
        if args.file:
            artifact_path = Path(args.file)
            if not artifact_path.exists():
                print(f"❌ File not found: {artifact_path}")
                return 1
            
            # Load artifact
            try:
                with open(artifact_path, 'r') as f:
                    data = json.load(f)
            except Exception as e:
                print(f"❌ Failed to parse artifact: {e}")
                return 1
            
            # Build index if directory provided (for inbound links)
            inbound = []
            if args.directory:
                directory = Path(args.directory)
                index = get_index(directory)
                primary_ids = get_primary_ids(data)
                for pid in primary_ids:
                    if pid in index['inbound_links']:
                        inbound.extend(index['inbound_links'][pid])
        
        elif args.id and args.directory:
            # Search by ID in directory
            directory = Path(args.directory)
            index = get_index(directory)
            
            if args.id not in index['id_to_file']:
                print(f"❌ Artifact not found with ID: {args.id}")
                return 1
            
            # Load artifact
            artifact_path = Path(index['id_to_file'][args.id])
            with open(artifact_path, 'r') as f:
                data = json.load(f)
            
            # Get inbound links
            inbound = index['inbound_links'].get(args.id, [])
        
        else:
            print("❌ Must provide --file or (--id and --directory)")
            return 1
        
        # Print explanation
        print("=" * 60)
        print("🔍 Artifact Explanation")
        print("=" * 60)
        print()
        
        # Primary IDs
        primary_ids = get_primary_ids(data)
        print(f"🆔 Primary ID(s):")
        for pid in primary_ids:
            print(f"   {pid}")
        print()
        
        # Type
        artifact_type = data.get("@type", "Unknown")
        if isinstance(artifact_type, list):
            artifact_type = ", ".join(artifact_type)
        print(f"📦 Type: {artifact_type}")
        print()
        
        # Title/Name
        title = data.get("title") or data.get("name") or "Untitled"
        print(f"📄 Title: {title}")
        print()
        
        # Timestamps
        created = data.get("createdAt") or data.get("Temporal_Marker") or data.get("novyx:ingestedAt")
        updated = data.get("updatedAt") or data.get("novyx:updatedAt") or created
        if created:
            print(f"🕐 Created: {created}")
        if updated and updated != created:
            print(f"🕐 Updated: {updated}")
        if created or updated:
            print()
        
        # Outbound links
        internal_links = extract_internal_links(data)
        if internal_links:
            print(f"🔗 Outbound Links ({len(internal_links)}):")
            
            # Check if resolved (if directory provided)
            if args.directory and 'index' in locals():
                for link in sorted(internal_links):
                    resolved = "✅" if link in index['id_to_file'] else "❌ orphaned"
                    print(f"   {link} {resolved}")
            else:
                for link in sorted(internal_links):
                    print(f"   {link}")
            print()
        
        # Inbound links
        if inbound:
            print(f"⬅️  Inbound Links ({len(inbound)} artifacts reference this):")
            for source_file in sorted(set(inbound))[:10]:  # Show first 10
                print(f"   {source_file}")
            if len(inbound) > 10:
                print(f"   ... and {len(inbound) - 10} more")
            print()
        
        print("=" * 60)
        
        return 0
    
    except Exception as e:
        print(f"❌ Explain failed: {e}")
        import traceback
        if args.verbose if hasattr(args, 'verbose') else False:
            traceback.print_exc()
        return 1


def cmd_graph(args):
    """Show graph statistics."""
    from tools.graph import scan_artifacts, build_index, find_orphaned_links
    
    directory = Path(args.directory)
    
    if not directory.exists():
        print(f"❌ Directory not found: {directory}")
        return 1
    
    try:
        print(f"📊 Graph Analysis: {directory}")
        print("=" * 60)
        
        # Scan and build index
        artifacts, errors = scan_artifacts(directory)
        
        if errors and (args.verbose if hasattr(args, 'verbose') else False):
            print(f"\n⚠️  Scan errors ({len(errors)}):")
            for error in errors[:5]:
                print(f"  {error}")
            if len(errors) > 5:
                print(f"  ... and {len(errors) - 5} more")
            print()
        
        if not artifacts:
            print("No artifacts found.")
            return 0
        
        index = build_index(artifacts)
        orphaned = find_orphaned_links(index)
        
        # Statistics
        print(f"\n📈 Statistics:")
        print(f"   Total artifacts: {len(artifacts)}")
        print(f"   Total primary IDs: {len(index['id_to_file'])}")
        print(f"   Total internal links: {sum(len(links) for links in index['file_to_links'].values())}")
        print(f"   Orphaned links: {sum(len(orphans) for orphans in orphaned.values())}")
        print()
        
        # Top linked artifacts
        if index['inbound_links']:
            print(f"🔝 Top 5 Most-Linked Artifacts:")
            sorted_by_inbound = sorted(
                index['inbound_links'].items(),
                key=lambda x: len(x[1]),
                reverse=True
            )[:5]
            
            for target_id, sources in sorted_by_inbound:
                print(f"   {target_id[:50]}... ({len(sources)} inbound)")
            print()
        
        # Orphaned links detail
        if orphaned:
            print(f"❌ Orphaned Links ({len(orphaned)} files affected):")
            for file_path, orphan_ids in list(orphaned.items())[:5]:
                print(f"   {file_path}: {len(orphan_ids)} orphans")
            if len(orphaned) > 5:
                print(f"   ... and {len(orphaned) - 5} more files")
            print()
        
        print("=" * 60)
        
        return 0
    
    except Exception as e:
        print(f"❌ Graph analysis failed: {e}")
        import traceback
        if args.verbose if hasattr(args, 'verbose') else False:
            traceback.print_exc()
        return 1


def cmd_repair(args):
    """Repair graph by removing orphaned links or creating stubs."""
    from tools.graph import scan_artifacts, build_index, find_orphaned_links, get_primary_ids, extract_internal_links
    from tools.graph_cache import invalidate as invalidate_graph_cache
    from tools.util import calculate_hash
    
    directory = Path(args.directory)
    
    # Phase 9: Multi-Tenant filtering
    tenant_id = getattr(args, 'tenant', None)
    # Only apply tenant filter if it's an actual string value
    if tenant_id and isinstance(tenant_id, str):
        directory = directory / tenant_id
        print(f"🏢 Repairing tenant: {tenant_id}")
    
    if not directory.exists():
        print(f"❌ Directory not found: {directory}")
        if tenant_id:
            print(f"   (Tenant '{tenant_id}' may not have any artifacts)")
        return 1
    
    try:
        print(f"🔧 Repair Mode: {directory}")
        if args.dry_run:
            print("   (DRY RUN - no changes will be made)")
        print("=" * 60)
        
        # Scan and find orphans
        artifacts, errors = scan_artifacts(directory)
        if not artifacts:
            print("No artifacts found.")
            return 0
        
        index = build_index(artifacts)
        orphaned = find_orphaned_links(index)
        
        if not orphaned:
            print("✅ No orphaned links found. Graph is healthy!")
            return 0
        
        total_orphans = sum(len(orphans) for orphans in orphaned.values())
        print(f"Found {total_orphans} orphaned link(s) in {len(orphaned)} file(s)")
        print()
        
        if args.create_stubs:
            # Create stub artifacts for missing IDs
            print("Creating stub artifacts for orphaned IDs...")
            
            # Collect all unique orphaned IDs
            all_orphaned_ids = set()
            for orphans in orphaned.values():
                all_orphaned_ids.update(orphans)
            
            # Determine stub directory
            stub_dir = directory / "stubs"
            
            if not args.dry_run:
                stub_dir.mkdir(exist_ok=True)
            
            from datetime import datetime, timezone
            timestamp = datetime.now(timezone.utc).isoformat()
            
            for orphan_id in all_orphaned_ids:
                stub_artifact = {
                    "@context": {
                        "@vocab": "https://schema.org/",
                        "novyx": "https://novyx.ai/ns/core#"
                    },
                    "@type": "Artifact",
                    "uuid": orphan_id,
                    "Persistent_ID": orphan_id,
                    "@id": orphan_id,
                    "name": f"Stub for {orphan_id[:20]}",
                    "createdAt": timestamp,
                    "updatedAt": timestamp,
                    "Temporal_Marker": timestamp,
                    "novyx:ingestedAt": timestamp,
                    "novyx:updatedAt": timestamp,
                    "novyx:integrityHash": "pending_calculation",
                    "Integrity_Hash": "pending_calculation",
                    "novyx:semanticLinks": [],
                    "sameAs": []
                }
                
                stub_filename = f"stub_{orphan_id.split(':')[-1][:16]}.jsonld"
                stub_path = stub_dir / stub_filename
                
                if args.dry_run:
                    print(f"   Would create: {stub_path}")
                else:
                    # Write temp file, calculate hash, write final
                    temp_path = stub_dir / f"{stub_filename}.temp"
                    with open(temp_path, 'w') as f:
                        json.dump(stub_artifact, f, indent=2)
                    
                    file_hash = calculate_hash(temp_path)
                    stub_artifact["novyx:integrityHash"] = file_hash
                    stub_artifact["Integrity_Hash"] = file_hash
                    
                    with open(stub_path, 'w') as f:
                        json.dump(stub_artifact, f, indent=2)
                    
                    temp_path.unlink()
                    print(f"   Created: {stub_path}")
            
            print(f"\n✅ {'Would create' if args.dry_run else 'Created'} {len(all_orphaned_ids)} stub artifact(s)")
        
        else:
            # Remove orphaned links from source artifacts
            print("Removing orphaned links from artifacts...")
            
            for file_path, orphan_ids in orphaned.items():
                artifact_path = Path(file_path)
                
                if args.dry_run:
                    print(f"   Would clean: {artifact_path} ({len(orphan_ids)} orphans)")
                    continue
                
                # Load artifact
                with open(artifact_path, 'r') as f:
                    data = json.load(f)
                
                # Remove orphaned links from known fields
                modified = False
                
                # Context_Link
                if 'Context_Link' in data and data['Context_Link'] in orphan_ids:
                    del data['Context_Link']
                    modified = True
                
                # novyx:semanticLinks
                if 'novyx:semanticLinks' in data and isinstance(data['novyx:semanticLinks'], list):
                    original_len = len(data['novyx:semanticLinks'])
                    data['novyx:semanticLinks'] = [
                        link for link in data['novyx:semanticLinks']
                        if not (isinstance(link, dict) and link.get('targetId') in orphan_ids)
                    ]
                    if len(data['novyx:semanticLinks']) != original_len:
                        modified = True
                
                # Depends_On
                if 'Depends_On' in data:
                    if isinstance(data['Depends_On'], str) and data['Depends_On'] in orphan_ids:
                        del data['Depends_On']
                        modified = True
                    elif isinstance(data['Depends_On'], list):
                        original_len = len(data['Depends_On'])
                        data['Depends_On'] = [d for d in data['Depends_On'] if d not in orphan_ids]
                        if len(data['Depends_On']) != original_len:
                            modified = True
                
                if modified:
                    # Recompute hash
                    temp_path = artifact_path.parent / f"{artifact_path.name}.temp"
                    with open(temp_path, 'w') as f:
                        json.dump(data, f, indent=2)
                    
                    file_hash = calculate_hash(temp_path)
                    data["novyx:integrityHash"] = file_hash
                    data["Integrity_Hash"] = file_hash
                    
                    # Write final
                    with open(artifact_path, 'w') as f:
                        json.dump(data, f, indent=2)
                    
                    temp_path.unlink()
                    print(f"   Cleaned: {artifact_path}")
            
            print(f"\n✅ {'Would remove' if args.dry_run else 'Removed'} orphaned links from {len(orphaned)} file(s)")
        
        print("=" * 60)

        # Invalidate graph cache after successful repair (not dry-run)
        if not args.dry_run:
            invalidate_graph_cache(directory)

        return 0

    except Exception as e:
        print(f"❌ Repair failed: {e}")
        import traceback
        if args.verbose if hasattr(args, 'verbose') else False:
            traceback.print_exc()
        return 1


def cmd_report(args):
    """Generate operator report."""
    from tools.operator_report import generate_report
    
    directory = Path(args.directory)
    output_dir = Path(args.out)
    
    if not directory.exists():
        print(f"❌ Directory not found: {directory}")
        return 1
    
    return generate_report(directory, output_dir)


def cmd_install_hooks(args):
    """Install git hooks for integrity enforcement."""
    import subprocess
    
    # Get repo root
    try:
        repo_root = Path.cwd()
        scripts_dir = repo_root / "scripts"
        install_script = scripts_dir / "install_git_hooks.sh"
        
        if not install_script.exists():
            print(f"❌ Install script not found: {install_script}")
            print(f"   Make sure you're running from the repository root.")
            return 1
        
        # Run install script
        result = subprocess.run(
            ["bash", str(install_script)],
            cwd=repo_root,
            capture_output=False
        )
        
        return result.returncode
    
    except Exception as e:
        print(f"❌ Hook installation failed: {e}")
        import traceback
        if args.verbose if hasattr(args, 'verbose') else False:
            traceback.print_exc()
        return 1


def cmd_version(args):
    """Manage artifact versions (list, rollback). Phase 10."""
    from tools.versioning import list_versions, rollback_to_version
    from datetime import datetime, timezone
    
    # Determine tenant_id from args
    tenant_id = args.tenant if hasattr(args, 'tenant') and args.tenant else None
    
    try:
        if args.action == "list":
            # List all versions for an artifact
            if not args.artifact_id:
                print("❌ --artifact-id is required for --list")
                return 1
            
            # Handle tenant-specific versions directory
            versions_dir = Path("memory/versions")
            if tenant_id:
                versions_dir = Path("memory/versions") / tenant_id
            
            versions = list_versions(args.artifact_id, versions_dir=versions_dir)
            
            if not versions:
                print(f"📋 No versions found for artifact {args.artifact_id}")
                if tenant_id:
                    print(f"   (Tenant: {tenant_id})")
                return 0
            
            print(f"📋 Version History for {args.artifact_id}")
            if tenant_id:
                print(f"   Tenant: {tenant_id}")
            print(f"   Total versions: {len(versions)}\n")
            
            for v in sorted(versions, key=lambda x: x.get("version_number", 0)):
                version_num = v.get("version_number", "?")
                timestamp = v.get("timestamp", "?")
                version_id = v.get("uuid", "?")
                print(f"   v{version_num}: {timestamp}")
                print(f"      UUID: {version_id}")
            
            return 0
        
        elif args.action == "rollback":
            # Rollback to a specific version
            if not args.artifact_id:
                print("❌ --artifact-id is required for --rollback")
                return 1
            if not args.version:
                print("❌ --version is required for --rollback")
                return 1
            
            target_version = int(args.version)
            
            print(f"🔄 Rolling back artifact {args.artifact_id} to version {target_version}...")
            if tenant_id:
                print(f"   Tenant: {tenant_id}")
            
            # Get the restored artifact content
            restored_artifact = rollback_to_version(args.artifact_id, target_version)
            
            if not restored_artifact:
                print(f"❌ Could not restore artifact to version {target_version}")
                print(f"   Version may not exist or snapshot unavailable")
                return 1
            
            # Persist the restored artifact using factory
            from tools import factory
            from tools.artifacts import get_artifact_config
            
            # Determine artifact type and output directory
            artifact_type_raw = restored_artifact.get("@type", "")
            # Handle both string and list formats for @type
            if isinstance(artifact_type_raw, list):
                artifact_type_raw = artifact_type_raw[0] if artifact_type_raw else ""
            artifact_type = artifact_type_raw.split(":")[-1].lower() if artifact_type_raw else ""
            
            config = get_artifact_config(artifact_type)
            if not config:
                print(f"❌ Unknown artifact type: {artifact_type}")
                return 1
            
            output_dir = Path(config["output_dir"])
            if tenant_id:
                output_dir = output_dir / tenant_id
            
            # Update timestamps
            restored_artifact["updatedAt"] = datetime.now(timezone.utc).isoformat()
            restored_artifact["novyx:updatedAt"] = restored_artifact["updatedAt"]
            
            # Persist without creating a new version (to avoid circular versioning)
            updated_path = factory.persist_artifact(restored_artifact, output_dir, skip_versioning=True)
            
            print(f"✅ Rollback successful!")
            # Handle both absolute and relative paths for display
            try:
                display_path = updated_path.relative_to(Path.cwd())
            except ValueError:
                display_path = updated_path
            print(f"   Updated: {display_path}")
            
            return 0
        
        else:
            print(f"❌ Unknown version action: {args.action}")
            return 1
    
    except ValueError as e:
        print(f"❌ Invalid version number: {e}")
        return 1
    except FileNotFoundError as e:
        print(f"❌ Artifact or version not found: {e}")
        return 1
    except Exception as e:
        print(f"❌ Version operation failed: {e}")
        import traceback
        if args.verbose if hasattr(args, 'verbose') else False:
            traceback.print_exc()
        return 1


def cmd_batch(args):
    """Batch operations (streaming, bulk create). Phase 13."""
    from pathlib import Path
    import json
    import time

    directory = Path(args.directory)
    tenant_id = args.tenant if hasattr(args, 'tenant') and args.tenant else None
    batch_size = args.batch_size if hasattr(args, 'batch_size') else 50

    try:
        from tools.batch import (
            count_artifacts, stream_artifacts, batch_create,
            batch_validate, is_streaming_available
        )

        if args.count:
            # Fast artifact count (no parsing)
            print(f"📊 Counting artifacts in {directory}")
            if tenant_id:
                print(f"   Tenant: {tenant_id}")

            start = time.time()
            count = count_artifacts(directory, tenant_id=tenant_id)
            elapsed = time.time() - start

            print(f"\n   Total artifacts: {count}")
            print(f"   Elapsed: {elapsed:.3f}s")
            return 0

        elif args.stream_test:
            # Test streaming performance
            print(f"📊 Streaming test for {directory}")
            print(f"   Batch size: {batch_size}")
            print(f"   ijson available: {is_streaming_available()}")
            if tenant_id:
                print(f"   Tenant: {tenant_id}")

            start = time.time()
            total = 0
            batches = 0

            for batch in stream_artifacts(directory, batch_size=batch_size, tenant_id=tenant_id):
                batches += 1
                total += len(batch)
                print(f"   Batch {batches}: {len(batch)} artifacts")

            elapsed = time.time() - start
            print(f"\n   Total: {total} artifacts in {batches} batches")
            print(f"   Elapsed: {elapsed:.3f}s")
            if total > 0:
                print(f"   Rate: {total/elapsed:.1f} artifacts/sec")
            return 0

        elif args.create:
            # Batch create from JSON file
            json_file = Path(args.create)
            if not json_file.exists():
                print(f"❌ File not found: {json_file}")
                return 1

            print(f"📦 Batch create from {json_file}")
            if tenant_id:
                print(f"   Tenant: {tenant_id}")

            with open(json_file, 'r') as f:
                artifacts = json.load(f)

            if not isinstance(artifacts, list):
                artifacts = [artifacts]

            print(f"   Artifacts to create: {len(artifacts)}")

            result = batch_create(
                artifacts,
                tenant_id=tenant_id,
                directory=directory,
                skip_versioning=args.skip_versioning if hasattr(args, 'skip_versioning') else False
            )

            print(f"\n✅ Batch create complete")
            print(f"   Created: {result['created']}")
            print(f"   Failed: {result['failed']}")

            if result['errors']:
                print(f"\n   Errors:")
                for error in result['errors'][:5]:
                    print(f"     - Index {error['index']}: {error['error']}")

            return 0 if result['failed'] == 0 else 1

        elif args.validate:
            # Batch validate with streaming
            print(f"🛡️  Batch validate for {directory}")
            print(f"   Batch size: {batch_size}")
            if tenant_id:
                print(f"   Tenant: {tenant_id}")

            start = time.time()
            total_passed = 0
            total_failed = 0
            batches = 0

            for result in batch_validate(directory, tenant_id=tenant_id, batch_size=batch_size):
                batches += 1
                total_passed += result['passed']
                total_failed += result['failed']
                print(f"   Batch {batches}: {result['passed']}/{result['batch_size']} passed")

            elapsed = time.time() - start
            total = total_passed + total_failed

            print(f"\n📊 SUMMARY:")
            print(f"   Total: {total} artifacts")
            print(f"   ✅ Passed: {total_passed}")
            print(f"   ❌ Failed: {total_failed}")
            print(f"   Batches: {batches}")
            print(f"   Elapsed: {elapsed:.3f}s")

            return 0 if total_failed == 0 else 1

        else:
            print("❌ No batch operation specified. Use --count, --stream-test, --create, or --validate")
            return 1

    except ImportError as e:
        print(f"❌ Batch module not available: {e}")
        return 1
    except Exception as e:
        print(f"❌ Batch operation failed: {e}")
        import traceback
        traceback.print_exc()
        return 1


def cmd_export(args):
    """Export artifacts to external formats (Phase 11)."""
    try:
        from tools.export import (
            export_artifacts, ExportFormat, check_dependencies
        )

        # Map format string to enum
        format_map = {
            "rdf": ExportFormat.RDF,
            "neo4j": ExportFormat.NEO4J,
            "cypher": ExportFormat.CYPHER,
            "jsonld": ExportFormat.JSONLD,
        }
        export_format = format_map[args.format]

        # Check dependencies
        available, msg = check_dependencies(export_format)
        if not available:
            print(f"❌ {msg}")
            return 1

        directory = Path(args.directory)
        output_path = Path(args.output)

        print(f"📤 Exporting from {directory} to {args.format.upper()}...")

        stats = export_artifacts(
            directory,
            export_format,
            output_path,
            tenant_id=args.tenant,
            include_embeddings=args.include_embeddings,
            rdf_format=args.rdf_format
        )

        print(f"\n✅ Export Complete:")
        print(f"   Total artifacts: {stats['total_artifacts']}")
        print(f"   Exported: {stats['exported']}")
        print(f"   Failed: {stats['failed']}")
        if 'triples' in stats:
            print(f"   RDF triples: {stats['triples']}")
        if 'nodes' in stats:
            print(f"   Neo4j nodes: {stats['nodes']}")
        print(f"   Output: {stats['output_path']}")

        return 0 if stats['failed'] == 0 else 1

    except ImportError as e:
        print(f"❌ Export module not available: {e}")
        return 1
    except Exception as e:
        print(f"❌ Export failed: {e}")
        import traceback
        traceback.print_exc()
        return 1


def cmd_import(args):
    """Import artifacts from external formats (Phase 11)."""
    try:
        from tools.import_data import (
            import_artifacts, ImportFormat, check_dependencies
        )

        # Map format string to enum
        format_map = {
            "rdf": ImportFormat.RDF,
            "jsonld": ImportFormat.JSONLD,
        }
        import_format = format_map[args.format]

        # Check dependencies
        available, msg = check_dependencies(import_format)
        if not available:
            print(f"❌ {msg}")
            return 1

        input_path = Path(args.input)
        output_dir = Path(args.output)

        if not input_path.exists():
            print(f"❌ Input file not found: {input_path}")
            return 1

        print(f"📥 Importing from {input_path} ({args.format.upper()})...")

        stats = import_artifacts(
            input_path,
            import_format,
            output_dir,
            tenant_id=args.tenant,
            regenerate_hashes=not args.no_regenerate_hashes,
            rdf_format=args.rdf_format
        )

        print(f"\n✅ Import Complete:")
        total = stats.get('total_artifacts', stats.get('total_subjects', 0))
        print(f"   Total items: {total}")
        print(f"   Imported: {stats['imported']}")
        print(f"   Failed: {stats['failed']}")
        print(f"   Skipped: {stats['skipped']}")
        print(f"   Output: {stats['output_dir']}")

        return 0 if stats['failed'] == 0 else 1

    except ImportError as e:
        print(f"❌ Import module not available: {e}")
        return 1
    except Exception as e:
        print(f"❌ Import failed: {e}")
        import traceback
        traceback.print_exc()
        return 1


def cmd_sync(args):
    """Sync remote artifacts via federation (Phase 14)."""
    try:
        from tools.federation import (
            federation_status, get_remote_refs, sync_refs,
            pull_artifact, REQUESTS_AVAILABLE
        )

        if args.action == "status":
            status = federation_status()
            print("Federation Status")
            print("=" * 50)
            print(f"Total RemoteRefs: {status['total']}")
            print(f"HTTP client (requests): {'available' if status['requests_available'] else 'NOT AVAILABLE'}")
            print("\nBy Status:")
            for s, count in status['by_status'].items():
                if count > 0:
                    print(f"  {s}: {count}")
            print("\nBy Direction:")
            for d, count in status['by_direction'].items():
                if count > 0:
                    print(f"  {d}: {count}")
            print(f"\nRemote Instances ({len(status['remote_instances'])}):")
            for inst in status['remote_instances']:
                print(f"  - {inst}")
            return 0

        elif args.action == "list":
            refs = get_remote_refs(
                status=args.status,
                direction=args.direction,
                tenant_id=args.tenant
            )
            print(f"Found {len(refs)} RemoteRef(s)")
            for ref in refs:
                print(f"\n- {ref.get('remote_artifact_id')}")
                print(f"  Instance: {ref.get('remote_instance')}")
                print(f"  Status: {ref.get('sync_status')}")
                print(f"  Direction: {ref.get('direction')}")
                if ref.get('local_artifact_id'):
                    print(f"  Local copy: {ref.get('local_artifact_id')}")
            return 0

        elif args.action == "run":
            if not REQUESTS_AVAILABLE:
                print("WARNING: requests library not available.")
                print("         Install with: pip install requests")
                print("         Sync will mark refs but cannot connect to remotes.")

            print("Syncing remote references...")
            results = sync_refs(
                status=args.status,
                direction=args.direction,
                tenant_id=args.tenant,
                conflict_resolution=args.conflict_resolution
            )

            print(f"\n{'='*50}")
            print("Sync Results")
            print(f"{'='*50}")
            print(f"Synced: {results['synced']}")
            print(f"Failed: {results['failed']}")
            print(f"Conflicts: {results['conflicts']}")
            if results['errors']:
                print("\nErrors:")
                for err in results['errors']:
                    print(f"  - {err['ref_id'][:30]}...: {err['message']}")
            return 0 if results['failed'] == 0 else 1

        elif args.action == "pull":
            if not args.remote or not args.artifact_id:
                print("--remote and --artifact-id required for pull")
                return 1

            if not REQUESTS_AVAILABLE:
                print("requests library required for pull.")
                print("Install with: pip install requests")
                return 1

            print(f"Pulling {args.artifact_id} from {args.remote}...")
            artifact, ref = pull_artifact(
                args.remote,
                args.artifact_id,
                tenant_id=args.tenant
            )
            print(f"\n Pulled successfully!")
            print(f"   Local ID: {artifact.get('uuid')}")
            print(f"   Ref status: {ref.get('sync_status')}")
            return 0

        else:
            print(f" Unknown sync action: {args.action}")
            return 1

    except ImportError as e:
        print(f" Federation module not available: {e}")
        return 1
    except Exception as e:
        print(f" Sync failed: {e}")
        import traceback
        traceback.print_exc()
        return 1


def main():
    """Main CLI entrypoint."""
    parser = argparse.ArgumentParser(
        description="Novyx Core - Unified CLI for artifact management",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # Create Decision interactively
  python3 -m tools.cli create decision

  # Create Decision from JSON
  python3 -m tools.cli create decision --json decision.json --link https://github.com/user

  # Validate artifacts
  python3 -m tools.cli validate --directory memory/decisions --verbose
  
  # Query artifact statistics
  python3 -m tools.cli query --directory memory
  
  # Explain an artifact
  python3 -m tools.cli explain --file memory/decisions/dec_001.jsonld --directory memory
  
  # Graph analysis
  python3 -m tools.cli graph --directory memory
  
  # Repair graph (dry run)
  python3 -m tools.cli repair --directory memory --dry-run
        """
    )
    
    subparsers = parser.add_subparsers(dest="command", help="Available commands")
    
    # Create command
    create_parser = subparsers.add_parser("create", help="Create artifacts")
    create_subparsers = create_parser.add_subparsers(dest="artifact_type", help="Artifact type")
    
    # Create decision
    decision_parser = create_subparsers.add_parser("decision", help="Create a Decision artifact")
    decision_parser.add_argument("--json", help="Path to JSON file with Decision payload")
    decision_parser.add_argument("--link", action="append", help="External authority URL (repeatable)")
    decision_parser.add_argument("--verbose", action="store_true", help="Verbose output")
    
    # Validate command (Phase 9: Multi-Tenant)
    validate_parser = subparsers.add_parser("validate", help="Validate artifact integrity")
    validate_parser.add_argument("--directory", required=True, help="Directory to validate")
    validate_parser.add_argument("--tenant", help="Filter by tenant ID (Phase 9)")
    validate_parser.add_argument("--verbose", action="store_true", help="Verbose output")
    
    # Query command (Phase 8: Enhanced with semantic search, Phase 9: Multi-Tenant)
    query_parser = subparsers.add_parser("query", help="Query artifact statistics or perform semantic search")
    query_parser.add_argument("--directory", required=True, help="Directory to query")
    query_parser.add_argument("--search", help="Semantic search query (Phase 8)")
    query_parser.add_argument("--tenant", help="Filter by tenant ID (Phase 9)")
    query_parser.add_argument("--min-score", type=float, default=0.2, 
                             help="Minimum similarity score for semantic search (0.0-1.0, default: 0.2)")
    query_parser.add_argument("--top-k", type=int, default=10,
                             help="Number of top results for semantic search (default: 10)")
    query_parser.add_argument("--json", action="store_true",
                             help="Output semantic search results as JSON")
    query_parser.add_argument("--verbose", action="store_true", help="Verbose output")
    
    # Explain command (Layer 4)
    explain_parser = subparsers.add_parser("explain", help="Explain an artifact (human-readable)")
    explain_parser.add_argument("--file", help="Path to artifact file")
    explain_parser.add_argument("--id", help="Artifact UUID (urn:uuid:...)")
    explain_parser.add_argument("--directory", help="Directory to search (required with --id)")
    explain_parser.add_argument("--verbose", action="store_true", help="Verbose output")
    
    # Graph command (Layer 4)
    graph_parser = subparsers.add_parser("graph", help="Graph analysis and statistics")
    graph_parser.add_argument("--directory", required=True, help="Directory to analyze")
    graph_parser.add_argument("--verbose", action="store_true", help="Verbose output")
    
    # Repair command (Layer 4, Phase 9: Multi-Tenant)
    repair_parser = subparsers.add_parser("repair", help="Repair graph (remove orphans or create stubs)")
    repair_parser.add_argument("--directory", required=True, help="Directory to repair")
    repair_parser.add_argument("--tenant", help="Filter by tenant ID (Phase 9)")
    repair_parser.add_argument("--create-stubs", action="store_true", help="Create stub artifacts for missing IDs")
    repair_parser.add_argument("--dry-run", action="store_true", help="Show what would change without making edits")
    repair_parser.add_argument("--verbose", action="store_true", help="Verbose output")
    
    # Report command (Layer 5)
    report_parser = subparsers.add_parser("report", help="Generate operator report")
    report_parser.add_argument("--directory", default="memory", help="Directory to analyze")
    report_parser.add_argument("--out", default="reports", help="Output directory for reports")
    
    # Install-hooks command (Layer 5)
    hooks_parser = subparsers.add_parser("install-hooks", help="Install git hooks for integrity enforcement")
    
    # Version command (Phase 10)
    version_parser = subparsers.add_parser("version", help="Manage artifact versions (list, rollback)")
    version_parser.add_argument("--list", dest="action", action="store_const", const="list", help="List all versions for an artifact")
    version_parser.add_argument("--rollback", dest="action", action="store_const", const="rollback", help="Rollback artifact to a specific version")
    version_parser.add_argument("--artifact-id", help="Artifact UUID to manage versions for")
    version_parser.add_argument("--version", type=int, help="Target version number for rollback")
    version_parser.add_argument("--tenant", help="Filter by tenant ID (Phase 9)")
    version_parser.add_argument("--verbose", action="store_true", help="Verbose output")

    # Batch command (Phase 13)
    batch_parser = subparsers.add_parser("batch", help="Batch operations (streaming, bulk create)")
    batch_parser.add_argument("--count", action="store_true", help="Count artifacts (fast, no parsing)")
    batch_parser.add_argument("--stream-test", action="store_true", help="Test streaming performance")
    batch_parser.add_argument("--create", metavar="JSON_FILE", help="Batch create from JSON file")
    batch_parser.add_argument("--validate", action="store_true", help="Batch validate with streaming")
    batch_parser.add_argument("--directory", default="memory", help="Directory to process")
    batch_parser.add_argument("--tenant", help="Filter by tenant ID")
    batch_parser.add_argument("--batch-size", type=int, default=50, help="Batch size for streaming (default: 50)")
    batch_parser.add_argument("--skip-versioning", action="store_true", help="Skip version tracking for bulk imports")

    # Export command (Phase 11)
    export_parser = subparsers.add_parser("export", help="Export artifacts to external formats (RDF, Neo4j, JSON-LD)")
    export_parser.add_argument("--directory", default="memory", help="Source directory (default: memory)")
    export_parser.add_argument("--format", "-f", choices=["rdf", "neo4j", "cypher", "jsonld"], default="rdf",
                               help="Export format (default: rdf)")
    export_parser.add_argument("--output", "-o", required=True, help="Output file path")
    export_parser.add_argument("--tenant", help="Filter by tenant ID")
    export_parser.add_argument("--include-embeddings", action="store_true", help="Include vector embeddings")
    export_parser.add_argument("--rdf-format", choices=["turtle", "xml", "n3", "nt"], default="turtle",
                               help="RDF serialization format (default: turtle)")

    # Import command (Phase 11)
    import_parser = subparsers.add_parser("import", help="Import artifacts from external formats (RDF, JSON-LD)")
    import_parser.add_argument("--input", "-i", required=True, help="Input file path")
    import_parser.add_argument("--format", "-f", choices=["rdf", "jsonld"], default="jsonld",
                               help="Import format (default: jsonld)")
    import_parser.add_argument("--output", "-o", default="memory", help="Output directory (default: memory)")
    import_parser.add_argument("--tenant", help="Tenant ID for imported artifacts")
    import_parser.add_argument("--no-regenerate-hashes", action="store_true", help="Preserve original integrity hashes")
    import_parser.add_argument("--rdf-format", choices=["turtle", "xml", "n3", "nt"], default="turtle",
                               help="RDF serialization format (default: turtle)")

    # Sync command (Phase 14: Federation)
    sync_parser = subparsers.add_parser("sync", help="Federation sync (pull/push artifacts from remote instances)")
    sync_parser.add_argument("action", choices=["status", "list", "run", "pull"],
                             help="Sync action: status, list refs, run sync, or pull artifact")
    sync_parser.add_argument("--remote", help="Remote instance URL (for pull)")
    sync_parser.add_argument("--artifact-id", help="Remote artifact ID (for pull)")
    sync_parser.add_argument("--status", choices=["synced", "pending", "conflict", "stale"],
                             help="Filter refs by sync status")
    sync_parser.add_argument("--direction", choices=["pull", "push", "bidirectional"],
                             help="Filter refs by sync direction")
    sync_parser.add_argument("--tenant", help="Filter by tenant ID")
    sync_parser.add_argument("--conflict-resolution", choices=["remote_wins", "local_wins", "manual"],
                             help="Override conflict resolution strategy")

    # Parse args
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        return 0
    
    # Route to command handler
    if args.command == "create":
        if not args.artifact_type:
            create_parser.print_help()
            return 1
        
        if args.artifact_type == "decision":
            return cmd_create_decision(args)
        else:
            print(f"❌ Unknown artifact type: {args.artifact_type}")
            return 1
    
    elif args.command == "validate":
        return cmd_validate(args)
    
    elif args.command == "query":
        return cmd_query(args)
    
    elif args.command == "explain":
        return cmd_explain(args)
    
    elif args.command == "graph":
        return cmd_graph(args)
    
    elif args.command == "repair":
        return cmd_repair(args)
    
    elif args.command == "report":
        return cmd_report(args)
    
    elif args.command == "install-hooks":
        return cmd_install_hooks(args)
    
    elif args.command == "version":
        return cmd_version(args)

    elif args.command == "batch":
        return cmd_batch(args)

    elif args.command == "export":
        return cmd_export(args)

    elif args.command == "import":
        return cmd_import(args)

    elif args.command == "sync":
        return cmd_sync(args)

    else:
        print(f"❌ Unknown command: {args.command}")
        return 1


if __name__ == "__main__":
    sys.exit(main())
