"""HTTP client for the adampowell.pro remote server.

Wraps the /api/daemon/* endpoints (mirrors of /api/extension/*). Bearer auth via
config.token. SSE streaming for inbound commands. Fire-and-forget ack for
outbound command results. Vault save for credential persistence.
"""
import json
import time
from typing import Any, Dict, Iterator, Optional

import requests

from config import Config
from utils.logger import child

log = child("vault")


def _headers(cfg: Config, json_body: bool = False) -> Dict[str, str]:
    h = {"Authorization": f"Bearer {cfg.token}"}
    if json_body:
        h["Content-Type"] = "application/json"
    return h


def stream_commands(cfg: Config) -> Iterator[Dict[str, Any]]:
    """Infinite generator: connect to SSE, yield each parsed command dict.

    Reconnects silently on transient failures with backoff. Skips `hello` and
    server heartbeats. Each yielded item is the decoded JSON event body.
    """
    import sseclient  # type: ignore

    quiet = False
    backoff = 1.0
    while True:
        try:
            headers = _headers(cfg)
            headers["Accept"] = "text/event-stream"
            # /api/daemon/stream is the nginx-whitelisted bearer-only SSE path.
            # /api/extension/queue in server.js broadcasts to both extensions
            # and daemons Sets, so we receive the same ext_* command types the
            # Brave extension used to.
            r = requests.get(
                cfg.url("/api/daemon/stream"),
                headers=headers,
                stream=True,
                timeout=(10, None),
            )
            if r.status_code != 200:
                log.warning(f"SSE got HTTP {r.status_code}: {r.text[:200]}")
                time.sleep(min(backoff, 30))
                backoff = min(backoff * 2, 30)
                continue
            if not quiet:
                log.info("SSE connected")
            quiet = False
            backoff = 1.0
            client = sseclient.SSEClient(r)
            got_any = False
            for event in client.events():
                if not event.data:
                    continue
                try:
                    data = json.loads(event.data)
                except Exception:
                    continue
                got_any = True
                t = data.get("type")
                if t == "hello":
                    continue
                yield data
            # Clean iterator end = stream closed. Don't spin.
            if not quiet:
                log.info("SSE stream ended cleanly; reconnecting")
                quiet = True
            time.sleep(1.5)
        except requests.exceptions.ChunkedEncodingError:
            if not quiet:
                log.info("SSE stream ended; reconnecting")
                quiet = True
            time.sleep(1.5)
        except requests.exceptions.ConnectionError as e:
            log.warning(f"SSE connection error: {e}; reconnecting in {backoff:.1f}s")
            time.sleep(min(backoff, 30))
            backoff = min(backoff * 2, 30)
        except Exception as e:
            log.warning(f"SSE unexpected error: {e}; reconnecting in {backoff:.1f}s")
            time.sleep(min(backoff, 30))
            backoff = min(backoff * 2, 30)


def ack(
    cfg: Config,
    command_id: str,
    ok: bool,
    *,
    log_text: Optional[str] = None,
    notes: Optional[str] = None,
    error: Optional[str] = None,
    email: Optional[str] = None,
    password: Optional[str] = None,
    save_as_site: Optional[str] = None,
) -> bool:
    body = {
        "ok": ok,
        "log": log_text,
        "notes": notes,
        "error": error,
        "email": email,
        "password": password,
        "save_as_site": save_as_site,
    }
    try:
        r = requests.post(
            cfg.url(f"/api/daemon/ack/{command_id}"),
            headers=_headers(cfg, json_body=True),
            data=json.dumps(body),
            timeout=15,
        )
        if r.status_code >= 300:
            log.warning(f"ack {command_id} HTTP {r.status_code}: {r.text[:200]}")
            return False
        return True
    except Exception as e:
        log.warning(f"ack {command_id} failed: {e}")
        return False


def save_credentials(
    cfg: Config,
    site: str,
    email: str,
    password: str,
    note: Optional[str] = None,
) -> bool:
    body = {"site": site, "email": email, "password": password, "note": note}
    try:
        r = requests.post(
            cfg.url("/api/daemon/credentials"),
            headers=_headers(cfg, json_body=True),
            data=json.dumps(body),
            timeout=20,
        )
        if r.status_code >= 300:
            log.warning(f"save_credentials HTTP {r.status_code}: {r.text[:200]}")
            return False
        log.info(f"vault saved: site={site} email={email}")
        return True
    except Exception as e:
        log.warning(f"save_credentials failed: {e}")
        return False


def prepare_full_flow(cfg: Config) -> Optional[Dict[str, Any]]:
    try:
        r = requests.post(
            cfg.url("/api/daemon/prepare-full-flow"),
            headers=_headers(cfg, json_body=True),
            data="{}",
            timeout=20,
        )
        if r.status_code >= 300:
            log.warning(f"prepare-full-flow HTTP {r.status_code}: {r.text[:200]}")
            return None
        return r.json()
    except Exception as e:
        log.warning(f"prepare-full-flow failed: {e}")
        return None
