#!/usr/bin/env python3
"""
Novyx Core - Federation Module (Phase 14)
==========================================
Distributed artifact synchronization between Novyx instances.

Features:
- Pull artifacts from remote instances
- Push local artifacts to remote instances
- Bidirectional sync with conflict resolution
- RemoteRef tracking and management
- Multi-tenant aware federation

Usage:
    from tools.federation import pull_artifact, push_artifact, sync_refs

    # Pull a remote artifact
    local_artifact, ref = pull_artifact(
        "https://remote.novyx.ai",
        "urn:uuid:abc-123"
    )

    # Push a local artifact
    ref = push_artifact(
        local_artifact,
        "https://remote.novyx.ai"
    )

    # Sync all pending refs
    results = sync_refs()

Dependencies:
    - requests (optional, for HTTP federation)
"""

import json
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Dict, List, Optional, Any, Tuple

# Import internal utilities
from tools.factory import (
    create_remote_ref,
    update_remote_ref_status,
    persist_artifact,
    generate_uuid,
    generate_timestamp
)
from tools.artifacts import get_artifact_config
from tools.util import calculate_hash

# Optional requests dependency
try:
    import requests
    REQUESTS_AVAILABLE = True
except ImportError:
    REQUESTS_AVAILABLE = False

# Federation configuration
DEFAULT_TIMEOUT = 30  # seconds
DEFAULT_API_PATH = "/api/artifacts"
REMOTE_REF_DIR = Path("memory/federation")


class FederationError(Exception):
    """Base exception for federation operations."""
    pass


class RemoteConnectionError(FederationError):
    """Failed to connect to remote instance."""
    pass


class ConflictError(FederationError):
    """Conflict detected during sync."""
    def __init__(self, message: str, local_hash: str, remote_hash: str):
        super().__init__(message)
        self.local_hash = local_hash
        self.remote_hash = remote_hash


def _check_requests():
    """Check if requests library is available."""
    if not REQUESTS_AVAILABLE:
        raise FederationError(
            "requests library required for federation. "
            "Install with: pip install requests"
        )


def _build_artifact_url(remote_instance: str, artifact_id: str) -> str:
    """Build URL to access a remote artifact."""
    base = remote_instance.rstrip("/")
    return f"{base}{DEFAULT_API_PATH}/{artifact_id}"


def fetch_remote_artifact(
    remote_instance: str,
    artifact_id: str,
    timeout: int = DEFAULT_TIMEOUT,
    api_key: Optional[str] = None
) -> Dict:
    """
    Fetch an artifact from a remote Novyx instance.

    Args:
        remote_instance: Base URL of remote instance
        artifact_id: UUID of the artifact to fetch
        timeout: Request timeout in seconds
        api_key: Optional API key for authentication

    Returns:
        Artifact dict from remote instance

    Raises:
        FederationError: If fetch fails

    Example:
        >>> artifact = fetch_remote_artifact(
        ...     "https://remote.novyx.ai",
        ...     "urn:uuid:abc-123"
        ... )
    """
    _check_requests()

    url = _build_artifact_url(remote_instance, artifact_id)
    headers = {"Accept": "application/ld+json"}

    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    try:
        response = requests.get(url, headers=headers, timeout=timeout)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.ConnectionError as e:
        raise RemoteConnectionError(f"Failed to connect to {remote_instance}: {e}")
    except requests.exceptions.Timeout:
        raise FederationError(f"Request to {remote_instance} timed out")
    except requests.exceptions.HTTPError as e:
        raise FederationError(f"HTTP error fetching {artifact_id}: {e}")
    except json.JSONDecodeError:
        raise FederationError(f"Invalid JSON response from {remote_instance}")


def push_to_remote(
    artifact: Dict,
    remote_instance: str,
    timeout: int = DEFAULT_TIMEOUT,
    api_key: Optional[str] = None
) -> Dict:
    """
    Push an artifact to a remote Novyx instance.

    Args:
        artifact: Local artifact to push
        remote_instance: Base URL of remote instance
        timeout: Request timeout in seconds
        api_key: Optional API key for authentication

    Returns:
        Response dict from remote (usually the created/updated artifact)

    Raises:
        FederationError: If push fails
    """
    _check_requests()

    url = f"{remote_instance.rstrip('/')}{DEFAULT_API_PATH}"
    headers = {
        "Content-Type": "application/ld+json",
        "Accept": "application/ld+json"
    }

    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    try:
        response = requests.post(url, json=artifact, headers=headers, timeout=timeout)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.ConnectionError as e:
        raise RemoteConnectionError(f"Failed to connect to {remote_instance}: {e}")
    except requests.exceptions.Timeout:
        raise FederationError(f"Request to {remote_instance} timed out")
    except requests.exceptions.HTTPError as e:
        raise FederationError(f"HTTP error pushing artifact: {e}")


def pull_artifact(
    remote_instance: str,
    artifact_id: str,
    tenant_id: Optional[str] = None,
    api_key: Optional[str] = None,
    persist: bool = True
) -> Tuple[Dict, Dict]:
    """
    Pull an artifact from a remote instance and create a local copy.

    Creates a RemoteRef to track the relationship between local and remote.

    Args:
        remote_instance: Base URL of remote instance
        artifact_id: UUID of artifact to pull
        tenant_id: Optional tenant for local storage
        api_key: Optional API key for authentication
        persist: Whether to persist the local artifact (default: True)

    Returns:
        Tuple of (local_artifact, remote_ref)

    Example:
        >>> artifact, ref = pull_artifact(
        ...     "https://remote.novyx.ai",
        ...     "urn:uuid:abc-123"
        ... )
        >>> print(ref["sync_status"])
        'synced'
    """
    # Fetch the remote artifact
    remote_artifact = fetch_remote_artifact(
        remote_instance, artifact_id, api_key=api_key
    )

    # Get remote integrity hash
    remote_hash = (
        remote_artifact.get("novyx:integrityHash") or
        remote_artifact.get("Integrity_Hash") or
        ""
    )

    # Create local copy with new UUID
    local_artifact = remote_artifact.copy()
    local_uuid = generate_uuid()
    timestamp = generate_timestamp()

    local_artifact["uuid"] = local_uuid
    local_artifact["id"] = local_uuid
    local_artifact["@id"] = local_uuid
    local_artifact["Persistent_ID"] = local_uuid
    local_artifact["createdAt"] = timestamp
    local_artifact["updatedAt"] = timestamp
    local_artifact["novyx:ingestedAt"] = timestamp
    local_artifact["novyx:updatedAt"] = timestamp
    local_artifact["novyx:integrityHash"] = "pending_calculation"
    local_artifact["Integrity_Hash"] = "pending_calculation"

    # Add federation metadata
    local_artifact["novyx:federatedFrom"] = {
        "remote_instance": remote_instance,
        "remote_artifact_id": artifact_id,
        "remote_hash": remote_hash,
        "pulled_at": timestamp
    }

    # Add tenant if specified
    if tenant_id:
        local_artifact["novyx:tenantId"] = tenant_id
        local_artifact["tenant_id"] = tenant_id

    # Persist local artifact if requested
    local_path = None
    if persist:
        # Determine output directory from artifact type
        artifact_type = _detect_artifact_type(local_artifact)
        config = get_artifact_config(artifact_type)
        if config:
            output_dir = Path(config["output_dir"])
            local_path = persist_artifact(local_artifact, output_dir)

    # Extract metadata for RemoteRef
    metadata = {
        "name": local_artifact.get("name") or local_artifact.get("title") or artifact_id,
        "type": _detect_artifact_type(local_artifact)
    }

    # Create RemoteRef to track relationship
    remote_ref = create_remote_ref(
        remote_url=_build_artifact_url(remote_instance, artifact_id),
        remote_artifact_id=artifact_id,
        remote_instance=remote_instance,
        sync_status="synced",
        direction="pull",
        local_artifact_id=local_uuid,
        remote_hash=remote_hash,
        metadata=metadata,
        tenant_id=tenant_id
    )

    # Persist RemoteRef
    persist_artifact(remote_ref, REMOTE_REF_DIR)

    return local_artifact, remote_ref


def push_artifact(
    artifact: Dict,
    remote_instance: str,
    tenant_id: Optional[str] = None,
    api_key: Optional[str] = None
) -> Dict:
    """
    Push a local artifact to a remote instance.

    Creates a RemoteRef to track the push relationship.

    Args:
        artifact: Local artifact to push
        remote_instance: Base URL of remote instance
        tenant_id: Optional tenant for RemoteRef storage
        api_key: Optional API key for authentication

    Returns:
        RemoteRef tracking the push

    Example:
        >>> ref = push_artifact(artifact, "https://remote.novyx.ai")
        >>> print(ref["sync_status"])
        'synced'
    """
    # Get local artifact info
    local_id = artifact.get("uuid") or artifact.get("id")
    local_hash = (
        artifact.get("novyx:integrityHash") or
        artifact.get("Integrity_Hash") or
        ""
    )

    # Push to remote
    response = push_to_remote(artifact, remote_instance, api_key=api_key)

    # Get remote artifact ID from response
    remote_id = (
        response.get("uuid") or
        response.get("id") or
        response.get("@id") or
        local_id  # Fallback to local ID if remote doesn't return one
    )

    # Extract metadata
    metadata = {
        "name": artifact.get("name") or artifact.get("title") or local_id,
        "type": _detect_artifact_type(artifact)
    }

    # Create RemoteRef
    remote_ref = create_remote_ref(
        remote_url=_build_artifact_url(remote_instance, remote_id),
        remote_artifact_id=remote_id,
        remote_instance=remote_instance,
        sync_status="synced",
        direction="push",
        local_artifact_id=local_id,
        remote_hash=local_hash,  # On push, remote should have same hash initially
        metadata=metadata,
        tenant_id=tenant_id
    )

    # Persist RemoteRef
    persist_artifact(remote_ref, REMOTE_REF_DIR)

    return remote_ref


def _detect_artifact_type(artifact: Dict) -> str:
    """Detect artifact type from @type field."""
    artifact_types = artifact.get("@type", [])
    if isinstance(artifact_types, str):
        artifact_types = [artifact_types]

    # Map schema types to artifact types
    type_map = {
        "Decision": "decision",
        "APIEndpoint": "apiendpoint",
        "GraphQLOperation": "graphqloperation",
        "Tenant": "tenant",
        "Version": "version",
        "RemoteRef": "remoteref"
    }

    for t in artifact_types:
        # Handle novyx: prefix
        t_clean = t.replace("novyx:", "").replace("schema:", "")
        if t_clean in type_map:
            return type_map[t_clean]

    return "decision"  # Default fallback


def get_remote_refs(
    status: Optional[str] = None,
    direction: Optional[str] = None,
    tenant_id: Optional[str] = None
) -> List[Dict]:
    """
    Get all RemoteRef artifacts, optionally filtered.

    Args:
        status: Filter by sync_status ('synced', 'pending', 'conflict', 'stale')
        direction: Filter by direction ('pull', 'push', 'bidirectional')
        tenant_id: Filter by tenant

    Returns:
        List of RemoteRef artifacts matching filters

    Example:
        >>> pending_refs = get_remote_refs(status="pending")
        >>> pull_refs = get_remote_refs(direction="pull")
    """
    refs = []
    ref_dir = REMOTE_REF_DIR

    if tenant_id:
        ref_dir = ref_dir / tenant_id

    if not ref_dir.exists():
        return refs

    for ref_file in ref_dir.glob("**/*.jsonld"):
        try:
            with open(ref_file, 'r', encoding='utf-8') as f:
                ref = json.load(f)

            # Check if it's a RemoteRef
            types = ref.get("@type", [])
            if isinstance(types, str):
                types = [types]
            if "RemoteRef" not in types:
                continue

            # Apply filters
            if status and ref.get("sync_status") != status:
                continue
            if direction and ref.get("direction") != direction:
                continue

            refs.append(ref)
        except Exception:
            continue

    return refs


def sync_ref(
    ref: Dict,
    conflict_resolution: Optional[str] = None,
    api_key: Optional[str] = None
) -> Dict:
    """
    Synchronize a single RemoteRef.

    Args:
        ref: RemoteRef artifact to sync
        conflict_resolution: Override conflict resolution strategy
        api_key: Optional API key for authentication

    Returns:
        Updated RemoteRef with new sync status

    Raises:
        ConflictError: If conflict detected and resolution is 'manual'
    """
    direction = ref.get("direction", "pull")
    resolution = conflict_resolution or ref.get("conflict_resolution", "remote_wins")
    remote_instance = ref.get("remote_instance")
    remote_id = ref.get("remote_artifact_id")
    local_id = ref.get("local_artifact_id")
    stored_remote_hash = ref.get("remote_hash")

    if direction == "pull":
        # Fetch current remote state
        try:
            remote_artifact = fetch_remote_artifact(
                remote_instance, remote_id, api_key=api_key
            )
        except FederationError as e:
            update_remote_ref_status(ref, "stale")
            ref["sync_error"] = str(e)
            persist_artifact(ref, REMOTE_REF_DIR)
            return ref

        current_remote_hash = (
            remote_artifact.get("novyx:integrityHash") or
            remote_artifact.get("Integrity_Hash") or
            ""
        )

        # Check for changes
        if current_remote_hash == stored_remote_hash:
            # No changes, just update timestamp
            update_remote_ref_status(ref, "synced")
        else:
            # Remote changed - check for conflicts
            if local_id:
                # We have a local copy - potential conflict
                if resolution == "manual":
                    update_remote_ref_status(ref, "conflict")
                    persist_artifact(ref, REMOTE_REF_DIR)
                    raise ConflictError(
                        f"Conflict for {remote_id}: remote changed",
                        local_hash=stored_remote_hash or "",
                        remote_hash=current_remote_hash
                    )
                elif resolution == "remote_wins":
                    # Re-pull and overwrite local
                    # (simplified - just update ref status)
                    update_remote_ref_status(
                        ref, "synced", remote_hash=current_remote_hash
                    )
                # local_wins: keep local, mark synced
                else:
                    update_remote_ref_status(ref, "synced")
            else:
                # No local copy yet - just update hash
                update_remote_ref_status(
                    ref, "synced", remote_hash=current_remote_hash
                )

    elif direction == "push":
        # For push, we check if local changed and push updates
        # This is a simplified implementation
        update_remote_ref_status(ref, "synced")

    elif direction == "bidirectional":
        # Bidirectional is complex - simplified to pull behavior
        return sync_ref(
            {**ref, "direction": "pull"},
            conflict_resolution=resolution,
            api_key=api_key
        )

    # Persist updated ref
    persist_artifact(ref, REMOTE_REF_DIR)
    return ref


def sync_refs(
    status: Optional[str] = None,
    direction: Optional[str] = None,
    tenant_id: Optional[str] = None,
    conflict_resolution: Optional[str] = None,
    api_key: Optional[str] = None
) -> Dict[str, Any]:
    """
    Synchronize multiple RemoteRefs.

    Args:
        status: Filter refs by status (default: sync 'pending' and 'stale')
        direction: Filter refs by direction
        tenant_id: Filter refs by tenant
        conflict_resolution: Override conflict resolution for all refs
        api_key: Optional API key for authentication

    Returns:
        Dict with sync results: {synced: int, failed: int, conflicts: int, errors: list}

    Example:
        >>> results = sync_refs()
        >>> print(f"Synced: {results['synced']}, Failed: {results['failed']}")
    """
    # Default: sync pending and stale refs
    statuses_to_sync = [status] if status else ["pending", "stale"]

    results = {
        "synced": 0,
        "failed": 0,
        "conflicts": 0,
        "errors": []
    }

    for sync_status in statuses_to_sync:
        refs = get_remote_refs(
            status=sync_status,
            direction=direction,
            tenant_id=tenant_id
        )

        for ref in refs:
            try:
                sync_ref(ref, conflict_resolution=conflict_resolution, api_key=api_key)
                results["synced"] += 1
            except ConflictError as e:
                results["conflicts"] += 1
                results["errors"].append({
                    "ref_id": ref.get("uuid"),
                    "error": "conflict",
                    "message": str(e)
                })
            except FederationError as e:
                results["failed"] += 1
                results["errors"].append({
                    "ref_id": ref.get("uuid"),
                    "error": "federation_error",
                    "message": str(e)
                })

    return results


def federation_status() -> Dict[str, Any]:
    """
    Get overall federation status.

    Returns:
        Dict with federation statistics

    Example:
        >>> status = federation_status()
        >>> print(f"Total refs: {status['total']}, Synced: {status['synced']}")
    """
    all_refs = get_remote_refs()

    status_counts = {
        "synced": 0,
        "pending": 0,
        "conflict": 0,
        "stale": 0
    }

    direction_counts = {
        "pull": 0,
        "push": 0,
        "bidirectional": 0
    }

    instances = set()

    for ref in all_refs:
        s = ref.get("sync_status", "pending")
        d = ref.get("direction", "pull")
        instance = ref.get("remote_instance", "")

        status_counts[s] = status_counts.get(s, 0) + 1
        direction_counts[d] = direction_counts.get(d, 0) + 1
        if instance:
            instances.add(instance)

    return {
        "total": len(all_refs),
        "by_status": status_counts,
        "by_direction": direction_counts,
        "remote_instances": list(instances),
        "requests_available": REQUESTS_AVAILABLE
    }


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Novyx Federation CLI")
    subparsers = parser.add_subparsers(dest="command", help="Commands")

    # Status command
    status_parser = subparsers.add_parser("status", help="Show federation status")

    # List command
    list_parser = subparsers.add_parser("list", help="List remote refs")
    list_parser.add_argument("--status", help="Filter by status")
    list_parser.add_argument("--direction", help="Filter by direction")

    # Sync command
    sync_parser = subparsers.add_parser("sync", help="Sync remote refs")
    sync_parser.add_argument("--status", help="Filter by status")
    sync_parser.add_argument("--direction", help="Filter by direction")
    sync_parser.add_argument("--conflict-resolution",
                             choices=["remote_wins", "local_wins", "manual"])

    args = parser.parse_args()

    if args.command == "status":
        status = federation_status()
        print("Federation Status")
        print("=" * 40)
        print(f"Total RemoteRefs: {status['total']}")
        print(f"Requests available: {status['requests_available']}")
        print("\nBy Status:")
        for s, count in status['by_status'].items():
            print(f"  {s}: {count}")
        print("\nBy Direction:")
        for d, count in status['by_direction'].items():
            print(f"  {d}: {count}")
        print(f"\nRemote Instances: {len(status['remote_instances'])}")
        for inst in status['remote_instances']:
            print(f"  - {inst}")

    elif args.command == "list":
        refs = get_remote_refs(status=args.status, direction=args.direction)
        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')}")

    elif args.command == "sync":
        results = sync_refs(
            status=args.status,
            direction=args.direction,
            conflict_resolution=args.conflict_resolution
        )
        print("Sync Results")
        print("=" * 40)
        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']}: {err['message']}")

    else:
        parser.print_help()
