"""Sprint 6H-B capture contracts: strict, non-diagnostic and versioned."""

from __future__ import annotations

import math
import re
from datetime import datetime
from typing import Any
from zoneinfo import ZoneInfo

from dashboard_v5.medication_contract import ACTION_CONTRACT_VERSIONS, validate_action_data

CONTRACT_VERSION = 1
TIMEZONE = ZoneInfo("Europe/Zurich")
CAPTURE_TYPES = frozenset(
    {
        "symptom",
        "medication",
        "supplement",
        "event",
        "measurement",
        "photo",
    }
)
SYMPTOM_CATEGORIES = frozenset(
    {
        "aphthae",
        "headache",
        "eyes",
        "skin",
        "joints",
        "gi",
        "fatigue",
        "swelling",
        "pain",
        "vascular_warning",
        "other",
    }
)
EVENT_CATEGORIES = frozenset(
    {
        "stress",
        "infection",
        "travel",
        "unusual_activity",
        "sport",
        "heat",
        "sleep_disruption",
        "alcohol",
        "nutrition_deviation",
        "appointment",
        "vaccination",
        "other",
    }
)
STATUSES = frozenset({"administered", "missed", "corrected"})
UNITS = {
    "blood_pressure": "mmHg",
    "pulse": "bpm",
    "weight": "kg",
    "temperature": "°C",
    "oxygen_saturation": "%",
    "blood_glucose": "mmol/L",
}
RANGES = {
    "systolic": (40.0, 300.0),
    "diastolic": (25.0, 200.0),
    "pulse": (20.0, 250.0),
    "weight": (20.0, 400.0),
    "temperature": (30.0, 45.0),
    "oxygen_saturation": (50.0, 100.0),
    "blood_glucose": (1.0, 40.0),
}
TOKEN_RE = re.compile(r"att_[a-f0-9]{32}")
OPAQUE_RE = re.compile(r"cap_[a-f0-9]{24}")


def _text(value: Any, maximum: int, *, required: bool = False) -> str:
    if not isinstance(value, str):
        raise ValueError("invalid text")
    cleaned = " ".join(value.split())
    if (
        (required and not cleaned)
        or len(cleaned) > maximum
        or re.search(r"[\x00-\x1f\x7f]|https?://|/|\\|\.hermes", cleaned, re.I)
    ):
        raise ValueError("invalid text")
    return cleaned


def _when(value: Any, *, optional: bool = False) -> str | None:
    if optional and value in {None, ""}:
        return None
    if not isinstance(value, str):
        raise ValueError("invalid datetime")
    try:
        parsed = datetime.fromisoformat(value)
    except ValueError as exc:
        raise ValueError("invalid datetime") from exc
    if (
        parsed.tzinfo is not None
        or parsed.year < 2000
        or parsed.replace(tzinfo=TIMEZONE) > datetime.now(TIMEZONE)
    ):
        raise ValueError("invalid datetime")
    return parsed.isoformat(timespec="minutes")


def _number(value: Any, field: str) -> float:
    if isinstance(value, bool):
        raise ValueError("invalid number")
    try:
        result = float(value)
    except (TypeError, ValueError) as exc:
        raise ValueError("invalid number") from exc
    low, high = RANGES[field]
    if not math.isfinite(result) or not low <= result <= high:
        raise ValueError("value outside technical range")
    return result


def _bounded_number(
    value: Any,
    low: float,
    high: float,
    *,
    optional: bool = False,
    integer: bool = False,
) -> float | int | None:
    if optional and value in {None, ""}:
        return None
    if isinstance(value, bool):
        raise ValueError("invalid number")
    try:
        result = float(value)
    except (TypeError, ValueError) as exc:
        raise ValueError("invalid number") from exc
    if not math.isfinite(result) or not low <= result <= high:
        raise ValueError("value outside technical range")
    if integer:
        if not result.is_integer():
            raise ValueError("invalid integer")
        return int(result)
    return result


def validate_capture_payload(payload: Any) -> dict[str, Any]:
    if (
        not isinstance(payload, dict)
        or payload.get("version") != CONTRACT_VERSION
        or payload.get("action") != "capture_entry"
    ):
        raise ValueError("invalid capture payload")
    required = {
        "version",
        "action",
        "capture_type",
        "request_version",
        "idempotency_key",
        "occurred_at",
        "ended_at",
        "data",
        "attachments",
        "corrects_entry_id",
        "withdraws_entry_id",
    }
    if set(payload) != required:
        raise ValueError("invalid capture shape")
    capture_type = payload["capture_type"]
    if capture_type not in CAPTURE_TYPES:
        raise ValueError("invalid capture type")
    request_version = payload["request_version"]
    if type(request_version) is not int or not 1 <= request_version <= 1_000_000:
        raise ValueError("invalid request version")
    key = payload["idempotency_key"]
    if not isinstance(key, str) or not re.fullmatch(r"[a-f0-9]{32}", key):
        raise ValueError("invalid idempotency key")
    attachments = payload["attachments"]
    if (
        not isinstance(attachments, list)
        or len(attachments) > 5
        or any(
            not isinstance(token, str) or not TOKEN_RE.fullmatch(token)
            for token in attachments
        )
        or len(set(attachments)) != len(attachments)
    ):
        raise ValueError("invalid attachments")
    corrects = payload["corrects_entry_id"]
    withdraws = payload["withdraws_entry_id"]
    if corrects and (
        not isinstance(corrects, str) or not OPAQUE_RE.fullmatch(corrects)
    ):
        raise ValueError("invalid correction target")
    if withdraws and (
        not isinstance(withdraws, str) or not OPAQUE_RE.fullmatch(withdraws)
    ):
        raise ValueError("invalid withdrawal target")
    if corrects and withdraws:
        raise ValueError("ambiguous version action")
    data = payload["data"]
    if not isinstance(data, dict):
        raise ValueError("invalid capture data")
    occurred = _when(payload["occurred_at"])
    ended = _when(payload["ended_at"], optional=True)
    if ended and ended < occurred:
        raise ValueError("end before start")
    common = {"note", "title"}
    if capture_type == "symptom":
        expected = common | {"category", "intensity", "count", "body_region", "ongoing"}
        if (
            set(data) != expected
            or data["category"] not in SYMPTOM_CATEGORIES
            or data["ongoing"] not in {"yes", "no", "unknown"}
        ):
            raise ValueError("invalid symptom")
        intensity = data["intensity"]
        if intensity != "unknown" and (
            type(intensity) is not int or intensity not in range(4)
        ):
            raise ValueError("invalid intensity")
        count = data["count"]
        if count is not None and (type(count) is not int or not 0 <= count <= 999):
            raise ValueError("invalid count")
        normalized = {
            "category": data["category"],
            "intensity": intensity,
            "count": count,
            "body_region": _text(data["body_region"], 80),
            "ongoing": data["ongoing"],
        }
    elif capture_type == "medication" and data.get("contract") in ACTION_CONTRACT_VERSIONS:
        normalized = validate_action_data(data)
        if data.get("contract") == "health.medication_action.v2":
            if normalized.get("mode") == "correction" and not corrects:
                raise ValueError("structured correction requires capture target")
            if normalized.get("mode") != "correction" and corrects:
                raise ValueError("capture correction target on non-correction")
    elif capture_type in {"medication", "supplement"}:
        expected = common | {
            "status",
            "plan_id",
            "name",
            "amount",
            "unit",
            "route",
            "plan_value_confirmed",
            "deviation_confirmed",
        }
        if set(data) != expected or data["status"] not in STATUSES:
            raise ValueError("invalid administration")
        amount = data["amount"]
        if amount not in {None, ""}:
            try:
                amount = float(amount)
            except (TypeError, ValueError) as exc:
                raise ValueError("invalid amount") from exc
            if not math.isfinite(amount) or amount < 0 or amount > 1_000_000:
                raise ValueError("invalid amount")
        if (
            type(data["plan_value_confirmed"]) is not bool
            or type(data["deviation_confirmed"]) is not bool
        ):
            raise ValueError("invalid confirmation")
        normalized = {
            "status": data["status"],
            "plan_id": _text(data["plan_id"], 80),
            "name": _text(data["name"], 120, required=True),
            "amount": amount,
            "unit": _text(data["unit"], 30),
            "route": _text(data["route"], 60),
            "plan_value_confirmed": data["plan_value_confirmed"],
            "deviation_confirmed": data["deviation_confirmed"],
        }
    elif capture_type == "event":
        event_kind = data.get("event_kind", "generic")
        if event_kind == "sauna":
            expected = common | {
                "event_kind",
                "duration_minutes",
                "rounds",
                "temperature_c",
                "cooling",
                "hydration_ml",
            }
            if set(data) != expected:
                raise ValueError("invalid sauna")
            normalized = {
                "event_kind": "sauna",
                "duration_minutes": _bounded_number(
                    data["duration_minutes"], 1, 720, integer=True
                ),
                "rounds": _bounded_number(
                    data["rounds"], 1, 20, optional=True, integer=True
                ),
                "temperature_c": _bounded_number(
                    data["temperature_c"], 30, 130, optional=True
                ),
                "cooling": _text(data["cooling"], 80),
                "hydration_ml": _bounded_number(
                    data["hydration_ml"], 0, 10_000, optional=True, integer=True
                ),
            }
        elif event_kind == "training":
            expected = common | {
                "event_kind",
                "activity_type",
                "duration_minutes",
                "active_kcal",
                "distance_km",
            }
            if set(data) != expected:
                raise ValueError("invalid training")
            normalized = {
                "event_kind": "training",
                "activity_type": _text(data["activity_type"], 80, required=True),
                "duration_minutes": _bounded_number(
                    data["duration_minutes"], 1, 1_440, integer=True
                ),
                "active_kcal": _bounded_number(
                    data["active_kcal"], 0, 10_000, optional=True
                ),
                "distance_km": _bounded_number(
                    data["distance_km"], 0, 1_000, optional=True
                ),
            }
        else:
            expected = common | {"category", "intensity"}
            if set(data) != expected or data["category"] not in EVENT_CATEGORIES:
                raise ValueError("invalid event")
            intensity = data["intensity"]
            if intensity != "unknown" and (
                type(intensity) is not int or intensity not in range(4)
            ):
                raise ValueError("invalid intensity")
            normalized = {
                "category": data["category"],
                "intensity": intensity,
            }
    elif capture_type == "measurement":
        expected = common | {
            "measurement_type",
            "values",
            "unit",
            "context",
            "device",
            "plausibility_confirmed",
            "duplicate_confirmed",
        }
        if (
            set(data) != expected
            or data["measurement_type"] not in UNITS
            or data["unit"] != UNITS[data["measurement_type"]]
            or type(data["plausibility_confirmed"]) is not bool
            or type(data["duplicate_confirmed"]) is not bool
        ):
            raise ValueError("invalid measurement")
        values = data["values"]
        if not isinstance(values, dict):
            raise ValueError("invalid measurement values")
        expected_values = (
            {"systolic", "diastolic"}
            if data["measurement_type"] == "blood_pressure"
            else {data["measurement_type"]}
        )
        if set(values) != expected_values:
            raise ValueError("invalid measurement panel")
        if data["measurement_type"] == "blood_glucose" and not _text(data["context"], 80):
            raise ValueError("blood glucose requires documented measurement method")
        normalized = {
            "measurement_type": data["measurement_type"],
            "values": {key: _number(value, key) for key, value in values.items()},
            "unit": data["unit"],
            "context": _text(data["context"], 80),
            "device": _text(data["device"], 80),
            "plausibility_confirmed": data["plausibility_confirmed"],
            "duplicate_confirmed": data["duplicate_confirmed"],
        }
    else:
        expected = common | {"body_region", "description"}
        if set(data) != expected or not attachments:
            raise ValueError("photo requires attachment")
        normalized = {
            "body_region": _text(data["body_region"], 80),
            "description": _text(data["description"], 160),
        }
    if not (capture_type == "medication" and normalized.get("contract") in ACTION_CONTRACT_VERSIONS):
        normalized["title"] = _text(data["title"], 100, required=capture_type == "event")
        normalized["note"] = _text(data["note"], 500)
    if capture_type == "event" and normalized.get("event_kind") in {"sauna", "training"}:
        if ended is None or occurred is None:
            raise ValueError("duration requires end")
        start_value = datetime.fromisoformat(occurred)
        end_value = datetime.fromisoformat(ended)
        if int((end_value - start_value).total_seconds() // 60) != normalized["duration_minutes"]:
            raise ValueError("duration does not match end")
    return {
        "version": CONTRACT_VERSION,
        "action": "capture_entry",
        "capture_type": capture_type,
        "request_version": request_version,
        "idempotency_key": key,
        "occurred_at": occurred,
        "ended_at": ended,
        "data": normalized,
        "attachments": attachments,
        "corrects_entry_id": corrects or None,
        "withdraws_entry_id": withdraws or None,
    }
