"""Receipt validation utilities and helpers reused across CLI tooling."""
from __future__ import annotations

import json
import sys
import warnings
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Iterable, List, Mapping, Optional

from jsonschema import Draft202012Validator

from receipts.chain import ReceiptChainReceipt
from receipts.models import BaseReceipt, DIPReceipt, compute_hash as _compute_hash, load_receipt

_SCHEMA_DIR = Path(__file__).resolve().parents[1] / "schemas" / "receipts"
_SCHEMAS: Dict[str, Draft202012Validator] = {}


# ============================================================================
# Tri-Temporal Validation
# ============================================================================

@dataclass
class TriTemporalViolation:
    """Records a tri-temporal ordering violation."""
    receipt_id: str
    violation_type: str  # "valid_gt_observed", "observed_gt_recorded", "missing_field"
    field_a: Optional[str] = None
    value_a: Optional[datetime] = None
    field_b: Optional[str] = None
    value_b: Optional[datetime] = None
    message: str = ""


@dataclass
class TriTemporalResult:
    """Result of tri-temporal validation."""
    valid: bool
    violations: List[TriTemporalViolation] = field(default_factory=list)
    warnings: List[str] = field(default_factory=list)


def _parse_datetime(value: Any) -> Optional[datetime]:
    """Parse a datetime from various formats."""
    if value is None:
        return None
    if isinstance(value, datetime):
        return value
    if isinstance(value, str):
        token = value.strip()
        if not token:
            return None
        if token.endswith("Z"):
            token = token[:-1] + "+00:00"
        try:
            parsed = datetime.fromisoformat(token)
            if parsed.tzinfo is None:
                parsed = parsed.replace(tzinfo=timezone.utc)
            return parsed
        except ValueError:
            return None
    return None


def _extract_valid_time(payload: Mapping[str, Any]) -> Optional[datetime]:
    """Extract valid_time from receipt payload (handles nested ValidTime object)."""
    vt = payload.get("valid_time")
    if vt is None:
        return None
    if isinstance(vt, datetime):
        return vt
    if isinstance(vt, Mapping):
        # TriTemporalReceipt uses ValidTime object with start/end
        return _parse_datetime(vt.get("start"))
    return _parse_datetime(vt)


def _extract_observed_at(payload: Mapping[str, Any]) -> Optional[datetime]:
    """Extract observed_at from receipt payload."""
    # Try explicit observed_at field
    observed = payload.get("observed_at")
    if observed:
        return _parse_datetime(observed)
    # Fall back to ts (creation timestamp) as observed proxy
    return _parse_datetime(payload.get("ts"))


def _extract_recorded_at(payload: Mapping[str, Any]) -> Optional[datetime]:
    """Extract recorded_at (transaction time) from receipt payload."""
    # Try explicit transaction_time object
    tt = payload.get("transaction_time")
    if isinstance(tt, Mapping):
        return _parse_datetime(tt.get("recorded_at"))
    if isinstance(tt, datetime):
        return tt
    # Fall back to system_time (when persisted)
    return _parse_datetime(payload.get("system_time"))


def validate_tri_temporal(
    payload: Mapping[str, Any],
    *,
    strict: bool = False,
) -> TriTemporalResult:
    """
    Validate tri-temporal ordering constraints on a receipt.
    
    Tri-temporal model:
    - valid_time: When the fact was true in the world
    - observed_at: When a sensor/agent observed the fact  
    - recorded_at: When the system recorded the receipt
    
    Invariants:
    - valid_time ≤ observed_at (can't observe before fact is valid)
    - observed_at ≤ recorded_at (can't record before observing)
    
    Args:
        payload: Receipt payload to validate
        strict: If True, missing timestamps are errors; if False, warnings
        
    Returns:
        TriTemporalResult with validation status and any violations
    """
    receipt_id = str(payload.get("receipt_id", "unknown"))
    violations: List[TriTemporalViolation] = []
    warnings_list: List[str] = []
    
    valid_time = _extract_valid_time(payload)
    observed_at = _extract_observed_at(payload)
    recorded_at = _extract_recorded_at(payload)
    
    # Check for missing fields
    if valid_time is None:
        msg = f"Receipt {receipt_id} missing valid_time"
        if strict:
            violations.append(TriTemporalViolation(
                receipt_id=receipt_id,
                violation_type="missing_field",
                field_a="valid_time",
                message=msg,
            ))
        else:
            warnings_list.append(msg)
    
    if observed_at is None:
        msg = f"Receipt {receipt_id} missing observed_at (or ts)"
        if strict:
            violations.append(TriTemporalViolation(
                receipt_id=receipt_id,
                violation_type="missing_field",
                field_a="observed_at",
                message=msg,
            ))
        else:
            warnings_list.append(msg)
    
    if recorded_at is None:
        # recorded_at is often set by the writer, so missing is usually ok
        warnings_list.append(f"Receipt {receipt_id} missing recorded_at (system_time)")
    
    # Check ordering: valid_time <= observed_at
    if valid_time is not None and observed_at is not None:
        if valid_time > observed_at:
            violations.append(TriTemporalViolation(
                receipt_id=receipt_id,
                violation_type="valid_gt_observed",
                field_a="valid_time",
                value_a=valid_time,
                field_b="observed_at",
                value_b=observed_at,
                message=f"valid_time ({valid_time.isoformat()}) > observed_at ({observed_at.isoformat()})",
            ))
    
    # Check ordering: observed_at <= recorded_at
    if observed_at is not None and recorded_at is not None:
        if observed_at > recorded_at:
            violations.append(TriTemporalViolation(
                receipt_id=receipt_id,
                violation_type="observed_gt_recorded",
                field_a="observed_at",
                value_a=observed_at,
                field_b="recorded_at",
                value_b=recorded_at,
                message=f"observed_at ({observed_at.isoformat()}) > recorded_at ({recorded_at.isoformat()})",
            ))
    
    return TriTemporalResult(
        valid=len(violations) == 0,
        violations=violations,
        warnings=warnings_list,
    )


def validate_lineage(
    payload: Mapping[str, Any],
    *,
    strict: bool = False,
) -> TriTemporalResult:
    """
    Validate lineage fields are present for chain-linked receipts.
    
    Lineage fields (for non-root receipts):
    - parent_id or parent_ids: Reference to parent receipt(s)
    - parent_hash or parent_hashes: Hash(es) of parent receipt(s)
    
    Args:
        payload: Receipt payload to validate
        strict: If True, missing lineage is an error; if False, warning
        
    Returns:
        TriTemporalResult with validation status
    """
    receipt_id = str(payload.get("receipt_id", "unknown"))
    violations: List[TriTemporalViolation] = []
    warnings_list: List[str] = []
    
    # Check if this is a root receipt (DIP, etc.) which don't need parent refs
    receipt_type = payload.get("receipt_type") or payload.get("type")
    is_root = receipt_type in {"DIPReceipt", "GenesisReceipt", "RecoveryReceipt"}
    allow_orphan = payload.get("allow_orphan", False)
    
    if is_root or allow_orphan:
        return TriTemporalResult(valid=True, violations=[], warnings=[])
    
    # Check for parent references
    parent_id = payload.get("parent_id")
    parent_ids = payload.get("parent_ids", [])
    has_parent_ref = bool(parent_id) or bool(parent_ids)
    
    parent_hash = payload.get("parent_hash")
    parent_hashes = payload.get("parent_hashes", [])
    has_parent_hash = bool(parent_hash) or bool(parent_hashes)
    
    if not has_parent_ref:
        msg = f"Receipt {receipt_id} missing parent_id/parent_ids (no lineage)"
        if strict:
            violations.append(TriTemporalViolation(
                receipt_id=receipt_id,
                violation_type="missing_field",
                field_a="parent_ids",
                message=msg,
            ))
        else:
            warnings_list.append(msg)
    
    if has_parent_ref and not has_parent_hash:
        msg = f"Receipt {receipt_id} has parent_ids but missing parent_hash/parent_hashes"
        if strict:
            violations.append(TriTemporalViolation(
                receipt_id=receipt_id,
                violation_type="missing_field",
                field_a="parent_hashes",
                message=msg,
            ))
        else:
            warnings_list.append(msg)
    
    return TriTemporalResult(
        valid=len(violations) == 0,
        violations=violations,
        warnings=warnings_list,
    )


def _load(name: str) -> Draft202012Validator:
    if name in _SCHEMAS:
        return _SCHEMAS[name]
    path = _SCHEMA_DIR / name
    schema = json.loads(path.read_text(encoding="utf-8"))
    validator = Draft202012Validator(schema)
    _SCHEMAS[name] = validator
    return validator


def _drop_none(data):
    if isinstance(data, dict):
        return {
            key: _drop_none(value)
            for key, value in data.items()
            if value is not None
        }
    if isinstance(data, list):
        return [_drop_none(item) for item in data if item is not None]
    return data


def validate_receipt(obj: Dict[str, object]) -> None:
    receipt_type = obj.get("type")
    schema_name = {
        "SenseSampleReceipt": "sense_sample.receipt.schema.json",
        "SenseDecisionReceipt": "sense_decision.receipt.schema.json",
    }.get(receipt_type)
    if not schema_name:
        return
    validator = _load(schema_name)
    properties = set(validator.schema.get("properties", {}).keys())
    pruned = {
        key: value for key, value in obj.items() if key in properties and value is not None
    }
    pruned = _drop_none(pruned)
    validator.validate(pruned)


def _read_json(path: Path) -> Dict[str, object]:
    return json.loads(path.read_text(encoding="utf-8"))


def _resolve_manifest_path(root: Path, path_value: str) -> Path:
    candidate = Path(path_value)
    if candidate.is_absolute():
        return candidate
    return (root / candidate).resolve()


def _find_manifest(root: Path, chain_id: Optional[str], dip_id: str) -> Optional[Path]:
    if not root.exists():
        return None

    patterns = []
    if chain_id:
        patterns.append(f"CHAIN_{chain_id}_*.json")
    patterns.append("*ReceiptChainReceipt*.json")

    for pattern in patterns:
        for candidate in root.rglob(pattern):
            if not candidate.is_file():
                continue
            try:
                payload = json.loads(candidate.read_text(encoding="utf-8"))
            except Exception:
                continue
            if payload.get("receipt_type") != "ReceiptChainReceipt" and payload.get("type") != "ReceiptChainReceipt":
                continue
            if payload.get("dip_receipt_id") != dip_id:
                continue
            if chain_id and payload.get("chain_id") != chain_id:
                continue
            return candidate
    return None


@dataclass(frozen=True)
class ReceiptRecord:
    """
    Holds a receipt plus the path it was loaded from.

    .. deprecated:: 2025-11-13
        This class is a compatibility shim during semantic-layer migration.
        **Sunset Date**: 2025-12-27
        **Migration**: Use receipts.lineage APIs instead
        See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md
    """

    receipt: BaseReceipt
    path: Path


@dataclass(frozen=True)
class ReceiptChain:
    """
    Represents a receipt chain with DIP and descendants.

    .. deprecated:: 2025-11-13
        This class is a compatibility shim during semantic-layer migration.
        **Sunset Date**: 2025-12-27
        **Migration**: Use receipts.lineage APIs instead
        See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md
    """

    dip: DIPReceipt
    records: Dict[str, ReceiptRecord]
    descendants: List[ReceiptRecord]
    manifest: Optional[ReceiptChainReceipt] = None


def load_receipt_chain(
    dip_path: Path,
    *,
    receipts_root: Optional[Path] = None,
) -> ReceiptChain:
    """
    Load receipt chain using legacy API.

    .. deprecated:: 2025-11-13
        This function is a compatibility shim during semantic-layer migration.
        **Sunset Date**: 2025-12-27
        **Migration**: Use receipts.lineage.load_chain() instead
        See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md
    """
    warnings.warn(
        "load_receipt_chain is deprecated and will be removed 2025-12-27. "
        "Use receipts.lineage.load_chain instead. "
        "See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md",
        DeprecationWarning,
        stacklevel=2
    )
    dip_path = Path(dip_path)
    if not dip_path.exists():
        raise FileNotFoundError(f"DIP receipt not found: {dip_path}")

    root = Path(receipts_root) if receipts_root is not None else dip_path.parent
    payload = _read_json(dip_path)
    dip = load_receipt(payload)
    if not isinstance(dip, DIPReceipt):
        raise ValueError(f"{dip_path} is not a DIPReceipt")

    records: Dict[str, ReceiptRecord] = {
        dip.receipt_id: ReceiptRecord(receipt=dip, path=dip_path)
    }
    descendants: List[ReceiptRecord] = []

    chain_id = (getattr(dip, "related_ids", None) or {}).get("receipt_chain_id")
    manifest_path = _find_manifest(root, chain_id, dip.receipt_id)
    manifest: Optional[ReceiptChainReceipt] = None

    if manifest_path is not None:
        manifest_payload = _read_json(manifest_path)
        model = load_receipt(manifest_payload)
        if isinstance(model, ReceiptChainReceipt):
            manifest = model
            for entry in manifest.receipts:
                record_path = _resolve_manifest_path(root, entry.path)
                if not record_path.exists():
                    continue
                record_payload = _read_json(record_path)
                record_receipt = load_receipt(record_payload)
                record = ReceiptRecord(receipt=record_receipt, path=record_path)
                records[record_receipt.receipt_id] = record
                if record_receipt.receipt_type not in {"DIPReceipt", "ReceiptChainReceipt"}:
                    descendants.append(record)

    return ReceiptChain(
        dip=dip,
        records=records,
        descendants=descendants,
        manifest=manifest,
    )


def verify_receipt_chain(chain: ReceiptChain) -> bool:
    """
    Verify receipt chain integrity using legacy API.

    .. deprecated:: 2025-11-13
        This function is a compatibility shim during semantic-layer migration.
        **Sunset Date**: 2025-12-27
        **Migration**: Use receipts.lineage.verify_chain() instead
        See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md
    """
    warnings.warn(
        "verify_receipt_chain is deprecated and will be removed 2025-12-27. "
        "Use receipts.lineage.verify_chain instead. "
        "See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md",
        DeprecationWarning,
        stacklevel=2
    )
    dip_chain_id = (getattr(chain.dip, "related_ids", None) or {}).get("receipt_chain_id")
    errors: List[str] = []
    for record in chain.descendants:
        related = getattr(record.receipt, "related_ids", {}) or {}
        if related.get("dip_receipt_id") not in {chain.dip.receipt_id, None}:
            errors.append(
                f"Receipt {record.receipt.receipt_id} does not reference DIP "
                f"{chain.dip.receipt_id}"
            )
        if dip_chain_id and related.get("receipt_chain_id") not in {dip_chain_id, None}:
            errors.append(
                f"Receipt {record.receipt.receipt_id} has inconsistent chain_id "
                f"{related.get('receipt_chain_id')} (expected {dip_chain_id})"
            )
    if errors:
        for failure in errors:
            print(failure, file=sys.stderr)
    return not errors


def validate_path(path: Path, *, strict: bool = False) -> int:
    """
    Validate receipts at path using legacy API.

    .. deprecated:: 2025-11-13
        This function is a compatibility shim during semantic-layer migration.
        **Sunset Date**: 2025-12-27
        **Migration**: Use receipts.lineage validation APIs instead
        See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md
    """
    warnings.warn(
        "validate_path is deprecated and will be removed 2025-12-27. "
        "Use receipts.lineage validation APIs instead. "
        "See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md",
        DeprecationWarning,
        stacklevel=2
    )
    target = Path(path)
    if not target.exists():
        raise FileNotFoundError(f"Path not found: {target}")

    receipts: Iterable[Path]
    if target.is_dir():
        receipts = target.rglob("*.json")
    else:
        receipts = [target]

    errors = []
    count = 0
    for receipt_path in receipts:
        if receipt_path.is_dir():
            continue
        count += 1
        try:
            payload = _read_json(receipt_path)
        except Exception as exc:
            errors.append(f"{receipt_path}: failed to read ({exc})")
            continue
        try:
            validate_receipt(payload)
        except Exception as exc:
            errors.append(f"{receipt_path}: validation failed ({exc})")

    if strict and count == 0:
        errors.append(f"{target} contained no receipts")

    for error in errors:
        print(error, file=sys.stderr)
    return 0 if not errors else 1


def compute_hash(*args, **kwargs):
    """
    Expose receipts.models.compute_hash for compatibility.

    .. deprecated:: 2025-11-13
        This function is a compatibility shim during semantic-layer migration.
        **Sunset Date**: 2025-12-27
        **Migration**: Import directly from receipts.models.compute_hash
        See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md
    """
    warnings.warn(
        "compute_hash passthrough is deprecated and will be removed 2025-12-27. "
        "Import directly from receipts.models.compute_hash instead. "
        "See docs/migrations/LEGACY_RECEIPT_CHAIN_SUNSET.md",
        DeprecationWarning,
        stacklevel=2
    )
    return _compute_hash(*args, **kwargs)


__all__ = [
    "ReceiptChain",
    "ReceiptRecord",
    "TriTemporalViolation",
    "TriTemporalResult",
    "validate_tri_temporal",
    "validate_lineage",
    "compute_hash",
    "load_receipt_chain",
    "validate_path",
    "verify_receipt_chain",
    "validate_receipt",
]
