#!/usr/bin/env python3
"""Consume locally queued Dashboard actions outside the network-facing service."""
from __future__ import annotations

import json
import os
import re
import stat
import subprocess
import sys
from datetime import date, datetime
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo

BASE = Path.home() / ".hermes" / "assets" / "Gesundheit"
DEFAULT_DASHBOARD_DB = (BASE / "health_data.db").resolve()
DASHBOARD_DB = (
    Path(os.environ["HEALTH_DASHBOARD_DB"]).resolve()
    if os.environ.get("HEALTH_DASHBOARD_DB")
    else None
)
ACTION_INBOX = Path(
    os.environ.get(
        "HEALTH_DASHBOARD_ACTION_INBOX",
        str(BASE / "runtime" / "dashboard-actions"),
    )
).resolve()
DASHBOARD_FILE = Path(
    os.environ.get(
        "HEALTH_DASHBOARD_FILE",
        str(BASE / "reports" / "health_dashboard_v4.html"),
    )
).resolve()
DASHBOARD_V5_FILE = (
    Path(os.environ["HEALTH_DASHBOARD_V5_FILE"]).resolve()
    if os.environ.get("HEALTH_DASHBOARD_V5_FILE")
    else None
)
SCRIPT_DIR = Path(__file__).resolve().parent
QUICK_ADD = SCRIPT_DIR / "health_symptom_quick_add.py"
DASHBOARD_V4 = SCRIPT_DIR / "health_dashboard_v4.py"
DASHBOARD_V5 = SCRIPT_DIR / "health_dashboard_v5.py"
FIELDS = ("aphthen", "gi", "fatigue", "skin", "eyes", "joints", "vascular")
NAME_RE = re.compile(r"[0-9a-f]{32}\.json")
LOCAL_TIMEZONE = ZoneInfo("Europe/Zurich")


def local_today() -> date:
    return datetime.now(LOCAL_TIMEZONE).date()


def validate_payload(payload: Any) -> tuple[str, dict[str, int], str]:
    if not isinstance(payload, dict) or set(payload) != {
        "version",
        "action",
        "date",
        "scores",
        "notes",
    }:
        raise ValueError("invalid payload shape")
    if payload["version"] != 1 or payload["action"] != "symptom_checkin":
        raise ValueError("unsupported action")
    try:
        day = date.fromisoformat(payload["date"])
    except (TypeError, ValueError) as exc:
        raise ValueError("invalid date") from exc
    if day.year < 2000 or day > local_today():
        raise ValueError("date outside allowed range")
    scores = payload["scores"]
    if not isinstance(scores, dict) or set(scores) != set(FIELDS):
        raise ValueError("incomplete scores")
    if any(type(scores[field]) is not int or scores[field] not in range(4) for field in FIELDS):
        raise ValueError("invalid score")
    notes = payload["notes"]
    if not isinstance(notes, str) or len(notes) > 300:
        raise ValueError("invalid notes")
    return day.isoformat(), {field: scores[field] for field in FIELDS}, notes.strip()


def reject_duplicate_object_pairs(pairs: list[tuple[str, object]]) -> dict[str, object]:
    result: dict[str, object] = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("duplicate JSON key")
        result[key] = value
    return result


def ensure_private_inbox() -> None:
    ACTION_INBOX.mkdir(mode=0o700, parents=True, exist_ok=True)
    metadata = ACTION_INBOX.lstat()
    if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != os.getuid():
        raise OSError("action inbox is not a private owned directory")
    os.chmod(ACTION_INBOX, 0o700)


def load_action(path: Path) -> tuple[str, dict[str, int], str]:
    if not NAME_RE.fullmatch(path.name):
        raise ValueError("invalid filename")
    descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
    try:
        metadata = os.fstat(descriptor)
        if (
            not stat.S_ISREG(metadata.st_mode)
            or metadata.st_uid != os.getuid()
            or stat.S_IMODE(metadata.st_mode) != 0o600
        ):
            raise ValueError("invalid action file permissions")
        if metadata.st_size > 4096:
            raise ValueError("invalid action file")
        raw = os.read(descriptor, 4097)
    finally:
        os.close(descriptor)
    if len(raw) > 4096:
        raise ValueError("action too large")
    return validate_payload(
        json.loads(raw.decode("utf-8"), object_pairs_hook=reject_duplicate_object_pairs)
    )


def apply_action(day: str, scores: dict[str, int], notes: str) -> None:
    if DASHBOARD_DB is None:
        raise RuntimeError("HEALTH_DASHBOARD_DB is required")
    command = [
        sys.executable,
        str(QUICK_ADD),
        "--db",
        str(DASHBOARD_DB),
        "--date",
        day,
        "--notes",
        notes,
    ]
    for field in FIELDS:
        command.extend((f"--{field}", str(scores[field])))
    subprocess.run(command, check=True, timeout=120, capture_output=True, text=True)
    if DASHBOARD_DB == DEFAULT_DASHBOARD_DB:
        subprocess.run(
            [sys.executable, str(DASHBOARD_V4), "--output", str(DASHBOARD_FILE)],
            check=True,
            timeout=120,
            capture_output=True,
            text=True,
        )
    if DASHBOARD_V5_FILE is not None:
        subprocess.run(
            [
                sys.executable,
                str(DASHBOARD_V5),
                "--db",
                str(DASHBOARD_DB),
                "--output",
                str(DASHBOARD_V5_FILE),
                "--today",
                local_today().isoformat(),
            ],
            check=True,
            timeout=120,
            capture_output=True,
            text=True,
        )


def main() -> int:
    if DASHBOARD_DB is None:
        print("HEALTH_DASHBOARD_DB is required", file=sys.stderr)
        return 2
    ensure_private_inbox()
    failures = 0
    for path in sorted(ACTION_INBOX.glob("*.json")):
        try:
            day, scores, notes = load_action(path)
            apply_action(day, scores, notes)
            path.unlink()
        except Exception:
            failures += 1
            try:
                path.replace(path.with_suffix(".failed"))
            except OSError:
                pass
    return 1 if failures else 0


if __name__ == "__main__":
    raise SystemExit(main())
