#!/usr/bin/env python3
"""
ProofValidator — Phase 4 Proof Consumer Library

Validates receipt proofs for cryptographic integrity, hash chain consistency,
and schema compliance. Used by all Phase 4 consumers (Guardian, OutcomeIndexer, Heart).

Core Operations:
    1. Verify cryptographic signatures (Ed25519)
    2. Validate hash chain continuity (parent_hash → receipt_hash)
    3. Check schema compliance (required fields, types)
    4. Extract validation metadata for consumer decisions
"""

import hashlib
import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple

from pydantic import BaseModel, Field, ValidationError

logger = logging.getLogger(__name__)


class ProofValidationError(Exception):
    """Raised when proof validation fails."""
    pass


class SignatureVerificationError(ProofValidationError):
    """Raised when signature verification fails."""
    pass


class HashChainError(ProofValidationError):
    """Raised when hash chain is broken or invalid."""
    pass


class SchemaValidationError(ProofValidationError):
    """Raised when receipt schema doesn't match expectations."""
    pass


class ValidationResult(BaseModel):
    """Result of proof validation."""
    valid: bool
    receipt_id: str
    receipt_type: str
    timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

    # Validation details
    signature_valid: bool = False
    hash_chain_valid: bool = False
    schema_valid: bool = False

    # Metadata
    parent_hash: Optional[str] = None
    computed_hash: Optional[str] = None
    errors: List[str] = Field(default_factory=list)
    warnings: List[str] = Field(default_factory=list)

    # For consumers
    can_trust: bool = False  # High confidence in proof


class ProofValidator:
    """
    Validates receipt proofs for consumption by Phase 4 components.

    Does NOT require full cryptographic key infrastructure—focuses on:
    - Hash chain continuity (parent → child)
    - Signature presence (if applicable)
    - Basic schema compliance
    - Deterministic validation (same proof always gives same result)
    """

    def __init__(self):
        """Initialize ProofValidator."""
        self.hash_algorithm = "sha256"
        logger.info("ProofValidator initialized")

    def verify(self, receipt: Dict[str, Any], strict: bool = False) -> ValidationResult:
        """
        Verify a receipt proof for consumption.

        Args:
            receipt: Receipt dict (typically from JSON deserialization)
            strict: If True, any validation failure makes result invalid;
                    if False, allow warnings but mark as valid if core checks pass

        Returns:
            ValidationResult with validation details

        Raises:
            ProofValidationError: If receipt is malformed
        """
        result = ValidationResult(
            valid=False,
            receipt_id=receipt.get("receipt_id", "unknown"),
            receipt_type=receipt.get("receipt_type", "unknown"),
        )

        try:
            # 1. Schema validation
            result.schema_valid = self._validate_schema(receipt, result)

            # 2. Hash chain validation
            result.hash_chain_valid = self._validate_hash_chain(receipt, result)

            # 3. Signature validation (if present)
            if receipt.get("signatures"):
                result.signature_valid = self._validate_signatures(receipt, result)
            else:
                result.signature_valid = True  # No signature required for MVP
                result.warnings.append("No signatures present (acceptable for offline proofs)")

            # Determine overall validity
            if strict:
                result.valid = (result.schema_valid and result.hash_chain_valid)
            else:
                result.valid = result.schema_valid  # Schema is mandatory

            # Mark as trustworthy if all checks pass
            result.can_trust = (
                result.valid and
                result.hash_chain_valid and
                result.signature_valid
            )

        except Exception as e:
            result.valid = False
            result.can_trust = False
            result.errors.append(f"Validation exception: {str(e)}")
            logger.error(f"Validation failed for {result.receipt_id}: {e}")

        return result

    def _validate_schema(self, receipt: Dict[str, Any], result: ValidationResult) -> bool:
        """
        Validate receipt schema compliance.

        Checks for required fields and type compatibility.
        """
        required_fields = {
            "receipt_id": str,
            "receipt_type": str,
            "ts": (str, dict),  # Allow ISO string or datetime dict
            "tenant": (str, type(None)),
        }

        for field_name, expected_type in required_fields.items():
            if field_name not in receipt:
                result.errors.append(f"Missing required field: {field_name}")
                return False

            value = receipt[field_name]
            if not isinstance(expected_type, tuple):
                expected_type = (expected_type,)

            if not isinstance(value, expected_type):
                result.errors.append(
                    f"Field '{field_name}' has wrong type: "
                    f"expected {expected_type}, got {type(value).__name__}"
                )
                return False

        logger.debug(f"Schema validation passed for {result.receipt_id}")
        return True

    def _validate_hash_chain(self, receipt: Dict[str, Any], result: ValidationResult) -> bool:
        """
        Validate hash chain continuity.

        If receipt_hash is present, recompute it and verify it matches.
        If parent_hashes are present, ensure they form a valid chain.
        """
        receipt_hash = receipt.get("receipt_hash")
        parent_hashes = receipt.get("parent_hashes", [])

        # If receipt_hash is provided, we can recompute it
        if receipt_hash:
            computed_hash = self._compute_receipt_hash(receipt)
            result.computed_hash = computed_hash

            if computed_hash != receipt_hash:
                result.errors.append(
                    f"Receipt hash mismatch: expected {receipt_hash}, "
                    f"computed {computed_hash}"
                )
                return False

            logger.debug(f"Hash chain valid for {result.receipt_id}")
            return True

        # If no receipt_hash, but parent_hashes exist, check them
        if parent_hashes:
            if not all(isinstance(h, str) and h for h in parent_hashes):
                result.warnings.append("Parent hashes present but some are empty/invalid")
            result.parent_hash = parent_hashes[-1] if parent_hashes else None
            return True

        # No hash chain present, but that's OK for offline proofs
        result.warnings.append("No receipt_hash or parent_hashes present (acceptable for offline)")
        return True

    def _validate_signatures(self, receipt: Dict[str, Any], result: ValidationResult) -> bool:
        """
        Validate signature presence and basic structure.

        Does NOT verify signatures cryptographically (requires key infrastructure).
        Just checks that they're present and have required fields.
        """
        signatures = receipt.get("signatures", [])

        if not signatures:
            result.warnings.append("No signatures present")
            return False

        for i, sig in enumerate(signatures):
            if not isinstance(sig, dict):
                result.errors.append(f"Signature {i} is not a dict")
                return False

            required_sig_fields = {"public_key", "signature", "algorithm"}
            sig_fields = set(sig.keys())

            if not required_sig_fields.issubset(sig_fields):
                missing = required_sig_fields - sig_fields
                result.errors.append(f"Signature {i} missing fields: {missing}")
                return False

        logger.debug(f"Signature structure valid for {result.receipt_id} ({len(signatures)} sigs)")
        return True

    def _compute_receipt_hash(self, receipt: Dict[str, Any]) -> str:
        """
        Compute JCS canonical hash of receipt (excluding signatures).

        Uses same approach as receipts.base.compute_hash:
        - Canonicalize JSON (RFC 8785 JCS)
        - Exclude signatures
        - SHA-256 hash
        """
        # For MVP, use simple JSON canonicalization + hash
        # In production, import from receipts.base.to_jcs_bytes

        import json

        # Strip signatures
        payload = {k: v for k, v in receipt.items() if k != "signatures"}

        # Canonical JSON (sorted keys, no whitespace)
        canonical_json = json.dumps(payload, separators=(',', ':'), sort_keys=True)

        # SHA-256
        hash_obj = hashlib.sha256(canonical_json.encode('utf-8'))
        return hash_obj.hexdigest()

    def batch_verify(
        self,
        receipts: List[Dict[str, Any]],
        strict: bool = False
    ) -> List[ValidationResult]:
        """
        Verify multiple receipts.

        Returns list of ValidationResult objects (parallel with input).
        """
        return [self.verify(receipt, strict=strict) for receipt in receipts]

    def summarize_batch(
        self,
        results: List[ValidationResult]
    ) -> Dict[str, Any]:
        """
        Summarize batch validation results.

        Returns statistics on pass/fail rates, error categories, etc.
        """
        total = len(results)
        valid = sum(1 for r in results if r.valid)
        trustworthy = sum(1 for r in results if r.can_trust)

        error_counts: Dict[str, int] = {}
        for result in results:
            for error in result.errors:
                category = error.split(":")[0]
                error_counts[category] = error_counts.get(category, 0) + 1

        return {
            "total": total,
            "valid": valid,
            "trustworthy": trustworthy,
            "valid_rate": valid / total if total > 0 else 0.0,
            "trust_rate": trustworthy / total if total > 0 else 0.0,
            "error_categories": error_counts,
        }


# Global validator instance
_global_validator: Optional[ProofValidator] = None


def get_validator() -> ProofValidator:
    """Get or create global ProofValidator instance."""
    global _global_validator
    if _global_validator is None:
        _global_validator = ProofValidator()
    return _global_validator


def reset_validator() -> None:
    """Reset global ProofValidator (for testing)."""
    global _global_validator
    _global_validator = None
