#!/usr/bin/env python3
"""
Novyx Core - Artifact Factory
===============================
Schema-driven artifact creation and persistence with cryptographic integrity.

Features:
- Load CORE_SCHEMA.jsonld to resolve requiredProperties
- Create artifacts with standard fields (UUID, timestamps, hashes)
- Compute dual integrity hashes (novyx:integrityHash + Integrity_Hash)
- Atomic temp-safe persistence

Usage:
    from tools.factory import create_artifact, persist_artifact
    
    artifact = create_artifact("decision", {
        "title": "Choose tech stack",
        "context": "Building new feature",
        "options_considered": ["React", "Vue", "Svelte"],
        "chosen_option": "React",
        "reasoning": "Team expertise",
        "constraints": ["Time", "Budget"],
        "assumptions": ["Stable API"],
        "confidence": 0.8,
        "expected_outcome": "Faster dev",
        "category": "technical"
    })
    
    path = persist_artifact(artifact, Path("memory/decisions"))
"""

import json
import uuid as uuid_lib
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional, Any
import sys

# Import utilities
from tools.util import calculate_hash
from tools.artifacts import get_artifact_config, get_schema_id

# Load CORE_SCHEMA.jsonld
SCHEMA_PATH = Path(__file__).parent.parent / "CORE_SCHEMA.jsonld"


def load_schema() -> Dict:
    """Load CORE_SCHEMA.jsonld."""
    if not SCHEMA_PATH.exists():
        raise FileNotFoundError(f"Schema not found: {SCHEMA_PATH}")
    
    with open(SCHEMA_PATH, 'r', encoding='utf-8') as f:
        return json.load(f)


def get_required_properties(schema_id: str) -> List[str]:
    """
    Extract requiredProperties for a schema_id from CORE_SCHEMA.jsonld.
    
    Args:
        schema_id: Schema identifier (e.g., "novyx:Decision")
    
    Returns:
        List of required property names (supports v1.0|v1.1.0 aliases like "uuid|Persistent_ID")
    
    Example:
        >>> get_required_properties("novyx:Decision")
        ['uuid|Persistent_ID', 'createdAt|Temporal_Marker', ...]
    """
    schema = load_schema()
    
    # Search @graph for the schema_id
    for entity in schema.get("@graph", []):
        if entity.get("@id") == schema_id:
            return entity.get("novyx:requiredProperties", [])
    
    # Fallback: return empty list if not found
    return []


def generate_uuid() -> str:
    """Generate a URN:UUID identifier."""
    return f"urn:uuid:{str(uuid_lib.uuid4())}"


def generate_timestamp() -> str:
    """Generate ISO 8601 timestamp with UTC timezone."""
    return datetime.now(timezone.utc).isoformat()


def create_artifact(
    artifact_type: str,
    payload: Dict[str, Any],
    external_links: Optional[List[str]] = None,
    tenant_id: Optional[str] = None
) -> Dict:
    """
    Create an artifact with standard fields filled automatically.
    
    Args:
        artifact_type: Registered artifact type (e.g., "decision")
        payload: User-provided fields specific to the artifact type
        external_links: Optional list of external authority URLs (sameAs)
        tenant_id: Optional tenant identifier for multi-tenant isolation (Phase 9)
    
    Returns:
        Complete artifact dict ready for persistence (hashes will be "pending_calculation")
    
    Raises:
        ValueError: If artifact_type is not registered
        KeyError: If required fields are missing from payload
    
    Example:
        >>> artifact = create_artifact("decision", {
        ...     "title": "Tech Stack",
        ...     "context": "New feature",
        ...     "options_considered": ["React", "Vue"],
        ...     "chosen_option": "React",
        ...     "reasoning": "Team knows it",
        ...     "constraints": ["Time"],
        ...     "assumptions": ["Stable"],
        ...     "confidence": 0.8,
        ...     "expected_outcome": "Fast dev",
        ...     "category": "tech"
        ... }, tenant_id="acme-corp")
        >>> artifact["@type"]
        ['Decision', 'DigitalDocument']
        >>> artifact["novyx:tenantId"]
        'acme-corp'
    """
    # Get artifact configuration
    config = get_artifact_config(artifact_type)
    if not config:
        raise ValueError(f"Unknown artifact type: {artifact_type}. Register it in tools/artifacts.py")
    
    schema_id = config["schema_id"]
    
    # Get required properties from schema
    required_props = get_required_properties(schema_id)
    
    # Check required fields are in payload (handle aliases like "uuid|Persistent_ID")
    missing = []
    for prop in required_props:
        # Skip fields we auto-generate or auto-fill
        auto_fields = ["uuid", "Persistent_ID", "createdAt", "updatedAt", 
                      "Temporal_Marker", "integrityHash", "Integrity_Hash",
                      "novyx:updatedAt", "novyx:integrityHash", "novyx:ingestedAt",
                      "novyx:semanticLinks", "sameAs", "Integrity_Hash"]
        if any(auto in prop for auto in auto_fields):
            continue
        
        # Handle aliases (e.g., "uuid|Persistent_ID")
        aliases = [p.strip() for p in prop.split("|")]
        if not any(alias in payload for alias in aliases):
            missing.append(prop)
    
    if missing:
        raise KeyError(f"Missing required fields for {artifact_type}: {missing}")
    
    # Generate standard fields
    artifact_uuid = generate_uuid()
    timestamp = generate_timestamp()
    
    # Build artifact structure
    artifact = {
        "@context": {
            "@vocab": "https://schema.org/",
            "novyx": "https://novyx.ai/ns/core#"
        },
        "@type": [schema_id.split(":")[-1], "DigitalDocument"],  # e.g., ["Decision", "DigitalDocument"]
        "@id": artifact_uuid,
        "uuid": artifact_uuid,
        "id": artifact_uuid,
        "Persistent_ID": artifact_uuid,  # v1.0 compatibility
        "createdAt": timestamp,
        "updatedAt": timestamp,
        "Temporal_Marker": timestamp,  # v1.0 compatibility
        "novyx:ingestedAt": timestamp,
        "novyx:updatedAt": timestamp,
        "novyx:integrityHash": "pending_calculation",
        "Integrity_Hash": "pending_calculation",
        "novyx:semanticLinks": [],
        "sameAs": external_links if external_links else []
    }
    
    # Add tenant_id if provided (Phase 9: Multi-Tenant)
    if tenant_id:
        artifact["novyx:tenantId"] = tenant_id
        artifact["tenant_id"] = tenant_id  # Also add without namespace for convenience
    
    # Merge payload (user-provided fields)
    artifact.update(payload)
    
    # Ensure consistent field naming for common fields
    if "title" in payload and "name" not in artifact:
        artifact["name"] = payload["title"]
    
    # Build articleBody if not provided (for semantic search)
    if "articleBody" not in artifact:
        # Combine key text fields
        text_parts = []
        for field in ["title", "context", "reasoning", "expected_outcome"]:
            if field in artifact and artifact[field]:
                text_parts.append(str(artifact[field]))
        
        # Add list fields
        for field in ["options_considered", "constraints", "assumptions"]:
            if field in artifact and isinstance(artifact[field], list):
                text_parts.extend([str(item) for item in artifact[field]])
        
        artifact["articleBody"] = " ".join(text_parts).strip()
    
    return artifact


def persist_artifact(artifact: Dict, output_dir: Path, skip_versioning: bool = False) -> Path:
    """
    Persist artifact to disk with integrity hash calculation.
    
    Uses atomic temp-safe write:
    1. Write artifact (with "pending_calculation" hash) to temp file
    2. Calculate hash from temp file
    3. Update artifact with actual hash
    4. Write to final location with hash-based filename
    5. Clean up temp file
    
    Phase 9: Multi-Tenant Support
    If artifact has novyx:tenantId or tenant_id, persists to output_dir/tenant_id/
    
    Phase 10: Artifact Versioning
    If artifact already exists (same UUID), creates a Version artifact with diff.
    Set skip_versioning=True to prevent version creation (e.g., during rollback).
    
    Args:
        artifact: Complete artifact dict (must have uuid/id and standard fields)
        output_dir: Directory to save artifact (will be created if needed)
        skip_versioning: If True, skip version creation (default: False)
    
    Returns:
        Path to the persisted artifact file
    
    Raises:
        KeyError: If artifact lacks required identity fields
        OSError: If file operations fail
    
    Example:
        >>> artifact = create_artifact("decision", {...}, tenant_id="acme-corp")
        >>> path = persist_artifact(artifact, Path("memory/decisions"))
        >>> print(path)
        memory/decisions/acme-corp/tech_stack_a1b2c3d4.jsonld
    """
    # Phase 9: Check for tenant_id and create tenant-specific subdirectory
    tenant_id = artifact.get("novyx:tenantId") or artifact.get("tenant_id")
    if tenant_id:
        output_dir = output_dir / tenant_id
    
    # Ensure output directory exists
    output_dir.mkdir(parents=True, exist_ok=True)
    
    # Get base filename from artifact
    if "uuid" not in artifact and "id" not in artifact:
        raise KeyError("Artifact must have 'uuid' or 'id' field")
    
    # Phase 10: Check if artifact already exists (for versioning)
    artifact_id = artifact.get("uuid") or artifact.get("id")
    existing_artifact = None
    existing_artifact_path = None
    
    if not skip_versioning:
        # Search for existing artifact with same UUID
        for existing_file in output_dir.glob("*.jsonld"):
            try:
                with open(existing_file, 'r', encoding='utf-8') as f:
                    existing_data = json.load(f)
                existing_id = existing_data.get("uuid") or existing_data.get("id")
                if existing_id == artifact_id:
                    existing_artifact = existing_data
                    existing_artifact_path = existing_file
                    break
            except Exception:
                continue
    
    # Generate base filename from title or name
    base_name = "artifact"
    if "title" in artifact:
        base_name = artifact["title"].lower().replace(" ", "_")[:50]
    elif "name" in artifact:
        base_name = artifact["name"].lower().replace(" ", "_")[:50]
    
    # Clean filename (remove special chars)
    import re
    base_name = re.sub(r'[^a-z0-9_-]', '', base_name)
    if not base_name:
        base_name = "artifact"
    
    # Phase 10: Create version artifact if this is an update
    if existing_artifact and not skip_versioning:
        try:
            from tools.versioning import create_version, persist_version
            
            # Create version artifact with diff
            version_artifact = create_version(
                current_artifact=artifact,
                parent_artifact=existing_artifact,
                change_summary="Artifact updated",
                changed_by="novyx-factory",
                change_type="update"
            )
            
            # Persist version artifact
            persist_version(version_artifact, artifact_id)
            
        except Exception as e:
            import sys
            print(f"⚠️  Warning: Failed to create version for {artifact_id}: {e}", file=sys.stderr)
            # Continue with saving the artifact even if versioning fails
    
    # Step 1: Write to temp file
    temp_path = output_dir / f"{base_name}_temp.jsonld"
    
    try:
        with open(temp_path, 'w', encoding='utf-8') as f:
            json.dump(artifact, f, indent=2)
        
        # Step 2: Calculate hash from temp file (excludes hash fields)
        file_hash = calculate_hash(temp_path)
        
        # Step 3: Update artifact with actual hashes
        artifact["novyx:integrityHash"] = file_hash
        artifact["Integrity_Hash"] = file_hash
        
        # Step 4: Write to final location
        final_filename = f"{base_name}_{file_hash[:8]}.jsonld"
        final_path = output_dir / final_filename
        
        with open(final_path, 'w', encoding='utf-8') as f:
            json.dump(artifact, f, indent=2)
        
        # Phase 10: Remove old artifact file if hash changed
        if existing_artifact_path and existing_artifact_path != final_path:
            try:
                existing_artifact_path.unlink()
            except OSError:
                pass  # Ignore if we can't delete the old file
        
        return final_path
    
    finally:
        # Step 5: Clean up temp file
        if temp_path.exists():
            temp_path.unlink(missing_ok=True)


def create_and_persist_artifact(
    artifact_type: str,
    payload: Dict[str, Any],
    external_links: Optional[List[str]] = None
) -> tuple[Dict, Path]:
    """
    Convenience function: create and persist artifact in one call.
    
    Args:
        artifact_type: Registered artifact type (e.g., "decision")
        payload: User-provided fields
        external_links: Optional external authority URLs
    
    Returns:
        Tuple of (artifact dict, path to saved file)
    
    Example:
        >>> artifact, path = create_and_persist_artifact("decision", {
        ...     "title": "Tech Stack",
        ...     ...
        ... })
        >>> print(f"Saved to {path}")
        Saved to memory/decisions/tech_stack_a1b2c3d4.jsonld
    """
    # Get output directory from registry
    config = get_artifact_config(artifact_type)
    if not config:
        raise ValueError(f"Unknown artifact type: {artifact_type}")
    
    output_dir = Path(config["output_dir"])
    
    # Create and persist
    artifact = create_artifact(artifact_type, payload, external_links)
    path = persist_artifact(artifact, output_dir)
    
    return artifact, path


def create_remote_ref(
    remote_url: str,
    remote_artifact_id: str,
    remote_instance: str,
    sync_status: str = "pending",
    direction: str = "pull",
    local_artifact_id: Optional[str] = None,
    remote_hash: Optional[str] = None,
    auto_sync: bool = False,
    conflict_resolution: str = "remote_wins",
    metadata: Optional[Dict[str, Any]] = None,
    tenant_id: Optional[str] = None
) -> Dict:
    """
    Create a RemoteRef artifact for federation (Phase 14).

    RemoteRef tracks a reference to an artifact in a remote Novyx instance,
    enabling distributed synchronization.

    Args:
        remote_url: Full URL to the remote artifact endpoint
        remote_artifact_id: UUID of the artifact on the remote instance
        remote_instance: Identifier or URL of the remote Novyx instance
        sync_status: Current sync status ('pending', 'synced', 'conflict', 'stale')
        direction: Sync direction ('pull', 'push', 'bidirectional')
        local_artifact_id: UUID of local copy (if pulled)
        remote_hash: Integrity hash of remote artifact at last sync
        auto_sync: Whether to automatically sync this reference
        conflict_resolution: Strategy for conflicts ('remote_wins', 'local_wins', 'manual')
        metadata: Additional metadata about the remote artifact
        tenant_id: Optional tenant identifier for multi-tenant isolation

    Returns:
        Complete RemoteRef artifact dict ready for persistence

    Example:
        >>> ref = create_remote_ref(
        ...     remote_url="https://remote.novyx.ai/api/artifacts/urn:uuid:abc",
        ...     remote_artifact_id="urn:uuid:abc-123",
        ...     remote_instance="https://remote.novyx.ai",
        ...     sync_status="synced",
        ...     direction="pull",
        ...     metadata={"name": "Remote Decision", "type": "Decision"}
        ... )
        >>> ref["@type"]
        ['RemoteRef', 'DigitalDocument']
    """
    payload = {
        "remote_url": remote_url,
        "remote_artifact_id": remote_artifact_id,
        "remote_instance": remote_instance,
        "sync_status": sync_status,
        "direction": direction,
        "auto_sync": auto_sync,
        "conflict_resolution": conflict_resolution,
        "name": f"RemoteRef: {remote_artifact_id[:30]}..."
    }

    if local_artifact_id:
        payload["local_artifact_id"] = local_artifact_id

    if remote_hash:
        payload["remote_hash"] = remote_hash

    if metadata:
        payload["metadata"] = metadata

    # Create using standard factory
    artifact = create_artifact("remoteref", payload, tenant_id=tenant_id)

    # Add last_synced_at if status is synced
    if sync_status == "synced":
        artifact["last_synced_at"] = generate_timestamp()

    return artifact


def update_remote_ref_status(
    remote_ref: Dict,
    new_status: str,
    local_artifact_id: Optional[str] = None,
    remote_hash: Optional[str] = None
) -> Dict:
    """
    Update a RemoteRef artifact's sync status.

    Args:
        remote_ref: Existing RemoteRef artifact
        new_status: New sync status ('synced', 'pending', 'conflict', 'stale')
        local_artifact_id: Local artifact ID (set when pulled)
        remote_hash: Current remote hash (set when synced)

    Returns:
        Updated RemoteRef artifact (modifies in place and returns)

    Example:
        >>> ref = update_remote_ref_status(ref, "synced",
        ...     local_artifact_id="urn:uuid:local-123",
        ...     remote_hash="abc123...")
    """
    remote_ref["sync_status"] = new_status
    remote_ref["updatedAt"] = generate_timestamp()
    remote_ref["novyx:updatedAt"] = remote_ref["updatedAt"]

    if new_status == "synced":
        remote_ref["last_synced_at"] = remote_ref["updatedAt"]

    if local_artifact_id:
        remote_ref["local_artifact_id"] = local_artifact_id

    if remote_hash:
        remote_ref["remote_hash"] = remote_hash

    # Invalidate hash for recalculation on persist
    remote_ref["novyx:integrityHash"] = "pending_calculation"
    remote_ref["Integrity_Hash"] = "pending_calculation"

    return remote_ref


if __name__ == "__main__":
    # CLI for testing factory
    import argparse
    
    parser = argparse.ArgumentParser(description="Novyx Artifact Factory")
    parser.add_argument("--type", required=True, help="Artifact type (e.g., decision)")
    parser.add_argument("--payload", required=True, help="JSON payload file path")
    parser.add_argument("--link", action="append", help="External authority URL (can use multiple)")
    
    args = parser.parse_args()
    
    # Load payload
    payload_path = Path(args.payload)
    if not payload_path.exists():
        print(f"❌ Payload file not found: {payload_path}")
        sys.exit(1)
    
    with open(payload_path, 'r') as f:
        payload = json.load(f)
    
    # Create and persist
    try:
        artifact, path = create_and_persist_artifact(
            args.type,
            payload,
            args.link
        )
        print(f"✅ Artifact created: {path}")
        print(f"   UUID: {artifact['uuid']}")
        print(f"   Hash: {artifact['novyx:integrityHash'][:16]}...")
    except Exception as e:
        print(f"❌ Failed: {e}")
        sys.exit(1)
