"""
AIPTX Twilio Key Validator
==========================

Validates Twilio Account SID and Auth Token pairs.

Supports:
- Account SID + Auth Token
- API Keys (SK prefix)
"""

from __future__ import annotations

import re
import time
import logging
import base64
from typing import Any

import httpx

from ..base import (
    APIKeyInfo,
    ValidationResult,
    BaseKeyValidator,
    KeyType,
    RiskLevel,
)

logger = logging.getLogger(__name__)


class TwilioKeyValidator(BaseKeyValidator):
    """
    Validate Twilio credentials and enumerate permissions.

    Uses Twilio REST API to validate credentials and discover:
    - Account information
    - Phone numbers
    - Usage records
    - Accessible resources
    """

    API_BASE = "https://api.twilio.com/2010-04-01"

    def get_key_type(self) -> KeyType:
        return KeyType.TWILIO

    def get_key_patterns(self) -> dict[str, re.Pattern]:
        return {
            "twilio_account_sid": re.compile(r'AC[a-f0-9]{32}'),
            "twilio_auth_token": re.compile(r'[a-f0-9]{32}'),
            "twilio_api_key": re.compile(r'SK[a-f0-9]{32}'),
        }

    async def validate(self, key_info: APIKeyInfo) -> ValidationResult:
        """
        Validate Twilio credentials against API.

        Requires Account SID and Auth Token.
        """
        start_time = time.time()

        # Twilio requires SID + Auth Token pair
        account_sid = key_info.key_id or key_info.key_value
        auth_token = key_info.secret_value or key_info.key_value

        # If we only have one value, try to determine which it is
        if key_info.key_value.startswith("AC"):
            account_sid = key_info.key_value
            if not key_info.secret_value:
                duration_ms = (time.time() - start_time) * 1000
                return self._create_error_result(
                    key_info,
                    error="Missing Auth Token (Account SID detected but no secret)",
                    error_code="MISSING_AUTH_TOKEN",
                    duration_ms=duration_ms,
                )
            auth_token = key_info.secret_value

        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                # Use Basic auth with SID:Token
                auth_header = base64.b64encode(
                    f"{account_sid}:{auth_token}".encode()
                ).decode()

                headers = {
                    "Authorization": f"Basic {auth_header}",
                    "Accept": "application/json",
                }

                # Get account info
                response = await client.get(
                    f"{self.API_BASE}/Accounts/{account_sid}.json",
                    headers=headers,
                )

                duration_ms = (time.time() - start_time) * 1000

                if response.status_code == 200:
                    account_data = response.json()

                    # Build identity info
                    identity_info = {
                        "account_sid": account_data.get("sid"),
                        "friendly_name": account_data.get("friendly_name"),
                        "owner_account_sid": account_data.get("owner_account_sid"),
                        "status": account_data.get("status"),
                        "type": account_data.get("type"),
                        "date_created": account_data.get("date_created"),
                    }

                    # Enumerate resources
                    phone_numbers, permissions, resources = await self._enumerate_access(
                        client, headers, account_sid
                    )

                    # Add phone numbers to resources
                    if phone_numbers:
                        identity_info["phone_numbers"] = phone_numbers[:5]  # First 5

                    result = ValidationResult(
                        key_info=key_info,
                        is_valid=True,
                        validation_method="GET /Accounts/{sid}.json",
                        validation_endpoint=f"{self.API_BASE}/Accounts/{account_sid}.json",
                        identity_info=identity_info,
                        scopes=[],  # Twilio doesn't use OAuth scopes
                        permissions=permissions,
                        resources_accessible=resources,
                        raw_response=account_data,
                        validated_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                        validation_duration_ms=duration_ms,
                    )

                    # Assess risk
                    result.risk_level = self._assess_twilio_risk(
                        permissions, phone_numbers, identity_info
                    )
                    result.exploitation_potential = self._get_exploitation_potential(result)
                    result.attack_vectors = self._get_attack_vectors(permissions, phone_numbers)

                    return result

                elif response.status_code == 401:
                    return self._create_error_result(
                        key_info,
                        error="Invalid Account SID or Auth Token",
                        error_code="401_UNAUTHORIZED",
                        duration_ms=duration_ms,
                    )
                elif response.status_code == 404:
                    return self._create_error_result(
                        key_info,
                        error="Account not found",
                        error_code="404_NOT_FOUND",
                        duration_ms=duration_ms,
                    )
                else:
                    return self._create_error_result(
                        key_info,
                        error=f"Unexpected status: {response.status_code}",
                        error_code=str(response.status_code),
                        duration_ms=duration_ms,
                    )

        except httpx.TimeoutException:
            duration_ms = (time.time() - start_time) * 1000
            return self._create_error_result(
                key_info,
                error="Request timeout",
                error_code="TIMEOUT",
                duration_ms=duration_ms,
            )
        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000
            logger.error(f"Twilio validation error: {e}")
            return self._create_error_result(
                key_info,
                error=str(e),
                error_code="ERROR",
                duration_ms=duration_ms,
            )

    async def _enumerate_access(
        self,
        client: httpx.AsyncClient,
        headers: dict,
        account_sid: str,
    ) -> tuple[list[str], list[str], list[str]]:
        """Enumerate Twilio resources and permissions."""
        phone_numbers = []
        permissions = []
        resources = []

        # Get phone numbers
        try:
            response = await client.get(
                f"{self.API_BASE}/Accounts/{account_sid}/IncomingPhoneNumbers.json",
                headers=headers,
            )
            if response.status_code == 200:
                data = response.json()
                numbers = data.get("incoming_phone_numbers", [])
                phone_numbers = [n.get("phone_number") for n in numbers]
                resources.append(f"phone_numbers:{len(phone_numbers)}")
                permissions.append("IncomingPhoneNumbers:read")
        except Exception as e:
            logger.debug(f"Failed to list phone numbers: {e}")

        # Check messages
        try:
            response = await client.get(
                f"{self.API_BASE}/Accounts/{account_sid}/Messages.json",
                headers=headers,
                params={"PageSize": 1},
            )
            if response.status_code == 200:
                permissions.append("Messages:read")
                resources.append("messages")
        except Exception:
            pass

        # Check calls
        try:
            response = await client.get(
                f"{self.API_BASE}/Accounts/{account_sid}/Calls.json",
                headers=headers,
                params={"PageSize": 1},
            )
            if response.status_code == 200:
                permissions.append("Calls:read")
                resources.append("calls")
        except Exception:
            pass

        # Check recordings
        try:
            response = await client.get(
                f"{self.API_BASE}/Accounts/{account_sid}/Recordings.json",
                headers=headers,
                params={"PageSize": 1},
            )
            if response.status_code == 200:
                permissions.append("Recordings:read")
                resources.append("recordings")
        except Exception:
            pass

        # Check usage records
        try:
            response = await client.get(
                f"{self.API_BASE}/Accounts/{account_sid}/Usage/Records.json",
                headers=headers,
            )
            if response.status_code == 200:
                data = response.json()
                usage = data.get("usage_records", [])
                if usage:
                    # Get total spend
                    total_price = sum(
                        float(r.get("price", 0)) for r in usage if r.get("price")
                    )
                    resources.append(f"usage:${total_price:.2f}")
                permissions.append("Usage:read")
        except Exception:
            pass

        # Check SIP domains
        try:
            response = await client.get(
                f"{self.API_BASE}/Accounts/{account_sid}/SIP/Domains.json",
                headers=headers,
            )
            if response.status_code == 200:
                data = response.json()
                domains = data.get("sip_domains", [])
                if domains:
                    resources.append(f"sip_domains:{len(domains)}")
                permissions.append("SIP:read")
        except Exception:
            pass

        return phone_numbers, permissions, resources

    def _assess_twilio_risk(
        self,
        permissions: list[str],
        phone_numbers: list[str],
        identity_info: dict,
    ) -> RiskLevel:
        """Assess risk level based on credentials capabilities."""
        # Critical: Has phone numbers and message/call access
        if phone_numbers and (
            "Messages:read" in permissions or "Calls:read" in permissions
        ):
            return RiskLevel.CRITICAL

        # High: Has phone numbers or recording access
        if phone_numbers or "Recordings:read" in permissions:
            return RiskLevel.HIGH

        # Medium: Can read messages or calls
        if "Messages:read" in permissions or "Calls:read" in permissions:
            return RiskLevel.MEDIUM

        # Low: Basic account access
        if permissions:
            return RiskLevel.LOW

        return RiskLevel.INFO

    def _get_exploitation_potential(self, result: ValidationResult) -> str:
        """Get exploitation potential description."""
        if result.risk_level == RiskLevel.CRITICAL:
            return "Critical - Full SMS/voice access with phone numbers"
        elif result.risk_level == RiskLevel.HIGH:
            return "High - Can access phone numbers or call recordings"
        elif result.risk_level == RiskLevel.MEDIUM:
            return "Medium - Can read SMS messages or call logs"
        elif result.risk_level == RiskLevel.LOW:
            return "Low - Limited account access"
        return "Minimal - Basic account information only"

    def _get_attack_vectors(
        self,
        permissions: list[str],
        phone_numbers: list[str],
    ) -> list[str]:
        """Identify potential attack vectors."""
        vectors = []

        if phone_numbers:
            vectors.append(f"Send SMS from {len(phone_numbers)} phone number(s)")
            vectors.append("Make calls for vishing attacks")
            vectors.append("Receive SMS for 2FA bypass")

        if "Messages:read" in permissions:
            vectors.append("Read SMS messages (may contain 2FA codes)")
            vectors.append("Intercept password reset messages")

        if "Calls:read" in permissions:
            vectors.append("Access call logs for reconnaissance")

        if "Recordings:read" in permissions:
            vectors.append("Download call recordings (sensitive conversations)")

        if "Usage:read" in permissions:
            vectors.append("View spending patterns and account activity")

        return vectors
