"""Pure cross-validation logic for inventory cross-validate command.

Extracted from inventory.py for testability and KISS compliance.
All functions are stateless and have no I/O side effects.

Accuracy thresholds match the APRA CPS 234 asset register SLO:
- PASS: >= 99.5%
- WARN: >= 95.0%
- FAIL: < 95.0%
"""

from __future__ import annotations

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

ACCURACY_PASS = 99.5
ACCURACY_WARN = 95.0

# Resource Explorer does not index these types.
# When V2 returns 0 for them, skip accuracy rather than scoring 0%.
V2_UNSUPPORTED_TYPES: frozenset[str] = frozenset({"iam_role", "acm"})

# Retry config for all cross-validate AWS API clients (Rule 6 compliance)
try:
    from botocore.config import Config as BotoConfig

    CROSS_VALIDATE_CLIENT_CONFIG = BotoConfig(
        connect_timeout=10,
        read_timeout=30,
        retries={"max_attempts": 3, "mode": "adaptive"},
    )
except ImportError:
    CROSS_VALIDATE_CLIENT_CONFIG = None


# ---------------------------------------------------------------------------
# Core normalization
# ---------------------------------------------------------------------------


def normalize_resource_id(rid: str) -> str:
    """Normalize a resource ID to its short form for cross-source comparison.

    Config Aggregator returns the native resourceId (already short for most
    types, but may be an ARN for IAM/ACM). Resource Explorer extracts the
    last path or colon segment from the ARN. Normalizing both to the short
    segment makes Jaccard comparison meaningful.

    The "/" branch handles path-style ARNs:
    - CloudFormation: stack/StackName/GUID  -> StackName   (index 1, not GUID)
    - IAM role:       role/MyRole           -> MyRole      (last segment)
    - EC2 instance:   instance/i-abc        -> i-abc       (last segment)

    The ":" branch handles colon-prefixed ARNs:
    - Lambda qualified: function:name:$LATEST -> name       (index 1, not qualifier)
    - Lambda versioned:  function:name:1       -> name       (index 1, not version)
    - RDS:               db:mydb              -> mydb       (index 1 == last for 2 segs)

    Args:
        rid: Raw resource ID or ARN string.

    Returns:
        Short resource identifier suitable for set comparison.

    Examples:
        >>> normalize_resource_id("arn:aws:rds:ap-southeast-2:123:db:mydb")
        'mydb'
        >>> normalize_resource_id("arn:aws:iam::123:role/MyRole")
        'MyRole'
        >>> normalize_resource_id("arn:aws:cloudformation:ap-southeast-2:123:stack/MyStack/guid")
        'MyStack'
        >>> normalize_resource_id("arn:aws:lambda:ap-southeast-2:123:function:my-func:$LATEST")
        'my-func'
        >>> normalize_resource_id("i-0abc1234")
        'i-0abc1234'
    """
    if not rid or not rid.startswith("arn:"):
        return rid

    # arn:aws:service:region:account:resource
    parts = rid.split(":", 5)
    if len(parts) < 6:
        return rid

    resource_part = parts[5]  # e.g. "db:mydb", "role/MyRole", "stack/StackName/GUID"

    if "/" in resource_part:
        segments = resource_part.split("/")
        # CloudFormation: stack/StackName/GUID -> StackName (human name at index 1)
        if segments[0] == "stack" and len(segments) >= 3:
            return segments[1]
        # All other path-style: take last segment (IAM role, EC2 instance, etc.)
        return segments[-1]

    if ":" in resource_part:
        segments = resource_part.split(":")
        # function:name[:qualifier] -> name at index 1 (ignore $LATEST, version numbers)
        # db:mydb -> mydb at index 1
        if len(segments) >= 2:
            return segments[1]
        return segments[-1]

    return resource_part


# ---------------------------------------------------------------------------
# Accuracy computation
# ---------------------------------------------------------------------------


def compute_type_accuracy(
    v1_ids: set,
    v2_ids: set,
) -> tuple:
    """Compute Jaccard accuracy between V1 and V2 ID sets for one resource type.

    Args:
        v1_ids: Set of normalized resource IDs from Config Aggregator (V1).
        v2_ids: Set of normalized resource IDs from Resource Explorer (V2).

    Returns:
        Tuple of (matched, total, accuracy_pct, status) where:
            matched    -- intersection size
            total      -- union size
            accuracy_pct -- 0.0-100.0
            status     -- "PASS" | "WARN" | "FAIL"
    """
    if v1_ids or v2_ids:
        matched = len(v1_ids & v2_ids)
        total = len(v1_ids | v2_ids)
        accuracy = (matched / total * 100) if total > 0 else 0.0
    else:
        matched = total = 0
        accuracy = 0.0

    status = _accuracy_status(accuracy, total)
    return matched, total, accuracy, status


# ---------------------------------------------------------------------------
# V2 undercount detection
# ---------------------------------------------------------------------------


def detect_v2_undercount(v1_ids: set, v2_ids: set, threshold: float = 0.10) -> tuple:
    """Detect V2 undercount caused by Resource Explorer index lag.

    When V2 IDs are a proper subset of V1 IDs and the gap exceeds *threshold*,
    the discrepancy is most likely index lag rather than a genuine data mismatch.
    Displaying a FAIL status in this case is misleading — V2 simply has not
    caught up with V1 yet.

    Args:
        v1_ids: Set of normalized resource IDs from Config Aggregator (V1).
        v2_ids: Set of normalized resource IDs from Resource Explorer (V2).
        threshold: Minimum fraction of V1 resources missing from V2 required to
            report an undercount (default 0.10 = 10%).

    Returns:
        Tuple of (is_undercount, is_subset) where:
            is_undercount -- True when V2 is a proper subset of V1 AND the
                             missing fraction exceeds *threshold*.
            is_subset     -- True when every V2 ID exists in V1 (superset check).

    Examples:
        >>> v1 = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
        >>> v2 = {"a", "b", "c", "d", "e", "f", "g", "h"}  # 20% missing
        >>> detect_v2_undercount(v1, v2)
        (True, True)
    """
    if not v1_ids or not v2_ids:
        return False, False
    is_subset = v2_ids.issubset(v1_ids)
    undercount_ratio = (len(v1_ids) - len(v2_ids)) / len(v1_ids)
    return undercount_ratio > threshold and is_subset, is_subset


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------


def _accuracy_status(accuracy: float, total: int) -> str:
    """Map accuracy percentage + total to a status string."""
    if accuracy >= ACCURACY_PASS or total == 0:
        return "PASS"
    if accuracy >= ACCURACY_WARN:
        return "WARN"
    return "FAIL"
