"""One HTTP client for the /v1 API, shared by the CLI and the MCP server.

Deliberately stdlib-only (urllib, no requests, no httpx). This file is the one people
copy onto their own machine to talk to us — a laptop, a research box, an MCP host — and
every dependency it carries is a dependency they have to install before anything works.

The two frontends differ only in how they present results; the requests, the pagination,
the retry-after handling and the error messages are the same for both, and live here so
they cannot drift.
"""
from __future__ import annotations

import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Iterator, Optional

DEFAULT_BASE = os.getenv("GUAQ_API_BASE", "https://jobs.guaqai.me")
USER_AGENT = "guaq-client/1.0"


class GuaqError(Exception):
    def __init__(self, message: str, status: int = 0, payload: Optional[dict] = None):
        super().__init__(message)
        self.status = status
        self.payload = payload or {}


class Guaq:
    def __init__(self, api_key: str = "", base: str = "", timeout: int = 120):
        self.api_key = api_key or os.getenv("GUAQ_API_KEY", "")
        self.base = (base or DEFAULT_BASE).rstrip("/")
        self.timeout = timeout
        self.last_quota: tuple = (None, None)

    # -- plumbing ----------------------------------------------------------------------

    def _request(self, path: str, params: Optional[dict] = None, raw: bool = False):
        qs = urllib.parse.urlencode(
            {k: v for k, v in (params or {}).items() if v not in (None, "", [])},
            doseq=True)
        url = f"{self.base}{path}" + (f"?{qs}" if qs else "")
        req = urllib.request.Request(url, headers={
            "Authorization": f"Bearer {self.api_key}",
            "User-Agent": USER_AGENT,
            "Accept": "application/json",
        })
        try:
            resp = urllib.request.urlopen(req, timeout=self.timeout)
        except urllib.error.HTTPError as e:
            body = e.read()
            try:
                payload = json.loads(body)
            except Exception:
                payload = {"error": body[:400].decode("utf-8", "replace")}
            # A quota refusal is the one error worth naming precisely: it is not a bug in
            # the caller's script, and the fix is time or a plan, not a code change.
            msg = payload.get("error") or f"HTTP {e.code}"
            raise GuaqError(msg, e.code, payload)
        except urllib.error.URLError as e:
            raise GuaqError(f"Could not reach {self.base}: {e.reason}", 0)

        self.last_quota = (resp.headers.get("X-RateLimit-Used"),
                           resp.headers.get("X-RateLimit-Limit"))
        if raw:
            return resp
        return json.loads(resp.read())

    # -- endpoints ---------------------------------------------------------------------

    def index(self) -> dict:
        return self._request("/v1")

    def meta(self) -> dict:
        return self._request("/v1/meta")

    def jobs(self, **filters) -> dict:
        return self._request("/v1/jobs", filters)

    def count(self, **filters) -> int:
        return self._request("/v1/jobs/count", filters)["count"]

    def job(self, job_id: int) -> dict:
        return self._request(f"/v1/jobs/{int(job_id)}")

    def companies(self, **filters) -> list:
        return self._request("/v1/companies", filters)["data"]

    def company(self, name: str) -> dict:
        return self._request("/v1/companies/" + urllib.parse.quote(name, safe=""))

    def signal(self, name: str, **params) -> dict:
        return self._request(f"/v1/signals/{name}", params)

    def iter_jobs(self, page_size: int = 100, max_rows: Optional[int] = None,
                  **filters) -> Iterator[dict]:
        """Every matching row, following the cursor.

        This is the paging loop every consumer would otherwise write themselves, and get
        subtly wrong — the usual mistake being an offset, which silently duplicates and
        skips rows whenever a scrape lands between two pages.
        """
        cursor, seen = None, 0
        while True:
            page = self.jobs(limit=page_size, cursor=cursor, **filters)
            for row in page["data"]:
                yield row
                seen += 1
                if max_rows and seen >= max_rows:
                    return
            cursor = page.get("next_cursor")
            if not cursor:
                return

    def export(self, out_path: str, fmt: str = "jsonl", chunk: int = 1 << 20,
               progress=None, **filters) -> int:
        """Stream a bulk export straight to disk. Returns bytes written.

        Written to a temporary file and renamed at the end, so an interrupted download
        never leaves a half-file that looks complete to whatever reads it next.
        """
        params = dict(filters)
        params["format"] = fmt
        resp = self._request("/v1/export", params, raw=True)
        tmp = out_path + ".part"
        total = 0
        with open(tmp, "wb") as fh:
            while True:
                buf = resp.read(chunk)
                if not buf:
                    break
                fh.write(buf)
                total += len(buf)
                if progress:
                    progress(total)
        os.replace(tmp, out_path)
        return total

    def retry(self, fn, attempts: int = 3, backoff: float = 2.0):
        """Retry a transient failure. 4xx is never retried — a bad filter or an exhausted
        quota does not get better by being asked again."""
        for i in range(attempts):
            try:
                return fn()
            except GuaqError as e:
                if e.status and 400 <= e.status < 500:
                    raise
                if i == attempts - 1:
                    raise
                time.sleep(backoff ** i)
