#!/usr/bin/env python3
import json
import os
import re
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request

"""Sandbox smoke/reference client.

Replace the diagnostic callbacks below with database transactions before
production use. Never log response bodies.
"""

API_BASE = "https://api.integration.boringordertracker.com/v1/"
MAX_RESPONSE_BYTES = 8 * 1024 * 1024


def required_environment(name: str) -> str:
    value = os.environ.get(name)
    if not value:
        raise RuntimeError(f"Set {name}.")
    return value


context = ssl.create_default_context()
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.load_cert_chain(
    required_environment("BOT_CERT"),
    required_environment("BOT_KEY"),
    password=os.environ.get("BOT_KEY_PASSWORD") or None,
)


class NoRedirect(urllib.request.HTTPRedirectHandler):
    """Do not present the client certificate to a redirect target."""

    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


opener = urllib.request.build_opener(
    urllib.request.HTTPSHandler(context=context),
    NoRedirect(),
)


class BotApiProblem(RuntimeError):
    def __init__(
        self,
        status: int,
        problem_code: str,
        request_id: str | None,
        retry_after: int | None,
    ) -> None:
        super().__init__(
            f"BOT API HTTP {status} ({problem_code}), "
            f"request_id={request_id or 'unavailable'}"
        )
        self.status = status
        self.problem_code = problem_code
        self.request_id = request_id
        self.retry_after = retry_after


def read_limited(response) -> bytes:
    raw = response.read(MAX_RESPONSE_BYTES + 1)
    if len(raw) > MAX_RESPONSE_BYTES:
        raise RuntimeError("BOT API response exceeded the local safety limit.")
    return raw


def decode_object(raw: bytes):
    if not raw:
        return None
    decoded = json.loads(raw)
    if not isinstance(decoded, dict):
        raise RuntimeError("BOT API returned a non-object JSON response.")
    return decoded


def request(method: str, path: str, body=None, headers=None):
    encoded = None if body is None else json.dumps(body).encode("utf-8")
    request_headers = {"Accept": "application/json", **dict(headers or {})}
    if encoded is not None:
        request_headers["Content-Type"] = "application/json"
        request_headers["Content-Length"] = str(len(encoded))
    url = urllib.parse.urljoin(API_BASE, path.lstrip("/"))
    parsed = urllib.parse.urlsplit(url)
    if (
        parsed.scheme != "https"
        or parsed.netloc != "api.integration.boringordertracker.com"
        or not parsed.path.startswith("/v1/")
    ):
        raise RuntimeError("Refusing an unexpected API URL.")
    req = urllib.request.Request(
        url,
        data=encoded,
        headers=request_headers,
        method=method,
    )
    try:
        with opener.open(req, timeout=20) as response:
            raw = read_limited(response)
            return response.status, response.headers, decode_object(raw)
    except urllib.error.HTTPError as error:
        if error.code == 304:
            read_limited(error)
            return 304, error.headers, None
        raw = read_limited(error)
        try:
            problem = decode_object(raw) or {}
        except (json.JSONDecodeError, RuntimeError):
            problem = {}
        retry_text = error.headers.get("Retry-After")
        retry_after = (
            max(1, int(retry_text))
            if isinstance(retry_text, str) and retry_text.isdecimal()
            else None
        )
        raise BotApiProblem(
            error.code,
            problem.get("code")
            if isinstance(problem.get("code"), str)
            else "unknown_problem",
            problem.get("request_id")
            if isinstance(problem.get("request_id"), str)
            else None,
            retry_after,
        ) from error


def conditional_headers(etag: str | None) -> dict[str, str]:
    if not etag:
        return {}
    if re.fullmatch(r'(?:W/)?"[A-Za-z0-9._~:-]{1,96}"', etag) is None:
        raise RuntimeError("Refusing an invalid ETag header value.")
    return {"If-None-Match": etag}


def synchronize(cursor, etag, commit_page):
    while True:
        query = {"limit": "500"}
        if cursor:
            query["updated_since"] = cursor
        status, headers, payload = request(
            "GET",
            "/sync?" + urllib.parse.urlencode(query),
            headers=conditional_headers(etag),
        )
        if status == 304:
            return {"cursor": cursor, "etag": etag, "not_modified": True}
        if (
            not isinstance(payload, dict)
            or not isinstance(payload.get("items"), list)
            or not isinstance(payload.get("next_cursor"), str)
            or not isinstance(payload.get("has_more"), bool)
        ):
            raise RuntimeError("Invalid sync response contract.")
        next_cursor = payload["next_cursor"]
        next_etag = headers.get("ETag")

        # This callback must transactionally apply every item in order and
        # persist next_cursor + next_etag. On rollback, keep the prior pair.
        commit_page(
            payload["items"], next_cursor, next_etag, payload["has_more"]
        )
        cursor = next_cursor
        etag = next_etag
        if not payload["has_more"]:
            return {"cursor": cursor, "etag": etag, "not_modified": False}


def read_complete_history(
    collection: str, resource_id: str, cursor, etag, commit_page
):
    if collection not in {"orders", "vehicles"} or re.fullmatch(
        r"[A-Za-z0-9_-]{8,80}", resource_id
    ) is None:
        raise RuntimeError("Invalid resource history target.")
    while True:
        query = {"limit": "500"}
        if cursor:
            query["cursor"] = cursor
        path = (
            f"/{collection}/{urllib.parse.quote(resource_id, safe='')}/history?"
            + urllib.parse.urlencode(query)
        )
        status, headers, payload = request(
            "GET", path, headers=conditional_headers(etag)
        )
        if status == 304:
            return {
                "cursor": cursor,
                "etag": etag,
                "not_modified": True,
                "history_complete": None,
            }
        if (
            not isinstance(payload, dict)
            or not isinstance(payload.get("items"), list)
            or not isinstance(payload.get("next_cursor"), str)
            or not isinstance(payload.get("has_more"), bool)
            or not isinstance(payload.get("history_complete"), bool)
        ):
            raise RuntimeError("Invalid history response contract.")
        next_cursor = payload["next_cursor"]
        next_etag = headers.get("ETag")
        commit_page(
            payload["items"],
            next_cursor,
            next_etag,
            payload["has_more"],
            payload["history_complete"],
        )
        cursor = next_cursor
        etag = next_etag
        if not payload["has_more"]:
            return {
                "cursor": cursor,
                "etag": etag,
                "not_modified": False,
                "history_complete": payload["history_complete"],
            }


def acknowledge_deletion(revocation_id: str):
    if re.fullmatch(r"[A-Za-z0-9_-]{8,80}", revocation_id) is None:
        raise RuntimeError("Invalid revocation ID.")
    return request(
        "POST",
        f"/revocations/{urllib.parse.quote(revocation_id, safe='')}/ack",
        {"deletion_completed": True},
        {"Idempotency-Key": f"ack-{revocation_id}"},
    )


def main() -> None:
    expected_environment = os.environ.get("BOT_EXPECTED_ENVIRONMENT", "sandbox")
    if expected_environment not in {"sandbox", "production"}:
        raise RuntimeError(
            "BOT_EXPECTED_ENVIRONMENT must be sandbox or production."
        )
    capabilities = request("GET", "/capabilities")[2]
    if not isinstance(capabilities, dict) or capabilities.get(
        "environment"
    ) != expected_environment:
        raise RuntimeError("Certificate is bound to the wrong environment.")
    if expected_environment == "production" and (
        capabilities.get("integration_status") != "active"
        or capabilities.get("production_data_enabled") is not True
    ):
        raise RuntimeError("Production is not active/data-enabled; stopping safely.")
    scopes = capabilities.get("scopes")
    print(
        f"Authenticated to {expected_environment}; "
        f"{len(scopes) if isinstance(scopes, list) else 0} scope(s)."
    )

    def diagnostic_sync(items, next_cursor, next_etag, has_more):
        # SANDBOX DIAGNOSTIC ONLY. Production must replace this callback with
        # one DB transaction that applies every upsert/delete in order and
        # persists next_cursor + next_etag only when the transaction commits.
        deletions = sum(
            1
            for item in items
            if isinstance(item, dict) and item.get("operation") == "delete"
        )
        print(
            f"Sync page: {len(items)} item(s), {deletions} deletion(s), "
            f"has_more={has_more}."
        )

    synchronize(
        os.environ.get("BOT_SYNC_CURSOR"),
        os.environ.get("BOT_SYNC_ETAG"),
        diagnostic_sync,
    )

    resource_id = os.environ.get("BOT_RESOURCE_ID")
    collection = os.environ.get("BOT_RESOURCE_COLLECTION")
    if resource_id and collection:

        def diagnostic_history(
            items, next_cursor, next_etag, has_more, history_complete
        ):
            # SANDBOX DIAGNOSTIC ONLY. Persist events and state atomically. Do
            # not label the downloaded prefix complete while this is false.
            print(
                f"History page: {len(items)} event(s), "
                f"complete={history_complete}, has_more={has_more}."
            )

        read_complete_history(
            collection,
            resource_id,
            os.environ.get("BOT_HISTORY_CURSOR"),
            os.environ.get("BOT_HISTORY_ETAG"),
            diagnostic_history,
        )

    revocation_id = os.environ.get("BOT_REVOCATION_ID")
    if revocation_id:
        if os.environ.get("BOT_DELETION_COMPLETED") != "1":
            raise RuntimeError(
                "Refusing revocation ACK: set BOT_DELETION_COMPLETED=1 only "
                "after all copies are deleted."
            )
        acknowledge_deletion(revocation_id)
        print("Deletion acknowledgement accepted.")


try:
    main()
except BotApiProblem as problem:
    if problem.status == 429 and problem.retry_after is not None:
        print(
            f"Rate limited; retry no earlier than {problem.retry_after} seconds.",
            file=sys.stderr,
        )
        raise SystemExit(75) from None
    print(str(problem), file=sys.stderr)
    raise SystemExit(1) from None
except Exception as error:
    print(str(error), file=sys.stderr)
    raise SystemExit(1) from None
