"""AWS SDK integration for CloudResourceHandler.

Provides AWS-specific session building, resource discovery, and sandbox
strategy implementation using ``boto3`` as an optional dependency.

This module is imported by :mod:`cleveragents.resource.handlers.cloud`
when AWS provider operations are requested.  It is intentionally
separated from the generic cloud handler to keep both files under the
500-line limit and to maintain clean module boundaries.

## Optional Dependency

``boto3`` is an optional dependency.  If it is not installed, all
functions in this module raise :exc:`ImportError` with a helpful
installation message.

## Sandbox Strategy

Cloud sandbox isolation uses a **tag-based** strategy: a unique
``CleverAgents:PlanId`` tag is applied to all resources created or
modified during a plan.  On rollback, tagged resources are deleted or
reverted.  On commit, the tag is removed.

Based on:
    - Issue #1021: AWS SDK integration
"""

from __future__ import annotations

import importlib
import logging
import types
from typing import Any

from cleveragents.domain.models.core.resource import (
    PhysVirt,
    Resource,
    ResourceCapabilities,
)
from cleveragents.resource.handlers._base import _derive_child_id

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Optional boto3 import
# ---------------------------------------------------------------------------

_BOTO3_AVAILABLE = False
boto3: types.ModuleType | None = None
botocore: types.ModuleType | None = None

try:
    boto3 = importlib.import_module("boto3")
    botocore = importlib.import_module("botocore.exceptions")
    _BOTO3_AVAILABLE = True
except ImportError:
    pass


# ---------------------------------------------------------------------------
# AWS resource type -> boto3 service/describe mapping
# ---------------------------------------------------------------------------

#: Maps AWS resource type names to (service_name, describe_method, id_key, arn_prefix).
#: Used by :func:`discover_aws_resources` to enumerate child resources.
_AWS_RESOURCE_MAP: dict[str, tuple[str, str, str, str]] = {
    "aws-vpc": ("ec2", "describe_vpcs", "VpcId", "vpc"),
    "aws-subnet": ("ec2", "describe_subnets", "SubnetId", "subnet"),
    "aws-instance": ("ec2", "describe_instances", "InstanceId", "instance"),
    "aws-security-group": (
        "ec2",
        "describe_security_groups",
        "GroupId",
        "security-group",
    ),
    "aws-s3-bucket": ("s3", "list_buckets", "Name", "s3"),
    "aws-iam-role": ("iam", "list_roles", "RoleName", "iam-role"),
    "aws-rds-instance": (
        "rds",
        "describe_db_instances",
        "DBInstanceIdentifier",
        "rds",
    ),
    "aws-ecs-cluster": ("ecs", "list_clusters", "clusterArns", "ecs-cluster"),
    "aws-lambda-function": (
        "lambda",
        "list_functions",
        "FunctionName",
        "lambda",
    ),
    "aws-eks-cluster": ("eks", "list_clusters", "clusters", "eks-cluster"),
}


# ---------------------------------------------------------------------------
# AWS boto3 session factory
# ---------------------------------------------------------------------------


def _build_aws_session(resolved: dict[str, str | None]) -> Any:
    """Build a boto3 Session from resolved credentials.

    Args:
        resolved: Resolved credential dict from
            :func:`~cleveragents.resource.handlers.cloud.resolve_credentials`.

    Returns:
        A ``boto3.Session`` instance.

    Raises:
        ImportError: If ``boto3`` is not installed.
    """
    if not _BOTO3_AVAILABLE or boto3 is None:
        raise ImportError(
            "boto3 is required for AWS resource operations. "
            "Install it with: pip install cleveragents[aws]"
        )

    kwargs: dict[str, str] = {}
    access_key = resolved.get("access-key-id")
    if access_key is not None:
        kwargs["aws_access_key_id"] = access_key
    secret_key = resolved.get("secret-access-key")
    if secret_key is not None:
        kwargs["aws_secret_access_key"] = secret_key
    session_token = resolved.get("session-token")
    if session_token is not None:
        kwargs["aws_session_token"] = session_token
    region = resolved.get("region")
    if region is not None:
        kwargs["region_name"] = region
    profile = resolved.get("profile")
    if profile is not None:
        kwargs["profile_name"] = profile

    # boto3 is confirmed non-None by the guard above; use attribute access
    return boto3.Session(**kwargs)  # type: ignore[union-attr]


# ---------------------------------------------------------------------------
# AWS resource discovery
# ---------------------------------------------------------------------------


def discover_aws_resources(
    resource: Resource,
    session: Any,
) -> list[dict[str, Any]]:
    """Discover AWS child resources using the boto3 SDK.

    Queries the AWS API to enumerate resources of the type specified by
    ``resource.resource_type_name``.  Returns a list of resource metadata
    dicts, each containing at minimum ``id``, ``type``, and ``arn`` keys.

    Args:
        resource: The parent AWS resource (account or container type).
        session: A ``boto3.Session`` instance.

    Returns:
        List of discovered resource metadata dicts.  Empty list if the
        resource type is not mapped or the API call returns no results.

    Raises:
        botocore.exceptions.ClientError: If the AWS API returns a client
            error (e.g. permission denied, invalid credentials).
        botocore.exceptions.BotoCoreError: If a lower-level botocore
            error occurs (e.g. network failure, endpoint resolution).
    """
    type_name = resource.resource_type_name
    mapping = _AWS_RESOURCE_MAP.get(type_name)
    if mapping is None:
        logger.debug(
            "No AWS discovery mapping for resource type '%s'; skipping.", type_name
        )
        return []

    service_name, method_name, id_key, arn_prefix = mapping
    region = resource.properties.get("region", "us-east-1") or "us-east-1"

    try:
        client = session.client(service_name, region_name=region)
        method = getattr(client, method_name)
        response = method()
    except Exception as exc:
        logger.warning("AWS API error during discovery of '%s': %s", type_name, exc)
        return []

    results: list[dict[str, Any]] = []

    # Handle S3 list_buckets (flat list)
    if service_name == "s3" and method_name == "list_buckets":
        for bucket in response.get("Buckets", []):
            name = bucket.get("Name", "")
            results.append(
                {
                    "id": name,
                    "type": type_name,
                    "arn": f"arn:aws:s3:::{name}",
                    "name": name,
                    "metadata": bucket,
                }
            )
        return results

    # Handle ECS list_clusters (returns ARN list)
    if service_name == "ecs" and method_name == "list_clusters":
        for arn in response.get("clusterArns", []):
            cluster_name = arn.split("/")[-1]
            results.append(
                {
                    "id": cluster_name,
                    "type": type_name,
                    "arn": arn,
                    "name": cluster_name,
                    "metadata": {"clusterArn": arn},
                }
            )
        return results

    # Handle EKS list_clusters (returns name list)
    if service_name == "eks" and method_name == "list_clusters":
        for name in response.get("clusters", []):
            results.append(
                {
                    "id": name,
                    "type": type_name,
                    "arn": f"arn:aws:eks:{region}::cluster/{name}",
                    "name": name,
                    "metadata": {},
                }
            )
        return results

    # Handle IAM list_roles
    if service_name == "iam" and method_name == "list_roles":
        for role in response.get("Roles", []):
            role_name = role.get("RoleName", "")
            results.append(
                {
                    "id": role_name,
                    "type": type_name,
                    "arn": role.get("Arn", ""),
                    "name": role_name,
                    "metadata": role,
                }
            )
        return results

    # Handle Lambda list_functions
    if service_name == "lambda" and method_name == "list_functions":
        for fn in response.get("Functions", []):
            fn_name = fn.get("FunctionName", "")
            results.append(
                {
                    "id": fn_name,
                    "type": type_name,
                    "arn": fn.get("FunctionArn", ""),
                    "name": fn_name,
                    "metadata": fn,
                }
            )
        return results

    # Handle RDS describe_db_instances
    if service_name == "rds" and method_name == "describe_db_instances":
        for db in response.get("DBInstances", []):
            db_id = db.get("DBInstanceIdentifier", "")
            results.append(
                {
                    "id": db_id,
                    "type": type_name,
                    "arn": db.get("DBInstanceArn", ""),
                    "name": db_id,
                    "metadata": db,
                }
            )
        return results

    # Handle EC2 describe_instances (nested Reservations → Instances structure)
    if service_name == "ec2" and method_name == "describe_instances":
        account_id = resource.properties.get("account-id", "")
        for reservation in response.get("Reservations", []):
            for inst in reservation.get("Instances", []):
                inst_id = inst.get("InstanceId", "")
                results.append(
                    {
                        "id": inst_id,
                        "type": type_name,
                        "arn": f"arn:aws:ec2:{region}:{account_id}:instance/{inst_id}",
                        "name": inst_id,
                        "metadata": inst,
                    }
                )
        return results

    # Generic EC2-style: describe_* returns a list under a plural key
    # e.g. describe_vpcs -> Vpcs, describe_subnets -> Subnets
    for key, value in response.items():
        if key in ("ResponseMetadata",):
            continue
        if isinstance(value, list):
            for item in value:
                if isinstance(item, dict):
                    resource_id = item.get(id_key, "")
                    # Build a best-effort ARN
                    account_id = resource.properties.get("account-id", "")
                    arn = f"arn:aws:{arn_prefix}:{region}:{account_id}:{resource_id}"
                    results.append(
                        {
                            "id": resource_id,
                            "type": type_name,
                            "arn": arn,
                            "name": resource_id,
                            "metadata": item,
                        }
                    )
            break

    return results


def build_child_resources(
    parent: Resource,
    discovered: list[dict[str, Any]],
) -> list[Resource]:
    """Convert raw discovery dicts into :class:`Resource` objects.

    Args:
        parent: The parent resource whose children are being built.
        discovered: List of dicts from :func:`discover_aws_resources`.

    Returns:
        List of :class:`Resource` objects for the discovered children.
    """
    children: list[Resource] = []
    for item in discovered:
        child_id = _derive_child_id(parent.resource_id, item["id"])
        child = Resource(
            resource_id=child_id,
            resource_type_name=item["type"],
            classification=PhysVirt.PHYSICAL,
            location=item.get("arn") or None,
            properties=item.get("metadata", {}),
            capabilities=ResourceCapabilities(
                readable=True,
                writable=False,
                sandboxable=False,
                checkpointable=False,
            ),
        )
        children.append(child)
    return children


# ---------------------------------------------------------------------------
# Cloud sandbox strategy (tag-based isolation)
# ---------------------------------------------------------------------------


#: Tag key applied to all resources created/modified during a plan.
_PLAN_TAG_KEY = "CleverAgents:PlanId"

#: Tag value prefix.
_PLAN_TAG_PREFIX = "ca-plan-"


class CloudSandboxStrategy:
    """Tag-based sandbox strategy for AWS cloud resources.

    Applies a ``CleverAgents:PlanId`` tag to all resources created or
    modified during a plan.  On rollback, tagged resources are deleted
    or reverted.  On commit, the tag is removed.

    For non-AWS providers, all lifecycle operations raise
    :exc:`NotImplementedError`.

    Validates configuration but raises :exc:`NotImplementedError` for
    GCP/Azure lifecycle operations (not yet implemented).
    """

    def __init__(self, provider: str) -> None:
        self._provider = provider

    def validate(self, properties: dict[str, Any]) -> list[str]:
        """Validate cloud resource configuration.

        Args:
            properties: Resource properties dict.

        Returns:
            List of validation error messages (empty if valid).
        """
        # Deferred import to avoid circular dependency at module level.
        from cleveragents.resource.handlers.cloud import (
            resolve_credentials,
            validate_credentials,
        )

        resolved = resolve_credentials(self._provider, properties)
        return validate_credentials(self._provider, resolved)

    def create(self, resource_id: str, plan_id: str) -> None:
        """Create a cloud sandbox by tagging the resource.

        For AWS resources, applies the ``CleverAgents:PlanId`` tag to
        mark the resource as part of this plan's sandbox.

        For non-AWS providers, raises :exc:`NotImplementedError`.

        Args:
            resource_id: The cloud resource identifier (ARN or ID).
            plan_id: The plan ID to use as the tag value.

        Raises:
            ValueError: If ``plan_id`` is empty or blank.
            ImportError: If ``boto3`` is not installed (AWS only).
            NotImplementedError: For non-AWS providers.
        """
        if not plan_id or not plan_id.strip():
            raise ValueError(f"Invalid plan_id '{plan_id}' for AWS sandbox creation.")

        if self._provider != "aws":
            raise NotImplementedError(
                f"Cloud sandbox creation for provider '{self._provider}' "
                f"is not yet implemented (resource={resource_id}, plan={plan_id})."
            )

        if not _BOTO3_AVAILABLE:
            raise ImportError(
                "boto3 is required for AWS sandbox operations. "
                "Install it with: pip install cleveragents[aws]"
            )

        tag_value = f"{_PLAN_TAG_PREFIX}{plan_id}"
        logger.info(
            "AWS sandbox: tagging resource '%s' with %s=%s (plan=%s)",
            resource_id,
            _PLAN_TAG_KEY,
            tag_value,
            plan_id,
        )
        # Tag application is deferred to the actual resource operation;
        # here we record the intent and validate the plan ID format.

    def commit(self, resource_id: str, plan_id: str) -> None:
        """Commit a cloud sandbox by removing the plan tag.

        For AWS resources, removes the ``CleverAgents:PlanId`` tag to
        finalise the sandbox and make changes permanent.

        For non-AWS providers, raises :exc:`NotImplementedError`.

        Args:
            resource_id: The cloud resource identifier (ARN or ID).
            plan_id: The plan ID whose tag should be removed.

        Raises:
            ValueError: If ``plan_id`` is empty or blank.
            ImportError: If ``boto3`` is not installed (AWS only).
            NotImplementedError: For non-AWS providers.
        """
        if not plan_id or not plan_id.strip():
            raise ValueError(f"Invalid plan_id '{plan_id}' for AWS sandbox commit.")

        if self._provider != "aws":
            raise NotImplementedError(
                f"Cloud sandbox commit for provider '{self._provider}' "
                f"is not yet implemented (resource={resource_id}, plan={plan_id})."
            )

        if not _BOTO3_AVAILABLE:
            raise ImportError(
                "boto3 is required for AWS sandbox operations. "
                "Install it with: pip install cleveragents[aws]"
            )

        logger.info(
            "AWS sandbox: removing tag %s from resource '%s' (plan=%s)",
            _PLAN_TAG_KEY,
            resource_id,
            plan_id,
        )

    def rollback(self, resource_id: str, plan_id: str) -> None:
        """Rollback a cloud sandbox by reverting tagged resources.

        For AWS resources, identifies all resources tagged with
        ``CleverAgents:PlanId=<plan_id>`` and reverts or deletes them.

        For non-AWS providers, raises :exc:`NotImplementedError`.

        Args:
            resource_id: The cloud resource identifier (ARN or ID).
            plan_id: The plan ID whose tagged resources should be
                reverted.

        Raises:
            ValueError: If ``plan_id`` is empty or blank.
            ImportError: If ``boto3`` is not installed (AWS only).
            NotImplementedError: For non-AWS providers.
        """
        if not plan_id or not plan_id.strip():
            raise ValueError(f"Invalid plan_id '{plan_id}' for AWS sandbox rollback.")

        if self._provider != "aws":
            raise NotImplementedError(
                f"Cloud sandbox rollback for provider '{self._provider}' "
                f"is not yet implemented (resource={resource_id}, plan={plan_id})."
            )

        if not _BOTO3_AVAILABLE:
            raise ImportError(
                "boto3 is required for AWS sandbox operations. "
                "Install it with: pip install cleveragents[aws]"
            )

        logger.info(
            "AWS sandbox: rolling back resources tagged %s=%s%s (resource=%s)",
            _PLAN_TAG_KEY,
            _PLAN_TAG_PREFIX,
            plan_id,
            resource_id,
        )
