"""Fail-closed contracts for Sprint 6F-C observation actions."""

from __future__ import annotations

from datetime import date
import re
from typing import Any

from dashboard_v5.association_engine import EVENTS
from dashboard_v5.metric_catalog_v2 import BY_ID_V2
from dashboard_v5.sprint6f_c_schema import METHOD_VERSION

CONTRACT_VERSION = 1
PUBLIC_CONTRACT_VERSION = "health.observation_plan.v1"
STATUS = {"draft", "active", "paused", "completed", "archived"}
TRANSITIONS = {
    "draft": {"draft", "active", "archived"},
    "active": {"active", "paused", "completed", "archived"},
    "paused": {"paused", "active", "completed", "archived"},
    "completed": {"completed", "archived"},
    "archived": {"archived"},
}
PHASE_TYPES = {"baseline", "observation", "change", "followup"}
ADHERENCE = {"yes", "partial", "no", "unknown"}
TRISTATE = {"yes", "no", "unknown"}
STRESS = {"low", "moderate", "high", "unknown"}
METRIC_IDS = frozenset(BY_ID_V2)
EVENT_IDS = frozenset(item.id for item in EVENTS)
OBSERVATION_INFLUENCES = frozenset(
    {"event.sauna", "event.training", "nutrition.profile", "nutrition.histamine"}
)
ALLOWED_IDS = METRIC_IDS | EVENT_IDS | OBSERVATION_INFLUENCES
OPAQUE = {
    "observation": re.compile(r"obs_[0-9a-f]{24}"),
    "phase": re.compile(r"phase_[0-9a-f]{24}"),
}

TEMPLATES = (
    (
        "sauna_following_night",
        "Sauna und folgende Nacht",
        ("event.sauna",),
        ("apple.sleep", "apple.hrv"),
    ),
    (
        "training_recovery",
        "Training und Erholung",
        ("event.training",),
        ("apple.hrv", "apple.resting_heart_rate"),
    ),
    (
        "nutrition_symptoms",
        "Ernährung und Beschwerden",
        ("nutrition.histamine",),
        ("symptom.total", "apple.sleep"),
    ),
)


def templates() -> list[dict[str, Any]]:
    return [
        {
            "id": key,
            "title": title,
            "question": "Vorhandene dokumentierte Daten zeitlich gemeinsam betrachten.",
            "influences": list(influences),
            "outcomes": list(outcomes),
            "lag_min": 0,
            "lag_max": 1,
            "medical_statement": "Keine Diagnose-, Kausalitäts- oder Therapieaussage.",
        }
        for key, title, influences, outcomes in TEMPLATES
    ]


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:
        raise ValueError("invalid_text")
    if re.search(
        r"[\x00-\x1f\x7f]|https?://|(?:^|\s)[~/]|\\\\|\.hermes|[A-Za-z]:\\",
        cleaned,
        re.I,
    ):
        raise ValueError("invalid_text")
    return cleaned


def iso_day(value: Any) -> str:
    if not isinstance(value, str):
        raise ValueError("invalid_date")
    try:
        parsed = date.fromisoformat(value)
    except ValueError as exc:
        raise ValueError("invalid_date") from exc
    if not 2000 <= parsed.year <= 2100:
        raise ValueError("invalid_date")
    return parsed.isoformat()


def date_range(start: Any, end: Any, *, maximum_days: int = 1095) -> tuple[str, str]:
    first, last = iso_day(start), iso_day(end)
    span = (date.fromisoformat(last) - date.fromisoformat(first)).days + 1
    if span < 1 or span > maximum_days:
        raise ValueError("invalid_date_range")
    return first, last


def integer(value: Any, low: int, high: int) -> int:
    if type(value) is not int or not low <= value <= high:
        raise ValueError("invalid_integer")
    return value


def opaque(value: Any, kind: str, *, optional: bool = False) -> str:
    if optional and value == "":
        return ""
    if not isinstance(value, str) or OPAQUE[kind].fullmatch(value) is None:
        raise ValueError("invalid_opaque_id")
    return value


def ids(
    value: Any, allowed: frozenset[str], maximum: int, *, required: bool = False
) -> list[str]:
    if (
        not isinstance(value, list)
        or len(value) > maximum
        or any(not isinstance(item, str) for item in value)
    ):
        raise ValueError("invalid_identifier_list")
    result = list(dict.fromkeys(value))
    if (
        len(result) != len(value)
        or (required and not result)
        or any(item not in allowed for item in result)
    ):
        raise ValueError("identifier_not_allowed")
    return result


def lag_pair(low: Any, high: Any) -> tuple[int, int]:
    first, last = integer(low, 0, 14), integer(high, 0, 14)
    if last < first:
        raise ValueError("invalid_lag")
    return first, last


def validate_action(payload: Any) -> dict[str, Any]:
    if not isinstance(payload, dict) or payload.get("version") != CONTRACT_VERSION:
        raise ValueError("invalid_observation_action")
    action = payload.get("action")
    if action == "observation_upsert":
        required = {
            "version",
            "action",
            "observation_id",
            "title",
            "question",
            "influences",
            "outcomes",
            "lag_min",
            "lag_max",
            "start_date",
            "end_date",
            "status",
            "note",
            "include_doctor",
            "method_version",
        }
        if (
            set(payload) != required
            or payload["method_version"] != METHOD_VERSION
            or payload["status"] not in STATUS
            or type(payload["include_doctor"]) is not bool
        ):
            raise ValueError("invalid_observation")
        start, end = date_range(payload["start_date"], payload["end_date"])
        low, high = lag_pair(payload["lag_min"], payload["lag_max"])
        influences = ids(
            payload["influences"], OBSERVATION_INFLUENCES | EVENT_IDS | METRIC_IDS, 1, required=True
        )
        outcomes = ids(payload["outcomes"], METRIC_IDS | EVENT_IDS, 2, required=True)
        if len(set(influences + outcomes)) > 3:
            raise ValueError("too_many_metrics")
        return {
            "version": 1,
            "action": action,
            "observation_id": opaque(
                payload["observation_id"], "observation", optional=True
            ),
            "title": text(payload["title"], 120, required=True),
            "question": text(payload["question"], 800),
            "influences": influences,
            "outcomes": outcomes,
            "lag_min": low,
            "lag_max": high,
            "start_date": start,
            "end_date": end,
            "status": payload["status"],
            "note": text(payload["note"], 1000),
            "include_doctor": payload["include_doctor"],
            "method_version": METHOD_VERSION,
        }
    if action == "observation_phase_upsert":
        required = {
            "version",
            "action",
            "observation_id",
            "phase_id",
            "phase_type",
            "name",
            "start_date",
            "end_date",
            "behavior_goal",
            "metrics",
            "events",
            "lag_min",
            "lag_max",
            "adherence",
            "notes",
        }
        if (
            set(payload) != required
            or payload["phase_type"] not in PHASE_TYPES
            or payload["adherence"] not in ADHERENCE
        ):
            raise ValueError("invalid_phase")
        start, end = date_range(payload["start_date"], payload["end_date"])
        low, high = lag_pair(payload["lag_min"], payload["lag_max"])
        return {
            "version": 1,
            "action": action,
            "observation_id": opaque(payload["observation_id"], "observation"),
            "phase_id": opaque(payload["phase_id"], "phase", optional=True),
            "phase_type": payload["phase_type"],
            "name": text(payload["name"], 100, required=True),
            "start_date": start,
            "end_date": end,
            "behavior_goal": text(payload["behavior_goal"], 500),
            "metrics": ids(payload["metrics"], METRIC_IDS, 8),
            "events": ids(payload["events"], EVENT_IDS, 8),
            "lag_min": low,
            "lag_max": high,
            "adherence": payload["adherence"],
            "notes": text(payload["notes"], 800),
        }
    if action == "observation_checkin":
        required = {
            "version",
            "action",
            "observation_id",
            "phase_id",
            "day",
            "adherence",
            "stress",
            "sleep_disruption",
            "infection",
            "unusual_activity",
            "travel",
            "medication_change_fact",
            "supplement_change_fact",
            "note",
        }
        if (
            set(payload) != required
            or payload["adherence"] not in ADHERENCE
            or payload["stress"] not in STRESS
            or any(
                payload[key] not in TRISTATE
                for key in (
                    "sleep_disruption",
                    "infection",
                    "unusual_activity",
                    "travel",
                )
            )
        ):
            raise ValueError("invalid_checkin")
        return {
            "version": 1,
            "action": action,
            "observation_id": opaque(payload["observation_id"], "observation"),
            "phase_id": opaque(payload["phase_id"], "phase", optional=True),
            "day": iso_day(payload["day"]),
            "adherence": payload["adherence"],
            "stress": payload["stress"],
            "sleep_disruption": payload["sleep_disruption"],
            "infection": payload["infection"],
            "unusual_activity": payload["unusual_activity"],
            "travel": payload["travel"],
            "medication_change_fact": text(payload["medication_change_fact"], 300),
            "supplement_change_fact": text(payload["supplement_change_fact"], 300),
            "note": text(payload["note"], 500),
        }
    if action == "observation_status":
        if (
            set(payload) != {"version", "action", "observation_id", "status"}
            or payload["status"] not in STATUS
        ):
            raise ValueError("invalid_status")
        return {
            "version": 1,
            "action": action,
            "observation_id": opaque(payload["observation_id"], "observation"),
            "status": payload["status"],
        }
    if action == "observation_result_snapshot":
        if set(payload) != {"version", "action", "observation_id"}:
            raise ValueError("invalid_snapshot")
        return {
            "version": 1,
            "action": action,
            "observation_id": opaque(payload["observation_id"], "observation"),
        }
    raise ValueError("unsupported_observation_action")
