# Copyright (c) 2025 CoReason, Inc.
#
# This software is proprietary and dual-licensed.
# Licensed under the Prosperity Public License 3.0 (the "License").
# A copy of the license is available at https://prosperitylicense.com/versions/3.0.0
# For details, see the LICENSE file.
# Commercial use beyond a 30-day trial requires a separate license.
#
# Source Code: https://github.com/CoReason-AI/coreason_vault

import asyncio
from typing import Any, Dict

import httpx
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential

from coreason_vault.auth_async import VaultAuthenticationAsync
from coreason_vault.config import CoreasonVaultConfig
from coreason_vault.exceptions import SecretNotFoundError, VaultConnectionError
from coreason_vault.utils.logger import logger

RETRYABLE_EXCEPTIONS = (
    httpx.RequestError,
    httpx.HTTPStatusError,
)


class SecretKeeperAsync:
    """Async version of SecretKeeper."""

    def __init__(self, auth: VaultAuthenticationAsync, config: CoreasonVaultConfig):
        self._auth = auth
        self._config = config
        # We can implement async cache later if needed. For now, prompt implies resource management.
        # But we should probably have cache to be compatible with SecretKeeper logic?
        # The prompt says "Refactor Core Logic ... Create the Async class".
        # It doesn't explicitly ask for caching in Async, but if it replaces sync, it should behave similarly.
        # I'll add a simple async cache if possible or just direct calls first.
        # Existing SecretKeeper has cache. I should probably add it.
        # Using cachetools.TTLCache with asyncio.Lock.
        from cachetools import TTLCache

        self._cache: TTLCache[str, Dict[str, Any]] = TTLCache(maxsize=1024, ttl=60)
        self._lock = asyncio.Lock()

    async def get_secret(self, path: str) -> Dict[str, Any]:
        """Retrieves a KV v2 secret."""
        async with self._lock:
            if path in self._cache:
                logger.debug(f"Secret {path} fetched from cache")
                return self._cache[path]

            try:
                secret_data = await self._fetch_from_vault(path)
            except Exception as e:
                # Wrap
                if isinstance(e, (SecretNotFoundError, PermissionError)):
                    raise
                logger.error(f"Failed to fetch secret {path} after retries: {e}")
                raise VaultConnectionError(f"Failed to fetch secret after retries: {e}") from e

            self._cache[path] = secret_data
            logger.info(f"Secret {path} fetched from Vault (cached: False)")
            return secret_data  # type: ignore[no-any-return]

    @retry(  # type: ignore[misc]
        retry=retry_if_exception_type(RETRYABLE_EXCEPTIONS),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        reraise=True,
    )
    async def _fetch_from_vault(self, path: str) -> Dict[str, Any]:
        client = await self._auth.get_client()
        mount_point = self._config.VAULT_MOUNT_POINT

        api_path = f"v1/{mount_point}/data/{path}"
        response = await client.get(api_path)

        if response.status_code == 404:
            raise SecretNotFoundError(f"Secret not found: {path}")
        if response.status_code == 403:
            raise PermissionError(f"Permission denied: {path}")

        response.raise_for_status()

        data = response.json()
        if not isinstance(data, dict):
            raise ValueError("Secret payload must be a dictionary")

        secret_data = data.get("data", {}).get("data")
        if not isinstance(secret_data, dict):
            raise ValueError("Secret data payload is not a dictionary")

        return secret_data

    async def get_dynamic_secret(self, path: str) -> Dict[str, Any]:
        """Retrieves a dynamic secret."""
        try:
            return await self._fetch_dynamic_secret(path)  # type: ignore[no-any-return]
        except Exception as e:
            if isinstance(e, (SecretNotFoundError, PermissionError)):
                raise
            logger.error(f"Failed to fetch dynamic secret {path} after retries: {e}")
            raise VaultConnectionError(f"Failed to fetch dynamic secret after retries: {e}") from e

    @retry(  # type: ignore[misc]
        retry=retry_if_exception_type(RETRYABLE_EXCEPTIONS),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        reraise=True,
    )
    async def _fetch_dynamic_secret(self, path: str) -> Dict[str, Any]:
        client = await self._auth.get_client()

        # Path for dynamic secrets is usually straight up path
        # But client.read(path) implies path includes mount?
        # Existing code: client.read(path)
        # So user provides full path? "database/creds/my-role"
        # Yes.

        api_path = f"v1/{path}"
        response = await client.get(api_path)

        if response.status_code == 404:
            # client.read returns None in hvac.
            raise SecretNotFoundError(f"Dynamic secret not found: {path}")
        if response.status_code == 403:
            raise PermissionError(f"Permission denied: {path}")

        response.raise_for_status()
        data = response.json()

        if not isinstance(data, dict):
            raise ValueError("Response must be a dictionary")

        return data

    get = get_secret
