"""Paperclip REST API helper.

All HTTP calls from legal-ai backend (FastAPI) to Paperclip should go through
``pc_request`` so that auth + audit headers are applied consistently.

The bash counterpart for agents lives at ``scripts/pc.sh``.

Notes
-----
* Uses ``PAPERCLIP_BOARD_API_KEY`` (long-lived) — these are *board* actions
  (wakeups, comments-as-user) initiated from outside a heartbeat, not agent
  actions. Board API keys are not JWTs, so they do **not** carry a ``run_id``
  claim; pass ``run_id=`` explicitly when you have one (rare for board flows).
* For agent actions inside a heartbeat run, agents use the bash helper with
  the auto-injected ``PAPERCLIP_API_KEY`` JWT — those carry ``run_id`` in
  claims, so the header is informational/future-proofing.
"""
from __future__ import annotations

import logging
import os
from datetime import datetime, timezone
from typing import Any

import httpx

logger = logging.getLogger(__name__)

PAPERCLIP_API_URL = os.environ.get("PAPERCLIP_API_URL", "http://localhost:3100")
PAPERCLIP_BOARD_API_KEY = os.environ.get("PAPERCLIP_BOARD_API_KEY", "")

DEFAULT_TIMEOUT = 15.0


def require_paperclip_db_url() -> str:
    """Return ``PAPERCLIP_DB_URL`` or fail loud — never fall back to a default.

    INV-ENV4 / GAP-57: a credential must not live in source as a default value.
    The Coolify container and local tooling supply this explicitly; its absence
    is a misconfiguration we surface immediately rather than silently connecting
    with a well-known default credential (CWE-798). Mirrors the fail-loud guard
    on ``PAPERCLIP_BOARD_API_KEY`` in ``_build_headers``.
    """
    url = os.environ.get("PAPERCLIP_DB_URL", "")
    if not url:
        raise RuntimeError(
            "PAPERCLIP_DB_URL not set — refusing hard-coded credential fallback "
            "(INV-ENV4 / GAP-57). Set PAPERCLIP_DB_URL in the environment "
            "(Coolify for the container, or your shell/.env for local tooling)."
        )
    return url


def _build_headers(run_id: str | None, has_body: bool) -> dict[str, str]:
    if not PAPERCLIP_BOARD_API_KEY:
        raise RuntimeError("PAPERCLIP_BOARD_API_KEY not set — cannot call Paperclip API")
    headers = {
        "Authorization": f"Bearer {PAPERCLIP_BOARD_API_KEY}",
        "X-Paperclip-Run-Id": run_id or "",
    }
    if has_body:
        headers["Content-Type"] = "application/json"
    return headers


async def pc_request(
    method: str,
    path: str,
    *,
    json: dict[str, Any] | None = None,
    run_id: str | None = None,
    timeout: float = DEFAULT_TIMEOUT,
    raise_on_error: bool = False,
) -> httpx.Response:
    """Make a Paperclip REST request.

    Parameters
    ----------
    method : str
        HTTP method (GET, POST, PATCH, DELETE).
    path : str
        Path relative to PAPERCLIP_API_URL (must start with ``/``).
    json : dict, optional
        Request body — sent as JSON.
    run_id : str, optional
        Heartbeat run ID for audit trail (X-Paperclip-Run-Id header).
        Rare for board actions; provide when initiating from inside a run.
    timeout : float
        httpx timeout (default 15s).
    raise_on_error : bool
        If True, calls ``response.raise_for_status()`` on 4xx/5xx.

    Returns
    -------
    httpx.Response
    """
    headers = _build_headers(run_id, has_body=json is not None)
    url = f"{PAPERCLIP_API_URL}{path}"
    async with httpx.AsyncClient(timeout=timeout) as client:
        resp = await client.request(method, url, headers=headers, json=json)
    if raise_on_error:
        resp.raise_for_status()
    return resp


async def emit_case_status_webhook(
    case_number: str,
    old_status: str,
    new_status: str,
    company_id: str | None = None,
    run_id: str | None = None,
) -> None:
    """Notify the Paperclip plugin that a case status changed.

    Fire-and-forget: logs errors but never raises, so callers aren't blocked.
    """
    try:
        await pc_request(
            "POST",
            "/api/plugins/marcusgroup.legal-ai/webhooks/case-status",
            json={
                "eventType": "status_change",
                "caseNumber": case_number,
                "oldStatus": old_status,
                "newStatus": new_status,
                "companyId": company_id,
                "timestamp": datetime.now(timezone.utc).isoformat(),
            },
            run_id=run_id,
            timeout=5.0,
        )
    except Exception as exc:
        logger.warning(
            "emit_case_status_webhook failed for case %s (%s → %s): %s",
            case_number, old_status, new_status, exc,
        )


async def emit_missing_precedent_webhook(
    *,
    case_number: str,
    missing_precedent_id: str,
    citation: str,
    cited_by_party: str | None = None,
    cited_by_party_name: str | None = None,
    legal_topic: str | None = None,
    legal_issue: str | None = None,
    company_id: str | None = None,
    run_id: str | None = None,
) -> None:
    """Tell the plugin that a missing precedent was logged for a case.

    The plugin uses this to surface an ``askUserQuestions`` interaction
    on the linked Paperclip issue so the chair can decide whether to
    upload the cited precedent or mark it irrelevant.

    Fire-and-forget.
    """
    try:
        await pc_request(
            "POST",
            "/api/plugins/marcusgroup.legal-ai/webhooks/case-status",
            json={
                "eventType": "missing_precedent_created",
                "caseNumber": case_number,
                "companyId": company_id,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "missingPrecedent": {
                    "id": missing_precedent_id,
                    "citation": citation,
                    "citedByParty": cited_by_party,
                    "citedByPartyName": cited_by_party_name,
                    "legalTopic": legal_topic,
                    "legalIssue": legal_issue,
                },
            },
            run_id=run_id,
            timeout=5.0,
        )
    except Exception as exc:
        logger.warning(
            "emit_missing_precedent_webhook failed for case %s (%s): %s",
            case_number, citation, exc,
        )


async def emit_export_complete_webhook(
    *,
    case_number: str,
    docx_filename: str,
    docx_title: str | None = None,
    company_id: str | None = None,
    run_id: str | None = None,
) -> None:
    """Tell the plugin that a final DOCX was exported for a case.

    The plugin uses this to attach a "final decision" document to the
    linked Paperclip issue (markdown body with a download link to the
    DOCX). Binary attachment is intentionally avoided — the SDK's
    ``documents.upsert`` accepts text only.

    Fire-and-forget.
    """
    try:
        await pc_request(
            "POST",
            "/api/plugins/marcusgroup.legal-ai/webhooks/case-status",
            json={
                "eventType": "export_complete",
                "caseNumber": case_number,
                "companyId": company_id,
                "timestamp": datetime.now(timezone.utc).isoformat(),
                "docxFilename": docx_filename,
                "docxTitle": docx_title or f"החלטה סופית — {case_number}",
            },
            run_id=run_id,
            timeout=5.0,
        )
    except Exception as exc:
        logger.warning(
            "emit_export_complete_webhook failed for case %s (%s): %s",
            case_number, docx_filename, exc,
        )
