"""Secure credential storage using OS keychain with file-based fallback.

Uses the keyring library to store tokens securely in:
- macOS: Keychain
- Windows: Credential Manager (WinCred)
- Linux: Secret Service (GNOME Keyring, KWallet)

When the OS keyring is unavailable or encounters errors, automatically
falls back to encrypted file-based storage at ~/.aicippy/credentials.enc.

Cross-platform error handling:
- Windows: Handles WinCred backend errors and access denied
- macOS: Handles Keychain access permission errors (e.g. user denied)
- Linux: Handles missing SecretService/GNOME Keyring/KWallet
"""

from __future__ import annotations

import json
import sys
from typing import Any

from aicippy.auth.models import TokenInfo
from aicippy.utils.logging import get_logger

logger = get_logger(__name__)

# Service name for keyring
SERVICE_NAME = "aicippy"

# Key names
KEY_TOKENS = "tokens"
KEY_USER_ID = "user_id"
KEY_USER_EMAIL = "user_email"

# Probe key used to test keyring availability (never stored long-term)
_PROBE_SERVICE = "aicippy-probe"
_PROBE_KEY = "probe"


def _is_keyring_available() -> tuple[bool, str]:
    """Test whether the OS keyring is functional.

    Performs a non-destructive probe by calling ``keyring.get_password``
    on a throwaway service/key. This detects:
    - Missing keyring backends (Linux without SecretService)
    - Permission errors (macOS Keychain access denied)
    - WinCred backend failures on Windows

    Returns:
        Tuple of (available: bool, backend_name: str).
        If unavailable, backend_name describes the failure reason.

    """
    try:
        import keyring
        from keyring.errors import KeyringError

        backend = keyring.get_keyring()
        backend_name = type(backend).__name__

        # Detect known non-functional backends
        # On Linux, keyring may fall back to a plaintext or fail backend
        # when SecretService/GNOME Keyring/KWallet is not available
        _fail_backends = {
            "fail",
            "Keyring",  # base class fallback
            "NullKeyring",
            "ChainerBackend",  # may chain to fail
        }

        # Check if the backend is a known non-functional type
        if backend_name in _fail_backends:
            logger.info(
                "keyring_backend_non_functional",
                backend=backend_name,
            )
            return False, f"non-functional backend: {backend_name}"

        # Probe: attempt a read operation to verify the backend works
        # This will surface permission errors, missing D-Bus, etc.
        keyring.get_password(_PROBE_SERVICE, _PROBE_KEY)

    except KeyringError as e:
        _reason = _classify_keyring_error(e)
        logger.warning(
            "keyring_probe_failed",
            error=str(e),
            reason=_reason,
            platform=sys.platform,
        )
        return False, _reason

    except ImportError:
        logger.warning("keyring_import_failed")
        return False, "keyring package not installed"

    except Exception as e:
        logger.warning(
            "keyring_probe_unexpected_error",
            error=str(e),
            error_type=type(e).__name__,
            platform=sys.platform,
        )
        return False, f"unexpected: {type(e).__name__}: {e}"
    else:
        logger.debug(
            "keyring_probe_success",
            backend=backend_name,
        )
        return True, backend_name


def _classify_keyring_error(error: Exception) -> str:
    """Classify a keyring error into a human-readable reason.

    Provides platform-specific context for common failure modes.

    Args:
        error: The exception from keyring operations.

    Returns:
        Human-readable classification of the error.

    """
    error_str = str(error).lower()
    error_type = type(error).__name__

    if sys.platform == "darwin":
        # macOS Keychain errors
        if "security" in error_str or "authorization" in error_str:
            return "macOS Keychain: access denied by user or system policy"
        if "errsecinternalcomponent" in error_str:
            return "macOS Keychain: internal error (try unlocking Keychain Access)"
        if "locked" in error_str:
            return "macOS Keychain: keychain is locked"
        return f"macOS Keychain error: {error_type}"

    if sys.platform == "win32":
        # Windows Credential Manager errors
        if "wincred" in error_str or "credential" in error_str:
            return "Windows Credential Manager: access error"
        if "access" in error_str and "denied" in error_str:
            return "Windows Credential Manager: access denied"
        if "element not found" in error_str:
            return "Windows Credential Manager: element not found"
        return f"Windows WinCred error: {error_type}"

    # Linux / other platforms
    if "secretservice" in error_str or "dbus" in error_str:
        return "Linux: SecretService/D-Bus unavailable (install gnome-keyring or kwallet)"
    if "no recommended backend" in error_str:
        return "Linux: no keyring backend available (install gnome-keyring or kwallet)"
    if "locked" in error_str:
        return "Linux: keyring is locked"

    return f"keyring error: {error_type}: {error}"


class KeychainStorage:
    """Secure storage for authentication credentials.

    Uses the OS-native keychain for secure token storage, with
    automatic fallback to encrypted file-based storage when the
    keychain is unavailable.

    NEVER logs or prints sensitive values.

    Attributes:
        _service: Service identifier for keyring entries.
        _use_keyring: Whether the OS keyring is being used.
        _backend: Name of the active storage backend.
        _file_storage: File-based fallback storage (lazy-initialized).

    """

    __slots__ = ("_backend", "_file_storage", "_service", "_use_keyring")

    def __init__(self, service_name: str = SERVICE_NAME) -> None:
        """Initialize keychain storage.

        Probes the OS keyring on construction. If the keyring is
        unavailable, initializes encrypted file-based fallback.

        Args:
            service_name: Service identifier for keychain entries.

        """
        self._service = service_name
        self._file_storage: Any = None  # Lazy: FileCredentialStorage | None

        # Probe keyring availability
        available, backend_info = _is_keyring_available()
        self._use_keyring = available
        self._backend = backend_info if available else f"file (reason: {backend_info})"

        if not available:
            self._init_file_fallback()
            logger.info(
                "keychain_using_file_fallback",
                reason=backend_info,
                platform=sys.platform,
            )
        else:
            logger.debug(
                "keychain_using_keyring",
                backend=backend_info,
            )

    def _init_file_fallback(self) -> None:
        """Initialize the file-based fallback storage."""
        from aicippy.auth.file_storage import FileCredentialStorage

        self._file_storage = FileCredentialStorage()

    def _fallback_to_file(self) -> None:
        """Switch to file-based storage after a keyring failure.

        Called when a keyring operation fails at runtime (as opposed
        to failing during the initial probe). Ensures subsequent
        operations use the file backend.
        """
        if self._file_storage is None:
            self._init_file_fallback()
        self._use_keyring = False
        self._backend = "file (keyring failed at runtime)"
        logger.warning(
            "keychain_fallback_to_file",
            reason="keyring_operation_failed",
        )

    @property
    def backend_name(self) -> str:
        """Get the name of the active storage backend.

        Returns:
            Human-readable backend description.

        """
        return self._backend

    def store_tokens(self, tokens: TokenInfo) -> None:
        """Store tokens securely.

        Uses OS keyring if available, falls back to encrypted file.

        Args:
            tokens: Token information to store.

        Raises:
            StorageError: If storage fails in both backends.

        """
        token_data = json.dumps(tokens.to_dict())

        if self._use_keyring:
            try:
                import keyring

                keyring.set_password(self._service, KEY_TOKENS, token_data)
                logger.info("tokens_stored", backend="keyring")
                return
            except Exception as e:
                logger.warning(
                    "keyring_store_tokens_failed",
                    error=str(e),
                    error_type=type(e).__name__,
                )
                self._fallback_to_file()

        # File fallback
        try:
            self._file_storage.store(self._service, KEY_TOKENS, token_data)
            logger.info("tokens_stored", backend="file")
        except Exception as e:
            logger.exception("token_storage_failed_all_backends", error=str(e))
            raise StorageError(f"Failed to store tokens: {e}") from e

    def get_tokens(self) -> TokenInfo | None:
        """Retrieve tokens from storage.

        Returns:
            TokenInfo if found and valid, None otherwise.

        """
        token_data: str | None = None

        if self._use_keyring:
            try:
                import keyring

                token_data = keyring.get_password(self._service, KEY_TOKENS)
            except Exception as e:
                logger.warning(
                    "keyring_get_tokens_failed",
                    error=str(e),
                    error_type=type(e).__name__,
                )
                self._fallback_to_file()

        if token_data is None and self._file_storage is not None:
            try:
                token_data = self._file_storage.get(self._service, KEY_TOKENS)
            except Exception as e:
                logger.error(
                    "file_get_tokens_failed",
                    error=str(e),
                    error_type=type(e).__name__,
                    backend="file",
                )
                raise StorageError(
                    f"Failed to retrieve tokens from file storage: {e}"
                ) from e

        if token_data is None:
            logger.debug("no_token_data_found_in_storage", backend=self._backend)
            return None

        try:
            data = json.loads(token_data)
            return TokenInfo.from_dict(data)
        except (json.JSONDecodeError, KeyError) as e:
            logger.warning("token_data_corrupted", error=str(e))
            self.clear_tokens()
            return None

    def clear_tokens(self) -> None:
        """Remove all stored tokens from both backends."""
        if self._use_keyring:
            try:
                import keyring
                from keyring.errors import KeyringError

                keyring.delete_password(self._service, KEY_TOKENS)
            except (KeyringError, Exception):
                logger.debug("keyring_delete_tokens_failed", exc_info=True)

        if self._file_storage is not None:
            try:
                self._file_storage.delete(self._service, KEY_TOKENS)
            except Exception:
                logger.warning("file_storage_delete_tokens_failed", exc_info=True)

        logger.info("tokens_cleared")

    def store_user_info(self, user_id: str, email: str) -> None:
        """Store user information securely.

        Args:
            user_id: User's Cognito sub identifier.
            email: User's email address.

        Raises:
            StorageError: If storage fails in both backends.

        """
        if self._use_keyring:
            try:
                import keyring

                keyring.set_password(self._service, KEY_USER_ID, user_id)
                keyring.set_password(self._service, KEY_USER_EMAIL, email)
                logger.info("user_info_stored", backend="keyring")
                return
            except Exception as e:
                logger.warning(
                    "keyring_store_user_info_failed",
                    error=str(e),
                    error_type=type(e).__name__,
                )
                self._fallback_to_file()

        # File fallback
        try:
            self._file_storage.store(self._service, KEY_USER_ID, user_id)
            self._file_storage.store(self._service, KEY_USER_EMAIL, email)
            logger.info("user_info_stored", backend="file")
        except Exception as e:
            logger.exception("user_info_storage_failed_all_backends", error=str(e))
            raise StorageError(f"Failed to store user info: {e}") from e

    def get_user_info(self) -> tuple[str | None, str | None]:
        """Retrieve user information from storage.

        Returns:
            Tuple of (user_id, email), with None for missing values.

        """
        user_id: str | None = None
        email: str | None = None

        if self._use_keyring:
            try:
                import keyring

                user_id = keyring.get_password(self._service, KEY_USER_ID)
                email = keyring.get_password(self._service, KEY_USER_EMAIL)
                return user_id, email
            except Exception as e:
                logger.warning(
                    "keyring_get_user_info_failed",
                    error=str(e),
                    error_type=type(e).__name__,
                )
                self._fallback_to_file()

        if self._file_storage is not None:
            try:
                user_id = self._file_storage.get(self._service, KEY_USER_ID)
                email = self._file_storage.get(self._service, KEY_USER_EMAIL)
            except Exception as e:
                logger.exception("file_get_user_info_failed", error=str(e))

        return user_id, email

    def clear_user_info(self) -> None:
        """Remove all stored user information from both backends."""
        if self._use_keyring:
            try:
                import keyring
                from keyring.errors import KeyringError

                keyring.delete_password(self._service, KEY_USER_ID)
                keyring.delete_password(self._service, KEY_USER_EMAIL)
            except (KeyringError, Exception):
                logger.debug("keyring_delete_user_info_failed", exc_info=True)

        if self._file_storage is not None:
            try:
                self._file_storage.delete(self._service, KEY_USER_ID)
                self._file_storage.delete(self._service, KEY_USER_EMAIL)
            except Exception:
                logger.warning("file_storage_delete_user_info_failed", exc_info=True)

        logger.info("user_info_cleared")

    def clear_all(self) -> None:
        """Remove all stored credentials from both backends."""
        self.clear_tokens()
        self.clear_user_info()
        logger.info("all_credentials_cleared")

    def has_valid_tokens(self) -> bool:
        """Check if valid (non-expired) tokens exist.

        Returns:
            True if valid tokens are stored, False otherwise.

        """
        tokens = self.get_tokens()
        if tokens is None:
            return False
        return not tokens.is_expired()


class StorageError(Exception):
    """Raised when credential storage operations fail."""
