"""Tesla Fleet API – čtení stavu nabití vozidla (SoC) po příjezdu k wallboxu.

Zásady:
- Volat JEN při příjezdu (vehicle_data budí auto → vampire drain); žádný polling.
- Refresh token Tesla při každém použití ROTUJE → runtime hodnota žije v DB
  (ems.tesla_token, fn_tesla_token_get/upsert); env TESLA_REFRESH_TOKEN je jen
  prvotní seed. Access token cache ~8 h dle expires_in.
- Bez credentials (env prázdné) modul tiše nic nedělá — EV plánování běží na
  defaultech z asset_vehicle.

Postup zřízení: docs/tesla-fleet-api.md.
"""

from __future__ import annotations

import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Optional

import asyncpg
import httpx

from app.config import get_settings
from app.db_json import fetch_json

logger = logging.getLogger(__name__)

AUTH_TOKEN_URL = "https://fleet-auth.prd.vn.cloud.tesla.com/oauth2/v3/token"
API_BASE = "https://fleet-api.prd.eu.vn.cloud.tesla.com"
HTTP_TIMEOUT_S = 15.0
#: rezerva před expirací access tokenu
ACCESS_EXPIRY_MARGIN_S = 120


MILES_TO_KM = 1.609344


def parse_charge_state(vehicle_data: dict[str, Any]) -> dict[str, Any] | None:
    """Parser vehicle_data → battery_level, charge_limit_soc, charging_state, odometer_km.

    POZOR: Tesla API vrací odometer v MÍLÍCH → převod na km.
    """
    resp = vehicle_data.get("response") or {}
    cs = resp.get("charge_state") or {}
    vs = resp.get("vehicle_state") or {}
    level = cs.get("battery_level")
    if level is None:
        return None
    odo_miles = vs.get("odometer")
    ds = resp.get("drive_state") or {}
    return {
        "latitude": ds.get("latitude"),
        "longitude": ds.get("longitude"),
        "shift_state": ds.get("shift_state"),
        "vin": resp.get("vin"),
        "battery_level": int(level),
        "charge_limit_soc": int(cs.get("charge_limit_soc") or 0) or None,
        "charging_state": cs.get("charging_state"),
        "odometer_km": round(float(odo_miles) * MILES_TO_KM, 1) if odo_miles is not None else None,
    }


async def _get_access_token(db: asyncpg.Connection) -> Optional[str]:
    s = get_settings()
    client_id = (getattr(s, "tesla_client_id", "") or "").strip()
    if not client_id:
        return None

    tok = await fetch_json(db, "select ems.fn_tesla_token_get()")
    if not isinstance(tok, dict):
        tok = {}
    refresh = (tok.get("refresh_token") or "").strip()
    if not refresh:
        refresh = (getattr(s, "tesla_refresh_token", "") or "").strip()
        if not refresh:
            logger.debug("Tesla: žádný refresh token (env ani DB) — přeskočeno")
            return None

    access = tok.get("access_token")
    exp_raw = tok.get("access_expires_at")
    if access and exp_raw:
        try:
            exp = datetime.fromisoformat(str(exp_raw))
            if exp - timedelta(seconds=ACCESS_EXPIRY_MARGIN_S) > datetime.now(timezone.utc):
                return str(access)
        except ValueError:
            pass

    async with httpx.AsyncClient(timeout=HTTP_TIMEOUT_S) as client:
        r = await client.post(
            AUTH_TOKEN_URL,
            data={
                "grant_type": "refresh_token",
                "client_id": client_id,
                "refresh_token": refresh,
            },
        )
        if r.status_code >= 400:
            # 400 invalid_grant = token spálený rotací NEBO ~10min výpadek po
            # revokaci souhlasu (Tesla docs). Neshazovat volajícího tracebackem.
            body = r.text[:300]
            logger.error(
                "Tesla token refresh selhal (HTTP %s): %s — pokud jsi právě "
                "revokoval souhlas, počkej ~10 min; jinak obnov token dle "
                "docs/tesla-fleet-api.md (deploy/tesla/reauth.sh)",
                r.status_code,
                body,
            )
            return None
        data = r.json()

    new_access = str(data["access_token"])
    # rotace: Tesla vrací nový refresh token — starý přestává platit, ULOŽIT
    new_refresh = str(data.get("refresh_token") or refresh)
    expires_at = datetime.now(timezone.utc) + timedelta(seconds=int(data.get("expires_in") or 3600))
    await db.execute(
        "select ems.fn_tesla_token_upsert($1::text, $2::text, $3::timestamptz)",
        new_refresh,
        new_access,
        expires_at,
    )
    return new_access


async def get_charge_state(
    db: asyncpg.Connection, vin: str | None
) -> dict[str, Any] | None:
    """SoC vozidla: dle VIN, nebo jediného vozidla na účtu (VIN vrací pro doplnění).

    Vrací parse_charge_state dict, nebo None (bez credentials / vozidlo nenalezeno /
    offline). Výjimky síťové vrstvy propadají volajícímu (hook je loguje).
    """
    token = await _get_access_token(db)
    if token is None:
        return None
    headers = {"Authorization": f"Bearer {token}"}

    async with httpx.AsyncClient(timeout=HTTP_TIMEOUT_S, headers=headers) as client:
        r = await client.get(f"{API_BASE}/api/1/vehicles")
        r.raise_for_status()
        vehicles = (r.json().get("response") or [])
        if not vehicles:
            logger.warning("Tesla: účet nemá žádná vozidla")
            return None
        chosen = None
        if vin:
            chosen = next((v for v in vehicles if v.get("vin") == vin), None)
            if chosen is None:
                logger.warning("Tesla: VIN %s na účtu nenalezen", vin)
                return None
        elif len(vehicles) == 1:
            chosen = vehicles[0]
        else:
            logger.warning(
                "Tesla: %s vozidel na účtu a VIN v asset_vehicle chybí — doplň VIN",
                len(vehicles),
            )
            return None

        r = await client.get(
            f"{API_BASE}/api/1/vehicles/{chosen['id']}/vehicle_data",
            params={"endpoints": "charge_state;vehicle_state;location_data"},
        )
        if r.status_code == 408:
            logger.info("Tesla: vozidlo spí / nedostupné (408) — SoC nedoplněno")
            return None
        r.raise_for_status()
        return parse_charge_state(r.json())


def haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
    """Vzdálenost dvou GPS bodů v metrech (čisté, testovatelné)."""
    import math

    r = 6_371_000.0
    p1, p2 = math.radians(lat1), math.radians(lat2)
    dp = math.radians(lat2 - lat1)
    dl = math.radians(lon2 - lon1)
    a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
    return 2 * r * math.asin(math.sqrt(a))


async def get_vehicle_api_state(db: asyncpg.Connection, vin: str | None) -> str | None:
    """Jen state z /vehicles (online/asleep/offline) — NIKDY nebudí auto."""
    token = await _get_access_token(db)
    if token is None:
        return None
    async with httpx.AsyncClient(
        timeout=HTTP_TIMEOUT_S, headers={"Authorization": f"Bearer {token}"}
    ) as client:
        r = await client.get(f"{API_BASE}/api/1/vehicles")
        r.raise_for_status()
        vehicles = r.json().get("response") or []
        if vin:
            v = next((x for x in vehicles if x.get("vin") == vin), None)
        else:
            v = vehicles[0] if len(vehicles) == 1 else None
        return str(v["state"]) if v else None
