"""
Encrypted file-based credential storage as keyring fallback.

Uses Fernet symmetric encryption with a machine-derived key.
Credentials stored at ~/.aicippy/credentials.enc

This module provides a cross-platform fallback when the OS keyring
(macOS Keychain, Windows Credential Manager, Linux SecretService)
is unavailable or fails. The encryption key is derived from
machine-specific data using PBKDF2HMAC with SHA-256.

Note: This is not high-security storage. It prevents casual reading
of credentials but a determined attacker with filesystem access and
knowledge of the derivation scheme could recover the key. The OS
keyring remains the preferred storage backend.
"""

from __future__ import annotations

import base64
import getpass
import json
import platform
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any

# fcntl is Unix-only; on Windows we fall back to no-op locking
# (Windows file locking uses msvcrt, but concurrent CLI instances
# on Windows are far less common and the atomic temp-file rename
# already provides basic safety).
if sys.platform != "win32":
    import fcntl
else:
    fcntl = None  # type: ignore[assignment]

from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

from aicippy.utils.logging import get_logger

if TYPE_CHECKING:
    from collections.abc import Generator

logger = get_logger(__name__)

# Fixed iteration count for PBKDF2 key derivation
_PBKDF2_ITERATIONS: int = 480000

# Static passphrase component for key derivation
_KEY_PASSPHRASE: bytes = b"aicippy-credential-store"


class FileCredentialStorage:
    """Encrypted file-based credential storage as keyring fallback.

    Uses Fernet symmetric encryption with a machine-derived key.
    Credentials stored at ~/.aicippy/credentials.enc

    All credentials are stored in a single encrypted JSON file. Each
    credential is keyed by ``{service}:{key}`` to match the keyring
    API semantics (service name + key name).

    Attributes:
        _cred_path: Path to the encrypted credentials file.
        _fernet: Fernet cipher instance for encrypt/decrypt operations.
    """

    __slots__ = ("_cred_path", "_fernet", "_lock_path")

    def __init__(self) -> None:
        """Initialize file credential storage.

        Creates the ~/.aicippy directory if it does not exist and
        derives the encryption key from machine-specific data.
        """
        self._cred_path = Path.home() / ".aicippy" / "credentials.enc"
        self._lock_path = Path.home() / ".aicippy" / "credentials.lock"
        self._cred_path.parent.mkdir(parents=True, exist_ok=True)

        # Restrict directory permissions to owner only (rwx------)
        try:
            self._cred_path.parent.chmod(0o700)
        except OSError:
            logger.warning("chmod_dir_not_supported", exc_info=True)

        key = self._derive_key()
        self._fernet = Fernet(key)

        logger.debug(
            "file_credential_storage_initialized",
            path=str(self._cred_path),
        )

    def _derive_key(self) -> bytes:
        """Derive encryption key from machine-specific data.

        Uses PBKDF2HMAC with SHA-256, combining the machine hostname
        and current OS username as salt. This ensures the key is
        unique per machine/user combination.

        Returns:
            URL-safe base64-encoded 32-byte key suitable for Fernet.
        """
        salt = f"{platform.node()}-{getpass.getuser()}-aicippy".encode()
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=_PBKDF2_ITERATIONS,
        )
        key = base64.urlsafe_b64encode(kdf.derive(_KEY_PASSPHRASE))
        return key

    def _load_store(self) -> dict[str, str]:
        """Load and decrypt the credential store from disk.

        Returns:
            Dictionary of ``{service:key} -> value`` mappings.
            Returns empty dict if file does not exist or cannot be
            decrypted.
        """
        if not self._cred_path.exists():
            return {}

        try:
            encrypted_data = self._cred_path.read_bytes()
            decrypted = self._fernet.decrypt(encrypted_data)
            store: dict[str, str] = json.loads(decrypted.decode("utf-8"))
            return store
        except InvalidToken:
            logger.warning(
                "file_credential_store_decrypt_failed",
                reason="invalid_token_or_key_mismatch",
            )
            return {}
        except (json.JSONDecodeError, UnicodeDecodeError) as e:
            logger.warning(
                "file_credential_store_corrupted",
                error=str(e),
            )
            return {}
        except OSError as e:
            logger.warning(
                "file_credential_store_read_failed",
                error=str(e),
            )
            return {}

    def _save_store(self, store: dict[str, str]) -> None:
        """Encrypt and save the credential store to disk.

        Args:
            store: Dictionary of ``{service:key} -> value`` mappings.

        Raises:
            FileStorageError: If the file cannot be written.
        """
        tmp_path = self._cred_path.with_suffix(".tmp")
        try:
            data = json.dumps(store).encode("utf-8")
            encrypted = self._fernet.encrypt(data)
            # Write atomically via temp file to prevent corruption
            tmp_path.write_bytes(encrypted)

            # Set restrictive permissions on temp file BEFORE rename
            # to prevent a window where credentials are world-readable
            try:
                tmp_path.chmod(0o600)
            except OSError:
                logger.warning("chmod_tmp_not_supported", exc_info=True)

            tmp_path.replace(self._cred_path)

            # Ensure final file also has restrictive permissions
            try:
                self._cred_path.chmod(0o600)
            except OSError:
                logger.warning("chmod_not_supported", exc_info=True)

        except OSError as e:
            try:
                tmp_path.unlink(missing_ok=True)
            except OSError:
                logger.debug("tmp_cleanup_failed", exc_info=True)
            logger.error(
                "file_credential_store_write_failed",
                error=str(e),
            )
            raise FileStorageError(f"Failed to write credential store: {e}") from e

    def _with_file_lock(self, exclusive: bool = True) -> Any:
        """Acquire a file-level lock for cross-process safety.

        Uses a separate .lock file alongside the credentials file.
        Returns a context manager that holds the lock.

        On Windows (where fcntl is unavailable), returns a no-op
        context manager. The atomic temp-file rename in _save_store
        still provides basic write safety.

        Args:
            exclusive: True for write operations (LOCK_EX),
                       False for read-only operations (LOCK_SH).

        Returns:
            A context manager wrapping the lock file descriptor.
        """
        import contextlib

        if fcntl is None:
            # Windows fallback: no-op context manager
            @contextlib.contextmanager
            def _noop_lock() -> Generator[None, None, None]:
                yield

            return _noop_lock()

        @contextlib.contextmanager
        def _lock() -> Generator[None, None, None]:
            lock_fd = None
            try:
                lock_fd = Path(self._lock_path).open("w")  # noqa: SIM115
                lock_op = fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH
                fcntl.flock(lock_fd, lock_op)
                yield
            finally:
                if lock_fd is not None:
                    try:
                        fcntl.flock(lock_fd, fcntl.LOCK_UN)
                    finally:
                        lock_fd.close()

        return _lock()

    def store(self, service: str, key: str, value: str) -> None:
        """Store an encrypted credential.

        Uses file-level locking to prevent TOCTOU races between
        concurrent CLI processes performing read-modify-write.

        Args:
            service: Service name (e.g. ``aicippy``).
            key: Key name (e.g. ``tokens``).
            value: Value to store (will be encrypted on disk).

        Raises:
            FileStorageError: If the credential cannot be saved.
        """
        store_key = f"{service}:{key}"
        with self._with_file_lock(exclusive=True):
            current = self._load_store()
            current[store_key] = value
            self._save_store(current)
        logger.debug("file_credential_stored", service=service, key=key)

    def get(self, service: str, key: str) -> str | None:
        """Get a decrypted credential.

        Uses a shared file lock to allow concurrent reads while
        blocking during writes.

        Args:
            service: Service name (e.g. ``aicippy``).
            key: Key name (e.g. ``tokens``).

        Returns:
            The decrypted value if found, None otherwise.
        """
        store_key = f"{service}:{key}"
        with self._with_file_lock(exclusive=False):
            current = self._load_store()
        value = current.get(store_key)
        if value is not None:
            logger.debug("file_credential_retrieved", service=service, key=key)
        return value

    def delete(self, service: str, key: str) -> None:
        """Delete a credential.

        Silently succeeds if the credential does not exist.
        Uses file-level locking for cross-process safety.

        Args:
            service: Service name (e.g. ``aicippy``).
            key: Key name (e.g. ``tokens``).
        """
        store_key = f"{service}:{key}"
        with self._with_file_lock(exclusive=True):
            current = self._load_store()
            if store_key in current:
                del current[store_key]
                self._save_store(current)
                logger.debug("file_credential_deleted", service=service, key=key)


class FileStorageError(Exception):
    """Raised when file-based credential storage operations fail."""
