"""Cloud infrastructure resource handler for CleverAgents.

Provides the :class:`CloudResourceHandler` for cloud infrastructure
resource types.  The handler supports a hierarchical type model where
provider-specific types (``aws-account``, ``aws-vpc``, etc.) inherit
from generic ``cloud-*`` base types.

This handler validates configuration, resolves credentials, and -- when
``boto3`` is installed -- executes real AWS SDK operations to discover
and resolve cloud resources.

AWS-specific SDK logic lives in
:mod:`cleveragents.resource.handlers.cloud_aws`.  Provider credential
specifications live in
:mod:`cleveragents.resource.handlers.cloud_providers`.

Cloud resource types are registered as built-in types at bootstrap and
use ``sandbox_strategy = "none"`` because cloud sandbox isolation is
implemented via resource tagging (tag-based isolation strategy).

## Provider Detection

The handler extracts the cloud provider from the resource type name:

- ``aws-*`` or ``aws`` -> AWS provider
- ``gcp-*`` or ``gcp`` -> GCP provider
- ``azure-*`` or ``azure`` -> Azure provider
- ``cloud-*`` -> generic (no provider-specific validation)

## Credential Resolution

Credentials are resolved in this priority order:

1. Explicit configuration values (passed via ``properties``)
2. Environment variables
3. Profile names (AWS only -- ``AWS_PROFILE``)

Credential values are never logged; the existing
:mod:`cleveragents.shared.redaction` patterns handle masking.

Based on:
    - Issue #343: Cloud Infrastructure Resources
    - Issue #1021: AWS SDK integration
    - implementation_plan.md group M7.post-resource-cloud
"""

from __future__ import annotations

import hashlib
import logging
import os
from typing import Any

from cleveragents.domain.models.core.resource import Resource
from cleveragents.infrastructure.sandbox.manager import SandboxManager
from cleveragents.resource.handlers._base import EMPTY_CONTENT_HASH
from cleveragents.resource.handlers.cloud_aws import (
    _BOTO3_AVAILABLE,
    _build_aws_session,
    build_child_resources,
    discover_aws_resources,
)
from cleveragents.resource.handlers.cloud_providers import (
    _PROVIDER_PREFIXES,
    CLOUD_PROVIDERS,
    _CredentialField,
)
from cleveragents.resource.handlers.protocol import (
    Content,
    DeleteResult,
    DiffResult,
    WriteResult,
)
from cleveragents.shared.redaction import REDACTED, is_sensitive_key
from cleveragents.tool.context import BoundResource

logger = logging.getLogger(__name__)


def extract_provider(type_name: str) -> str | None:
    """Extract the cloud provider key from a resource type name.

    Supports both flat names (``aws``, ``gcp``, ``azure``) and
    hierarchical names (``aws-account``, ``aws-vpc``, etc.).
    Returns ``None`` for generic ``cloud-*`` base types.

    Args:
        type_name: Resource type name.

    Returns:
        Provider key (``aws``, ``gcp``, ``azure``) or ``None``.
    """
    # Direct match (flat provider names)
    if type_name in CLOUD_PROVIDERS:
        return type_name
    # Hierarchical names: aws-*, gcp-*, azure-*
    for prefix in _PROVIDER_PREFIXES:
        if type_name.startswith(prefix):
            return prefix.rstrip("-")
    # Generic cloud-* base types have no specific provider
    return None


def _safe_value_repr(field_def: _CredentialField, value: str | None) -> str:
    """Return a safe representation of a credential value for logging."""
    if value is None:
        return "<not set>"
    if field_def.sensitive or is_sensitive_key(field_def.name):
        return REDACTED
    return value


def resolve_credentials(
    provider: str,
    properties: dict[str, Any],
) -> dict[str, str | None]:
    """Resolve credentials for a cloud provider from properties and env vars.

    Resolution priority:
    1. Explicit values in ``properties``
    2. Environment variables

    Args:
        provider: Cloud provider name (``aws``, ``gcp``, ``azure``).
        properties: Resource properties dict with explicit values.

    Returns:
        Dict mapping field names to resolved values (``None`` if unresolved).

    Raises:
        ValueError: If the provider name is not recognised.
    """
    spec = CLOUD_PROVIDERS.get(provider)
    if spec is None:
        raise ValueError(
            f"Unknown cloud provider '{provider}'. "
            f"Supported providers: {', '.join(sorted(CLOUD_PROVIDERS))}."
        )

    resolved: dict[str, str | None] = {}
    for field_def in spec.all_fields:
        # Priority 1: explicit property
        explicit = properties.get(field_def.name)
        if explicit is not None and str(explicit).strip():
            resolved[field_def.name] = str(explicit)
            continue

        # Priority 2: environment variable
        env_val = os.environ.get(field_def.env_var)
        if env_val is not None and env_val.strip():
            resolved[field_def.name] = env_val
            continue

        resolved[field_def.name] = None

    return resolved


def validate_credentials(
    provider: str,
    resolved: dict[str, str | None],
) -> list[str]:
    """Validate that all required credentials are present.

    Args:
        provider: Cloud provider name.
        resolved: Dict from :func:`resolve_credentials`.

    Returns:
        List of error messages (empty if valid).
    """
    spec = CLOUD_PROVIDERS.get(provider)
    if spec is None:
        return [
            f"Unknown cloud provider '{provider}'. "
            f"Supported providers: {', '.join(sorted(CLOUD_PROVIDERS))}."
        ]

    # Check if a profile is set (AWS only) -- profile can satisfy
    # required credential fields
    has_profile = False
    if spec.profile_env_var is not None:
        profile_val = resolved.get("profile")
        if profile_val is not None and profile_val.strip():
            has_profile = True

    errors: list[str] = []
    for field_def in spec.all_fields:
        if not field_def.required:
            continue
        value = resolved.get(field_def.name)
        # If a profile is configured, skip credential-field checks
        # (the SDK will resolve them from the profile).
        if has_profile and field_def in spec.credential_fields:
            continue
        if value is None:
            safe_repr = _safe_value_repr(field_def, value)
            errors.append(
                f"Required field '{field_def.name}' for provider "
                f"'{provider}' is missing (current value: {safe_repr}). "
                f"Set it via resource properties or the "
                f"{field_def.env_var} environment variable."
            )

    return errors


class CloudResourceHandler:
    """Handler for cloud infrastructure resource types.

    Validates cloud resource configuration, resolves credentials, and --
    when ``boto3`` is installed -- executes real AWS SDK operations to
    discover and resolve cloud resources.

    Supports hierarchical type names (``aws-account``, ``aws-vpc``,
    etc.) in addition to flat provider names (``aws``, ``gcp``,
    ``azure``).  Generic ``cloud-*`` base types skip provider-specific
    credential validation.

    This handler satisfies the
    :class:`~cleveragents.resource.handlers.protocol.ResourceHandler`
    protocol.
    """

    def resolve(
        self,
        *,
        resource: Resource,
        plan_id: str,
        slot_name: str,
        sandbox_manager: SandboxManager,
        access: str = "read_only",
    ) -> BoundResource:
        """Resolve a cloud resource into a :class:`BoundResource`.

        For AWS resources, builds a boto3 session and returns a
        :class:`BoundResource` with the resource ARN as ``sandbox_path``.
        For GCP and Azure, raises :exc:`NotImplementedError`.

        Args:
            resource: A cloud resource instance.
            plan_id: The plan requesting the sandbox.
            slot_name: Name of the tool resource slot being filled.
            sandbox_manager: The sandbox lifecycle manager (unused for
                cloud resources).
            access: Access mode (``read_only`` or ``read_write``).

        Returns:
            A :class:`BoundResource` with ``sandbox_path`` set to the
            resource ARN or location.

        Raises:
            ValueError: If required credentials are missing.
            ImportError: If ``boto3`` is not installed (AWS only).
            NotImplementedError: For GCP/Azure or generic ``cloud-*`` types.
        """
        type_name = resource.resource_type_name
        provider = extract_provider(type_name)

        if provider is None:
            raise NotImplementedError(
                f"Cloud resource execution for generic type '{type_name}' "
                f"is not yet implemented. Resource '{resource.resource_id}' "
                f"(plan={plan_id}, slot={slot_name}). "
                f"Generic cloud base types cannot be resolved directly."
            )

        if provider not in CLOUD_PROVIDERS:
            raise ValueError(
                f"Resource type '{type_name}' maps to unknown provider "
                f"'{provider}'. "
                f"Supported: {', '.join(sorted(CLOUD_PROVIDERS))}."
            )

        resolved = resolve_credentials(provider, dict(resource.properties))

        for field_def in CLOUD_PROVIDERS[provider].all_fields:
            safe = _safe_value_repr(field_def, resolved.get(field_def.name))
            logger.debug("Cloud credential %s/%s = %s", provider, field_def.name, safe)

        is_account_type = type_name in ("aws", "gcp", "azure", "aws-account")
        if is_account_type:
            errors = validate_credentials(provider, resolved)
            if errors:
                raise ValueError(
                    f"Cloud resource validation failed for '{type_name}' "
                    f"(provider={provider}): " + "; ".join(errors)
                )

        if provider == "aws":
            return self._resolve_aws(
                resource=resource,
                plan_id=plan_id,
                slot_name=slot_name,
                resolved=resolved,
                access=access,
            )

        raise NotImplementedError(
            f"Cloud resource execution for '{type_name}' "
            f"(provider={provider}) is not yet implemented. "
            f"Resource '{resource.resource_id}' "
            f"(plan={plan_id}, slot={slot_name}) validated successfully "
            f"but SDK integration for {provider} is pending."
        )

    def _resolve_aws(
        self,
        *,
        resource: Resource,
        plan_id: str,
        slot_name: str,
        resolved: dict[str, str | None],
        access: str,
    ) -> BoundResource:
        """Resolve an AWS resource using boto3.

        Args:
            resource: The AWS resource to resolve.
            plan_id: The plan ID (used for tagging).
            slot_name: The tool slot name.
            resolved: Resolved credential dict.
            access: Access mode.

        Returns:
            A :class:`BoundResource` for the AWS resource.

        Raises:
            ImportError: If ``boto3`` is not installed.
            ValueError: If AWS credentials are invalid or the API call fails.
        """
        if not _BOTO3_AVAILABLE:
            raise ImportError(
                "boto3 is required for AWS resource operations. "
                "Install it with: pip install cleveragents[aws]"
            )

        session = _build_aws_session(resolved)
        location = resource.location or ""
        type_name = resource.resource_type_name

        is_account_type = type_name in ("aws", "aws-account")
        if is_account_type:
            try:
                sts = session.client(
                    "sts",
                    region_name=resolved.get("region") or "us-east-1",
                )
                identity = sts.get_caller_identity()
                account_id = identity.get("Account", "")
                location = location or f"arn:aws:iam::{account_id}:root"
                logger.debug(
                    "AWS STS identity verified for account %s (plan=%s)",
                    account_id,
                    plan_id,
                )
            except Exception as exc:
                raise ValueError(
                    f"AWS credential verification failed for resource "
                    f"'{resource.resource_id}' (plan={plan_id}): {exc}"
                ) from exc

        return BoundResource(
            slot_name=slot_name,
            resource_id=resource.resource_id,
            resource_type=type_name,
            sandbox_path=location or None,
            access=access,
        )

    def read(self, *, resource: Resource, path: str = "") -> Content:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support read()")

    def write(self, *, resource: Resource, path: str, data: bytes) -> WriteResult:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support write()")

    def delete(self, *, resource: Resource, path: str = "") -> DeleteResult:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support delete()")

    def list_children(self, *, resource: Resource) -> list[str]:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support list_children()")

    def diff(self, *, resource: Resource, other_location: str) -> DiffResult:
        """Not supported by the cloud handler."""
        raise NotImplementedError("Cloud handler does not support diff()")

    def discover_children(self, *, resource: Resource) -> list[Resource]:
        """Discover child AWS resources using the boto3 SDK.

        For AWS resource types that have a discovery mapping (VPCs,
        subnets, instances, etc.), this method queries the AWS API and
        returns a list of child :class:`Resource` objects.

        Args:
            resource: The parent AWS resource.

        Returns:
            List of discovered child :class:`Resource` objects.

        Raises:
            ImportError: If ``boto3`` is not installed.
            NotImplementedError: For non-AWS providers.
        """
        type_name = resource.resource_type_name
        provider = extract_provider(type_name)

        if provider != "aws":
            raise NotImplementedError(
                f"Cloud handler discover_children() is only implemented "
                f"for aws resources (got provider={provider!r})."
            )

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

        resolved = resolve_credentials("aws", dict(resource.properties))
        session = _build_aws_session(resolved)
        discovered = discover_aws_resources(resource, session)
        return build_child_resources(resource, discovered)

    def create_sandbox(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: Any,
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support create_sandbox()")

    def create_checkpoint(
        self,
        *,
        resource: Resource,
        plan_id: str,
        sandbox_manager: Any,
        phase: str = "execution",
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support create_checkpoint()")

    def rollback_to(
        self,
        *,
        resource: Resource,
        plan_id: str,
        checkpoint_id: str,
        sandbox_manager: Any,
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support rollback_to()")

    def project_access(
        self,
        *,
        resource: Resource,
        principal: str,
        action: str = "read",
        project_id: str = "",
    ) -> Any:
        """Not supported for cloud resources."""
        raise NotImplementedError("Cloud handler does not support project_access()")

    def content_hash(
        self,
        resource: Resource,
        *,
        algorithm: str = "sha256",
    ) -> str:
        """Compute content hash for cloud resources.

        Returns :data:`EMPTY_CONTENT_HASH` if no location is set.
        """
        if not resource.location:
            return EMPTY_CONTENT_HASH

        h = hashlib.new(algorithm)
        h.update(resource.resource_type_name.encode("utf-8"))
        h.update(b"\0")
        h.update(resource.location.encode("utf-8"))
        return h.hexdigest()
