"""KWIK L for Python: the public API v1 in one file, with nothing to install beyond Python 3.9 or later.

    from kwikl import Kwikl

    k = Kwikl("kl_live_...")          # Settings, API keys; the key is shown once
    print(k.me()["workspace"])
    for item in k.day()["ten"]:
        print(item["title"])

Every call returns the API's JSON as Python values. A refusal raises KwiklError with the HTTP status, the
problem's code and its plain reason. Keep the key out of source code: read it from the environment.

Scopes: read (every GET), write (save, draft, decline, say no), send (approve, send, say yes: these act in
the business's name). Licence: MIT. Source: https://kwikl.com/sdk/kwikl.py
"""

from __future__ import annotations

import json
import os
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Callable
from typing import Any

__version__ = "1.0.0"

Opener = Callable[[urllib.request.Request, float], tuple[int, bytes]]


class KwiklError(Exception):
    def __init__(self, status: int, code: str, detail: str) -> None:
        super().__init__(f"{status} {code}: {detail}")
        self.status, self.code, self.detail = status, code, detail


def _urlopen(req: urllib.request.Request, timeout: float) -> tuple[int, bytes]:
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:  # noqa: S310 — the API's own https address
            return r.status, r.read()
    except urllib.error.HTTPError as e:
        return e.code, e.read()


class Kwikl:
    """One workspace, through one API key."""

    def __init__(
        self,
        key: str | None = None,
        base: str = "https://kwikl.com",
        timeout: float = 30.0,
        opener: Opener | None = None,
    ) -> None:
        self.key = key or os.environ.get("KWIKL_API_KEY", "")
        if not self.key:
            raise KwiklError(401, "no_key", "Pass the key, or set KWIKL_API_KEY.")
        self.base = base.rstrip("/")
        self.timeout = timeout
        self._open = opener or _urlopen

    def _call(self, method: str, path: str, body: Any = None, query: dict[str, Any] | None = None) -> Any:
        url = f"{self.base}/api/v1{path}"
        q = {k: v for k, v in (query or {}).items() if v is not None}
        if q:
            url += "?" + urllib.parse.urlencode(q)
        data = json.dumps(body).encode() if body is not None else None
        req = urllib.request.Request(url, data=data, method=method)
        req.add_header("Authorization", f"Bearer {self.key}")
        req.add_header("Accept", "application/json")
        req.add_header("User-Agent", f"kwikl-python/{__version__}")
        if data is not None:
            req.add_header("Content-Type", "application/json")
        status, raw = self._open(req, self.timeout)
        try:
            out = json.loads(raw.decode() or "null")
        except ValueError:
            out = None
        if status >= 400:
            p = out if isinstance(out, dict) else {}
            raise KwiklError(status, str(p.get("code") or "error"), str(p.get("detail") or p.get("title") or raw[:200]))
        return out

    # the workspace
    def me(self) -> dict[str, Any]:
        return self._call("GET", "/me")

    # businesses and messages
    def businesses(self, min_fit: int = 0, limit: int = 50) -> Any:
        return self._call("GET", "/businesses", query={"min_fit": min_fit, "limit": limit})

    def save_businesses(self, rows: list[dict[str, Any]]) -> Any:
        return self._call("POST", "/businesses", {"rows": rows})

    def messages(self, status: str | None = None) -> Any:
        return self._call("GET", "/messages", query={"status": status})

    def draft(self, prospect_ids: list[str] | None = None, min_fit: int = 0, limit: int = 50) -> Any:
        return self._call("POST", "/messages/draft", {"prospect_ids": prospect_ids, "min_fit": min_fit, "limit": limit})

    def send(self, message_ids: list[str] | None = None) -> Any:
        return self._call("POST", "/messages/send", {"message_ids": message_ids})

    # the yes cards of outreach
    def approvals(self, status: str = "pending") -> Any:
        return self._call("GET", "/approvals", query={"status": status})

    def approve(self, approval_id: str, note: str = "") -> Any:
        return self._call("POST", f"/approvals/{approval_id}/approve", {"note": note})

    def decline(self, approval_id: str, note: str = "") -> Any:
        return self._call("POST", f"/approvals/{approval_id}/decline", {"note": note})

    # results, meetings, proposals, strategy
    def outcomes(self) -> Any:
        return self._call("GET", "/outcomes")

    def meetings(self) -> Any:
        return self._call("GET", "/meetings")

    def book_meeting(self, starts_at: str, with_name: str, with_email: str, title: str = "", notes: str = "") -> Any:
        return self._call("POST", "/meetings", {"starts_at": starts_at, "with_name": with_name, "with_email": with_email,
                                                "title": title, "notes": notes})

    def proposals(self) -> Any:
        return self._call("GET", "/proposals")

    def strategy(self) -> Any:
        return self._call("GET", "/strategy")

    def opportunities(self) -> Any:
        return self._call("GET", "/opportunities")

    # the world
    def country(self, code: str) -> Any:
        return self._call("GET", f"/countries/{urllib.parse.quote(code)}")

    def consent(self, subject: str) -> Any:
        return self._call("GET", f"/consent/{urllib.parse.quote(subject)}")

    def record_consent(self, subject: str, action: str, channel: str = "all", country: str = "", note: str = "") -> Any:
        return self._call("POST", "/consent", {"subject": subject, "action": action, "channel": channel,
                                               "country": country, "note": note})

    def invoices(self) -> Any:
        return self._call("GET", "/invoices")

    # the owner's day (6.2)
    def day(self) -> Any:
        return self._call("GET", "/day")

    def day_cards(self) -> Any:
        return self._call("GET", "/day/cards")

    def yes(self, card_id: str) -> Any:
        return self._call("POST", f"/day/cards/{card_id}/yes")

    def no(self, card_id: str) -> Any:
        return self._call("POST", f"/day/cards/{card_id}/no")

    def deal_desk(self) -> Any:
        return self._call("GET", "/deal-desk")

    def customers(self) -> Any:
        return self._call("GET", "/customers")

    def numbers(self) -> Any:
        return self._call("GET", "/numbers")
