from __future__ import annotations

import hashlib
import json
import re
from contextvars import ContextVar
from datetime import datetime, timezone
from typing import Any, Dict, Mapping, Sequence, Optional, Union

from receipts.models import LineIntegrityReceipt, LineIntegritySource, _canonicalize
from receipts.writer import write_receipt

__all__ = [
    "LineIntegritySource",
    "emit_on_ingest",
    "mint_line_integrity_receipt",
    "current_receipt_id",
    "current_related_ids",
    "clear_run_context",
]

MAX_PREVIEW_ITEMS = 5
MAX_PREVIEW_STRING = 160
_RELATED_KEY = "line_integrity"
_LAST_KEY = f"{_RELATED_KEY}::last"
_LAST_SOURCE_KEY = f"{_RELATED_KEY}::last_source"
_LINE_CONTEXT: ContextVar[Dict[str, Any]] = ContextVar("line_integrity_run_context", default={})

LineIntegritySourceLike = Union[LineIntegritySource, str]


def _canonical_payload_bytes(payload: Any) -> bytes:
    """
    Render payload into canonical JSON bytes so hashes are stable across ordering differences.
    """
    canonical = _canonicalize(payload)
    text = json.dumps(canonical, ensure_ascii=True, separators=(",", ":"), sort_keys=True)
    return text.encode("utf-8")


def _summarise_value(value: Any, depth: int = 0) -> Any:
    """Return a preview-friendly representation with bounded depth and length."""
    canonical = _canonicalize(value)
    if depth >= 1:
        # Avoid deep nesting by converting complex types to JSON strings.
        snippet = json.dumps(canonical, ensure_ascii=True, separators=(",", ":"), sort_keys=True)
        if len(snippet) > MAX_PREVIEW_STRING:
            snippet = snippet[: MAX_PREVIEW_STRING - 3] + "..."
        return snippet

    if isinstance(canonical, dict):
        sample: Dict[str, Any] = {}
        for key in list(canonical.keys())[:MAX_PREVIEW_ITEMS]:
            sample[str(key)] = _summarise_value(canonical[key], depth=depth + 1)
        return sample

    if isinstance(canonical, list):
        return [_summarise_value(item, depth=depth + 1) for item in canonical[:MAX_PREVIEW_ITEMS]]

    if isinstance(canonical, str) and len(canonical) > MAX_PREVIEW_STRING:
        return canonical[: MAX_PREVIEW_STRING - 3] + "..."

    return canonical


def _build_preview(payload: Any) -> Dict[str, Any]:
    """Construct a lightweight human-auditable preview of the payload."""
    if isinstance(payload, Mapping):
        try:
            total = len(payload)
        except TypeError:
            total = None
        sample_keys = sorted(payload.keys(), key=lambda key: str(key))[:MAX_PREVIEW_ITEMS]
        sample_map = {str(key): _summarise_value(payload[key]) for key in sample_keys}
        preview: Dict[str, Any] = {
            "type": "mapping",
            "sample": sample_map,
        }
        if total is not None:
            preview["total_keys"] = total
        return preview

    if isinstance(payload, Sequence) and not isinstance(payload, (str, bytes, bytearray)):
        try:
            total = len(payload)
        except TypeError:
            total = None
        sample_seq = [_summarise_value(item) for item in list(payload)[:MAX_PREVIEW_ITEMS]]
        preview = {
            "type": "sequence",
            "sample": sample_seq,
        }
        if total is not None:
            preview["total_items"] = total
        return preview

    return {
        "type": type(payload).__name__,
        "sample": _summarise_value(payload),
    }


def _safe_timestamp(value: datetime) -> datetime:
    """Ensure cutoff timestamp is timezone-aware in UTC."""
    if value.tzinfo is None:
        return value.replace(tzinfo=timezone.utc)
    return value.astimezone(timezone.utc)


def _mint_receipt_id(source: str, cutoff_ts: datetime, digest: str) -> str:
    safe_source = re.sub(r"[^A-Za-z0-9_.-]", "_", source or "unknown")
    ts_token = cutoff_ts.strftime("%Y%m%dT%H%M%SZ")
    return f"LINE_{safe_source}_{ts_token}_{digest[:8]}"


def _coerce_source(source: LineIntegritySourceLike) -> LineIntegritySource:
    if isinstance(source, LineIntegritySource):
        return source
    try:
        return LineIntegritySource(str(source))
    except ValueError as exc:  # pragma: no cover - defensive
        valid = ", ".join(item.value for item in LineIntegritySource)
        raise ValueError(f"Unsupported line integrity source '{source}'. Expected one of: {valid}") from exc


def mint_line_integrity_receipt(source: LineIntegritySourceLike, cutoff_ts: datetime, payload: Any) -> LineIntegrityReceipt:
    """
    Snapshot the payload at the decision cut-off and bind it with a deterministic hash.
    """
    data_source = _coerce_source(source)
    canonical_bytes = _canonical_payload_bytes(payload)
    digest = hashlib.sha256(canonical_bytes).hexdigest()
    cutoff_utc = _safe_timestamp(cutoff_ts)

    receipt_id = _mint_receipt_id(data_source.value, cutoff_utc, digest)
    preview = _build_preview(payload)

    return LineIntegrityReceipt(
        receipt_id=receipt_id,
        domain="line_integrity",
        data_source=data_source,
        cutoff_ts=cutoff_utc,
        raw_data_hash=digest,
        preview=preview,
    )


def _update_run_context(data_source: LineIntegritySource, receipt_id: str) -> None:
    current = dict(_LINE_CONTEXT.get())
    source_map = dict(current.get(_RELATED_KEY) or {})
    source_map[data_source.value] = receipt_id
    current[_RELATED_KEY] = source_map
    current[_LAST_KEY] = receipt_id
    current[_LAST_SOURCE_KEY] = data_source.value
    _LINE_CONTEXT.set(current)


def emit_on_ingest(data_source: LineIntegritySourceLike, cutoff_ts: datetime, payload: Any) -> LineIntegrityReceipt:
    """
    Mint and persist a LineIntegrityReceipt for the raw payload and expose its ID
    to downstream receipt emitters via a transient run context.
    """
    source_enum = _coerce_source(data_source)
    receipt = mint_line_integrity_receipt(source_enum, cutoff_ts, payload)
    write_receipt(receipt)
    _update_run_context(source_enum, receipt.receipt_id)
    return receipt


def current_receipt_id(data_source: Optional[LineIntegritySourceLike] = None) -> Optional[str]:
    """
    Return the latest line integrity receipt id for the given data source (if any).
    """
    context = _LINE_CONTEXT.get()
    if data_source is None:
        return context.get(_LAST_KEY)

    source_enum = _coerce_source(data_source)
    source_map = context.get(_RELATED_KEY)
    if isinstance(source_map, Mapping):
        candidate = source_map.get(source_enum.value)
        if candidate:
            return candidate

    # Fallback to last receipt if explicit source missing
    return context.get(_LAST_KEY)


def current_related_ids() -> Dict[str, Dict[str, str]]:
    """
    Snapshot the currently active line integrity linkage for downstream receipts.
    """
    context = _LINE_CONTEXT.get()
    source_map = context.get(_RELATED_KEY)
    if not isinstance(source_map, Mapping) or not source_map:
        return {}
    # Return a defensive copy to prevent accidental mutations leaking into context.
    return {_RELATED_KEY: dict(source_map)}


def clear_run_context() -> None:
    """
    Reset any stored line integrity linkage (used in tests and CLI entrypoints).
    """
    _LINE_CONTEXT.set({})
