"""LumaTrack emitters for Python automations and AI agents.

Stdlib only: vendor this file or copy it into your project. Every recorded
run is one POST to /api/v1/runs with your org-scoped API key. The base URL
defaults to https://lumatrack.io; pass your own host when self-hosting.

Quick start::

    from lumatrack_emitters import LumaTrackClient, track_run

    client = LumaTrackClient(api_key="lmt_...")  # or ("https://your-host", "lmt_...")

    # explicit
    client.record_run("invoice-agent", duration_seconds=12,
                      metadata={"tokens": 4210})

    # or wrap any block; failures are reported as failures, then re-raised
    with track_run(client, "invoice-agent", metadata={"model": "gpt-5"}):
        run_my_agent()

LangChain: pass ``LumaTrackLangChainHandler(client, "invoice-agent")`` in
``callbacks=[...]``; it reports on chain end/error with token usage when
the provider exposes it. OpenAI Agents SDK and everything else: wrap the
agent invocation in ``track_run``.
"""

from __future__ import annotations

import json
import os
import sys
import time
import uuid
from contextlib import contextmanager
from urllib.request import Request, urlopen

__all__ = ["LumaTrackClient", "track_run", "LumaTrackLangChainHandler"]


class LumaTrackClient:
    def __init__(
        self,
        base_url: str | None = None,
        api_key: str | None = None,
        *,
        source: str = "python",
        timeout: int = 10,
    ):
        # https, not http: the edge 301s plain http and a redirected POST
        # drops its body (and would resend the key over cleartext first).
        base_url = base_url or os.environ.get("LUMATRACK_URL", "") or "https://lumatrack.io"
        api_key = api_key or os.environ.get("LUMATRACK_API_KEY", "")
        if not api_key:
            raise ValueError("Pass api_key, or set LUMATRACK_API_KEY in the environment.")
        self.base_url = base_url.rstrip("/")
        self.api_key = api_key
        self.source = source
        self.timeout = timeout

    def record_run(
        self,
        automation: str,
        *,
        status: str = "success",
        duration_seconds: int | None = None,
        external_id: str | None = None,
        metadata: dict | None = None,
        executed_at: str | None = None,
    ) -> dict:
        """POST one run event. Returns the API's response body as a dict.

        Pass ``external_id`` (your run/job id) to make retries idempotent.
        ``executed_at`` (ISO 8601) is for evidence that genuinely happened
        earlier; leave it unset for live events.
        Raises urllib.error.HTTPError on 4xx/5xx so failures are loud.
        """
        payload = {"automation": automation, "status": status, "source": self.source}
        if duration_seconds is not None:
            payload["duration_seconds"] = int(duration_seconds)
        if external_id is not None:
            payload["external_id"] = str(external_id)
        if metadata:
            payload["metadata"] = metadata
        if executed_at is not None:
            payload["executed_at"] = str(executed_at)
        request = Request(
            f"{self.base_url}/api/v1/runs",
            data=json.dumps(payload).encode(),
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            },
        )
        with urlopen(request, timeout=self.timeout) as response:
            return json.loads(response.read())


@contextmanager
def track_run(client: LumaTrackClient, automation: str, *, metadata: dict | None = None):
    """Time a block and report it as one run; exceptions report a failure
    and propagate unchanged."""
    started = time.monotonic()
    external_id = uuid.uuid4().hex
    try:
        yield
    except BaseException:
        # The failure report must never replace the exception in flight:
        # the caller's error handling sees the ORIGINAL, always.
        try:
            client.record_run(
                automation,
                status="failure",
                duration_seconds=int(time.monotonic() - started),
                external_id=external_id,
                metadata=metadata,
            )
        except Exception as report_error:
            print(f"lumatrack: failure report failed: {report_error}", file=sys.stderr)
        raise
    client.record_run(
        automation,
        status="success",
        duration_seconds=int(time.monotonic() - started),
        external_id=external_id,
        metadata=metadata,
    )


class LumaTrackLangChainHandler:
    """LangChain callback handler, duck-typed so this module stays
    dependency-free. Pass an instance in ``callbacks=[...]``; token usage is
    attached when the LLM provider reports it.
    """

    def __init__(self, client: LumaTrackClient, automation: str):
        self.client = client
        self.automation = automation
        self._started: dict = {}

    # LangChain calls these; unknown hooks are simply never invoked.
    def on_chain_start(self, serialized, inputs, *, run_id, **kwargs):
        self._started[str(run_id)] = time.monotonic()

    def on_chain_end(self, outputs, *, run_id, **kwargs):
        self._report(run_id, "success", outputs)

    def on_chain_error(self, error, *, run_id, **kwargs):
        self._report(run_id, "failure", None)

    def _report(self, run_id, status, outputs):
        started = self._started.pop(str(run_id), None)
        if started is None:
            # Child chains propagate callbacks without a matching start we
            # tracked; reporting them would double-count one invocation.
            return
        duration = int(time.monotonic() - started)
        metadata = {}
        usage = getattr(outputs, "llm_output", None) or {}
        if isinstance(usage, dict) and usage.get("token_usage"):
            metadata["token_usage"] = usage["token_usage"]
        try:
            self.client.record_run(
                self.automation,
                status=status,
                duration_seconds=duration,
                external_id=str(run_id),
                metadata=metadata or None,
            )
        except Exception as report_error:
            # Telemetry must never crash the chain it is observing.
            print(f"lumatrack: run report failed: {report_error}", file=sys.stderr)
