"""Edge-to-cloud client for Sentinel V7.

Uses only the Python standard library so it can run on a Raspberry Pi without
adding another HTTP dependency. Local measurements are queued until the cloud
acknowledges them; a network outage therefore does not lose data while the Pi
remains powered.
"""
from __future__ import annotations

import json
import sqlite3
import urllib.error
import urllib.request
import uuid
from pathlib import Path
from typing import Iterable


class EdgeCloudClient:
    def __init__(self, *, cloud_url: str, device_id: str, equipment_id: str, api_key: str = "", outbox_path: Path) -> None:
        self.cloud_url = cloud_url.rstrip("/")
        self.device_id = device_id
        self.equipment_id = equipment_id
        self.api_key = api_key
        self.outbox_path = Path(outbox_path)
        self._initialize()

    def heartbeat(self, *, edge_version: str = "7.0.0", heartbeat_interval_seconds: int = 15) -> dict[str, object]:
        return self._post("/api/v7/devices/heartbeat", {
            "device_id": self.device_id,
            "equipment_id": self.equipment_id,
            "edge_version": edge_version,
            "heartbeat_interval_seconds": heartbeat_interval_seconds,
        })

    def enqueue(self, measurement: dict[str, object]) -> None:
        payload = dict(measurement)
        payload.setdefault("source_id", str(payload.get("id") or uuid.uuid4()))
        payload.setdefault("device_id", self.device_id)
        payload.setdefault("equipment_id", self.equipment_id)
        with self._connect() as connection:
            connection.execute(
                "INSERT OR IGNORE INTO outbox(source_id,payload_json) VALUES(?,?)",
                (str(payload["source_id"]), json.dumps(payload, default=str)),
            )

    def flush(self, limit: int = 250) -> dict[str, object]:
        with self._connect() as connection:
            rows = connection.execute(
                "SELECT source_id,payload_json FROM outbox ORDER BY id LIMIT ?", (max(1, min(limit, 2000)),)
            ).fetchall()
        if not rows:
            return {"ok": True, "accepted": 0, "duplicates": 0, "remaining": 0}
        measurements = [json.loads(str(row["payload_json"])) for row in rows]
        batch_id = str(uuid.uuid4())
        result = self._post("/api/v7/sync/measurements", {
            "batch_id": batch_id,
            "device_id": self.device_id,
            "equipment_id": self.equipment_id,
            "measurements": measurements,
        })
        if result.get("ok"):
            ids = [str(row["source_id"]) for row in rows]
            placeholders = ",".join("?" for _ in ids)
            with self._connect() as connection:
                connection.execute(f"DELETE FROM outbox WHERE source_id IN ({placeholders})", ids)
        with self._connect() as connection:
            remaining = connection.execute("SELECT COUNT(*) n FROM outbox").fetchone()["n"]
        result["remaining"] = int(remaining)
        return result

    def get_state(self, key: str, default: str = "") -> str:
        with self._connect() as connection:
            row = connection.execute("SELECT value FROM edge_state WHERE key=?", (key,)).fetchone()
        return str(row["value"]) if row else default

    def set_state(self, key: str, value: str) -> None:
        with self._connect() as connection:
            connection.execute(
                "INSERT INTO edge_state(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
                (key, value),
            )

    def _post(self, path: str, payload: dict[str, object]) -> dict[str, object]:
        body = json.dumps(payload).encode("utf-8")
        request = urllib.request.Request(
            self.cloud_url + path,
            data=body,
            method="POST",
            headers={"Content-Type": "application/json", **({"X-Sentinel-Key": self.api_key} if self.api_key else {})},
        )
        try:
            with urllib.request.urlopen(request, timeout=10) as response:
                return json.loads(response.read().decode("utf-8"))
        except urllib.error.HTTPError as exc:
            raise RuntimeError(f"Cloud HTTP {exc.code}: {exc.read().decode('utf-8', errors='replace')}") from exc

    def _initialize(self) -> None:
        self.outbox_path.parent.mkdir(parents=True, exist_ok=True)
        with self._connect() as connection:
            connection.execute(
                """CREATE TABLE IF NOT EXISTS outbox(
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    source_id TEXT NOT NULL UNIQUE,
                    payload_json TEXT NOT NULL,
                    created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
                )"""
            )
            connection.execute(
                """CREATE TABLE IF NOT EXISTS edge_state(
                    key TEXT PRIMARY KEY,
                    value TEXT NOT NULL
                )"""
            )

    def _connect(self) -> sqlite3.Connection:
        connection = sqlite3.connect(self.outbox_path)
        connection.row_factory = sqlite3.Row
        return connection
