"""Bounded, read-only temporal association analysis for Dashboard v5."""

from __future__ import annotations

import math
import sqlite3
from dataclasses import asdict, dataclass
from datetime import date, timedelta
from statistics import median
from typing import Any, Callable

METHOD_VERSION = "health_temporal_association_v1"
MAX_ANALYSIS_DAYS = 730
MAX_METRICS = 8
MAX_RESULT_ROWS = 4000
LAGS = tuple(range(15))
HEATMAP_LAGS = tuple(range(8))


@dataclass(frozen=True)
class AssociationMetric:
    id: str
    label: str
    kind: str
    role: str
    source: str
    unit: str = ""


CONTINUOUS_INFLUENCES = (
    "nutrition.histamine",
    "nutrition.sighi_max",
    "nutrition.mapping_coverage",
    "nutrition.energy",
    "nutrition.protein",
    "nutrition.carb",
    "nutrition.fat",
    "nutrition.sugar",
    "nutrition.fiber",
    "nutrition.saturated_fat",
    "apple.sleep",
    "apple.active_energy",
    "apple.steps",
)
CONTINUOUS_TARGETS = (
    "symptom.total",
    "apple.hrv",
    "apple.resting_heart_rate",
    "apple.sleep",
    "apple.blood_pressure.systolic",
    "apple.blood_pressure.diastolic",
    "apple.steps",
    "apple.active_energy",
    "lab.crp",
    "lab.d_dimer",
    "lab.fibrinogen",
    "lab.factor_viii",
    "lab.platelets",
    "lab.leukocytes",
    "lab.ferritin",
)
EVENTS = (
    AssociationMetric(
        "event.aphthae", "Aphthen/Mundulzera", "event", "target", "symptom"
    ),
    AssociationMetric("event.headache", "Kopfschmerzen", "event", "target", "symptom"),
    AssociationMetric("event.gi", "GI-Beschwerden", "event", "target", "symptom"),
    AssociationMetric("event.fatigue", "Fatigue", "event", "target", "symptom"),
    AssociationMetric("event.skin", "Haut", "event", "target", "symptom"),
    AssociationMetric("event.eyes", "Augen", "event", "target", "symptom"),
    AssociationMetric("event.joints", "Gelenke", "event", "target", "symptom"),
    AssociationMetric(
        "event.vascular", "Vaskuläre Beschwerden", "event", "target", "symptom"
    ),
    AssociationMetric(
        "event.medication", "Medikamentengabe", "event", "influence", "medication"
    ),
    AssociationMetric(
        "event.supplement", "Dokumentierte Supplementeinnahme", "event", "influence", "supplement"
    ),
    AssociationMetric(
        "event.stress", "Stressereignis", "event", "influence", "health_event"
    ),
    AssociationMetric(
        "event.sleep_disruption", "Schlafstörung", "event", "influence", "health_event"
    ),
    AssociationMetric(
        "event.infection", "Infekt", "event", "influence", "health_event"
    ),
    AssociationMetric(
        "event.heat", "Hitze/Sauna", "event", "influence", "health_event"
    ),
    AssociationMetric(
        "event.physical_load",
        "Besondere Belastung",
        "event",
        "influence",
        "health_event",
    ),
)
EVENT_BY_ID = {item.id: item for item in EVENTS}
SYMPTOM_NAMES = {
    "event.aphthae": {"aphthen/mundulzera", "aphte/mundulzera", "aphthae"},
    "event.headache": {"kopfschmerzen", "headache"},
    "event.gi": {"gi/darm", "gi-beschwerden", "durchfall/gi-beschwerden", "gi"},
    "event.fatigue": {"müdigkeit/fatigue", "fatigue"},
    "event.skin": {"haut", "hautveränderung"},
    "event.eyes": {"augen", "augenbeschwerden"},
    "event.joints": {"gelenke", "gelenkbeschwerden"},
    "event.vascular": {
        "vaskulär/thrombose-warnzeichen",
        "vaskuläre beschwerden",
        "vascular",
    },
}
HEALTH_EVENT_CATEGORIES = {
    "event.stress": {"stress"},
    "event.sleep_disruption": {"sleep_disruption", "schlafstörung"},
    "event.infection": {"infection", "infekt"},
    "event.heat": {"heat", "sauna", "hitze"},
    "event.physical_load": {"physical_load", "besondere belastung"},
}


def _table_exists(connection: sqlite3.Connection, name: str) -> bool:
    return (
        connection.execute(
            "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (name,)
        ).fetchone()
        is not None
    )


def _day(raw: Any) -> str | None:
    text = str(raw or "")[:10]
    try:
        return date.fromisoformat(text).isoformat()
    except ValueError:
        return None


def _score(raw: Any) -> float | None:
    try:
        value = float(str(raw).replace(",", "."))
    except (TypeError, ValueError):
        return None
    return value if math.isfinite(value) else None


def event_days(
    connection: sqlite3.Connection, metric_id: str, start: date, end: date
) -> list[str]:
    if metric_id not in EVENT_BY_ID:
        raise ValueError("event_not_allowed")
    found: set[str] = set()
    if metric_id == "event.medication" and _table_exists(
        connection, "medication_administrations"
    ):
        rows = list(
            connection.execute(
                """SELECT event.datum FROM medication_administrations AS event
                   WHERE lower(trim(COALESCE(
                         CASE WHEN lower(trim(COALESCE(event.event_type,'')))='corrected'
                              THEN event.corrected_target_status ELSE event.event_type END,''
                       )))='administered'
                     AND event.datum>=? AND event.datum<=?
                     AND NOT EXISTS (
                       SELECT 1 FROM medication_administrations AS correction
                        WHERE correction.corrects_event_id=event.id
                     )
                   ORDER BY event.datum LIMIT 1001""",
                (start.isoformat(), end.isoformat()),
            )
        )
        if len(rows) > 1000:
            raise ValueError("event_row_limit")
        for row in rows:
            if observed := _day(row[0]):
                found.add(observed)
    elif metric_id == "event.supplement" and _table_exists(
        connection, "supplement_intakes"
    ):
        rows = list(
            connection.execute(
                "SELECT occurred_at FROM supplement_intakes WHERE status='administered' AND substr(occurred_at,1,10)>=? AND substr(occurred_at,1,10)<=? ORDER BY occurred_at LIMIT 1001",
                (start.isoformat(), end.isoformat()),
            )
        )
        if len(rows) > 1000:
            raise ValueError("event_row_limit")
        for row in rows:
            if observed := _day(row[0]):
                found.add(observed)
    elif metric_id in SYMPTOM_NAMES and _table_exists(connection, "symptom_log"):
        names = SYMPTOM_NAMES[metric_id]
        rows = list(
            connection.execute(
                "SELECT datum,symptom,schwergrad FROM symptom_log WHERE datum>=? AND datum<=? ORDER BY datum,id LIMIT 10001",
                (start.isoformat(), end.isoformat()),
            )
        )
        if len(rows) > 10000:
            raise ValueError("event_row_limit")
        for raw_day, raw_name, raw_severity in rows:
            if (
                str(raw_name or "").strip().casefold() in names
                and (_score(raw_severity) or 0) > 0
            ):
                if observed := _day(raw_day):
                    found.add(observed)
        if metric_id == "event.aphthae" and _table_exists(
            connection, "health_event_periods"
        ):
            period_rows = list(
                connection.execute(
                    "SELECT start_date,event_type FROM health_event_periods WHERE start_date>=? AND start_date<=? ORDER BY start_date LIMIT 1001",
                    (start.isoformat(), end.isoformat()),
                )
            )
            if len(period_rows) > 1000:
                raise ValueError("event_row_limit")
            for row in period_rows:
                if "apht" in str(row[1] or "").casefold():
                    if observed := _day(row[0]):
                        found.add(observed)
    elif metric_id in HEALTH_EVENT_CATEGORIES and _table_exists(
        connection, "health_events"
    ):
        allowed = HEALTH_EVENT_CATEGORIES[metric_id]
        rows = list(
            connection.execute(
                "SELECT date,category,parameter FROM health_events WHERE date>=? AND date<=? ORDER BY date,id LIMIT 10001",
                (start.isoformat(), end.isoformat()),
            )
        )
        if len(rows) > 10000:
            raise ValueError("event_row_limit")
        for row in rows:
            values = {
                str(row[1] or "").strip().casefold(),
                str(row[2] or "").strip().casefold(),
            }
            if values & allowed and (observed := _day(row[0])):
                found.add(observed)
    if len(found) > 1000:
        raise ValueError("event_row_limit")
    return sorted(found)


def ranks(values: list[float]) -> list[float]:
    ordered = sorted(enumerate(values), key=lambda item: (item[1], item[0]))
    result = [0.0] * len(values)
    index = 0
    while index < len(ordered):
        end = index + 1
        while end < len(ordered) and ordered[end][1] == ordered[index][1]:
            end += 1
        rank = (index + 1 + end) / 2
        for position in range(index, end):
            result[ordered[position][0]] = rank
        index = end
    return result


def spearman(xs: list[float], ys: list[float]) -> float | None:
    if len(xs) != len(ys) or len(xs) < 2 or len(set(xs)) < 2 or len(set(ys)) < 2:
        return None
    rx, ry = ranks(xs), ranks(ys)
    mx, my = sum(rx) / len(rx), sum(ry) / len(ry)
    numerator = sum((x - mx) * (y - my) for x, y in zip(rx, ry, strict=True))
    denominator = math.sqrt(
        sum((x - mx) ** 2 for x in rx) * sum((y - my) ** 2 for y in ry)
    )
    return round(numerator / denominator, 4) if denominator else None


def calculation_status(n: int, coefficient: float | None) -> str:
    if n < 5 or coefficient is None:
        return "not_calculable"
    if n < 10:
        return "descriptive_only"
    if n < 20:
        return "preliminary"
    return "exploratory"


def pair_series(
    influence: dict[str, float], target: dict[str, float], lag: int
) -> list[dict[str, Any]]:
    pairs = []
    for raw_day, x in sorted(influence.items()):
        target_day = (date.fromisoformat(raw_day) + timedelta(days=lag)).isoformat()
        if target_day in target:
            pairs.append(
                {
                    "date": target_day,
                    "influence_date": raw_day,
                    "x": x,
                    "y": target[target_day],
                }
            )
    return pairs


def _series_map(payload: dict[str, Any]) -> dict[str, float]:
    result = {}
    for point in payload.get("points", []):
        value = _score(point.get("value"))
        if value is not None and (observed := _day(point.get("date"))):
            result[observed] = value
    return result


def metric_public(metric_id: str, metric_lookup: dict[str, Any]) -> dict[str, Any]:
    if metric_id in EVENT_BY_ID:
        return asdict(EVENT_BY_ID[metric_id])
    metric = metric_lookup[metric_id]
    return {
        "id": metric.id,
        "label": metric.label,
        "kind": "continuous",
        "role": "both",
        "source": metric.source,
        "unit": metric.unit,
    }


def catalog(metric_lookup: dict[str, Any]) -> dict[str, Any]:
    influences = [
        metric_public(item, metric_lookup)
        for item in CONTINUOUS_INFLUENCES
        if item in metric_lookup
    ]
    influences += [asdict(item) for item in EVENTS if item.role == "influence"]
    targets = [
        metric_public(item, metric_lookup)
        for item in CONTINUOUS_TARGETS
        if item in metric_lookup
    ]
    targets += [asdict(item) for item in EVENTS if item.role == "target"]
    return {
        "method_version": METHOD_VERSION,
        "timezone": "Europe/Zurich",
        "questions": [
            {"id": "continuous", "label": "Zwei kontinuierliche Werte"},
            {"id": "before_event", "label": "Was ging einem Ereignis voraus?"},
            {"id": "after_event", "label": "Was geschah nach einem Ereignis?"},
        ],
        "influences": influences,
        "targets": targets,
        "lags": list(LAGS),
        "presets": [
            {
                "id": "histamine_aphthae",
                "label": "Histaminindex → Aphthen",
                "question": "before_event",
                "influence": "nutrition.histamine",
                "target": "event.aphthae",
                "lag": 2,
            },
            {
                "id": "sleep_hrv",
                "label": "Schlafdauer → HRV",
                "question": "continuous",
                "influence": "apple.sleep",
                "target": "apple.hrv",
                "lag": 0,
            },
            {
                "id": "sleep_rhr",
                "label": "Schlafdauer → Ruhepuls",
                "question": "continuous",
                "influence": "apple.sleep",
                "target": "apple.resting_heart_rate",
                "lag": 0,
            },
            {
                "id": "stress_headache",
                "label": "Stress → Kopfschmerzen",
                "question": "before_event",
                "influence": "event.stress",
                "target": "event.headache",
                "lag": 1,
            },
            {
                "id": "stress_symptoms",
                "label": "Stress → Symptom-Gesamtscore",
                "question": "after_event",
                "influence": "event.stress",
                "target": "symptom.total",
                "lag": 1,
            },
            {
                "id": "medication_symptoms",
                "label": "Medikamentengabe → Symptome",
                "question": "after_event",
                "influence": "event.medication",
                "target": "symptom.total",
                "lag": 1,
            },
            {
                "id": "medication_hrv",
                "label": "Medikamentengabe → HRV",
                "question": "after_event",
                "influence": "event.medication",
                "target": "apple.hrv",
                "lag": 1,
            },
            {
                "id": "medication_labs",
                "label": "Medikamentengabe → Laborwerte",
                "question": "after_event",
                "influence": "event.medication",
                "target": "lab.crp",
                "lag": 1,
            },
        ],
    }


def analyze(
    connection: sqlite3.Connection,
    params: dict[str, str],
    metric_lookup: dict[str, Any],
    series_loader: Callable[[str, date, date], dict[str, Any]],
) -> dict[str, Any]:
    question = params["question"]
    influence_id, target_id = params["influence"], params["target"]
    start, end = date.fromisoformat(params["from"]), date.fromisoformat(params["to"])
    lag = int(params.get("lag", "0"))
    if question not in {"continuous", "before_event", "after_event"}:
        raise ValueError("question_not_allowed")
    if not 0 <= lag <= 14:
        raise ValueError("lag_out_of_range")
    if (end - start).days + 1 > MAX_ANALYSIS_DAYS:
        raise ValueError("range_too_large")
    if influence_id not in set(CONTINUOUS_INFLUENCES) | set(EVENT_BY_ID):
        raise ValueError("influence_not_allowed")
    if target_id not in set(CONTINUOUS_TARGETS) | set(EVENT_BY_ID):
        raise ValueError("target_not_allowed")
    influence_kind = "event" if influence_id in EVENT_BY_ID else "continuous"
    target_kind = "event" if target_id in EVENT_BY_ID else "continuous"
    expected = (end - start).days + 1
    if question == "continuous" and (influence_kind, target_kind) != (
        "continuous",
        "continuous",
    ):
        raise ValueError("incompatible_metrics")
    if question == "before_event" and target_kind != "event":
        raise ValueError("incompatible_metrics")
    if question == "after_event" and influence_kind != "event":
        raise ValueError("incompatible_metrics")
    influence_series = (
        _series_map(series_loader(influence_id, start, end))
        if influence_kind == "continuous"
        else {}
    )
    target_series = (
        _series_map(series_loader(target_id, start, end))
        if target_kind == "continuous"
        else {}
    )
    influence_events = (
        event_days(connection, influence_id, start, end)
        if influence_kind == "event"
        else []
    )
    target_events = (
        event_days(connection, target_id, start, end) if target_kind == "event" else []
    )
    pairs: list[dict[str, Any]] = []
    coefficient = None
    event_summary = None
    event_timelines: list[dict[str, Any]] = []
    if question == "continuous":
        pairs = pair_series(influence_series, target_series, lag)
        coefficient = spearman([p["x"] for p in pairs], [p["y"] for p in pairs])
    else:
        anchors = target_events if question == "before_event" else influence_events
        values: list[float] = []
        for anchor_text in anchors:
            anchor = date.fromisoformat(anchor_text)
            observed_day = (
                anchor - timedelta(days=lag)
                if question == "before_event"
                else anchor + timedelta(days=lag)
            )
            observed_text = observed_day.isoformat()
            if question == "before_event":
                value = (
                    (1.0 if observed_text in influence_events else None)
                    if influence_kind == "event"
                    else influence_series.get(observed_text)
                )
            else:
                value = (
                    (1.0 if observed_text in target_events else None)
                    if target_kind == "event"
                    else target_series.get(observed_text)
                )
            if value is not None:
                values.append(value)
            window = []
            for offset in range(-7, 4):
                day_text = (anchor + timedelta(days=offset)).isoformat()
                if question == "before_event":
                    item_value = (
                        (1.0 if day_text in influence_events else None)
                        if influence_kind == "event"
                        else influence_series.get(day_text)
                    )
                    event_here = (
                        day_text in target_events or day_text in influence_events
                    )
                else:
                    item_value = (
                        (1.0 if day_text in target_events else None)
                        if target_kind == "event"
                        else target_series.get(day_text)
                    )
                    event_here = (
                        day_text in influence_events or day_text in target_events
                    )
                if item_value is not None or event_here:
                    window.append(
                        {
                            "date": day_text,
                            "offset": offset,
                            "value": item_value,
                            "event": event_here,
                        }
                    )
            event_timelines.append({"event_date": anchor_text, "window": window})
        comparison = list(
            (influence_series if question == "before_event" else target_series).values()
        )
        event_summary = {
            "event_count": len(anchors),
            "observed_event_values": len(values),
            "comparison_days": len(comparison),
            "median": round(median(values), 4) if values else None,
            "range": [round(min(values), 4), round(max(values), 4)] if values else None,
            "summary_status": "single_case_only"
            if len(anchors) < 3
            else "descriptive_event_summary",
        }
    summary = event_summary or {"observed_event_values": 0, "event_count": 0}
    n = (
        len(pairs)
        if question == "continuous"
        else int(summary["observed_event_values"])
    )
    status = (
        calculation_status(n, coefficient)
        if question == "continuous"
        else (
            "single_case_only"
            if int(summary["event_count"]) < 3
            else "descriptive_only"
        )
    )
    heatmap = []
    if influence_kind == target_kind == "continuous":
        for heat_lag in HEATMAP_LAGS:
            heat_pairs = pair_series(influence_series, target_series, heat_lag)
            rho = spearman([p["x"] for p in heat_pairs], [p["y"] for p in heat_pairs])
            heatmap.append(
                {
                    "influence": influence_id,
                    "lag": heat_lag,
                    "coefficient": rho,
                    "paired_days": len(heat_pairs),
                    "coverage": round(len(heat_pairs) / expected, 4),
                    "status": calculation_status(len(heat_pairs), rho),
                }
            )
    mapping_coverage = None
    if influence_id.startswith("nutrition."):
        coverage_series = (
            influence_series
            if influence_id == "nutrition.mapping_coverage"
            else _series_map(series_loader("nutrition.mapping_coverage", start, end))
        )
        if coverage_series:
            mapping_coverage = round(
                sum(coverage_series.values()) / len(coverage_series), 4
            )
    payload = {
        "method_version": METHOD_VERSION,
        "method": "spearman_rank_complete_case"
        if question == "continuous"
        else "event_centered_descriptive",
        "timezone": "Europe/Zurich",
        "question": question,
        "influence": metric_public(influence_id, metric_lookup),
        "target": metric_public(target_id, metric_lookup),
        "period": {"from": start.isoformat(), "to": end.isoformat()},
        "lag_days": lag,
        "lag_label": f"{metric_public(influence_id, metric_lookup)['label']} {lag} Tage vor {metric_public(target_id, metric_lookup)['label']}"
        if lag
        else "Gleicher dokumentierter Tag",
        "quality": {
            "expected_days": expected,
            "influence_days": len(influence_series) or len(influence_events),
            "target_days": len(target_series) or len(target_events),
            "paired_days": n,
            "missing_days": max(0, expected - n),
            "event_count": len(target_events)
            if question == "before_event"
            else len(influence_events),
            "mapping_coverage": mapping_coverage,
            "status": status,
        },
        "coefficient": coefficient,
        "pairs": pairs[:MAX_RESULT_ROWS],
        "event_summary": event_summary,
        "event_timelines": event_timelines[:100],
        "heatmap": heatmap,
        "series": {
            "influence": [
                {"date": day, "value": value}
                for day, value in sorted(influence_series.items())
            ],
            "target": [
                {"date": day, "value": value}
                for day, value in sorted(target_series.items())
            ],
            "influence_events": influence_events,
            "target_events": target_events,
        },
        "statement": "Im dokumentierten Zeitraum zeitlich gemeinsam beobachtet. Explorative Auswertung; keine Kausalitätsaussage und keine Therapieempfehlung.",
    }
    if len(str(payload).encode("utf-8")) > 450_000:
        raise ValueError("response_too_large")
    return payload
