"""Internal action client for cross-app write operations.

Each coop app exposes JSON action endpoints at ``/internal/actions/{name}``.
This module provides helpers to call those endpoints so that callers don't
need direct access to another app's DB session or service layer.

Failures raise ``ActionError`` so callers can handle or propagate them.
"""
from __future__ import annotations

import logging
import os

import httpx

from shared.infrastructure.internal_auth import sign_internal_headers

log = logging.getLogger(__name__)

# Re-usable async client (created lazily, one per process)
_client: httpx.AsyncClient | None = None

# Default request timeout (seconds) — longer than fragments since these are writes
_DEFAULT_TIMEOUT = 5.0

# Header sent on every action request so providers can gate access.
ACTION_HEADER = "X-Internal-Action"


class ActionError(Exception):
    """Raised when an internal action call fails."""

    def __init__(self, message: str, status_code: int = 500, detail: dict | None = None):
        super().__init__(message)
        self.status_code = status_code
        self.detail = detail


def _get_client() -> httpx.AsyncClient:
    global _client
    if _client is None or _client.is_closed:
        _client = httpx.AsyncClient(
            timeout=httpx.Timeout(_DEFAULT_TIMEOUT),
            follow_redirects=False,
        )
    return _client


def _internal_url(app_name: str) -> str:
    """Resolve the Docker-internal base URL for *app_name*."""
    env_key = f"INTERNAL_URL_{app_name.upper()}"
    return os.getenv(env_key, f"http://{app_name}:8000").rstrip("/")


async def call_action(
    app_name: str,
    action_name: str,
    *,
    payload: dict | None = None,
    timeout: float = _DEFAULT_TIMEOUT,
) -> dict:
    """POST JSON to ``{INTERNAL_URL_APP}/internal/actions/{action_name}``.

    Returns the parsed JSON response on 2xx.
    Raises ``ActionError`` on network errors or non-2xx responses.
    """
    base = _internal_url(app_name)
    url = f"{base}/internal/actions/{action_name}"
    try:
        headers = {ACTION_HEADER: "1", **sign_internal_headers(app_name)}
        resp = await _get_client().post(
            url,
            json=payload or {},
            headers=headers,
            timeout=timeout,
        )
        if 200 <= resp.status_code < 300:
            return resp.json()
        msg = f"Action {app_name}/{action_name} returned {resp.status_code}"
        detail = None
        try:
            detail = resp.json()
        except Exception:
            pass
        log.error(msg)
        raise ActionError(msg, status_code=resp.status_code, detail=detail)
    except ActionError:
        raise
    except Exception as exc:
        msg = f"Action {app_name}/{action_name} failed: {exc}"
        log.error(msg)
        raise ActionError(msg) from exc
