"""Dependency-free GELYPTIC MD Public API v1 reference client (Python 3.10+)."""

from __future__ import annotations

import csv
import hashlib
import hmac
import json
import time
from dataclasses import dataclass
from io import StringIO
from typing import Any, Mapping, Sequence
from urllib.error import HTTPError
from urllib.parse import urlencode, quote
from urllib.request import Request, urlopen


@dataclass
class GelypticApiError(Exception):
    status: int
    code: str
    message: str
    request_id: str | None = None

    def __str__(self) -> str:
        suffix = f" (request {self.request_id})" if self.request_id else ""
        return f"{self.code}: {self.message}{suffix}"


class GelypticMD:
    def __init__(self, api_key: str, base_url: str = "https://gelypticmd.com/api/public/v1", timeout: int = 30):
        if not api_key:
            raise ValueError("api_key is required")
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    def _request(self, path: str, method: str = "GET", body: Mapping[str, Any] | None = None, accept: str = "application/json") -> Any:
        payload = json.dumps(body).encode() if body is not None else None
        headers = {"Authorization": f"Bearer {self.api_key}", "Accept": accept}
        if payload is not None:
            headers["Content-Type"] = "application/json"
        request = Request(f"{self.base_url}{path}", data=payload, headers=headers, method=method)
        try:
            with urlopen(request, timeout=self.timeout) as response:
                raw = response.read()
                return raw.decode() if accept == "application/hl7-v2" else json.loads(raw)
        except HTTPError as error:
            request_id = error.headers.get("x-request-id")
            try:
                detail = json.loads(error.read()).get("error", {})
            except (ValueError, AttributeError):
                detail = {}
            raise GelypticApiError(error.code, detail.get("code", "HTTP_ERROR"), detail.get("message", str(error)), detail.get("request_id", request_id)) from error

    def list_encounters(self, limit: int = 25, offset: int = 0) -> Mapping[str, Any]:
        return self._request(f"/clinical/encounters?{urlencode({'limit': limit, 'offset': offset})}")

    def create_encounter(self, external_encounter_id: str, encounter_type: str = "consultation") -> Mapping[str, Any]:
        return self._request("/clinical/encounters", "POST", {"external_encounter_id": external_encounter_id, "encounter_type": encounter_type})

    def list_notes(self, limit: int = 25, offset: int = 0) -> Mapping[str, Any]:
        return self._request(f"/clinical/notes?{urlencode({'limit': limit, 'offset': offset})}")

    def get_note(self, note_id: str) -> Mapping[str, Any]:
        return self._request(f"/clinical/notes/{quote(note_id, safe='')}")

    def get_fhir_bundle(self, note_id: str) -> Mapping[str, Any]:
        return self._request(f"/clinical/notes/{quote(note_id, safe='')}/fhir", accept="application/fhir+json")

    def get_hl7_message(self, note_id: str) -> str:
        return self._request(f"/clinical/notes/{quote(note_id, safe='')}/hl7", accept="application/hl7-v2")


def verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300, now: float | None = None) -> bool:
    try:
        parts = dict(part.split("=", 1) for part in signature_header.split(","))
        timestamp = int(parts["t"])
        supplied = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs((time.time() if now is None else now) - timestamp) >= tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, supplied)


AGGREGATE_FIELDS = ("group_key", "period", "metric_name", "metric_value", "suppressed")
FORBIDDEN_FIELDS = {"patient", "patient_id", "name", "email", "phone", "transcript", "soap_note", "clinician", "clinician_id"}


def aggregate_rows_to_csv(rows: Sequence[Mapping[str, Any]]) -> str:
    """Serialize already-aggregated rows; rejects patient, clinician, contact, transcript, and note fields."""
    for row in rows:
        lowered = {str(key).lower() for key in row}
        if lowered & FORBIDDEN_FIELDS or not lowered.issubset(AGGREGATE_FIELDS):
            raise ValueError("Only approved aggregate result fields can be exported")
    output = StringIO(newline="")
    writer = csv.DictWriter(output, fieldnames=AGGREGATE_FIELDS, extrasaction="raise")
    writer.writeheader()
    writer.writerows(rows)
    return output.getvalue()