"""Pydantic v2 data models for the Sika Recon API.

All models use ``frozen=True`` for immutability. Timestamp fields accept
both Unix integer timestamps and ISO 8601 strings via the :data:`Timestamp`
annotated type.

Models are grouped by purpose:

- **Scan lifecycle:** :class:`Scan`, :class:`ScanStatus`, :class:`ScanSummary`
- **Scan results:** :class:`ScanResult`, :class:`SubdomainResult`,
  :class:`DNSRecord`, :class:`IPAttribution`, :class:`HistoricalDNSRecord`,
  :class:`LeakedOrigin`, :class:`BGPResult`
- **Reports:** :class:`ReportResponse`
- **Intel & health:** :class:`IntelStatus`, :class:`HealthStatus`
"""

from __future__ import annotations

from datetime import UTC, date, datetime
from enum import StrEnum
from typing import Annotated, Any

from pydantic import BaseModel, BeforeValidator, ConfigDict

# ---------------------------------------------------------------------------
# Timestamp handling
# ---------------------------------------------------------------------------

def _coerce_timestamp(value: object) -> object:
    """Convert Unix timestamps (int/float) to UTC datetime objects.

    Passes through strings (ISO 8601) and existing datetime instances
    unchanged — Pydantic's built-in parsing handles those.
    """
    if isinstance(value, (int, float)):
        return datetime.fromtimestamp(value, tz=UTC)
    return value


Timestamp = Annotated[datetime, BeforeValidator(_coerce_timestamp)]
"""Datetime type that also accepts Unix timestamps (int/float)."""


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

class ScanStatus(StrEnum):
    """Status of a Recon scan throughout its lifecycle.

    Values match the Recon API ``ScanStatus`` enum. Processing stages
    run in approximate order from ``submitted`` through ``completed``.
    """

    submitted = "submitted"
    discovering = "discovering"
    resolving = "resolving"
    attributing = "attributing"
    analyzing_history = "analyzing_history"
    analyzing_bgp = "analyzing_bgp"
    expanding_asns = "expanding_asns"
    analyzing_peering = "analyzing_peering"
    detecting_scrubbing = "detecting_scrubbing"
    analyzing_customers = "analyzing_customers"
    scoring_resilience = "scoring_resilience"
    completed = "completed"
    failed = "failed"


# ---------------------------------------------------------------------------
# Result sub-models
# ---------------------------------------------------------------------------

class DNSRecord(BaseModel):
    """A single DNS record for a subdomain.

    Attributes:
        type: DNS record type (e.g. ``A``, ``AAAA``, ``CNAME``).
        value: Record value (IP address, hostname, etc.).
    """

    model_config = ConfigDict(frozen=True)

    type: str
    value: str


class SubdomainResult(BaseModel):
    """Discovered subdomain with its DNS records.

    Attributes:
        name: Fully-qualified subdomain name.
        records: DNS records associated with this subdomain.
    """

    model_config = ConfigDict(frozen=True)

    name: str
    records: list[DNSRecord] = []


class IPAttribution(BaseModel):
    """IP address attributed to an infrastructure provider.

    Attributes:
        ip: IPv4 or IPv6 address.
        provider: Hosting or CDN provider name (e.g. ``Cloudflare``).
        category: Provider category (e.g. ``cdn``, ``cloud``, ``isp``).
        service: Specific service name within the provider.
        region: Geographic region of the IP.
    """

    model_config = ConfigDict(frozen=True)

    ip: str
    provider: str | None = None
    category: str | None = None
    service: str | None = None
    region: str | None = None


class HistoricalDNSRecord(BaseModel):
    """A historical DNS record from passive DNS sources.

    Attributes:
        subdomain: Subdomain that held this record.
        ip: IP address the subdomain pointed to.
        first_seen: Earliest date this record was observed.
        last_seen: Most recent date this record was observed.
        source: Data source (e.g. ``passive_dns``, ``certificate_transparency``).
    """

    model_config = ConfigDict(frozen=True)

    subdomain: str
    ip: str
    first_seen: date
    last_seen: date
    source: str


class LeakedOrigin(BaseModel):
    """An origin IP address leaked through misconfiguration.

    Attributes:
        ip: The leaked origin IP address.
        confidence: Confidence level (e.g. ``high``, ``medium``, ``low``).
        evidence: Description of how the leak was detected.
        last_seen: Most recent date the leak was observed.
    """

    model_config = ConfigDict(frozen=True)

    ip: str
    confidence: str
    evidence: str
    last_seen: date


class BGPResult(BaseModel):
    """BGP routing analysis for an IP address.

    Attributes:
        ip: IP address analyzed.
        asn: Autonomous System Number, or ``None`` if unresolvable.
        asn_name: Human-readable name for the AS.
        prefix: BGP prefix covering this IP.
        upstreams: List of upstream AS numbers (as strings).
        peer_count: Number of BGP peers announcing this prefix.
        resilience_score: Routing resilience score (0-100).
    """

    model_config = ConfigDict(frozen=True)

    ip: str
    asn: int | None = None
    asn_name: str | None = None
    prefix: str | None = None
    upstreams: list[str] = []
    peer_count: int = 0
    resilience_score: int = 0


# ---------------------------------------------------------------------------
# Composed result models
# ---------------------------------------------------------------------------

class ScanSummary(BaseModel):
    """High-level statistics for a completed scan.

    Attributes:
        total_subdomains: Number of unique subdomains discovered.
        unique_ips: Number of unique IP addresses found.
        providers_found: Number of distinct infrastructure providers.
        leaked_origins: Number of leaked origin IPs detected.
        avg_resilience_score: Average BGP resilience score (0-100).
    """

    model_config = ConfigDict(frozen=True)

    total_subdomains: int
    unique_ips: int
    providers_found: int
    leaked_origins: int
    avg_resilience_score: float


class ScanResult(BaseModel):
    """Full scan results containing all analysis outputs.

    Attributes:
        subdomains: Discovered subdomains with DNS records.
        ip_attributions: IP-to-provider attributions.
        historical_dns: Historical DNS records from passive sources.
        leaked_origins: Detected leaked origin IPs.
        bgp_analysis: BGP routing analysis per IP.
    """

    model_config = ConfigDict(frozen=True)

    subdomains: list[SubdomainResult] = []
    ip_attributions: list[IPAttribution] = []
    historical_dns: list[HistoricalDNSRecord] = []
    leaked_origins: list[LeakedOrigin] = []
    bgp_analysis: list[BGPResult] = []


# ---------------------------------------------------------------------------
# Scan model
# ---------------------------------------------------------------------------

class Scan(BaseModel):
    """A Recon scan and its results.

    Represents both the lightweight list-item shape (from ``GET /scans``)
    and the full detail shape (from ``GET /scans/{id}``). Fields that are
    only present in the detail response default to ``None``.

    Attributes:
        scan_id: Unique scan identifier (UUID).
        domain: Target domain that was scanned.
        scan_type: Type of scan (``domain_scan`` or ``provider_scan``).
        status: Current scan status.
        tags: User-defined labels for grouping and filtering scans.
        created_at: When the scan was submitted.
        updated_at: When the scan status last changed.
        completed_at: When the scan finished (completed or failed).
        target_asn: Target ASN for provider scans.
        results: Inline scan results (small result sets).
        results_url: Presigned URL for large result sets (1-hour expiry).
        options: Additional scan configuration.
        summary: High-level statistics (when available).
    """

    model_config = ConfigDict(frozen=True)

    scan_id: str
    domain: str
    scan_type: str = "domain_scan"
    status: ScanStatus
    tags: list[str] | None = None
    created_at: Timestamp
    updated_at: Timestamp | None = None
    completed_at: Timestamp | None = None
    target_asn: int | None = None
    results: ScanResult | None = None
    results_url: str | None = None
    options: dict[str, Any] | None = None
    summary: ScanSummary | None = None


# ---------------------------------------------------------------------------
# Report
# ---------------------------------------------------------------------------

class ReportResponse(BaseModel):
    """Response from the scan report endpoint.

    Contains a presigned URL to download the report in the requested
    format.

    Attributes:
        format: Report format (``json``, ``html``, or ``csv``).
        url: Presigned download URL (1-hour expiry).
        expires_at: When the presigned URL expires.
    """

    model_config = ConfigDict(frozen=True)

    format: str
    url: str
    expires_at: Timestamp


# ---------------------------------------------------------------------------
# Intel & health (consumed by S1.2)
# ---------------------------------------------------------------------------

class IntelStatus(BaseModel):
    """Status of the threat intelligence database.

    Attributes:
        last_updated: When the intel database was last refreshed.
        version: Intel database version identifier.
        size_bytes: Size of the intel database in bytes.
    """

    model_config = ConfigDict(frozen=True)

    last_updated: Timestamp
    version: str | None = None
    size_bytes: int


class HealthStatus(BaseModel):
    """Health check response from the Recon service.

    Attributes:
        status: Overall health status (e.g. ``healthy``, ``degraded``).
        checks: Individual health check results keyed by check name.
        version: Service version string.
        timestamp: When the health check was performed.
    """

    model_config = ConfigDict(frozen=True)

    status: str
    checks: dict[str, str]
    version: str
    timestamp: Timestamp
