#!/usr/bin/env python3
from __future__ import annotations

import os
import time
import json
import random
from datetime import datetime, timezone
from typing import Any, Optional, Tuple
from uuid import uuid4

import requests
from token_service import get_or_create_epic_token


FHIR_BASE = os.getenv("FHIR_BASE_URL", "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4")
EPIC_APP_CONFIG_ID = int(os.getenv("EPIC_APP_CONFIG_ID", "3"))
PATIENT_ID = os.getenv("PATIENT_ID", "e63wRTbPfr1p8UW81d8Seiw3")  # or "Patient/xxx"
ENCOUNTER_REF = os.getenv("ENCOUNTER_REF")  # optional: "Encounter/{id}"
INTERVAL_SECONDS = float(os.getenv("INTERVAL_SECONDS", "1"))


def normalize_resource_url(fhir_base: str, resource: str) -> str:
    base = fhir_base.strip().rstrip("/")
    lowered = base.lower()
    r = resource.lower()

    if lowered.endswith(f"/{r}"):
        return base
    if lowered.endswith("/fhir/r4"):
        return base + f"/{resource}"
    return base + f"/{resource}"


def to_patient_reference(patient: str) -> str:
    p = patient.strip()
    if not p:
        return p
    if "/" in p:
        return p
    return f"Patient/{p}"


def now_fhir_dt() -> str:
    # FHIR dateTime with timezone offset
    return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")


def rand_float(lo: float, hi: float, ndigits: int = 1) -> float:
    return round(random.uniform(lo, hi), ndigits)


def rand_int(lo: int, hi: int) -> int:
    return random.randint(lo, hi)


def build_random_vitals_observation(patient_ref: str, encounter_ref: Optional[str] = None) -> Dict[str, Any]:
    """
    Builds a single FHIR Observation containing a small vital-signs panel:
      - Heart rate (LOINC 8867-4) bpm
      - Body temperature (LOINC 8310-5) degC
      - SpO2 (LOINC 2708-6) %
      - Mean arterial pressure (LOINC 8478-0) mmHg
      - Optional BP panel could be added, but keeping it simple.
    """
    hr = rand_int(55, 120)
    temp = rand_float(36.0, 39.2, 1)
    spo2 = rand_int(88, 100)
    map_mmHg = rand_int(55, 95)

    obs: Dict[str, Any] = {
        "resourceType": "Observation",
        "id": uuid4().hex,
        "status": "final",
        "category": [
            {
                "coding": [
                    {
                        "system": "http://terminology.hl7.org/CodeSystem/observation-category",
                        "code": "vital-signs",
                        "display": "Vital Signs",
                    }
                ]
            }
        ],
        "code": {
            "coding": [
                {
                    "system": "http://loinc.org",
                    "code": "85353-1",
                    "display": "Vital signs, weight, height, head circumference, oxygen saturation and BMI panel",
                }
            ],
            "text": "Vital signs panel",
        },
        "subject": {"reference": patient_ref},
        "effectiveDateTime": now_fhir_dt(),
        "component": [
            {
                "code": {"coding": [{"system": "http://loinc.org", "code": "8867-4", "display": "Heart rate"}]},
                "valueQuantity": {"value": hr, "unit": "bpm", "system": "http://unitsofmeasure.org", "code": "/min"},
            },
            {
                "code": {"coding": [{"system": "http://loinc.org", "code": "8310-5", "display": "Body temperature"}]},
                "valueQuantity": {"value": temp, "unit": "degC", "system": "http://unitsofmeasure.org", "code": "Cel"},
            },
            {
                "code": {"coding": [{"system": "http://loinc.org", "code": "2708-6", "display": "Oxygen saturation in Arterial blood by Pulse oximetry"}]},
                "valueQuantity": {"value": spo2, "unit": "%", "system": "http://unitsofmeasure.org", "code": "%"},
            },
            {
                "code": {"coding": [{"system": "http://loinc.org", "code": "8478-0", "display": "Mean blood pressure"}]},
                "valueQuantity": {"value": map_mmHg, "unit": "mmHg", "system": "http://unitsofmeasure.org", "code": "mm[Hg]"},
            },
        ],
    }

    if encounter_ref:
        obs["encounter"] = {"reference": encounter_ref}

    return obs

def _parse_fhir_datetime(dt_str: str) -> Optional[datetime]:
    """Parse FHIR dateTime-ish strings (best effort)."""
    if not dt_str:
        return None
    s = dt_str.strip()
    try:
        # Handle "Z"
        if s.endswith("Z"):
            s = s[:-1] + "+00:00"
        return datetime.fromisoformat(s)
    except Exception:
        return None
    
def pick_encounter_id(bundle: dict[str, Any]) -> Optional[str]:
    """
    Pick an Encounter id from a FHIR Bundle.
    Prefers the entry with the latest period.start if present.
    """
    entries = bundle.get("entry") or []
    if not entries:
        return None

    candidates: list[Tuple[Optional[datetime], Optional[str]]] = []
    for e in entries:
        res = (e or {}).get("resource") or {}
        if res.get("resourceType") != "Encounter":
            continue
        enc_id = res.get("id")
        start = ((res.get("period") or {}).get("start")) or ""
        dt = _parse_fhir_datetime(start)
        candidates.append((dt, enc_id))

    # If we found no Encounter resources, fall back to first entry.resource.id
    if not candidates:
        first_res = (entries[0] or {}).get("resource") or {}
        return first_res.get("id")

    # Sort with None dates last
    candidates.sort(key=lambda x: (x[0] is None, x[0]), reverse=True)
    return candidates[0][1]

def get_inprogress_encounter_ref(
    fhir_base: str,
    headers: Dict[str, str],
    patient_ref: str,
    timeout: int = 30,
) -> str:
    encounter_url = normalize_resource_url(fhir_base, "Encounter")
    params = {
        "patient": patient_ref,      # Epic accepts Patient/{id}
        "status": "in-progress",
    }
    r = requests.get(encounter_url, headers=headers, params=params, timeout=timeout)
    r.raise_for_status()
    bundle = r.json()

    enc_id = pick_inp_encounter_id(bundle)
    if not enc_id:
        # Optional: dump bundle for debugging
        raise RuntimeError("No in-progress Encounter found for this patient; cannot POST Observation without encounter.")
    return "Encounter/ewYPWmestytGzRSBFlutk7w3"

def list_encounters(bundle: dict) -> None:
    entries = bundle.get("entry") or []
    print(f"Encounter entries: {len(entries)}")
    for e in entries:
        r = (e or {}).get("resource") or {}
        if r.get("resourceType") != "Encounter":
            continue
        enc_id = r.get("id")
        status = r.get("status")
        cls = ((r.get("class") or {}).get("code")) or ""
        start = ((r.get("period") or {}).get("start")) or ""
        print(f"- id={enc_id} status={status} class={cls} start={start}")

def _parse_fhir_dt_to_epoch(s: str) -> Optional[float]:
    """
    Parse FHIR dateTime into epoch seconds (UTC).
    Returns None if missing/unparseable.
    Handles:
      - 2026-01-08T00:00:00Z
      - 2026-01-08T00:00:00+00:00
      - 2026-01-08T00:00:00 (naive -> assume UTC)
    """
    if not s:
        return None
    s = s.strip()
    try:
        if s.endswith("Z"):
            s = s[:-1] + "+00:00"
        dt = datetime.fromisoformat(s)

        # If naive, assume UTC
        if dt.tzinfo is None:
            dt = dt.replace(tzinfo=timezone.utc)

        return dt.timestamp()
    except Exception:
        return None

def pick_inp_encounter_id(bundle: dict[str, Any]) -> Optional[str]:
    entries = bundle.get("entry") or []
    candidates: list[Tuple[bool, float, str]] = []

    for e in entries:
        r = (e or {}).get("resource") or {}
        if r.get("resourceType") != "Encounter":
            continue

        enc_id = r.get("id")
        if not enc_id:
            continue

        status = r.get("status") or ""
        cls = ((r.get("class") or {}).get("code")) or ""
        start_s = ((r.get("period") or {}).get("start")) or ""
        start_epoch = _parse_fhir_dt_to_epoch(start_s)

        # Prefer in-progress + inpatient/ED
        preferred = (status == "in-progress" and cls in {"IMP", "EMER"})

        # Use -1 for missing date so it sorts last
        candidates.append((preferred, start_epoch if start_epoch is not None else -1.0, enc_id))

    if not candidates:
        return None

    # Sort: preferred first, then newest start
    candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
    return candidates[0][2]

def main() -> int:
    observation_url = normalize_resource_url(FHIR_BASE, "Observation")
    patient_ref = to_patient_reference(PATIENT_ID)

    # Token once; your token_service should refresh when needed.
    token = get_or_create_epic_token(EPIC_APP_CONFIG_ID)

    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/fhir+json",
        "Content-Type": "application/fhir+json",
    }

    print("Posting random vitals every", INTERVAL_SECONDS, "seconds")
    print("FHIR Observation endpoint:", observation_url)
    print("Patient:", patient_ref)
    if ENCOUNTER_REF:
        print("Encounter:", ENCOUNTER_REF)

    encounter_ref = get_inprogress_encounter_ref(FHIR_BASE, headers, patient_ref)
    print("Using encounter:", encounter_ref)
    while True:
        payload = build_random_vitals_observation(patient_ref, encounter_ref)

        try:
            resp = requests.post(observation_url, headers=headers, json=payload, timeout=15)
            ok = resp.status_code in (200, 201)

            print(
                f"[{datetime.now().isoformat(timespec='seconds')}] "
                f"POST {resp.status_code} ok={ok} id={payload.get('id')}"
            )

            # If you want to see server response on errors:
            if not ok:
                try:
                    print(json.dumps(resp.json(), indent=2))
                except Exception:
                    print(resp.text)

        except Exception as e:
            print("POST failed:", repr(e))

        time.sleep(INTERVAL_SECONDS)


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