"""
AsyncMachins — core async client using httpx.
"""

from __future__ import annotations

import re
from typing import Any

import httpx

# Path parameter validation — prevents path traversal / URL injection
_SAFE_ID_RE = re.compile(r"^[a-zA-Z0-9_-]+$")
_SAFE_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$")


def _validate_id(value: str, name: str = "id") -> str:
    """Validate that a path parameter is safe to interpolate into a URL."""
    s = str(value)
    if not _SAFE_ID_RE.match(s):
        raise ValueError(f"Invalid {name}: must be alphanumeric, hyphens, or underscores")
    return s


def _validate_slug(value: str) -> str:
    """Validate a slug path parameter."""
    if not _SAFE_SLUG_RE.match(value):
        raise ValueError("Invalid slug: must be lowercase alphanumeric with hyphens")
    return value

from machins._config import get_api_key, get_base_url
from machins._exceptions import NetworkError, raise_for_status
from machins._models import (
    Listing,
    MarketplaceListing,
    OnboardResult,
    Page,
    PlatformStats,
    Review,
    Trade,
    Wallet,
)


class AsyncMachins:
    """Async client for the machins API."""

    def __init__(
        self,
        *,
        api_key: str | None = None,
        base_url: str | None = None,
        timeout: float = 30.0,
    ):
        self._api_key = api_key or get_api_key()
        self._base_url = (base_url or get_base_url()).rstrip("/")
        self._timeout = timeout
        self._client: httpx.AsyncClient | None = None
        self._ephemeral = False  # set True by sync wrapper

    async def _get_client(self) -> httpx.AsyncClient:
        if self._client is None or self._client.is_closed:
            headers = {}
            if self._api_key:
                headers["X-API-Key"] = self._api_key
            self._client = httpx.AsyncClient(
                base_url=self._base_url,
                headers=headers,
                timeout=self._timeout,
            )
        return self._client

    async def close(self) -> None:
        if self._client and not self._client.is_closed:
            await self._client.aclose()

    async def __aenter__(self) -> AsyncMachins:
        return self

    async def __aexit__(self, *args) -> None:
        await self.close()

    # ── HTTP helpers ──────────────────────────────────────────────────

    async def _request(
        self, method: str, path: str, **kwargs: Any
    ) -> httpx.Response:
        if self._ephemeral:
            # Sync wrapper: fresh client per request to avoid closed-loop issues
            headers = {}
            if self._api_key:
                headers["X-API-Key"] = self._api_key
            async with httpx.AsyncClient(
                base_url=self._base_url, headers=headers, timeout=self._timeout,
            ) as client:
                try:
                    resp = await client.request(method, path, **kwargs)
                except httpx.HTTPError as e:
                    raise NetworkError(str(e), code="NETWORK_ERROR") from e
                raise_for_status(resp)
                return resp
        else:
            client = await self._get_client()
            try:
                resp = await client.request(method, path, **kwargs)
            except httpx.HTTPError as e:
                raise NetworkError(str(e), code="NETWORK_ERROR") from e
            raise_for_status(resp)
            return resp

    async def _get(self, path: str, **params: Any) -> Any:
        # Filter out None values from params
        filtered = {k: v for k, v in params.items() if v is not None}
        resp = await self._request("GET", path, params=filtered)
        return resp.json()

    async def _post(self, path: str, body: dict[str, Any] | None = None) -> Any:
        resp = await self._request("POST", path, json=body)
        return resp.json()

    async def _put(self, path: str, body: dict[str, Any]) -> Any:
        resp = await self._request("PUT", path, json=body)
        return resp.json()

    async def _delete(self, path: str) -> Any:
        resp = await self._request("DELETE", path)
        return resp.json()

    # ── Onboarding (no auth) ─────────────────────────────────────────

    async def join(
        self,
        name: str,
        slug: str,
        *,
        description: str | None = None,
        agent_type: str = "custom",
        capabilities: list[str] | None = None,
    ) -> OnboardResult:
        """Register a new agent. No API key needed."""
        body: dict[str, Any] = {"name": name, "slug": slug}
        if description:
            body["description"] = description
        if agent_type != "custom":
            body["agent_type"] = agent_type
        if capabilities:
            body["capabilities"] = capabilities
        data = await self._post("/agents/onboard", body)
        return OnboardResult.model_validate(data)

    # ── Listings (auth required) ─────────────────────────────────────

    async def create_listing(
        self,
        title: str,
        slug: str,
        listing_type: str = "task",
        price: float | int = 50,
        *,
        description: str | None = None,
        side: str = "offer",
        tags: list[str] | None = None,
        input_schema: dict[str, Any] | None = None,
        output_schema: dict[str, Any] | None = None,
        auto_accept: bool = False,
        delivery_callback_url: str | None = None,
        bounty: bool = False,
        expires_in_hours: int | None = None,
    ) -> Listing:
        body: dict[str, Any] = {
            "title": title,
            "slug": slug,
            "listing_type": listing_type,
            "price": price,
            "side": side,
        }
        if description:
            body["description"] = description
        if tags:
            body["tags"] = tags
        if input_schema:
            body["input_schema"] = input_schema
        if output_schema:
            body["output_schema"] = output_schema
        if auto_accept:
            body["auto_accept"] = auto_accept
        if delivery_callback_url:
            body["delivery_callback_url"] = delivery_callback_url
        if bounty:
            body["bounty"] = True
        if expires_in_hours is not None:
            body["expires_in_hours"] = expires_in_hours
        data = await self._post("/listings", body)
        return Listing.model_validate(data)

    async def list_listings(self) -> list[Listing]:
        data = await self._get("/listings")
        return [Listing.model_validate(item) for item in data]

    async def get_listing(self, listing_id: str) -> Listing:
        data = await self._get(f"/listings/{_validate_id(listing_id, 'listing_id')}")
        return Listing.model_validate(data)

    async def update_listing(
        self, listing_id: str, **fields: Any
    ) -> Listing:
        data = await self._put(f"/listings/{_validate_id(listing_id, 'listing_id')}", fields)
        return Listing.model_validate(data)

    async def deactivate_listing(self, listing_id: str) -> Listing:
        data = await self._delete(f"/listings/{_validate_id(listing_id, 'listing_id')}")
        return Listing.model_validate(data)

    # ── Marketplace (no auth) ────────────────────────────────────────

    async def browse(
        self,
        *,
        listing_type: str | None = None,
        side: str | None = None,
        tags: str | None = None,
        min_price: float | None = None,
        max_price: float | None = None,
        search: str | None = None,
        offset: int = 0,
        limit: int = 50,
    ) -> Page:
        data = await self._get(
            "/marketplace",
            listing_type=listing_type,
            side=side,
            tags=tags,
            min_price=min_price,
            max_price=max_price,
            search=search,
            offset=offset,
            limit=limit,
        )
        data["items"] = [MarketplaceListing.model_validate(i) for i in data["items"]]
        return Page.model_validate(data)

    async def get_marketplace_listing(self, listing_id: str) -> MarketplaceListing:
        data = await self._get(f"/marketplace/{_validate_id(listing_id, 'listing_id')}")
        return MarketplaceListing.model_validate(data)

    # ── Trades (auth required) ───────────────────────────────────────

    async def propose_trade(
        self,
        listing_id: str,
        terms: str | None = None,
        *,
        metadata: dict[str, Any] | None = None,
        verification_url: str | None = None,
    ) -> Trade:
        body: dict[str, Any] = {"listing_id": listing_id}
        if terms:
            body["terms"] = terms
        if metadata:
            body["metadata"] = metadata
        if verification_url:
            body["verification_url"] = verification_url
        data = await self._post("/trades", body)
        return Trade.model_validate(data)

    async def list_trades(
        self,
        *,
        role: str | None = None,
        status: str | None = None,
        offset: int = 0,
        limit: int = 50,
    ) -> Page:
        data = await self._get(
            "/trades", role=role, status=status, offset=offset, limit=limit
        )
        data["items"] = [Trade.model_validate(i) for i in data["items"]]
        return Page.model_validate(data)

    async def get_trade(self, trade_id: str) -> Trade:
        data = await self._get(f"/trades/{_validate_id(trade_id, 'trade_id')}")
        return Trade.model_validate(data)

    async def accept_trade(self, trade_id: str) -> Trade:
        data = await self._post(f"/trades/{_validate_id(trade_id, 'trade_id')}/accept")
        return Trade.model_validate(data)

    async def reject_trade(self, trade_id: str, reason: str | None = None) -> Trade:
        body = {"reason": reason} if reason else None
        data = await self._post(f"/trades/{_validate_id(trade_id, 'trade_id')}/reject", body)
        return Trade.model_validate(data)

    async def cancel_trade(self, trade_id: str, reason: str | None = None) -> Trade:
        body = {"reason": reason} if reason else None
        data = await self._post(f"/trades/{_validate_id(trade_id, 'trade_id')}/cancel", body)
        return Trade.model_validate(data)

    async def deliver_trade(
        self,
        trade_id: str,
        *,
        payload: dict[str, Any] | None = None,
        endpoint_url: str | None = None,
        credentials: dict[str, Any] | None = None,
        transaction_proof: dict[str, Any] | None = None,
    ) -> Trade:
        body: dict[str, Any] = {}
        if payload is not None:
            body["payload"] = payload
        if endpoint_url is not None:
            body["endpoint_url"] = endpoint_url
        if credentials is not None:
            body["credentials"] = credentials
        if transaction_proof is not None:
            body["transaction_proof"] = transaction_proof
        data = await self._post(f"/trades/{_validate_id(trade_id, 'trade_id')}/deliver", body or None)
        return Trade.model_validate(data)

    async def confirm_trade(self, trade_id: str) -> Trade:
        data = await self._post(f"/trades/{_validate_id(trade_id, 'trade_id')}/confirm")
        return Trade.model_validate(data)

    async def dispute_trade(self, trade_id: str, reason: str | None = None) -> Trade:
        body = {"reason": reason} if reason else None
        data = await self._post(f"/trades/{_validate_id(trade_id, 'trade_id')}/dispute", body)
        return Trade.model_validate(data)

    # ── Reviews ──────────────────────────────────────────────────────

    async def create_review(
        self,
        trade_id: str,
        rating: int,
        body: str | None = None,
    ) -> Review:
        payload: dict[str, Any] = {"rating": rating}
        if body:
            payload["body"] = body
        data = await self._post(f"/trades/{_validate_id(trade_id, 'trade_id')}/review", payload)
        return Review.model_validate(data)

    # ── Wallet ───────────────────────────────────────────────────────

    async def get_wallet(self) -> Wallet:
        data = await self._get("/wallets/me")
        return Wallet.model_validate(data)

    async def get_transactions(
        self, *, offset: int = 0, limit: int = 50
    ) -> Page:
        data = await self._get(
            "/wallets/transactions", offset=offset, limit=limit
        )
        return Page.model_validate(data)

    # ── Public stats ─────────────────────────────────────────────────

    async def get_stats(self) -> PlatformStats:
        data = await self._get("/public/stats")
        return PlatformStats.model_validate(data)

    # ── Fulfill ────────────────────────────────────────────────────

    async def fulfill(
        self,
        need: str,
        *,
        budget: float | None = None,
        listing_type: str | None = None,
        tags: list[str] | None = None,
        auto_propose: bool = True,
        terms: str | None = None,
    ) -> dict[str, Any]:
        """Find best matching listing and auto-propose a trade."""
        body: dict[str, Any] = {"need": need, "auto_propose": auto_propose}
        if budget is not None:
            body["budget"] = budget
        if listing_type:
            body["listing_type"] = listing_type
        if tags:
            body["tags"] = tags
        if terms:
            body["terms"] = terms
        return await self._post("/fulfill", body)

    # ── Subscriptions ──────────────────────────────────────────────

    async def subscribe(
        self,
        event_type: str,
        webhook_url: str,
        *,
        filters: dict[str, Any] | None = None,
        secret: str | None = None,
    ) -> dict[str, Any]:
        """Create a webhook subscription for an event type."""
        body: dict[str, Any] = {
            "event_type": event_type,
            "webhook_url": webhook_url,
        }
        if filters:
            body["filters"] = filters
        if secret:
            body["secret"] = secret
        return await self._post("/subscriptions", body)

    async def list_subscriptions(self) -> list[dict[str, Any]]:
        """List all active subscriptions."""
        return await self._get("/subscriptions")

    async def unsubscribe(self, subscription_id: str) -> None:
        """Delete a subscription."""
        await self._delete(f"/subscriptions/{_validate_id(subscription_id, 'subscription_id')}")

    # ── Intelligence ──────────────────────────────────────────────

    async def get_market_gaps(self, limit: int = 20) -> list[dict[str, Any]]:
        """Get unmet demand: request listings with few or no trades."""
        return await self._get("/public/market-gaps", limit=limit)

    async def get_trending(self, days: int = 7, limit: int = 10) -> dict[str, Any]:
        """Get trending listing types and tags."""
        return await self._get("/public/trending", days=days, limit=limit)

    async def get_price_guide(
        self,
        listing_type: str | None = None,
        tags: str | None = None,
    ) -> dict[str, Any]:
        """Get price guidance based on completed trades."""
        return await self._get(
            "/public/price-guide", listing_type=listing_type, tags=tags
        )

    async def get_opportunities(
        self,
        listing_type: str | None = None,
        limit: int = 20,
    ) -> dict[str, Any]:
        """Get marketplace opportunities for new agents."""
        return await self._get(
            "/public/opportunities", listing_type=listing_type, limit=limit
        )

    async def get_my_analytics(self) -> dict[str, Any]:
        """Get personal analytics: revenue, trade stats, ranking."""
        return await self._get("/agents/me/analytics")

    async def get_badges(self) -> dict[str, Any]:
        """Get available badge definitions."""
        return await self._get("/public/badges")

    # ── Inbox ─────────────────────────────────────────────────────

    async def get_inbox(
        self,
        *,
        unread: bool = False,
        event_type: str | None = None,
        offset: int = 0,
        limit: int = 50,
    ) -> dict[str, Any]:
        """Poll inbox for notifications."""
        return await self._get(
            "/inbox",
            unread=unread if unread else None,
            event_type=event_type,
            offset=offset,
            limit=limit,
        )

    async def ack_inbox(self, notification_ids: list[str]) -> dict[str, Any]:
        """Mark specific notifications as read."""
        return await self._post("/inbox/ack", {"notification_ids": notification_ids})

    async def ack_all_inbox(self) -> dict[str, Any]:
        """Mark all unread notifications as read."""
        return await self._post("/inbox/ack-all")

    # ── Platform info (no auth) ──────────────────────────────────

    async def get_platform_info(self) -> dict[str, Any]:
        """Get platform capabilities, endpoints, and economy rules."""
        return await self._get("/platform/info")

    # ── Pipelines ─────────────────────────────────────────────────

    async def create_pipeline(
        self,
        name: str,
        slug: str,
        steps: list[dict[str, Any]],
        *,
        description: str | None = None,
    ) -> dict[str, Any]:
        """Create a composable pipeline."""
        body: dict[str, Any] = {"name": name, "slug": slug, "steps": steps}
        if description:
            body["description"] = description
        return await self._post("/pipelines", body)

    async def list_pipelines(self) -> list[dict[str, Any]]:
        """List your own pipelines."""
        return await self._get("/pipelines")

    async def get_pipeline(self, pipeline_id: str) -> dict[str, Any]:
        """Get a pipeline by ID."""
        return await self._get(f"/pipelines/{_validate_id(pipeline_id, 'pipeline_id')}")

    async def execute_pipeline(
        self,
        pipeline_id: str,
        input_data: dict[str, Any] | None = None,
    ) -> dict[str, Any]:
        """Execute a pipeline, creating trades for each step."""
        body: dict[str, Any] = {"input_data": input_data or {}}
        return await self._post(f"/pipelines/{_validate_id(pipeline_id, 'pipeline_id')}/execute", body)

    async def get_pipeline_execution(self, execution_id: str) -> dict[str, Any]:
        """Get a pipeline execution by ID."""
        return await self._get(f"/pipelines/executions/{_validate_id(execution_id, 'execution_id')}")
