"""AUTO-GENERATED by scripts/generate_sync.py - DO NOT EDIT.

Source: infra/discovery.py

This module provides synchronous versions of the async classes.
Changes should be made to the source file, then regenerated.
"""

import logging
from typing import Any

import boto3

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,
    TYPE_TAG_KEY,
    VERSION_TAG_KEY,
)

logger = logging.getLogger(__name__)


class SyncInfrastructureDiscovery:
    """
    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 SyncStackManager for that).

    Example:
        async with SyncInfrastructureDiscovery(region="us-east-1") as discovery:
            limiters = 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: boto3.Session | None = None
        self._client: Any = None

    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 = boto3.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 = session.client("cloudformation", **kwargs)
        return self._client

    def list_limiters(self, stack_type: str | None = None) -> 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.

        Args:
            stack_type: Filter by stack type tag (e.g., ``"limiter"``,
                ``"load-test"``). ``None`` returns all types.

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

        """
        tagged_limiters = self._discover_via_tagging_api()
        describe_limiters = self._discover_via_describe_stacks()
        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
        if stack_type is not None:
            by_stack_name = {
                name: info for name, info in by_stack_name.items() if info.stack_type == stack_type
            }
        limiters = sorted(by_stack_name.values(), key=lambda x: x.user_name)
        return limiters

    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 = boto3.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
            tagging_client = session.client("resourcegroupstaggingapi", **kwargs)
            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 = tagging_client.get_resources(**request_kwargs)
                for resource in response.get("ResourceTagMappingList", []):
                    arn = resource["ResourceARN"]
                    arn_parts = arn.split("/")
                    if len(arn_parts) < 2:
                        continue
                    stack_name = arn_parts[1]
                    tag_dict = {t["Key"]: t["Value"] for t in resource.get("Tags", [])}
                    info = 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:
            return []

    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 = 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 = client.describe_stacks(**kwargs)
            for stack in response.get("Stacks", []):
                stack_name = stack["StackName"]
                stack_status = stack["StackStatus"]
                if stack_status == "DELETE_COMPLETE":
                    continue
                tag_dict = {t["Key"]: t["Value"] for t in stack.get("Tags", [])}
                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
                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),
                        stack_type=tag_dict.get(TYPE_TAG_KEY, "limiter"),
                    )
                )
            next_token = response.get("NextToken")
            if not next_token:
                break
        return limiters

    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 = self._get_client()
        region_display = self.region or "default"
        try:
            response = 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
            if tag_dict is None:
                tag_dict = {t["Key"]: t["Value"] for t in stack.get("Tags", [])}
            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
            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),
                stack_type=tag_dict.get(TYPE_TAG_KEY, "limiter"),
            )
        except Exception:
            return None

    def close(self) -> None:
        """Close the underlying session and client."""
        if self._client is not None:
            self._client = None
        self._session = None

    def __enter__(self) -> "SyncInfrastructureDiscovery":
        """Enter context manager."""
        return self

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