"""Scans resource for the Recon API.

Provides CRUD operations for recon scans and report downloads via
:class:`ScansResource`. All methods return typed Pydantic models and
raise exceptions from ``sika._errors`` on API errors.
"""

from __future__ import annotations

import time
from typing import TYPE_CHECKING, Any

from sika._errors import SikaError
from sika._pagination import PageIterator
from sika.recon.models import ReportResponse, Scan, ScanStatus

if TYPE_CHECKING:
    from sika._http import SikaHTTPClient

_SCANS_PATH = "/recon/v1/scans"


class ScansResource:
    """Client for Recon scan operations.

    Accessed via ``client.recon.scans``. Provides methods to create,
    retrieve, list, delete scans, and download reports.

    Args:
        http: The SDK HTTP client for making API requests.
    """

    def __init__(self, http: SikaHTTPClient) -> None:
        self._http = http

    def create(
        self,
        domain: str,
        *,
        scan_type: str = "domain_scan",
        target_asn: int | None = None,
        options: dict[str, Any] | None = None,
        tags: list[str] | None = None,
    ) -> Scan:
        """Submit a new recon scan.

        Args:
            domain: Target domain to scan (e.g. ``example.com``).
            scan_type: Type of scan — ``"domain_scan"`` (default) or
                ``"provider_scan"``.
            target_asn: Target ASN for provider scans.
            options: Additional scan configuration options.
            tags: Labels for grouping and filtering scans.

        Returns:
            The newly created :class:`~sika.recon.models.Scan`.

        Raises:
            ValidationError: If the domain is invalid (400).
            AuthenticationError: If not authenticated (401).
            QuotaExceededError: If the scan quota is exhausted (429).
        """
        body: dict[str, Any] = {"domain": domain, "scan_type": scan_type}
        if target_asn is not None:
            body["target_asn"] = target_asn
        if options is not None:
            body["options"] = options
        if tags is not None:
            body["tags"] = tags

        data = self._http.post(_SCANS_PATH, json=body)
        return Scan.model_validate(data)

    def get(self, scan_id: str) -> Scan:
        """Retrieve a scan by ID.

        Args:
            scan_id: UUID of the scan to retrieve.

        Returns:
            The :class:`~sika.recon.models.Scan` with full details.

        Raises:
            NotFoundError: If the scan does not exist (404).
            AuthenticationError: If not authenticated (401).
        """
        data = self._http.get(f"{_SCANS_PATH}/{scan_id}")
        return Scan.model_validate(data)

    def list(
        self,
        *,
        status: ScanStatus | None = None,
        limit: int = 20,
    ) -> PageIterator[Scan]:
        """List scans with optional filtering.

        Returns a :class:`~sika.PageIterator` that lazily fetches pages.
        Iterate over it to get individual :class:`~sika.recon.models.Scan`
        instances, or call ``.pages()`` for page-by-page access.

        Args:
            status: Filter by scan status.
            limit: Maximum items per page (1-100, default 20).

        Returns:
            A paginated iterator of scans.

        Raises:
            AuthenticationError: If not authenticated (401).
        """
        params: dict[str, Any] = {"limit": limit}
        if status is not None:
            params["status"] = status.value

        return PageIterator(
            self._http,
            _SCANS_PATH,
            params,
            Scan,
        )

    def delete(self, scan_id: str) -> None:
        """Delete a scan.

        Args:
            scan_id: UUID of the scan to delete.

        Raises:
            NotFoundError: If the scan does not exist (404).
            AuthenticationError: If not authenticated (401).
        """
        self._http.delete(f"{_SCANS_PATH}/{scan_id}")

    def report(
        self,
        scan_id: str,
        *,
        format: str = "json",
    ) -> ReportResponse:
        """Get a download URL for a scan report.

        Args:
            scan_id: UUID of the scan.
            format: Report format — ``"json"`` (default), ``"html"``,
                or ``"csv"``.

        Returns:
            A :class:`~sika.recon.models.ReportResponse` containing the
            presigned download URL.

        Raises:
            NotFoundError: If the scan does not exist (404).
            ConflictError: If the scan is not yet completed (409).
            AuthenticationError: If not authenticated (401).
        """
        data = self._http.get(
            f"{_SCANS_PATH}/{scan_id}/report",
            params={"format": format},
        )
        return ReportResponse.model_validate(data)

    def wait(
        self,
        scan_id: str,
        *,
        timeout: float = 300,
        poll_interval: float = 5,
    ) -> Scan:
        """Poll a scan until it completes or fails.

        Repeatedly calls :meth:`get` at ``poll_interval`` seconds until
        the scan reaches ``completed`` or ``failed`` status, or until
        ``timeout`` is exceeded.

        Args:
            scan_id: UUID of the scan to wait for.
            timeout: Maximum seconds to wait before raising
                :class:`TimeoutError`. Defaults to 300 (5 minutes).
            poll_interval: Seconds between poll requests. Defaults to 5.

        Returns:
            The completed :class:`~sika.recon.models.Scan`.

        Raises:
            TimeoutError: If the scan does not finish within ``timeout``.
            SikaError: If the scan status is ``failed``.
            NotFoundError: If the scan does not exist (404).
            AuthenticationError: If not authenticated (401).
        """
        deadline = time.monotonic() + timeout

        while True:
            scan = self.get(scan_id)

            if scan.status == ScanStatus.completed:
                return scan

            if scan.status == ScanStatus.failed:
                raise SikaError(
                    f"Scan {scan_id} failed",
                    status_code=0,
                    code="scan_failed",
                )

            if time.monotonic() + poll_interval > deadline:
                msg = (
                    f"Scan {scan_id} did not complete within "
                    f"{timeout}s (status: {scan.status})"
                )
                raise TimeoutError(msg)

            time.sleep(poll_interval)
