"""HTTP client for security audit via api.aicippy.com + aivedha.ai.

This client communicates with the AiCippy security audit API which proxies
requests to aivedha.ai's AI security analysis engine. Authentication uses
the same auth.aivibe.cloud JWT token but does NOT require CHAKRA plan
validation -- security audit is a cross-service feature available to all
authenticated users.

Architecture:
    CLI -> SecurityAuditClient -> api.aicippy.com/api/v1/security/audit
                                        |
                                        v
                                   aivedha.ai AI engine (HTTP polling)
"""

from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Any

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

from aicippy.security.models import (
    AuditFinding,
    AuditStatus,
    SecurityAuditResult,
)
from aicippy.utils.logging import get_logger

if TYPE_CHECKING:
    from collections.abc import Callable

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 AuditTimeoutError(TimeoutError):
    """Raised when an audit does not complete within the allotted time."""

    def __init__(self, audit_id: str, timeout: float) -> None:
        """Initialize the exception with the audit ID and timeout.

        Args:
            audit_id: The ID of the audit that timed out.
            timeout: The timeout duration in seconds.

        """
        super().__init__(f"Audit {audit_id} did not complete within {timeout}s")


class SecurityAuditClient:
    """Client for security audit via api.aicippy.com + aivedha.ai AI analysis.

    Special auth: Uses the same auth.aivibe.cloud JWT token but does NOT
    require CHAKRA plan validation. This is a cross-service feature
    powered by aivedha.ai's AI security analysis engine.
    """

    __slots__ = ("_base_url", "_client")

    def __init__(
        self,
        auth_token: str,
        base_url: str = "https://api.aicippy.com",
        timeout: float = 60.0,
    ) -> None:
        """Initialize the SecurityAuditClient.

        Args:
            auth_token: Bearer token from Cognito authentication.
            base_url: AiCippy API base URL (default: https://api.aicippy.com).
            timeout: HTTP request timeout in seconds.

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

    # ── Start Audit ────────────────────────────────────────────────────

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

        POST /api/v1/security/audit

        The backend forwards to aivedha.ai for AI-based analysis.
        No plan validation check is performed (special case for security audit).

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

        Returns:
            audit_id: Unique identifier for tracking the audit.

        Raises:
            httpx.HTTPStatusError: On non-2xx response.

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

        logger.info(
            "security_audit_start_request",
            scan_scope=scan_scope,
            target_file_count=len(target_files) if target_files else 0,
        )

        response = await self._client.post(
            "/api/v1/security/audit",
            json=payload,
        )
        response.raise_for_status()

        audit_id = response.json().get("audit_id", "")
        logger.info("security_audit_started", audit_id=audit_id)
        return audit_id

    # ── Get Audit Status ───────────────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def get_audit_status(self, audit_id: str) -> SecurityAuditResult:
        """Get current audit status and any findings so far.

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

        Args:
            audit_id: Identifier of the audit to query.

        Returns:
            SecurityAuditResult with current status and findings.

        Raises:
            httpx.HTTPStatusError: On non-2xx response.

        """
        response = await self._client.get(
            f"/api/v1/security/audit/{audit_id}",
        )
        response.raise_for_status()

        data = response.json()
        return SecurityAuditResult.from_dict(data)

    # ── Poll Audit Results ─────────────────────────────────────────────

    async def poll_audit_results(
        self,
        audit_id: str,
        callback: Callable[[AuditFinding], None] | None = None,
        max_polls: int = 60,
        poll_interval: float = 2.0,
    ) -> SecurityAuditResult:
        """Poll for audit results via HTTP until complete.

        Args:
            audit_id: Identifier of the audit to poll.
            callback: Optional callable invoked with each new AuditFinding.
            max_polls: Maximum number of poll attempts.
            poll_interval: Seconds between poll attempts.

        Returns:
            Final SecurityAuditResult after completion.

        Raises:
            TimeoutError: If audit does not complete within max_polls.

        """
        seen_findings: set[str] = set()

        for _attempt in range(max_polls):
            result = await self.get_audit_status(audit_id)

            # Notify callback of new findings
            if callback:
                for finding in result.findings:
                    fid = getattr(finding, "finding_id", str(id(finding)))
                    if fid not in seen_findings:
                        seen_findings.add(fid)
                        callback(finding)

            if result.status in (AuditStatus.COMPLETE, AuditStatus.ERROR):
                return result

            await asyncio.sleep(poll_interval)

        raise AuditTimeoutError(audit_id, max_polls * poll_interval)

    # ── Get Fix Recommendation ─────────────────────────────────────────

    @retry(**_RETRY_KWARGS)
    async def get_fix_recommendation(self, finding_id: str) -> dict[str, Any]:
        """Get detailed AI-generated fix recommendation from aivedha.ai.

        GET /api/v1/security/audit/findings/{finding_id}/fix

        Args:
            finding_id: Identifier of the finding to get a fix for.

        Returns:
            Dictionary containing:
                - fix_code: Suggested code fix
                - explanation: Detailed explanation of the fix
                - references: Related documentation links

        Raises:
            httpx.HTTPStatusError: On non-2xx response.

        """
        response = await self._client.get(
            f"/api/v1/security/audit/findings/{finding_id}/fix",
        )
        response.raise_for_status()

        result: dict[str, Any] = response.json()
        logger.info(
            "security_audit_fix_retrieved",
            finding_id=finding_id,
        )
        return result

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

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

    async def __aenter__(self) -> SecurityAuditClient:
        """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()
