"""
Cliente mínimo de la API de facturas de ToBooky (Python 3.8+, sólo biblioteca estándar).

    api = TobookyClient("https://www.tobooky.es", "tbk_...", "tbs_...")
    status, body, headers = api.request("POST", "/api/v1/invoices-issued", {...}, idempotency_key="pedido-1234")

Documentación: https://www.tobooky.es/developers/api
"""
import hashlib
import hmac
import json
import os
import secrets
import time
import urllib.error
import urllib.parse
import urllib.request


def canonical_query(query: str) -> str:
    """Parámetros ordenados por nombre y codificados según RFC 3986."""
    pairs = []
    for pair in query.split("&"):
        if not pair:
            continue
        name, _, value = pair.partition("=")
        pairs.append((
            urllib.parse.quote(urllib.parse.unquote_plus(name), safe="-_.~"),
            urllib.parse.quote(urllib.parse.unquote_plus(value), safe="-_.~"),
        ))
    pairs.sort()
    return "&".join(f"{n}={v}" for n, v in pairs)


def sign(secret: str, string: str) -> str:
    return "v1=" + hmac.new(secret.encode(), string.encode(), hashlib.sha256).hexdigest()


class TobookyClient:
    def __init__(self, base: str, key: str, secret: str):
        self.base = base.rstrip("/")
        self.key = key
        self.secret = secret

    def request(self, method, path, data=None, idempotency_key=None):
        method = method.upper()
        query, body = "", b""
        if method == "GET" and data:
            query = urllib.parse.urlencode(data, quote_via=urllib.parse.quote)
        elif data is not None:
            body = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8")

        timestamp = str(int(time.time()))
        nonce = secrets.token_hex(16)
        to_sign = "\n".join([
            "v1", timestamp, nonce, method, path, canonical_query(query), hashlib.sha256(body).hexdigest()
        ])

        headers = {
            "Accept": "application/json",
            "X-Api-Key": self.key,
            "X-Api-Timestamp": timestamp,
            "X-Api-Nonce": nonce,
            "X-Api-Signature": sign(self.secret, to_sign),
        }
        if body:
            headers["Content-Type"] = "application/json"
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key

        url = self.base + path + ("?" + query if query else "")
        req = urllib.request.Request(url, data=body or None, headers=headers, method=method)
        try:
            with urllib.request.urlopen(req, timeout=60) as res:
                status, raw, received = res.status, res.read(), res.headers
        except urllib.error.HTTPError as err:
            status, raw, received = err.code, err.read(), err.headers

        # Comprobamos que la respuesta la firma ToBooky y que contesta a ESTA petición
        signature = received.get("X-Api-Signature")
        if signature:
            expected = sign(self.secret, "\n".join([
                "v1", received.get("X-Api-Timestamp", ""), nonce, str(status), hashlib.sha256(raw).hexdigest()
            ]))
            if not hmac.compare_digest(expected, signature):
                raise RuntimeError("La firma de la respuesta no es válida")

        return status, json.loads(raw or b"null"), dict(received)


if __name__ == "__main__":
    api = TobookyClient(os.environ.get("TOBOOKY_URL", "https://www.tobooky.es"),
                        os.environ["TOBOOKY_KEY"], os.environ["TOBOOKY_SECRET"])
    print(api.request("GET", "/api/v1/ping"))
