"""
Runtime API layer.

This module provides the Credentials class for runtime access to
encrypted secrets with thread-safe, idempotent operations.
"""

from __future__ import annotations

import threading
from pathlib import Path
from typing import Any

from kilit.exceptions import KilitCredentialsError
from kilit.service import CredentialsService


def __repr__(self) -> str:
    """
    Safe repr that doesn't expose secrets.

    Returns:
        String representation without sensitive data
    """
    loaded = "loaded" if self._cache is not None else "not loaded"
    return f"Credentials(path={self.path!r}, {loaded})"


class Credentials:
    """
    Runtime API for accessing encrypted secrets.

    Features:
    - Thread-safe operations with explicit locking
    - Idempotent load() (calling multiple times doesn't reload)
    - Auto-load on first get()/require() call
    - Dotted path access (e.g., "database.password")
    - In-memory caching after first load

    Usage:
        creds = Credentials("config/credentials.json.enc")
        creds.load()  # Explicit load

        # Or auto-load on first access
        password = creds.get("database.password")
        api_key = creds.require("api.key")  # Raises if missing

        # Reload if file changes
        creds.reload()

    Thread Safety:
    - All public methods are thread-safe
    - Uses threading.Lock for synchronization
    - Multiple threads can safely call get()/require()
    - load() and reload() are also thread-safe

    Idempotency:
    - load() is idempotent (calling twice doesn't reload)
    - reload() explicitly clears cache and reloads
    - get()/require() auto-load if not loaded
    """

    def __init__(
        self,
        path: Path | str = "config/credentials.json.enc",
        master_key: bytes | str | None = None,
        service: CredentialsService | None = None,
    ):
        """
        Initialize credentials loader.

        Args:
            path: Path to encrypted credentials file
            master_key: Optional master key (bytes or base64 string)
                       If None, will be loaded from sources on first access
            service: Optional CredentialsService instance

        Note:
            Credentials are NOT loaded until load() is called or
            get()/require() is called (auto-load).
        """
        self.path = Path(path)
        self._master_key = master_key
        self._service = service or CredentialsService()
        self._cache: dict | None = None
        self._lock = threading.Lock()

    def load(self) -> None:
        """
        Load and decrypt credentials file.

        This method is idempotent - calling it multiple times will
        only load once. Use reload() to force a reload.

        Thread Safety:
            This method is thread-safe. Multiple threads can call it
            simultaneously, but only one will perform the actual load.

        Raises:
            KilitKeyError: If master key is invalid or missing
            KilitFileError: If file not found or corrupted
            KilitFormatError: If file format is invalid
        """
        with self._lock:
            # Idempotent - don't reload if already loaded
            if self._cache is not None:
                return

            # Load master key if not provided
            if self._master_key is None:
                master_key_bytes = self._service.load_master_key(
                    env_var=True,
                    cli_key=None,
                    cli_key_path=None,
                    default_path=Path("config/master.key"),
                )
            elif isinstance(self._master_key, str):
                # Decode from base64
                import base64

                padding = "=" * (4 - len(self._master_key) % 4) if len(self._master_key) % 4 else ""
                master_key_bytes = base64.urlsafe_b64decode(self._master_key + padding)
            else:
                master_key_bytes = self._master_key

            # Decrypt credentials
            self._cache = self._service.decrypt_credentials(
                path=self.path, master_key=master_key_bytes, codec=None
            )

    def reload(self) -> None:
        """
        Clear cache and reload credentials.

        Use this method if the credentials file has changed and you
        want to reload it.

        Thread Safety:
            This method is thread-safe.
        """
        with self._lock:
            self._cache = None
        self.load()

    def get(self, dotted_path: str, default: Any = None) -> Any:
        """
        Get value by dotted path.

        Args:
            dotted_path: Dot-separated path (e.g., "database.password")
            default: Default value if path not found

        Returns:
            Value at path, or default if not found

        Auto-Load:
            If credentials not loaded, this method will load them automatically.

        Thread Safety:
            This method is thread-safe.

        Examples:
            creds.get("database.password")
            creds.get("api.key", "default-key")
            creds.get("nested.deep.value")
        """
        # Auto-load if not loaded
        if self._cache is None:
            self.load()

        try:
            return self._resolve_path(dotted_path)
        except KeyError:
            return default

    def require(self, dotted_path: str) -> Any:
        """
        Get value by dotted path, raise if missing.

        Args:
            dotted_path: Dot-separated path (e.g., "database.password")

        Returns:
            Value at path

        Raises:
            KilitCredentialsError: If path not found

        Auto-Load:
            If credentials not loaded, this method will load them automatically.

        Thread Safety:
            This method is thread-safe.

        Examples:
            password = creds.require("database.password")
            api_key = creds.require("api.key")
        """
        # Auto-load if not loaded
        if self._cache is None:
            self.load()

        try:
            return self._resolve_path(dotted_path)
        except KeyError:
            raise KilitCredentialsError(
                f"Required credential '{dotted_path}' not found in {self.path}"
            )

    def _resolve_path(self, dotted_path: str) -> Any:
        """
        Traverse nested dict using dot-separated keys.

        Args:
            dotted_path: Dot-separated path (e.g., "database.password")

        Returns:
            Value at path

        Raises:
            KeyError: If path not found

        Examples:
            _resolve_path("database.password")
            -> data["database"]["password"]

            _resolve_path("api.key")
            -> data["api"]["key"]
        """
        if self._cache is None:
            raise RuntimeError("Credentials not loaded")

        keys = dotted_path.split(".")
        value = self._cache

        for key in keys:
            if not isinstance(value, dict):
                raise KeyError(f"Cannot traverse non-dict value at '{key}'")
            if key not in value:
                raise KeyError(f"Key '{key}' not found in path '{dotted_path}'")
            value = value[key]

        return value
