#!/usr/bin/env python3
"""BugTape server reporter — examples-only helper (not a PyPI package).

Posts one failure to POST /v1/ingest with platform=server.
Redacts query text and table-like tokens from description/metadata by default.

Usage:
  export BUGTAPE_KEY=bt_test_...
  export BUGTAPE_RELEASE=2026.09.21
  python3 bugtape_report.py   # runs a synthetic DeadlineExceeded demo
  # or: from bugtape_report import report_failure

Ingest contract unchanged: X-BugTape-Key + JSON body. No fingerprint changes.
"""

from __future__ import annotations

import json
import os
import re
import sys
import traceback
import urllib.error
import urllib.request
from typing import Any, Mapping, MutableMapping, Optional

DEFAULT_ENDPOINT = "https://app.bugtape.ai/v1/ingest"

# Redact SQL-ish and path tokens that often carry client identifiers.
_REDACT_PATTERNS: tuple[re.Pattern[str], ...] = (
    re.compile(r"(?i)\b(select|with|insert|update|delete|merge)\b[\s\S]{0,800}"),
    re.compile(r"(?i)\b[\w.-]+\.(?:parquet|csv|json|avro)\b"),
    re.compile(r"(?i)\b(?:`?[A-Za-z_][\w-]*`?\.){1,3}`?[A-Za-z_][\w-]*`?"),  # project.dataset.table
    re.compile(r"(?i)\bgs://[^\s]+"),
    re.compile(r"(?i)\b(?:client|customer|account)[_-]?id\s*[:=]\s*\S+"),
)


def redact_text(value: str, *, placeholder: str = "[REDACTED]") -> str:
    """Scrub query text / table paths from a free-form string."""
    out = value
    for pattern in _REDACT_PATTERNS:
        out = pattern.sub(placeholder, out)
    return out


def redact_mapping(data: Mapping[str, Any]) -> dict[str, Any]:
    """Shallow-redact string values in a metadata object."""
    cleaned: dict[str, Any] = {}
    for key, raw in data.items():
        if isinstance(raw, str):
            cleaned[key] = redact_text(raw)
        elif isinstance(raw, Mapping):
            cleaned[key] = redact_mapping(raw)
        else:
            cleaned[key] = raw
    return cleaned


def _error_title(exc: BaseException) -> str:
    name = type(exc).__name__
    if not name.endswith("Error") and not name.endswith("Exception"):
        name = f"{name}Error"
    # Prefer short message; fingerprint normalises ids/numbers later server-side.
    msg = str(exc).strip().splitlines()[0][:240] or name
    return f"{name}: {msg}"


def build_payload(
    exc: BaseException,
    *,
    url: str,
    release: str,
    environment: str = "production",
    user_id: Optional[str] = None,
    description: Optional[str] = None,
    metadata: Optional[Mapping[str, Any]] = None,
    severity: str = "high",
    redact: bool = True,
) -> dict[str, Any]:
    """Build a single ingest body. One call → one POST → one failure occurrence."""
    title = _error_title(exc)
    stack = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
    desc = description if description is not None else str(exc)
    meta: MutableMapping[str, Any] = dict(metadata or {})
    if redact:
        # Keep the Error-token prefix; scrub the message side (SQL / table paths).
        kind, _, rest = title.partition(": ")
        title = f"{kind}: {redact_text(rest)}" if rest else redact_text(title)
        desc = redact_text(desc)
        meta = redact_mapping(meta)
        stack = redact_text(stack)
    body: dict[str, Any] = {
        "title": title,
        "description": desc,
        "severity": severity,
        "platform": "server",
        "release": release,
        "environment": environment,
        "url": url,
        "userAgent": f"bugtape-python-example/{release}",
        "events": [
            {
                "type": "error",
                "data": {
                    "message": title,
                    "stack": stack,
                },
            }
        ],
    }
    if user_id:
        body["userId"] = user_id
    if meta:
        body["metadata"] = dict(meta)
    return body


def report_failure(
    exc: BaseException,
    *,
    url: str,
    api_key: Optional[str] = None,
    endpoint: Optional[str] = None,
    release: Optional[str] = None,
    environment: str = "production",
    user_id: Optional[str] = None,
    description: Optional[str] = None,
    metadata: Optional[Mapping[str, Any]] = None,
    severity: str = "high",
    redact: bool = True,
    timeout_s: float = 5.0,
) -> dict[str, Any]:
    """POST one failure. Never raises into the caller job — returns {ok, status, body|error}."""
    key = api_key or os.environ.get("BUGTAPE_KEY") or os.environ.get("BUGTAPE_API_KEY")
    if not key:
        return {"ok": False, "error": "missing BUGTAPE_KEY"}
    rel = release or os.environ.get("BUGTAPE_RELEASE") or os.environ.get("RELEASE") or "unknown"
    dest = endpoint or os.environ.get("BUGTAPE_ENDPOINT") or DEFAULT_ENDPOINT
    payload = build_payload(
        exc,
        url=url,
        release=rel,
        environment=environment,
        user_id=user_id,
        description=description,
        metadata=metadata,
        severity=severity,
        redact=redact,
    )
    raw = json.dumps(payload).encode("utf-8")
    req = urllib.request.Request(
        dest,
        data=raw,
        method="POST",
        headers={
            "Content-Type": "application/json",
            "X-BugTape-Key": key,
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout_s) as resp:
            body_text = resp.read().decode("utf-8", errors="replace")
            try:
                parsed: Any = json.loads(body_text) if body_text else {}
            except json.JSONDecodeError:
                parsed = {"raw": body_text}
            return {"ok": True, "status": resp.status, "body": parsed}
    except urllib.error.HTTPError as err:
        err_body = err.read().decode("utf-8", errors="replace")
        return {"ok": False, "status": err.code, "error": err_body}
    except Exception as err:  # noqa: BLE001 — reporter must not take down the job
        return {"ok": False, "error": f"{type(err).__name__}: {err}"}


class DeadlineExceededError(Exception):
    """Stand-in for google.api_core.exceptions.DeadlineExceeded in demos/tests."""


def _demo() -> int:
    """Synthetic BigQuery-timeout style failure → one ingest POST."""
    try:
        raise DeadlineExceededError(
            "404s timed out after 30s; query SELECT * FROM `acme-proj.analytics.clients` LIMIT 10"
        )
    except DeadlineExceededError as exc:
        result = report_failure(
            exc,
            url="job://analytics/bq/load",
            description="BigQuery load exceeded deadline; SQL redacted by default.",
            metadata={
                "job": "nightly_client_rollup",
                "table": "acme-proj.analytics.clients",
                "attempt": 2,
            },
        )
        print(json.dumps(result, indent=2))
        return 0 if result.get("ok") else 1


if __name__ == "__main__":
    sys.exit(_demo())
