from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timezone
import json
from pathlib import Path
from typing import Any

from ..core.runtime import RuntimeSnapshot


CONNECTED_FRESHNESS_SEC = 90
OFFLINE_FRESHNESS_SEC = 600


@dataclass(slots=True)
class CanonicalStatus:
    runtime_state: str
    trust_state: str
    device_state: str
    paired: bool
    last_heartbeat_at: str
    heartbeat_age_sec: int | None
    failure_count: int
    last_error: str
    backend_url: str
    app_version: str
    update_channel: str
    logs_path: str
    storage_path: str
    status_source_version: str
    computed_at: str

    def to_dict(self) -> dict[str, Any]:
        return {
            "runtime_state": self.runtime_state,
            "trust_state": self.trust_state,
            "device_state": self.device_state,
            "paired": self.paired,
            "last_heartbeat_at": self.last_heartbeat_at,
            "heartbeat_age_sec": self.heartbeat_age_sec,
            "failure_count": self.failure_count,
            "last_error": self.last_error,
            "backend_url": self.backend_url,
            "app_version": self.app_version,
            "update_channel": self.update_channel,
            "logs_path": self.logs_path,
            "storage_path": self.storage_path,
            "status_source_version": self.status_source_version,
            "computed_at": self.computed_at,
        }


class RuntimeEvidenceStore:
    """Shared persisted runtime evidence used by all status consumers."""

    def __init__(self, path: str | Path) -> None:
        self._path = Path(path)

    @property
    def path(self) -> Path:
        return self._path

    def load(self) -> dict[str, Any]:
        if not self._path.exists():
            return {}
        try:
            raw = self._path.read_text(encoding="utf-8")
            data = json.loads(raw)
            if isinstance(data, dict):
                return data
        except Exception:
            return {}
        return {}

    def save_snapshot(self, snapshot: RuntimeSnapshot) -> None:
        self._path.parent.mkdir(parents=True, exist_ok=True)
        payload = {
            "runtime_status": str(snapshot.status or "").strip() or "Offline",
            "last_heartbeat_at": str(snapshot.last_heartbeat_at or "").strip(),
            "last_error": str(snapshot.last_error or "").strip(),
            "failure_count": int(snapshot.failure_count or 0),
            "updated_at": datetime.now(tz=timezone.utc).isoformat(),
        }
        self._path.write_text(
            json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":")),
            encoding="utf-8",
        )


def _parse_iso(value: str) -> datetime | None:
    text = str(value or "").strip()
    if not text:
        return None
    try:
        parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
    except Exception:
        return None
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


def _heartbeat_age(last_heartbeat_at: str, now: datetime) -> int | None:
    dt = _parse_iso(last_heartbeat_at)
    if dt is None:
        return None
    age = int((now - dt).total_seconds())
    return max(0, age)


def _coerce_runtime_state(
    *,
    runtime_status: str,
    heartbeat_age_sec: int | None,
    paired: bool,
    last_error: str,
) -> str:
    status = str(runtime_status or "").strip().lower()
    error_text = str(last_error or "").strip().lower()

    if "revoked" in error_text:
        return "Offline"
    if not paired:
        return "Offline"
    if heartbeat_age_sec is None:
        if status in {"connected", "reconnecting", "error", "offline"}:
            return "Degraded" if status in {"connected", "reconnecting", "error"} else "Offline"
        return "Offline"

    if heartbeat_age_sec <= CONNECTED_FRESHNESS_SEC:
        return "Connected"
    if heartbeat_age_sec <= OFFLINE_FRESHNESS_SEC:
        return "Degraded"
    return "Offline"


def _trust_state(*, paired: bool, last_error: str, runtime_state: str) -> str:
    error_text = str(last_error or "").strip().lower()
    if "revoked" in error_text:
        return "Revoked"
    if not paired:
        return "Unpaired"
    if runtime_state in {"Connected", "Degraded"}:
        return "Trusted"
    return "Pending"


def _device_state(*, paired: bool, runtime_state: str) -> str:
    if not paired:
        return "Unknown"
    if runtime_state in {"Connected", "Degraded"}:
        return "Active"
    return "Offline"


def _map_backend_trust_state(value: str) -> str:
    state = str(value or "").strip().lower()
    if not state:
        return ""
    if state in {"trusted", "connected", "verified"}:
        return "Trusted"
    if state in {"pending", "verifying", "enrolled", "enrollment_pending", "review"}:
        return "Pending"
    if state in {"revoked", "blocked"}:
        return "Revoked"
    if state in {"unpaired", "not_paired"}:
        return "Unpaired"
    return ""


def normalize_trust_for_policy(trust_state: str) -> str:
    """Map canonical UI trust label to PolicyEngine trust_state (lowercase)."""
    lowered = str(trust_state or "").strip().lower()
    if lowered == "trusted":
        return "trusted"
    return lowered or "unknown"


def is_policy_trusted(trust_state: str) -> bool:
    return normalize_trust_for_policy(trust_state) == "trusted"


def is_official_channel_update_eligible(trust_state: str) -> bool:
    """Paired owner devices may use checksum-verified stable channel updates."""
    normalized = normalize_trust_for_policy(trust_state)
    return normalized in {"trusted", "pending", "connected", "verified"}


def _backend_implies_paired(value: str) -> bool:
    state = str(value or "").strip().lower()
    return state in {
        "trusted",
        "connected",
        "verified",
        "pending",
        "verifying",
        "enrolled",
        "enrollment_pending",
        "review",
        "revoked",
        "blocked",
    }


def resolve_canonical_status(
    *,
    runtime_snapshot: RuntimeSnapshot | None,
    runtime_evidence: dict[str, Any],
    legacy_fields: dict[str, Any],
    backend_fields: dict[str, Any] | None,
    paired: bool,
    backend_url: str,
    app_version: str,
    update_channel: str,
    logs_path: str,
    storage_path: str,
) -> CanonicalStatus:
    now = datetime.now(tz=timezone.utc)

    # Status Evidence Hierarchy
    # Priority 1: Runtime heartbeat evidence
    heartbeat_from_runtime = str((runtime_snapshot.last_heartbeat_at if runtime_snapshot else "") or "").strip()
    heartbeat_from_persisted = str(runtime_evidence.get("last_heartbeat_at") or "").strip()
    heartbeat_from_backend = str((backend_fields or {}).get("last_heartbeat_at") or "").strip()
    last_heartbeat_at = heartbeat_from_runtime or heartbeat_from_persisted or heartbeat_from_backend

    # Priority 2: Persisted runtime state (runtime snapshot of the running process overrides only if present)
    runtime_status = ""
    if runtime_snapshot is not None:
        has_runtime_signal = bool(
            str(runtime_snapshot.last_heartbeat_at or "").strip()
            or str(runtime_snapshot.last_error or "").strip()
            or int(runtime_snapshot.failure_count or 0) > 0
            or str(runtime_snapshot.status or "").strip().lower() in {"connected", "error", "reconnecting"}
        )
        if has_runtime_signal:
            runtime_status = str(runtime_snapshot.status or "").strip()
    if not runtime_status:
        runtime_status = str(runtime_evidence.get("runtime_status") or "").strip()

    # Priority 3: Persisted trust/pair state
    backend_trust_state = str((backend_fields or {}).get("trust_state") or "").strip()
    paired_state = bool(paired or _backend_implies_paired(backend_trust_state))

    # Priority 4: Legacy compatibility fields
    if not runtime_status:
        runtime_status = str(legacy_fields.get("status") or legacy_fields.get("runtime_state") or "").strip()
    if not runtime_status:
        runtime_status = str((backend_fields or {}).get("status") or "").strip()
    if not last_heartbeat_at:
        last_heartbeat_at = str(legacy_fields.get("last_heartbeat_at") or "").strip()

    failure_count = 0
    if runtime_snapshot is not None:
        failure_count = int(runtime_snapshot.failure_count or 0)
    elif runtime_evidence.get("failure_count") is not None:
        try:
            failure_count = int(runtime_evidence.get("failure_count") or 0)
        except Exception:
            failure_count = 0

    last_error = str((runtime_snapshot.last_error if runtime_snapshot else "") or "").strip()
    if not last_error:
        last_error = str(runtime_evidence.get("last_error") or "").strip()

    heartbeat_age_sec = _heartbeat_age(last_heartbeat_at, now)
    runtime_state = _coerce_runtime_state(
        runtime_status=runtime_status,
        heartbeat_age_sec=heartbeat_age_sec,
        paired=paired_state,
        last_error=last_error,
    )
    trust_state = _trust_state(paired=paired_state, last_error=last_error, runtime_state=runtime_state)
    mapped_backend_trust = _map_backend_trust_state(backend_trust_state)
    if mapped_backend_trust:
        trust_state = mapped_backend_trust
    device_state = _device_state(paired=paired_state, runtime_state=runtime_state)

    return CanonicalStatus(
        runtime_state=runtime_state,
        trust_state=trust_state,
        device_state=device_state,
        paired=paired_state,
        last_heartbeat_at=last_heartbeat_at,
        heartbeat_age_sec=heartbeat_age_sec,
        failure_count=failure_count,
        last_error=last_error,
        backend_url=backend_url,
        app_version=app_version,
        update_channel=update_channel,
        logs_path=logs_path,
        storage_path=storage_path,
        status_source_version="v1-runtime-evidence-hierarchy",
        computed_at=now.isoformat(),
    )
