"""Secure credential management for Yera using system keyring.

This module provides a wrapper around the system keyring for storing and
retrieving credentials securely. It maintains a manifest of stored keys
and validates key formats to ensure consistency across the application.

"""

import contextlib
import json
import re

import keyring
from keyring.backends.fail import Keyring as FailKeyring
from keyring.errors import KeyringError, NoKeyringError, PasswordDeleteError


class DevKeyring:
    """Manages secure credential storage using the system keyring.

    DevKeyring provides a high-level interface for storing, retrieving, and
    managing credentials in the system keyring. It maintains a manifest of
    all stored keys to enable listing and bulk operations, and enforces a
    consistent key naming scheme.

    All credentials are stored under the service name "yera" and use a
    dot-separated hierarchical key format (e.g., 'providers.anthropic.api_key').

    Attributes:
        SERVICE_NAME: The service identifier used for all keyring operations.
        MANIFEST_KEY: The special key used to store the list of credential keys.
        KEY_PATTERN: Regex pattern for validating credential key formats.
    """

    SERVICE_NAME = "yera"
    MANIFEST_KEY = "_yera_manifest"
    KEY_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*$")

    @classmethod
    def is_available(cls) -> bool:
        """Check if a valid keyring backend is available.

        Determines whether the system has a functional keyring backend
        that can be used for credential storage. Returns False if only
        the fail backend is available or if there are keyring errors.

        Returns:
            bool: True if a valid keyring backend is available, False otherwise.

        """
        try:
            backend = keyring.get_keyring()
            return not isinstance(backend, FailKeyring)
        except (NoKeyringError, KeyringError):
            return False

    @classmethod
    def _validate_key(cls, key: str) -> None:
        if not key or not key.strip():
            raise ValueError("Credential key cannot be empty")
        if not cls.KEY_PATTERN.match(key):
            raise ValueError(
                f"Invalid key format: '{key}'. "
                "Key must contain only letters, numbers, underscores, and hyphens, "
                "with parts separated by dots (e.g., 'providers.anthropic.api_key')"
            )

    @classmethod
    def _get_manifest(cls) -> set[str]:
        with contextlib.suppress(KeyError, json.JSONDecodeError):
            manifest_json = keyring.get_password(cls.SERVICE_NAME, cls.MANIFEST_KEY)
            if manifest_json:
                return set(json.loads(manifest_json))
        return set()

    @classmethod
    def _save_manifest(cls, keys: set[str]) -> None:
        with contextlib.suppress(KeyringError):
            keyring.set_password(
                cls.SERVICE_NAME, cls.MANIFEST_KEY, json.dumps(list(keys))
            )

    @classmethod
    def _add_to_manifest(cls, key: str) -> None:
        manifest = cls._get_manifest()
        manifest.add(key)
        cls._save_manifest(manifest)

    @classmethod
    def _remove_from_manifest(cls, key: str) -> None:
        manifest = cls._get_manifest()
        manifest.discard(key)
        cls._save_manifest(manifest)

    @classmethod
    def get_backend_info(cls) -> str:
        """Get information about the current keyring backend.

        Returns a string describing the active keyring backend, including
        its name and priority level. Useful for debugging credential storage
        issues.

        Returns:
            str: Backend name and priority (e.g., "macOS Keychain (5)"),
                or "No backend available" if no valid backend exists.

        Example:
            >>> print(DevKeyring.get_backend_info())
            macOS Keychain (5)
        """
        if cls.is_available():
            backend = keyring.get_keyring()
            return f"{backend.name} ({backend.priority})"
        return "No backend available"

    @classmethod
    def set(cls, key: str, value: str) -> None:
        """Store a credential in the keyring.

        Saves a credential value associated with the given key. The key is
        automatically added to the manifest for tracking purposes.

        Args:
            key: Credential identifier following the format requirements
                (alphanumeric, underscores, hyphens, dot-separated).
            value: The credential value to store (e.g., API key, password).

        Raises:
            ValueError: If the key format is invalid or if key/value is empty.
            KeyringError: If the keyring backend fails to store the credential.

        Example:
            >>> DevKeyring.set('providers.anthropic.api_key', 'sk-ant-...')
            >>> DevKeyring.set('databases.my_db.password', 'secret123')
        """
        cls._validate_key(key)
        if not value or not value.strip():
            raise ValueError("Credential value cannot be empty")
        keyring.set_password(cls.SERVICE_NAME, key.strip(), value.strip())
        cls._add_to_manifest(key)

    @classmethod
    def get(cls, key: str) -> str:
        """Retrieve a credential from the keyring.

        Fetches the credential value associated with the given key.

        Args:
            key: Credential identifier to retrieve.

        Returns:
            str: The credential value associated with the key.

        Raises:
            ValueError: If the key format is invalid or if no credential
                exists for the given key.

        Example:
            >>> api_key = DevKeyring.get('providers.anthropic.api_key')
            >>> print(api_key)
            sk-ant-...
        """
        cls._validate_key(key)
        try:
            cred = keyring.get_password(cls.SERVICE_NAME, key)
        except KeyringError:
            cred = None

        if cred:
            return cred
        raise ValueError(f"No credentials found for {key}") from None

    @classmethod
    def delete(cls, key: str) -> None:
        """Delete a credential from the keyring.

        Removes the credential associated with the given key and updates
        the manifest accordingly. Safe to call even if the key doesn't exist.

        Args:
            key: Credential identifier to delete.

        Raises:
            ValueError: If the key format is invalid.
            RuntimeError: If the keyring backend fails to delete the credential.

        Example:
            >>> DevKeyring.delete('providers.anthropic.api_key')
        """
        cls._validate_key(key)
        try:
            keyring.delete_password(cls.SERVICE_NAME, key)
            cls._remove_from_manifest(key)
        except PasswordDeleteError:
            # Key doesn't exist
            cls._remove_from_manifest(key)  # Clean up manifest anyway
        except KeyringError as e:
            raise RuntimeError(f"Failed to delete secret: {e}") from None

    @classmethod
    def exists(cls, key: str) -> bool:
        """Check if a credential exists in the keyring.

        Verifies whether a credential is stored for the given key without
        retrieving its value.

        Args:
            key: Credential identifier to check.

        Returns:
            bool: True if the credential exists, False otherwise.

        Raises:
            ValueError: If the key format is invalid.

        Example:
            >>> if DevKeyring.exists('api.key'):
            ...     print("Credential found")
            ... else:
            ...     print("Credential not found")
        """
        cls._validate_key(key)
        try:
            cls.get(key)
            return True
        except ValueError:
            return False

    @classmethod
    def list(cls) -> list[str]:
        """List all stored credential keys.

        Returns a sorted list of all credential keys currently stored in
        the keyring, as tracked by the manifest.

        Returns:
            list[str]: Sorted list of credential keys. Returns an empty
                list if no credentials are stored.

        Example:
            >>> keys = DevKeyring.list()
            >>> print(keys)
            ['database.password', 'providers.anthropic.api_key']
        """
        return sorted(cls._get_manifest())

    @classmethod
    def delete_all(cls) -> None:
        """Delete all credentials managed by DevKeyring.

        Removes all credentials tracked in the manifest and deletes the
        manifest itself. This operation is irreversible and will clear
        all Yera-related credentials from the system keyring.

        Note:
            This is a best-effort operation that continues even if individual
            deletions fail.

        Example:
            >>> DevKeyring.delete_all()  # Removes all stored credentials
        """
        if not cls.exists(cls.MANIFEST_KEY):
            # Nothing to delete
            return

        for key in cls.list():
            with contextlib.suppress(PasswordDeleteError, KeyringError):
                cls.delete(key)

        keyring.delete_password(cls.SERVICE_NAME, cls.MANIFEST_KEY)
