from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any

PROTOCOL_V1 = "niannian.external_data/1"
PROTOCOL_V2 = "niannian.external_data/2"
PROTOCOL = PROTOCOL_V1
VALID_OPS = frozenset({"add", "delete", "overwrite"})
VALID_TARGETS = frozenset({"cloud", "local"})
_SPACE_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")
_FUNC_RE = re.compile(r"^[a-z][a-z0-9_-]{0,63}$")

logger = logging.getLogger(__name__)


def utc_now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


@dataclass
class ProvenanceActor:
    user_id: str = ""
    display_name: str = ""


@dataclass
class Provenance:
    trace_id: str = ""
    actor: ProvenanceActor = field(default_factory=ProvenanceActor)
    reported_at: str = ""
    correlation_id: str = ""
    parent_record_id: str = ""
    source_ref: str = ""


def _parse_provenance(raw: Any) -> Provenance | None:
    if raw is None:
        return None
    if not isinstance(raw, dict):
        raise ValueError("provenance must be an object")
    actor_raw = raw.get("actor") or {}
    if not isinstance(actor_raw, dict):
        raise ValueError("provenance.actor must be an object")
    return Provenance(
        trace_id=str(raw.get("trace_id") or "").strip(),
        actor=ProvenanceActor(
            user_id=str(actor_raw.get("user_id") or "").strip(),
            display_name=str(actor_raw.get("display_name") or "").strip(),
        ),
        reported_at=str(raw.get("reported_at") or "").strip(),
        correlation_id=str(raw.get("correlation_id") or "").strip(),
        parent_record_id=str(raw.get("parent_record_id") or "").strip(),
        source_ref=str(raw.get("source_ref") or "").strip(),
    )


def _provenance_to_dict(provenance: Provenance) -> dict[str, Any]:
    out: dict[str, Any] = {
        "trace_id": provenance.trace_id,
        "actor": {
            "user_id": provenance.actor.user_id,
        },
        "reported_at": provenance.reported_at,
    }
    if provenance.actor.display_name:
        out["actor"]["display_name"] = provenance.actor.display_name
    if provenance.correlation_id:
        out["correlation_id"] = provenance.correlation_id
    if provenance.parent_record_id:
        out["parent_record_id"] = provenance.parent_record_id
    if provenance.source_ref:
        out["source_ref"] = provenance.source_ref
    return out


def _is_v2_request(
    *,
    protocol: str,
    capability_id: str,
    provenance_present: bool,
) -> bool:
    if protocol == PROTOCOL_V2:
        return True
    if capability_id and provenance_present:
        return True
    return False


def _validate_v2_provenance(provenance: Provenance | None, *, strict: bool) -> None:
    if provenance is None:
        if strict:
            raise ValueError("provenance required for v2 requests")
        return
    if strict and not provenance.trace_id:
        raise ValueError("provenance.trace_id required")
    if not provenance.actor.user_id:
        raise ValueError("provenance.actor.user_id required")
    if not provenance.reported_at:
        raise ValueError("provenance.reported_at required")


@dataclass
class ExternalDataRequest:
    """外部数据源写入信封 — MCP v1/v2。"""

    operation: str
    project: str
    function: str
    payload: dict[str, Any] | list[Any] | str
    record_id: str = ""
    dedup_key: str = ""
    occurred_at: str = ""
    caller: dict[str, Any] = field(default_factory=dict)
    storage_target: str = "cloud"
    local_dir: str = ""
    tags: list[str] = field(default_factory=list)
    capability_id: str = ""
    provenance: Provenance | None = None
    learn: dict[str, Any] = field(default_factory=lambda: {"mode": "off"})
    protocol: str = ""
    _provenance_explicit: bool = False

    @classmethod
    def from_dict(cls, data: dict[str, Any], *, strict_v2: bool = True) -> ExternalDataRequest:
        op = str(data.get("operation") or "add").strip().lower()
        project = str(data.get("project") or "").strip()
        function = str(data.get("function") or "").strip()
        if not project or not function:
            raise ValueError("project and function are required")
        if op not in VALID_OPS:
            raise ValueError(f"invalid operation: {op}")
        if not _SPACE_RE.match(project):
            raise ValueError(f"invalid project (memory_space): {project}")
        if not _FUNC_RE.match(function):
            raise ValueError(f"invalid function: {function}")
        target = str(data.get("storage_target") or "cloud").strip().lower()
        if target not in VALID_TARGETS:
            raise ValueError(f"invalid storage_target: {target}")
        payload = data.get("payload")
        if payload is None and op != "delete":
            raise ValueError("payload is required for add/overwrite")

        protocol = str(data.get("protocol") or "").strip()
        capability_id = str(data.get("capability_id") or "").strip()
        provenance_explicit = "provenance" in data
        provenance = _parse_provenance(data.get("provenance")) if provenance_explicit else None

        learn = dict(data.get("learn") or {})
        if "mode" not in learn:
            learn["mode"] = "off"

        is_v2 = _is_v2_request(
            protocol=protocol,
            capability_id=capability_id,
            provenance_present=provenance_explicit,
        )
        if is_v2:
            _validate_v2_provenance(provenance, strict=strict_v2)
        elif not provenance_explicit:
            logger.warning(
                "external_data v1 request missing provenance; actor.user_id defaults to unknown"
            )

        return cls(
            operation=op,
            project=project,
            function=function,
            payload=payload if payload is not None else {},
            record_id=str(data.get("record_id") or "").strip(),
            dedup_key=str(data.get("dedup_key") or "").strip(),
            occurred_at=str(data.get("occurred_at") or utc_now_iso()),
            caller=dict(data.get("caller") or {}),
            storage_target=target,
            local_dir=str(data.get("local_dir") or "").strip(),
            tags=[str(t) for t in (data.get("tags") or [])],
            capability_id=capability_id,
            provenance=provenance,
            learn=learn,
            protocol=protocol,
            _provenance_explicit=provenance_explicit,
        )

    @property
    def is_v2(self) -> bool:
        return _is_v2_request(
            protocol=self.protocol,
            capability_id=self.capability_id,
            provenance_present=self._provenance_explicit,
        )

    def to_envelope(self, *, record_id: str) -> dict[str, Any]:
        use_v2 = self.is_v2 and self.provenance is not None
        envelope: dict[str, Any] = {
            "protocol": PROTOCOL_V2 if use_v2 else PROTOCOL_V1,
            "operation": self.operation,
            "record_id": record_id,
            "project": self.project,
            "function": self.function,
            "dedup_key": self.dedup_key or None,
            "occurred_at": self.occurred_at,
            "received_at": utc_now_iso(),
            "caller": self.caller,
            "storage_target": self.storage_target,
            "local_dir": self.local_dir or None,
            "tags": self.tags,
            "archived": False,
            "payload": self.payload,
        }
        if self.capability_id:
            envelope["capability_id"] = self.capability_id
        if use_v2 and self.provenance is not None:
            envelope["provenance"] = _provenance_to_dict(self.provenance)
        if self.learn:
            envelope["learn"] = self.learn
        return envelope


@dataclass
class ExternalDataResult:
    ok: bool
    operation: str
    record_id: str
    rel_path: str
    storage_target: str
    project: str
    function: str
    synced_to_cloud: bool = False
    message: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "ok": self.ok,
            "operation": self.operation,
            "record_id": self.record_id,
            "rel_path": self.rel_path,
            "storage_target": self.storage_target,
            "project": self.project,
            "function": self.function,
            "synced_to_cloud": self.synced_to_cloud,
            "message": self.message,
        }
