"""
BloomOS Kernel - Syscall Registry
=================================

The central registry for all kernel syscalls.

Each syscall has:
- Canonical name (e.g., "cdna.decode.v1")
- Handler function
- Input schema
- Cost estimator

The registry enforces:
1. CellWall authorization (EPERM)
2. Schema validation (EINVAL)
3. IT token budget (ERESOURCE)
4. Receipt generation

Architecture:
    invoke("cdna.decode.v1", actor, inputs)
        ↓
    cell_wall.authorize_syscall()   → EPERM if denied
        ↓
    validate_inputs(schema)         → EINVAL if invalid
        ↓
    ledger.pre_authorize()          → ERESOURCE if over budget
        ↓
    handler(inputs)                 → Execute
        ↓
    ledger.commit()                 → Record actual cost
        ↓
    SyscallReceipt                  → Return proof
"""

from __future__ import annotations

import hashlib
import inspect
import json
import os
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

from .cell_wall import CellWall
from .it_token_ledger import ITTokenLedger
from .syscall_receipt import SyscallReceipt, create_receipt


def _canonicalize_inputs(inputs: Dict[str, Any], schema: Dict[str, Any]) -> Dict[str, Any]:
    """
    Normalize inputs for deterministic hashing.

    This ensures the same inputs always produce the same hash, regardless of:
    - Key ordering
    - Float precision variations
    - Path format differences

    Canonicalization rules:
    1. Remove unknown fields (not in schema types)
    2. Sort keys recursively
    3. Round floats to 9 decimal places
    4. Convert numpy arrays to lists
    5. Normalize paths (resolve, strip trailing slashes)
    6. Remove internal fields (prefixed with _)

    Args:
        inputs: Raw syscall inputs
        schema: Syscall schema with 'types' dict

    Returns:
        Canonicalized inputs dict
    """
    known_fields = set(schema.get("types", {}).keys())

    def _canonicalize_value(value: Any, field_type: Optional[str] = None) -> Any:
        """Recursively canonicalize a value."""
        if value is None:
            return None

        # Handle numpy arrays
        try:
            import numpy as np
            if isinstance(value, np.ndarray):
                return value.tolist()
            if isinstance(value, (np.floating, np.integer)):
                value = float(value) if isinstance(value, np.floating) else int(value)
        except ImportError:
            pass

        # Handle floats - round to 9 decimal places
        if isinstance(value, float):
            return round(value, 9)

        # Handle strings
        if isinstance(value, str):
            # Normalize paths if this looks like a path
            if field_type == "str" and (
                "/" in value or
                value.endswith(".json") or
                value.endswith(".cdna") or
                value.endswith(".gguf")
            ):
                # Try to normalize as path
                try:
                    p = Path(value)
                    if p.exists():
                        # Resolve to absolute, strip trailing slash
                        return str(p.resolve()).rstrip("/")
                except Exception:
                    pass
            return value

        # Handle lists - recursively canonicalize
        if isinstance(value, list):
            return [_canonicalize_value(v) for v in value]

        # Handle dicts - sort keys and recursively canonicalize
        if isinstance(value, dict):
            return {
                k: _canonicalize_value(v)
                for k, v in sorted(value.items())
            }

        # Handle bytes
        if isinstance(value, bytes):
            return value.hex()

        return value

    # Build canonical dict
    canonical = {}
    types = schema.get("types", {})

    for key, value in sorted(inputs.items()):
        # Skip internal fields (prefixed with _)
        if key.startswith("_"):
            continue

        # Include if in schema OR if schema has no types (allow all)
        if not known_fields or key in known_fields:
            field_type = types.get(key)
            canonical[key] = _canonicalize_value(value, field_type)

    return canonical


# Registry version (bump when syscall ABI changes)
REGISTRY_VERSION = "0.1.0"
ROUTER_VERSION_HASH = hashlib.sha256(f"bloomos_kernel_registry:{REGISTRY_VERSION}".encode()).hexdigest()[:16]


@dataclass
class SyscallSpec:
    """Specification for a registered syscall."""
    canonical_name: str              # "cdna.decode.v1"
    handler: Callable                # Function to execute
    schema: Dict[str, Any]           # Input schema for validation
    cost_estimator: Callable         # (inputs) -> estimated IT cost
    version: str = "v1"
    description: str = ""

    # Computed hashes
    schema_hash: str = ""
    handler_hash: str = ""

    def __post_init__(self):
        """Compute hashes."""
        if not self.schema_hash:
            self.schema_hash = hashlib.sha256(
                json.dumps(self.schema, sort_keys=True).encode()
            ).hexdigest()[:16]

        if not self.handler_hash:
            # Hash handler source code if available
            try:
                source = inspect.getsource(self.handler)
                self.handler_hash = hashlib.sha256(source.encode()).hexdigest()[:16]
            except (TypeError, OSError):
                self.handler_hash = hashlib.sha256(
                    self.canonical_name.encode()
                ).hexdigest()[:16]


class SyscallRegistry:
    """
    Central registry for kernel syscalls.

    Usage:
        registry = SyscallRegistry(ledger, cell_wall)

        # Register a syscall
        registry.register(
            canonical_name="cdna.decode.v1",
            handler=cdna_decode_handler,
            schema={"required": ["manifest_path"]},
            cost_estimator=lambda inputs: 10.0,
        )

        # Invoke a syscall
        result, receipt = registry.invoke("cdna.decode.v1", "bridge", inputs)
    """

    def __init__(
        self,
        ledger: ITTokenLedger,
        cell_wall: CellWall,
    ):
        """
        Initialize registry with ledger and cell wall.

        Args:
            ledger: IT Token Ledger for resource accounting
            cell_wall: Cell Wall for authorization
        """
        self.ledger = ledger
        self.cell_wall = cell_wall
        self._syscalls: Dict[str, SyscallSpec] = {}

    def register(
        self,
        canonical_name: str,
        handler: Callable,
        schema: Dict[str, Any],
        cost_estimator: Callable,
        version: str = "v1",
        description: str = "",
    ) -> None:
        """
        Register a new syscall.

        Args:
            canonical_name: Unique syscall name (e.g., "cdna.decode.v1")
            handler: Function to execute (signature: handler(inputs) -> result)
            schema: Input schema for validation
            cost_estimator: Function to estimate IT cost (signature: estimator(inputs) -> float)
            version: Syscall version
            description: Human-readable description
        """
        spec = SyscallSpec(
            canonical_name=canonical_name,
            handler=handler,
            schema=schema,
            cost_estimator=cost_estimator,
            version=version,
            description=description,
        )
        self._syscalls[canonical_name] = spec

    def unregister(self, canonical_name: str) -> bool:
        """Unregister a syscall. Returns True if found and removed."""
        if canonical_name in self._syscalls:
            del self._syscalls[canonical_name]
            return True
        return False

    def invoke(
        self,
        canonical_name: str,
        actor: str,
        inputs: Dict[str, Any],
    ) -> Tuple[Any, SyscallReceipt]:
        """
        Invoke a syscall with full enforcement.

        Flow:
        1. CellWall authorization (EPERM)
        2. Schema validation (EINVAL)
        3. IT token pre-authorization (ERESOURCE)
        4. Handler execution
        5. IT token commit
        6. Receipt generation

        Args:
            canonical_name: Syscall to invoke
            actor: Who is invoking
            inputs: Syscall inputs (dict)

        Returns:
            (result, SyscallReceipt)

        Raises:
            KeyError: If syscall not registered
        """
        t0 = time.time()
        auth_id = ""
        status = "OK"
        error_message = None
        result = None

        # Check syscall exists
        if canonical_name not in self._syscalls:
            receipt = create_receipt(
                syscall_id=canonical_name,
                actor=actor,
                status="FAIL",
                error_message=f"Unknown syscall: {canonical_name}",
                duration_ms=(time.time() - t0) * 1000,
                router_version_hash=ROUTER_VERSION_HASH,
            )
            return (None, receipt)

        spec = self._syscalls[canonical_name]

        # Extract capability token from inputs if present
        capability_token = inputs.get("_capability_token")

        # Estimate cost for capability authorization
        estimated_cost = spec.cost_estimator(inputs)

        # Estimate input size for capability authorization
        input_size = len(json.dumps(inputs, default=str).encode())

        # 1. CellWall authorization (with optional capability token)
        authorized, auth_error = self.cell_wall.authorize_syscall(
            actor,
            canonical_name,
            capability_token=capability_token,
            estimated_cost=estimated_cost,
            input_size=input_size,
        )
        if not authorized:
            receipt = create_receipt(
                syscall_id=canonical_name,
                actor=actor,
                status="EPERM",
                error_message=auth_error,
                duration_ms=(time.time() - t0) * 1000,
                schema_hash=spec.schema_hash,
                handler_hash=spec.handler_hash,
                router_version_hash=ROUTER_VERSION_HASH,
            )
            return (None, receipt)

        # 2. Schema validation
        valid, validation_error = self.cell_wall.validate_input(
            actor, None, spec.schema, inputs
        )
        if not valid:
            receipt = create_receipt(
                syscall_id=canonical_name,
                actor=actor,
                status="FAIL",
                error_message=validation_error,
                duration_ms=(time.time() - t0) * 1000,
                schema_hash=spec.schema_hash,
                handler_hash=spec.handler_hash,
                router_version_hash=ROUTER_VERSION_HASH,
            )
            return (None, receipt)

        # 3. Canonicalize inputs for deterministic hashing
        canonical_inputs = _canonicalize_inputs(inputs, spec.schema)
        input_hash = hashlib.sha256(
            json.dumps(canonical_inputs, sort_keys=True, separators=(",", ":")).encode()
        ).hexdigest()

        # 3. IT token pre-authorization
        estimated_cost = spec.cost_estimator(inputs)
        pre_auth_ok, pre_auth_error, auth_id = self.ledger.pre_authorize(
            actor, canonical_name, estimated_cost
        )

        if not pre_auth_ok:
            receipt = create_receipt(
                syscall_id=canonical_name,
                actor=actor,
                input_hash=input_hash,
                status="ERESOURCE",
                error_message=pre_auth_error,
                duration_ms=(time.time() - t0) * 1000,
                schema_hash=spec.schema_hash,
                handler_hash=spec.handler_hash,
                router_version_hash=ROUTER_VERSION_HASH,
            )
            return (None, receipt)

        # 4. Handler execution
        exec_start = time.time()
        actual_cost_breakdown = {"cpu_ms": 0.0, "io_bytes": 0.0, "ram_bytes": 0.0}

        try:
            result = spec.handler(inputs)
            status = "OK"

            # Estimate actual cost from execution
            actual_cost_breakdown["cpu_ms"] = (time.time() - exec_start) * 1000

            # If result has size info, estimate I/O
            if isinstance(result, dict):
                if "bytes_read" in result:
                    actual_cost_breakdown["io_bytes"] = result["bytes_read"]
                if "bytes_written" in result:
                    actual_cost_breakdown["io_bytes"] += result["bytes_written"]
                if "memory_bytes" in result:
                    actual_cost_breakdown["ram_bytes"] = result["memory_bytes"]

        except Exception as e:
            status = "FAIL"
            error_message = str(e)
            result = None

            # Rollback pre-authorization on failure
            self.ledger.rollback(auth_id)

            receipt = create_receipt(
                syscall_id=canonical_name,
                actor=actor,
                input_hash=input_hash,
                status=status,
                error_message=error_message,
                duration_ms=(time.time() - t0) * 1000,
                schema_hash=spec.schema_hash,
                handler_hash=spec.handler_hash,
                router_version_hash=ROUTER_VERSION_HASH,
            )
            return (None, receipt)

        # 5. IT token commit
        ledger_entry = self.ledger.commit(actor, canonical_name, actual_cost_breakdown, auth_id)
        it_tokens_spent = abs(ledger_entry.net_it_tokens)

        # Compute output hash
        output_hash = hashlib.sha256(
            json.dumps(result, sort_keys=True, default=str).encode()
        ).hexdigest()

        # 6. Create receipt
        receipt = create_receipt(
            syscall_id=canonical_name,
            actor=actor,
            input_hash=input_hash,
            output_hash=output_hash,
            status=status,
            duration_ms=(time.time() - t0) * 1000,
            it_tokens_spent=it_tokens_spent,
            cost_breakdown=actual_cost_breakdown,
            schema_hash=spec.schema_hash,
            handler_hash=spec.handler_hash,
            router_version_hash=ROUTER_VERSION_HASH,
            error_message=error_message,
        )

        return (result, receipt)

    def list_syscalls(self) -> List[Dict[str, Any]]:
        """List all registered syscalls."""
        return [
            {
                "canonical_name": spec.canonical_name,
                "version": spec.version,
                "description": spec.description,
                "schema": spec.schema,
                "schema_hash": spec.schema_hash,
                "handler_hash": spec.handler_hash,
            }
            for spec in self._syscalls.values()
        ]

    def get_syscall(self, canonical_name: str) -> Optional[SyscallSpec]:
        """Get syscall spec by name."""
        return self._syscalls.get(canonical_name)

    def stats(self) -> Dict[str, Any]:
        """Get registry statistics."""
        return {
            "registry_version": REGISTRY_VERSION,
            "router_version_hash": ROUTER_VERSION_HASH,
            "syscall_count": len(self._syscalls),
            "syscalls": list(self._syscalls.keys()),
            "ledger_stats": self.ledger.stats(),
            "cell_wall_stats": self.cell_wall.stats(),
        }
