from __future__ import annotations

import json
import time
import urllib.error
import urllib.parse
import urllib.request
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Any, Generic, TypedDict, TypeVar, cast

_RETRYABLE = frozenset({429, 502, 503, 504})
T = TypeVar("T")


class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):  # noqa: ANN001, ANN201
        return None


_NO_REDIRECT_OPENER = urllib.request.build_opener(_NoRedirectHandler)


def _open(request: urllib.request.Request, timeout: float):  # noqa: ANN201
    return _NO_REDIRECT_OPENER.open(request, timeout=timeout)


def _validated_origin(origin: str) -> str:
    parsed = urllib.parse.urlsplit(origin)
    local = parsed.hostname in {"localhost", "127.0.0.1", "::1"}
    if (
        parsed.scheme not in ({"http", "https"} if local else {"https"})
        or not parsed.hostname
        or parsed.username is not None
        or parsed.password is not None
        or parsed.path not in {"", "/"}
        or parsed.query
        or parsed.fragment
    ):
        raise ValueError("origin must be an HTTPS API origin (HTTP is allowed only for localhost)")
    return origin.rstrip("/")


class NextAction(TypedDict):
    id: str
    label: str
    method: str
    path: str
    required_scopes: list[str]
    requires_human: bool
    side_effect: str


class Envelope(TypedDict, Generic[T]):  # noqa: UP046 -- package supports Python 3.11
    data: T
    meta: dict[str, Any]
    next_actions: list[NextAction]


@dataclass
class V2ApiError(RuntimeError):
    status: int
    message: str
    code: str | None = None
    request_id: str | None = None
    retry_after: str | None = None
    body: Any = None

    def __str__(self) -> str:
        return f"SynthCrew Agent API V2 {self.status}: {self.message}" + (f" ({self.code})" if self.code else "")


class AgentV2Client:
    """Typed client for the uniform, task-first Agent API V2.

    The key stays private, mutations require replay-safe identifiers, and the
    client never exposes a human-approval method.
    """

    def __init__(self, origin: str, api_key: str, *, timeout: float = 30.0, max_retries: int = 2) -> None:
        if not api_key.startswith("asdr_") or len(api_key) > 512 or any(character.isspace() for character in api_key):
            raise ValueError("api_key must be a valid SynthCrew workspace key")
        if timeout <= 0 or max_retries < 0 or max_retries > 10:
            raise ValueError("timeout must be positive and max_retries must be between 0 and 10")
        self.origin = _validated_origin(origin)
        self._api_key = api_key
        self.timeout = timeout
        self.max_retries = max_retries

    def request(
        self,
        method: str,
        path: str,
        *,
        json_body: Mapping[str, Any] | None = None,
        idempotency_key: str | None = None,
        agent_run_id: str | None = None,
        human_approval_id: str | None = None,
    ) -> Any:
        method = method.upper()
        mutating = method in {"POST", "PUT", "PATCH", "DELETE"}
        if mutating and (not idempotency_key or not agent_run_id):
            raise ValueError("mutations require idempotency_key and agent_run_id")
        headers = {
            "Accept": "application/json",
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self._api_key}",
            "X-API-Version": "2",
        }
        if idempotency_key:
            headers["Idempotency-Key"] = idempotency_key
        if agent_run_id:
            headers["X-Agent-Run-Id"] = agent_run_id
        if human_approval_id:
            headers["X-Human-Approval-Id"] = human_approval_id
        data = None if json_body is None else json.dumps(dict(json_body)).encode()
        if path != "/api/v2/agent" and not path.startswith("/api/v2/agent/"):
            raise ValueError("path must be relative to /api/v2/agent")
        url = self.origin + path
        attempt = 0
        while True:
            request = urllib.request.Request(url, data=data, headers=headers, method=method)
            try:
                with _open(request, self.timeout) as response:
                    raw = response.read()
                    return json.loads(raw) if raw else None
            except urllib.error.HTTPError as error:
                raw = error.read()
                try:
                    payload = json.loads(raw) if raw else None
                except (UnicodeDecodeError, json.JSONDecodeError):
                    payload = None
                if error.code in _RETRYABLE and attempt < self.max_retries and (not mutating or idempotency_key):
                    time.sleep(min(2**attempt, 4))
                    attempt += 1
                    continue
                record = payload if isinstance(payload, dict) else {}
                detail = record.get("detail")
                message = (
                    detail
                    if isinstance(detail, str)
                    else str(detail.get("message"))
                    if isinstance(detail, dict)
                    else str(record.get("title") or error.reason)
                )
                code = (
                    record.get("code")
                    if isinstance(record.get("code"), str)
                    else detail.get("code")
                    if isinstance(detail, dict)
                    else None
                )
                raise V2ApiError(
                    error.code,
                    message,
                    code,
                    error.headers.get("X-Request-ID"),
                    error.headers.get("Retry-After"),
                    payload,
                ) from error
            except urllib.error.URLError as error:
                if attempt < self.max_retries and (not mutating or idempotency_key):
                    time.sleep(min(2**attempt, 4))
                    attempt += 1
                    continue
                raise V2ApiError(0, str(error.reason)) from error

    def _data(self, method: str, path: str, **kwargs: Any) -> Any:
        envelope = self.request(method, path, **kwargs)
        if not isinstance(envelope, dict) or "data" not in envelope:
            raise V2ApiError(0, "Response did not contain the Agent API V2 envelope")
        return envelope["data"]

    def self_test(self) -> dict[str, Any]:
        return cast(dict[str, Any], self._data("GET", "/api/v2/agent/self-test"))

    def catalog(self) -> dict[str, Any]:
        return cast(dict[str, Any], self._data("GET", "/api/v2/agent/catalog"))

    def workflows(self) -> list[dict[str, Any]]:
        return cast(list[dict[str, Any]], self._data("GET", "/api/v2/agent/workflows"))

    def workflow(self, workflow_id: str) -> dict[str, Any]:
        return cast(
            dict[str, Any], self._data("GET", f"/api/v2/agent/workflows/{urllib.parse.quote(workflow_id, safe='')}")
        )

    def actions(self, workflow_id: str | None = None) -> list[dict[str, Any]]:
        suffix = "" if workflow_id is None else "?workflow_id=" + urllib.parse.quote(workflow_id, safe="")
        return cast(list[dict[str, Any]], self._data("GET", "/api/v2/agent/actions" + suffix))

    def list_leads(self) -> Any:
        return self._data("GET", "/api/v2/agent/prospects")

    def list_campaigns(self) -> Any:
        return self._data("GET", "/api/v2/agent/campaigns")

    def list_mailboxes(self) -> Any:
        return self._data("GET", "/api/v2/agent/email-accounts")

    def list_replies(self) -> Any:
        return self._data("GET", "/api/v2/agent/inbox")

    def list_operations(self) -> Any:
        return self._data("GET", "/api/v2/agent/operations")

    def preview(self, payload: Mapping[str, Any], *, idempotency_key: str, agent_run_id: str) -> Any:
        return self._data(
            "POST",
            "/api/v2/agent/operations/preview",
            json_body=payload,
            idempotency_key=idempotency_key,
            agent_run_id=agent_run_id,
        )

    def execute(self, operation_id: str, *, approval_id: str, idempotency_key: str, agent_run_id: str) -> Any:
        return self._data(
            "POST",
            f"/api/v2/agent/operations/{urllib.parse.quote(operation_id, safe='')}/execute",
            idempotency_key=idempotency_key,
            agent_run_id=agent_run_id,
            human_approval_id=approval_id,
        )

    def get_operation(self, operation_id: str) -> Any:
        return self._data("GET", f"/api/v2/agent/operations/{urllib.parse.quote(operation_id, safe='')}")

    def workflow_runtime(self) -> dict[str, Any]:
        return cast(dict[str, Any], self._data("GET", "/api/v2/agent/workflow-runtime"))

    def start_workflow(
        self,
        workflow_input: Mapping[str, Any],
        *,
        idempotency_key: str,
        agent_run_id: str,
        workflow_id: str = "prepare_and_launch_campaign",
    ) -> dict[str, Any]:
        if workflow_id not in {"prepare_and_launch_campaign", "handle_inbound_reply"}:
            raise ValueError("unsupported workflow_id")
        return cast(
            dict[str, Any],
            self._data(
                "POST",
                "/api/v2/agent/workflow-runs",
                json_body={"workflow_id": workflow_id, "input": dict(workflow_input)},
                idempotency_key=idempotency_key,
                agent_run_id=agent_run_id,
            ),
        )

    def start_inbound_reply_workflow(
        self,
        workflow_input: Mapping[str, Any],
        *,
        idempotency_key: str,
        agent_run_id: str,
    ) -> dict[str, Any]:
        """Start the evidence-grounded inbound-reply workflow."""
        return self.start_workflow(
            workflow_input,
            idempotency_key=idempotency_key,
            agent_run_id=agent_run_id,
            workflow_id="handle_inbound_reply",
        )

    def list_workflow_runs(self, *, status: str | None = None, limit: int = 25) -> dict[str, Any]:
        query = urllib.parse.urlencode(
            {key: value for key, value in {"status": status, "limit": limit}.items() if value is not None}
        )
        return cast(dict[str, Any], self._data("GET", "/api/v2/agent/workflow-runs?" + query))

    def get_workflow_run(self, run_id: str) -> dict[str, Any]:
        value = urllib.parse.quote(run_id, safe="")
        return cast(dict[str, Any], self._data("GET", f"/api/v2/agent/workflow-runs/{value}"))

    def workflow_events(self, run_id: str, *, after_sequence: int = 0, limit: int = 50) -> dict[str, Any]:
        value = urllib.parse.quote(run_id, safe="")
        query = urllib.parse.urlencode({"after_sequence": after_sequence, "limit": limit})
        return cast(dict[str, Any], self._data("GET", f"/api/v2/agent/workflow-runs/{value}/events?{query}"))

    def advance_workflow(
        self,
        run_id: str,
        expected_step_id: str,
        *,
        idempotency_key: str,
        agent_run_id: str,
    ) -> dict[str, Any]:
        value = urllib.parse.quote(run_id, safe="")
        return cast(
            dict[str, Any],
            self._data(
                "POST",
                f"/api/v2/agent/workflow-runs/{value}/advance",
                json_body={"expected_step_id": expected_step_id},
                idempotency_key=idempotency_key,
                agent_run_id=agent_run_id,
            ),
        )

    def cancel_workflow(
        self,
        run_id: str,
        *,
        idempotency_key: str,
        agent_run_id: str,
    ) -> dict[str, Any]:
        value = urllib.parse.quote(run_id, safe="")
        return cast(
            dict[str, Any],
            self._data(
                "POST",
                f"/api/v2/agent/workflow-runs/{value}/cancel",
                idempotency_key=idempotency_key,
                agent_run_id=agent_run_id,
            ),
        )
