"""boto3 Session factory with module-level TTL cache.

Caches boto3 Sessions by (profile, region, access_key) for up to
``SESSION_TTL`` seconds. This saves 50–600 ms of credential resolution
overhead for batch scripts, shell loops, and TUI operations that create
multiple AWS clients in quick succession.

The cache key deliberately excludes the secret key so that only the
publicly-visible identifier (access key alias) distinguishes entries.

Thread-safety: A ``threading.Lock`` guards all cache mutations so the
module is safe under concurrent CLI invocations (e.g. ``alice batch``
with ``--concurrency``).
"""

from __future__ import annotations

import threading
import time

import boto3

# ── Configuration ─────────────────────────────────────────────────────────────

SESSION_TTL: float = 3600.0  # 1 hour

# ── Internal cache ────────────────────────────────────────────────────────────

_CacheKey = tuple[str | None, str, str | None]  # (profile, region, access_key)
_CacheEntry = tuple[boto3.Session, float]  # (session, created_at_monotonic)

_cache: dict[_CacheKey, _CacheEntry] = {}
_lock = threading.Lock()


# ── Public API ────────────────────────────────────────────────────────────────


def get_session(
    *,
    profile: str | None = None,
    region: str,
    access_key: str | None = None,
    secret_key: str | None = None,
) -> boto3.Session:
    """Return a boto3 Session, reusing a cached one if still valid.

    Args:
        profile: AWS CLI profile name (SSO mode).
        region: AWS region for the session.
        access_key: AWS access key ID (API-key mode).
        secret_key: AWS secret access key (API-key mode). Not part of cache key.

    Returns:
        A boto3.Session configured with the provided credentials.

    Example::

        # SSO mode
        session = get_session(profile="drcc-ai", region="us-east-1")

        # API-key mode
        session = get_session(
            region="us-east-1",
            access_key="BedrockAPIKey-testuser-at-123",
            secret_key="ABSK...",
        )
    """
    key: _CacheKey = (profile, region, access_key)
    now = time.monotonic()

    with _lock:
        entry = _cache.get(key)
        if entry is not None:
            session, created_at = entry
            if now - created_at < SESSION_TTL:
                return session

        session = _create_session(profile, region, access_key, secret_key)
        _cache[key] = (session, now)
        return session


def invalidate_session(
    profile: str | None = None,
    region: str = "",
    access_key: str | None = None,
) -> None:
    """Remove a cached session so the next call creates a fresh one.

    Call this after re-authentication (``alice auth``) to ensure new
    credentials are used immediately rather than after TTL expiry.

    Args:
        profile: AWS CLI profile name.
        region: AWS region.
        access_key: AWS access key ID.
    """
    key: _CacheKey = (profile, region, access_key)
    with _lock:
        _cache.pop(key, None)


def clear_all_sessions() -> None:
    """Evict all cached sessions. Primarily for use in tests."""
    with _lock:
        _cache.clear()


# ── Internal helpers ──────────────────────────────────────────────────────────


def _create_session(
    profile: str | None,
    region: str,
    access_key: str | None,
    secret_key: str | None,
) -> boto3.Session:
    """Create a new boto3 Session without caching."""
    if access_key and secret_key:
        return boto3.Session(
            aws_access_key_id=access_key,
            aws_secret_access_key=secret_key,
            region_name=region,
        )
    if profile:
        # boto3 still reads AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from the
        # environment even when profile_name is set, because botocore's 'env'
        # credential provider runs first in the chain.  Stale API key env vars
        # (e.g. from a previous `alice auth --claude`) would silently win over
        # the SSO profile, sending every S3/STS call to the wrong IAM identity.
        # Removing the 'env' provider forces botocore to fall through to the
        # profile/SSO credential providers instead.
        import botocore.exceptions as _botocore_exc
        import botocore.session as _botocore_session

        try:
            bc = _botocore_session.Session()
            bc.set_config_variable("profile", profile)
            bc.get_component("credential_provider").remove("env")
            return boto3.Session(botocore_session=bc, region_name=region)
        except _botocore_exc.ProfileNotFound:
            # Profile doesn't exist in ~/.aws/config (e.g. in tests); fall back
            # to the standard boto3 session so callers get a clean error later.
            pass
    if profile:
        return boto3.Session(profile_name=profile, region_name=region)
    return boto3.Session(region_name=region)
