"""
AIPTX S3 Bucket Scanner
=======================

Scans S3 buckets for public access and enumerates contents.

Checks:
- Public bucket listing
- Public object access
- ACL misconfigurations
- Sensitive file discovery
"""

from __future__ import annotations

import re
import time
import logging
from typing import Any, Optional
from dataclasses import dataclass, field

import httpx

from ..base import RiskLevel

logger = logging.getLogger(__name__)


@dataclass
class S3ScanResult:
    """Result of S3 bucket scan."""

    bucket_name: str
    region: str
    is_public: bool
    public_list: bool
    public_read: bool
    public_write: bool
    objects_found: list[str] = field(default_factory=list)
    sensitive_files: list[str] = field(default_factory=list)
    total_objects: int = 0
    total_size_bytes: int = 0
    risk_level: RiskLevel = RiskLevel.INFO
    error: Optional[str] = None
    scan_duration_ms: float = 0.0

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "bucket_name": self.bucket_name,
            "region": self.region,
            "is_public": self.is_public,
            "public_list": self.public_list,
            "public_read": self.public_read,
            "public_write": self.public_write,
            "objects_found": self.objects_found[:20],
            "sensitive_files": self.sensitive_files,
            "total_objects": self.total_objects,
            "total_size_bytes": self.total_size_bytes,
            "risk_level": self.risk_level.value,
            "error": self.error,
            "scan_duration_ms": self.scan_duration_ms,
        }


class S3BucketScanner:
    """
    Scan S3 buckets for public access and sensitive content.

    Works without AWS credentials by checking public bucket URLs.
    """

    # Sensitive file patterns
    SENSITIVE_PATTERNS = [
        r'\.env$',
        r'\.git/',
        r'config\.json$',
        r'credentials',
        r'password',
        r'secret',
        r'\.pem$',
        r'\.key$',
        r'\.p12$',
        r'\.pfx$',
        r'backup',
        r'dump',
        r'\.sql$',
        r'\.db$',
        r'\.sqlite$',
        r'api[_-]?key',
        r'access[_-]?key',
        r'\.htpasswd$',
        r'shadow$',
        r'passwd$',
        r'id_rsa',
        r'\.ssh/',
        r'token',
        r'\.bak$',
        r'\.old$',
    ]

    # Common bucket URL formats
    BUCKET_URL_FORMATS = [
        "https://{bucket}.s3.amazonaws.com",
        "https://{bucket}.s3.{region}.amazonaws.com",
        "https://s3.amazonaws.com/{bucket}",
        "https://s3.{region}.amazonaws.com/{bucket}",
    ]

    # AWS regions to try
    REGIONS = [
        "us-east-1",
        "us-west-2",
        "eu-west-1",
        "ap-northeast-1",
        "ap-southeast-1",
    ]

    def __init__(self, timeout: float = 30.0):
        """Initialize scanner."""
        self.timeout = timeout
        self.sensitive_regex = [
            re.compile(p, re.IGNORECASE) for p in self.SENSITIVE_PATTERNS
        ]

    async def scan_bucket(self, bucket_name: str) -> S3ScanResult:
        """
        Scan an S3 bucket for public access.

        Args:
            bucket_name: Name of the S3 bucket

        Returns:
            S3ScanResult with scan findings
        """
        start_time = time.time()

        result = S3ScanResult(
            bucket_name=bucket_name,
            region="unknown",
            is_public=False,
            public_list=False,
            public_read=False,
            public_write=False,
        )

        try:
            async with httpx.AsyncClient(timeout=self.timeout) as client:
                # Try to find the bucket and check public listing
                bucket_url, region = await self._find_bucket(client, bucket_name)

                if not bucket_url:
                    result.error = "Bucket not found or not accessible"
                    result.scan_duration_ms = (time.time() - start_time) * 1000
                    return result

                result.region = region

                # Check public listing (ListBucket)
                list_result = await self._check_public_list(client, bucket_url)
                result.public_list = list_result["accessible"]

                if list_result["accessible"]:
                    result.is_public = True
                    result.objects_found = list_result.get("objects", [])
                    result.total_objects = list_result.get("total_count", 0)

                    # Check for sensitive files
                    result.sensitive_files = self._find_sensitive_files(
                        result.objects_found
                    )

                # Check public read (GetObject)
                if result.objects_found:
                    result.public_read = await self._check_public_read(
                        client, bucket_url, result.objects_found[0]
                    )
                    if result.public_read:
                        result.is_public = True

                # Check public write (PutObject) - only if safe
                # We don't actually write, just check if the bucket allows it
                result.public_write = await self._check_public_write(
                    client, bucket_url
                )
                if result.public_write:
                    result.is_public = True

                # Assess risk
                result.risk_level = self._assess_risk(result)

        except Exception as e:
            result.error = str(e)
            logger.error(f"S3 scan error for {bucket_name}: {e}")

        result.scan_duration_ms = (time.time() - start_time) * 1000
        return result

    async def _find_bucket(
        self,
        client: httpx.AsyncClient,
        bucket_name: str,
    ) -> tuple[Optional[str], str]:
        """Find bucket URL and region."""
        # Clean bucket name
        bucket_name = bucket_name.strip().lower()
        bucket_name = bucket_name.replace("s3://", "").split("/")[0]

        # Try different URL formats and regions
        for region in self.REGIONS:
            for url_format in self.BUCKET_URL_FORMATS:
                url = url_format.format(bucket=bucket_name, region=region)

                try:
                    response = await client.head(url)

                    # Check for redirects that indicate region
                    if response.status_code in [200, 403]:
                        return url, region
                    elif response.status_code == 301:
                        # Bucket exists but in different region
                        # Check headers for correct region
                        new_region = response.headers.get("x-amz-bucket-region", region)
                        correct_url = url_format.format(bucket=bucket_name, region=new_region)
                        return correct_url, new_region

                except httpx.TimeoutException:
                    continue
                except Exception as e:
                    logger.debug(f"Error checking {url}: {e}")
                    continue

        return None, "unknown"

    async def _check_public_list(
        self,
        client: httpx.AsyncClient,
        bucket_url: str,
    ) -> dict:
        """Check if bucket contents can be listed publicly."""
        result = {"accessible": False, "objects": [], "total_count": 0}

        try:
            # List objects using REST API
            response = await client.get(
                bucket_url,
                params={"list-type": "2", "max-keys": "100"},
            )

            if response.status_code == 200:
                result["accessible"] = True

                # Parse XML response
                content = response.text
                objects = self._parse_list_objects(content)
                result["objects"] = objects
                result["total_count"] = len(objects)

                # Check if truncated
                if "<IsTruncated>true</IsTruncated>" in content:
                    result["total_count"] = "100+"

        except Exception as e:
            logger.debug(f"List check failed: {e}")

        return result

    async def _check_public_read(
        self,
        client: httpx.AsyncClient,
        bucket_url: str,
        object_key: str,
    ) -> bool:
        """Check if objects can be read publicly."""
        try:
            object_url = f"{bucket_url}/{object_key}"
            response = await client.head(object_url)
            return response.status_code == 200
        except Exception:
            return False

    async def _check_public_write(
        self,
        client: httpx.AsyncClient,
        bucket_url: str,
    ) -> bool:
        """Check if bucket allows public write (without actually writing)."""
        try:
            # Check bucket ACL for write permission
            # This usually requires authentication, but some buckets expose it
            acl_url = f"{bucket_url}?acl"
            response = await client.get(acl_url)

            if response.status_code == 200:
                content = response.text.lower()
                # Check for public write grants
                if "alluser" in content and ("write" in content or "full_control" in content):
                    return True

        except Exception:
            pass

        return False

    def _parse_list_objects(self, xml_content: str) -> list[str]:
        """Parse S3 ListObjects XML response."""
        objects = []

        # Simple XML parsing for Key elements
        key_pattern = re.compile(r'<Key>([^<]+)</Key>')
        matches = key_pattern.findall(xml_content)

        for key in matches:
            objects.append(key)

        return objects

    def _find_sensitive_files(self, objects: list[str]) -> list[str]:
        """Find sensitive files in object list."""
        sensitive = []

        for obj in objects:
            for pattern in self.sensitive_regex:
                if pattern.search(obj):
                    sensitive.append(obj)
                    break

        return sensitive

    def _assess_risk(self, result: S3ScanResult) -> RiskLevel:
        """Assess risk level of bucket."""
        # Critical: Sensitive files exposed publicly
        if result.sensitive_files and (result.public_list or result.public_read):
            return RiskLevel.CRITICAL

        # Critical: Public write access
        if result.public_write:
            return RiskLevel.CRITICAL

        # High: Large number of public objects
        if result.public_list and result.total_objects > 50:
            return RiskLevel.HIGH

        # High: Public read access with multiple objects
        if result.public_read and result.total_objects > 10:
            return RiskLevel.HIGH

        # Medium: Public listing
        if result.public_list:
            return RiskLevel.MEDIUM

        # Low: Any public access
        if result.is_public:
            return RiskLevel.LOW

        return RiskLevel.INFO

    async def scan_multiple(self, bucket_names: list[str]) -> list[S3ScanResult]:
        """Scan multiple buckets."""
        results = []

        for bucket_name in bucket_names:
            result = await self.scan_bucket(bucket_name)
            results.append(result)

        return results

    def generate_report(self, results: list[S3ScanResult]) -> dict:
        """Generate summary report from scan results."""
        report = {
            "total_scanned": len(results),
            "public_buckets": 0,
            "public_list": 0,
            "public_read": 0,
            "public_write": 0,
            "sensitive_files_found": 0,
            "by_risk_level": {
                "critical": [],
                "high": [],
                "medium": [],
                "low": [],
                "info": [],
            },
            "buckets": [],
        }

        for result in results:
            if result.is_public:
                report["public_buckets"] += 1
            if result.public_list:
                report["public_list"] += 1
            if result.public_read:
                report["public_read"] += 1
            if result.public_write:
                report["public_write"] += 1
            if result.sensitive_files:
                report["sensitive_files_found"] += len(result.sensitive_files)

            report["by_risk_level"][result.risk_level.value].append(result.bucket_name)
            report["buckets"].append(result.to_dict())

        return report
