"""HTTP client for the AiCippy app-specific API (api.aicippy.com).

Handles memory, profile, rules, connectors, and session logging
operations that are specific to the AiCippy application (not shared
across the AiVibe platform).
"""

from __future__ import annotations

from typing import Any

import httpx
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)

from aicippy import __version__
from aicippy.utils.logging import get_logger

logger = get_logger(__name__)

_RETRY_KWARGS: dict[str, Any] = {
    "stop": stop_after_attempt(3),
    "wait": wait_exponential(multiplier=1, min=1, max=4),
    "retry": retry_if_exception_type((httpx.ConnectError, httpx.TimeoutException)),
    "reraise": True,
}


class AppClient:
    """HTTP client for api.aicippy.com app-specific operations."""

    __slots__ = ("_base_url", "_client", "_tenant_id")

    def __init__(
        self,
        base_url: str,
        auth_token: str,
        tenant_id: str,
        timeout: float = 30.0,
    ) -> None:
        """Initialize the AppClient.

        Args:
            base_url: App API base URL (e.g. https://api.aicippy.com).
            auth_token: Bearer token from Cognito authentication.
            tenant_id: Tenant identifier for scoping requests.
            timeout: HTTP request timeout in seconds.

        """
        self._base_url = base_url.rstrip("/")
        self._tenant_id = tenant_id
        self._client = httpx.AsyncClient(
            base_url=self._base_url,
            headers={
                "Authorization": f"Bearer {auth_token}",
                "Content-Type": "application/json",
                "Accept": "application/json",
                "X-Tenant-ID": tenant_id,
                "User-Agent": f"aicippy-cli/{__version__}",
            },
            timeout=httpx.Timeout(timeout),
        )

    # ── Memory / Conversations ──────────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def save_conversation(
        self,
        user_id: str,
        session_id: str,
        messages: list[dict[str, Any]],
    ) -> str:
        """Save a conversation to the platform.

        POST /api/v1/memory/conversations

        Returns:
            Conversation ID.

        """
        response = await self._client.post(
            "/api/v1/memory/conversations",
            json={
                "user_id": user_id,
                "session_id": session_id,
                "messages": messages,
            },
        )
        response.raise_for_status()
        return response.json().get("conversation_id", "")

    @retry(**_RETRY_KWARGS)
    async def get_conversations(
        self,
        user_id: str,
        limit: int = 50,
    ) -> list[dict[str, Any]]:
        """List conversations for a user.

        GET /api/v1/memory/conversations?user_id={user_id}&limit={limit}

        Returns:
            List of conversation summaries.

        """
        response = await self._client.get(
            "/api/v1/memory/conversations",
            params={"user_id": user_id, "limit": limit},
        )
        response.raise_for_status()
        return response.json().get("conversations", [])

    # ── Tenant Memory (key-value) ───────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def save_memory(self, user_id: str, key: str, value: object) -> None:
        """Save a tenant memory entry.

        PUT /api/v1/memory/tenant/{key}
        """
        response = await self._client.put(
            f"/api/v1/memory/tenant/{key}",
            json={"user_id": user_id, "value": value},
        )
        response.raise_for_status()

    @retry(**_RETRY_KWARGS)
    async def get_memory(self, user_id: str, key: str) -> object:
        """Get a tenant memory entry.

        GET /api/v1/memory/tenant/{key}?user_id={user_id}

        Returns:
            The stored value, or None if not found.

        """
        response = await self._client.get(
            f"/api/v1/memory/tenant/{key}",
            params={"user_id": user_id},
        )
        if response.status_code == httpx.codes.NOT_FOUND:
            return None
        response.raise_for_status()
        return response.json().get("value")

    @retry(**_RETRY_KWARGS)
    async def list_memories(self, user_id: str) -> list[dict[str, Any]]:
        """List all tenant memory entries.

        GET /api/v1/memory/tenant?user_id={user_id}

        Returns:
            List of memory entries with key, value, updated_at.

        """
        response = await self._client.get(
            "/api/v1/memory/tenant",
            params={"user_id": user_id},
        )
        response.raise_for_status()
        return response.json().get("entries", [])

    # ── User Profile / Config ───────────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def get_profile(self) -> dict[str, Any]:
        """Get user profile.

        GET /api/v1/profile

        Returns:
            Profile data dict.

        """
        response = await self._client.get("/api/v1/profile")
        response.raise_for_status()
        return response.json()

    @retry(**_RETRY_KWARGS)
    async def update_profile(self, updates: dict[str, Any]) -> dict[str, Any]:
        """Update user profile.

        PATCH /api/v1/profile

        Returns:
            Updated profile data dict.

        """
        response = await self._client.patch(
            "/api/v1/profile",
            json={"updates": updates},
        )
        response.raise_for_status()
        return response.json()

    # ── Rules ───────────────────────────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def get_rules(self) -> list[dict[str, Any]]:
        """Get agent rules.

        GET /api/v1/rules

        Returns:
            List of rule definitions.

        """
        response = await self._client.get("/api/v1/rules")
        response.raise_for_status()
        return response.json().get("rules", [])

    @retry(**_RETRY_KWARGS)
    async def save_rule(self, rule: dict[str, Any]) -> str:
        """Save an agent rule.

        POST /api/v1/rules

        Returns:
            Rule ID.

        """
        response = await self._client.post(
            "/api/v1/rules",
            json={"rule": rule},
        )
        response.raise_for_status()
        return response.json().get("rule_id", "")

    # ── MCP Connectors ──────────────────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def list_connectors(self) -> list[dict[str, Any]]:
        """List MCP connectors.

        GET /api/v1/connectors

        Returns:
            List of connector configurations.

        """
        response = await self._client.get("/api/v1/connectors")
        response.raise_for_status()
        return response.json().get("connectors", [])

    @retry(**_RETRY_KWARGS)
    async def save_connector(self, connector: dict[str, Any]) -> str:
        """Save MCP connector configuration.

        POST /api/v1/connectors

        Returns:
            Connector ID.

        """
        response = await self._client.post(
            "/api/v1/connectors",
            json={"connector": connector},
        )
        response.raise_for_status()
        return response.json().get("connector_id", "")

    # ── Session Logs ────────────────────────────────────────────────────

    async def log_session(self, session_data: dict[str, Any]) -> None:
        """Log session data (fire-and-forget).

        POST /api/v1/logs/sessions
        """
        try:
            response = await self._client.post(
                "/api/v1/logs/sessions",
                json=session_data,
            )
            response.raise_for_status()
        except Exception as e:
            logger.warning("session_log_failed", error=str(e))

    async def log_invocation(self, invocation_data: dict[str, Any]) -> None:
        """Log invocation data (fire-and-forget).

        POST /api/v1/logs/invocations
        """
        try:
            response = await self._client.post(
                "/api/v1/logs/invocations",
                json=invocation_data,
            )
            response.raise_for_status()
        except Exception as e:
            logger.warning("invocation_log_failed", error=str(e))

    # ── Security Audit ─────────────────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def start_security_audit(
        self,
        scan_scope: str = "full",
        target_files: list[str] | None = None,
    ) -> dict[str, Any]:
        """Start a security audit.

        POST /api/v1/security/audit

        This endpoint proxies to aivedha.ai for AI-based security analysis.
        No CHAKRA plan validation is required (special cross-service feature).

        Args:
            scan_scope: Scope of the audit (full, targeted, quick).
            target_files: Optional list of specific file paths to audit.

        Returns:
            Response dict containing at minimum ``audit_id``.

        """
        payload: dict[str, Any] = {"scan_scope": scan_scope}
        if target_files is not None:
            payload["target_files"] = target_files

        response = await self._client.post(
            "/api/v1/security/audit",
            json=payload,
        )
        response.raise_for_status()
        result: dict[str, Any] = response.json()
        return result

    @retry(**_RETRY_KWARGS)
    async def get_security_audit(self, audit_id: str) -> dict[str, Any]:
        """Get security audit status and results.

        GET /api/v1/security/audit/{audit_id}

        Args:
            audit_id: Unique identifier for the audit.

        Returns:
            Response dict containing audit status and findings.

        """
        response = await self._client.get(
            f"/api/v1/security/audit/{audit_id}",
        )
        response.raise_for_status()
        result: dict[str, Any] = response.json()
        return result

    # ── Lifecycle ───────────────────────────────────────────────────────

    async def close(self) -> None:
        """Close the underlying HTTP client."""
        await self._client.aclose()

    async def __aenter__(self) -> AppClient:
        """Enter the async context manager."""
        return self

    async def __aexit__(self, *args: object) -> None:
        """Exit the async context manager and close the client."""
        await self.close()
