"""Infrastructure discovery for zae-limiter deployments.

This module provides multi-stack discovery operations across a region,
separate from the single-stack lifecycle management in StackManager.
"""

from typing import Any

import aioboto3

from ..models import LimiterInfo
from ..naming import PREFIX
from .stack_manager import (
    LAMBDA_VERSION_TAG_KEY,
    MANAGED_BY_TAG_KEY,
    MANAGED_BY_TAG_VALUE,
    NAME_TAG_KEY,
    SCHEMA_VERSION_TAG_KEY,
    VERSION_TAG_KEY,
)


class InfrastructureDiscovery:
    """
    Discovers deployed zae-limiter instances across a region.

    Provides read-only discovery of CloudFormation stacks managed by zae-limiter.
    Does NOT manage stack lifecycle (use StackManager for that).

    Example:
        async with InfrastructureDiscovery(region="us-east-1") as discovery:
            limiters = await discovery.list_limiters()
            for limiter in limiters:
                print(f"{limiter.user_name}: {limiter.stack_status}")

    Attributes:
        region: AWS region to discover stacks in
        endpoint_url: Optional CloudFormation endpoint (for LocalStack)
    """

    def __init__(
        self,
        region: str | None = None,
        endpoint_url: str | None = None,
    ) -> None:
        """
        Initialize infrastructure discovery.

        Args:
            region: AWS region (default: use boto3 defaults)
            endpoint_url: CloudFormation endpoint (for LocalStack)
        """
        self.region = region
        self.endpoint_url = endpoint_url
        self._session: aioboto3.Session | None = None
        self._client: Any = None

    async def _get_client(self) -> Any:
        """Get or create CloudFormation client."""
        if self._client is not None:
            return self._client

        if self._session is None:
            self._session = aioboto3.Session()

        kwargs: dict[str, Any] = {}
        if self.region:
            kwargs["region_name"] = self.region
        if self.endpoint_url:
            kwargs["endpoint_url"] = self.endpoint_url

        session = self._session
        self._client = await session.client("cloudformation", **kwargs).__aenter__()
        return self._client

    async def list_limiters(self) -> list[LimiterInfo]:
        """
        List all zae-limiter stacks in the region.

        Uses two discovery methods and merges results:

        1. **Tag-based** (primary): AWS Resource Groups Tagging API filtered
           by ``ManagedBy=zae-limiter``. Efficient server-side filtering.
        2. **describe_stacks** (fallback): CloudFormation ``describe_stacks``
           iterating all stacks with client-side tag filtering. Catches stacks
           in environments where the Tagging API is unavailable (e.g., LocalStack).

        Results are de-duplicated by stack name.

        Returns:
            List of LimiterInfo objects, sorted by user name.
            Excludes DELETE_COMPLETE stacks.

        """
        # Method 1: Tag-based discovery (primary)
        tagged_limiters = await self._discover_via_tagging_api()

        # Method 2: describe_stacks fallback
        describe_limiters = await self._discover_via_describe_stacks()

        # De-duplicate by stack_name (describe results first, tagged override)
        by_stack_name: dict[str, LimiterInfo] = {}
        for limiter in describe_limiters:
            by_stack_name[limiter.stack_name] = limiter
        for limiter in tagged_limiters:
            by_stack_name[limiter.stack_name] = limiter

        # Sort by user name for consistent output
        limiters = sorted(by_stack_name.values(), key=lambda x: x.user_name)
        return limiters

    async def _discover_via_tagging_api(self) -> list[LimiterInfo]:
        """
        Discover stacks via AWS Resource Groups Tagging API.

        Queries for CloudFormation stacks tagged with ``ManagedBy=zae-limiter``.
        Returns empty list if the API is unavailable (e.g., LocalStack).

        Returns:
            List of LimiterInfo objects discovered via tags.
        """
        if self._session is None:
            self._session = aioboto3.Session()

        kwargs: dict[str, Any] = {}
        if self.region:
            kwargs["region_name"] = self.region
        if self.endpoint_url:
            kwargs["endpoint_url"] = self.endpoint_url

        try:
            session = self._session
            async with session.client("resourcegroupstaggingapi", **kwargs) as tagging_client:
                limiters: list[LimiterInfo] = []
                pagination_token: str = ""

                while True:
                    request_kwargs: dict[str, Any] = {
                        "ResourceTypeFilters": ["cloudformation:stack"],
                        "TagFilters": [
                            {
                                "Key": MANAGED_BY_TAG_KEY,
                                "Values": [MANAGED_BY_TAG_VALUE],
                            }
                        ],
                    }
                    if pagination_token:
                        request_kwargs["PaginationToken"] = pagination_token

                    response = await tagging_client.get_resources(**request_kwargs)

                    for resource in response.get("ResourceTagMappingList", []):
                        arn = resource["ResourceARN"]
                        # ARN format: arn:aws:cloudformation:region:account:stack/name/id
                        arn_parts = arn.split("/")
                        if len(arn_parts) < 2:
                            continue
                        stack_name = arn_parts[1]

                        # Extract tags from the tagging API response
                        tag_dict = {t["Key"]: t["Value"] for t in resource.get("Tags", [])}

                        # Get full stack details from CloudFormation
                        info = await self._describe_stack_as_limiter_info(stack_name, tag_dict)
                        if info is not None:
                            limiters.append(info)

                    pagination_token = response.get("PaginationToken", "")
                    if not pagination_token:
                        break

                return limiters

        except Exception:
            # Tagging API unavailable or errored (e.g., LocalStack basic mocking).
            # Fall back silently — describe_stacks discovery will handle it.
            return []

    async def _discover_via_describe_stacks(self) -> list[LimiterInfo]:
        """
        Discover stacks via CloudFormation describe_stacks with client-side tag filtering.

        Iterates all stacks and filters by ``ManagedBy=zae-limiter`` tag.
        Also matches legacy stacks with ``ZAEL-`` prefix that lack tags.

        Returns:
            List of LimiterInfo objects discovered via describe_stacks.
        """
        client = await self._get_client()
        region_display = self.region or "default"

        limiters: list[LimiterInfo] = []
        next_token: str | None = None

        while True:
            kwargs: dict[str, Any] = {}
            if next_token:
                kwargs["NextToken"] = next_token

            response = await client.describe_stacks(**kwargs)

            for stack in response.get("Stacks", []):
                stack_name = stack["StackName"]
                stack_status = stack["StackStatus"]

                if stack_status == "DELETE_COMPLETE":
                    continue

                # Extract tags
                tag_dict = {t["Key"]: t["Value"] for t in stack.get("Tags", [])}

                # Match: has ManagedBy tag OR has legacy ZAEL- prefix
                is_managed = tag_dict.get(MANAGED_BY_TAG_KEY) == MANAGED_BY_TAG_VALUE
                is_legacy_prefix = stack_name.startswith(PREFIX)

                if not is_managed and not is_legacy_prefix:
                    continue

                # Derive user_name: tag first, then strip ZAEL- prefix, then raw
                user_name = tag_dict.get(NAME_TAG_KEY)
                if user_name is None and stack_name.startswith(PREFIX):
                    user_name = stack_name[len(PREFIX) :]
                if user_name is None:
                    user_name = stack_name

                creation_time = stack["CreationTime"].isoformat()
                last_updated = stack.get("LastUpdatedTime")
                last_updated_time = last_updated.isoformat() if last_updated else None

                limiters.append(
                    LimiterInfo(
                        stack_name=stack_name,
                        user_name=user_name,
                        region=region_display,
                        stack_status=stack_status,
                        creation_time=creation_time,
                        last_updated_time=last_updated_time,
                        version=tag_dict.get(VERSION_TAG_KEY),
                        lambda_version=tag_dict.get(LAMBDA_VERSION_TAG_KEY),
                        schema_version=tag_dict.get(SCHEMA_VERSION_TAG_KEY),
                    )
                )

            next_token = response.get("NextToken")
            if not next_token:
                break

        return limiters

    async def _describe_stack_as_limiter_info(
        self,
        stack_name: str,
        tag_dict: dict[str, str] | None = None,
    ) -> LimiterInfo | None:
        """
        Describe a CloudFormation stack and convert to LimiterInfo.

        Args:
            stack_name: Stack name to describe
            tag_dict: Pre-fetched tags (from tagging API). If None,
                tags are extracted from describe_stacks response.

        Returns:
            LimiterInfo or None if the stack doesn't exist or is deleted.
        """
        client = await self._get_client()
        region_display = self.region or "default"

        try:
            response = await client.describe_stacks(StackName=stack_name)
            stacks = response.get("Stacks", [])
            if not stacks:
                return None

            stack = stacks[0]
            if stack["StackStatus"] == "DELETE_COMPLETE":
                return None

            # Use provided tags or extract from stack
            if tag_dict is None:
                tag_dict = {t["Key"]: t["Value"] for t in stack.get("Tags", [])}

            # Derive user_name from tag or prefix
            user_name = tag_dict.get("zae-limiter:name")
            if user_name is None and stack_name.startswith(PREFIX):
                user_name = stack_name[len(PREFIX) :]
            if user_name is None:
                user_name = stack_name  # Last resort

            creation_time = stack["CreationTime"].isoformat()
            last_updated = stack.get("LastUpdatedTime")
            last_updated_time = last_updated.isoformat() if last_updated else None

            return LimiterInfo(
                stack_name=stack_name,
                user_name=user_name,
                region=region_display,
                stack_status=stack["StackStatus"],
                creation_time=creation_time,
                last_updated_time=last_updated_time,
                version=tag_dict.get(VERSION_TAG_KEY),
                lambda_version=tag_dict.get(LAMBDA_VERSION_TAG_KEY),
                schema_version=tag_dict.get(SCHEMA_VERSION_TAG_KEY),
            )
        except Exception:
            return None

    async def close(self) -> None:
        """Close the underlying session and client."""
        if self._client is not None:
            try:
                await self._client.__aexit__(None, None, None)
            except Exception:
                pass  # Best effort cleanup
            finally:
                self._client = None
        self._session = None

    async def __aenter__(self) -> "InfrastructureDiscovery":
        """Enter async context manager."""
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: Any,
    ) -> None:
        """Exit async context manager."""
        await self.close()
