# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Credential encryption for stored passwords (IMAP/SMTP, CalDAV).

A dedicated Fernet key is auto-generated at ~/.familiar/.keys/credentials.key
on first use. This is intentionally separate from FAMILIAR_ENCRYPTION_KEY
(which encrypts sessions and memory) so credential rotation is independent.

Graceful degradation: if the cryptography package is not installed, passwords
are stored as plaintext with a warning logged.

Migration: Fernet tokens always start with 'gAAAAA' (urlsafe-b64 of the
version byte). Anything else is treated as plaintext and transparently returned
as-is, so existing unencrypted registries work without a migration step.
"""

import logging
from pathlib import Path

logger = logging.getLogger(__name__)

# Fernet tokens always begin with this prefix (version byte 0x80 → urlsafe-b64).
# Used to detect plaintext passwords during migration.
_FERNET_PREFIX = "gAAAAA"

# Override this in tests via monkeypatch to redirect the key file.
_key_file_override: "Path | None" = None


def _key_file() -> Path:
    return _key_file_override or (Path.home() / ".familiar" / ".keys" / "credentials.key")


def _get_or_create_key() -> "bytes | None":
    """Load or auto-generate a Fernet key. Returns None if cryptography unavailable."""
    try:
        from cryptography.fernet import Fernet
    except ImportError:
        return None

    kf = _key_file()
    if kf.exists():
        return kf.read_bytes()

    # Generate and persist a new key.
    key = Fernet.generate_key()
    kf.parent.mkdir(parents=True, exist_ok=True)
    kf.write_bytes(key)
    kf.chmod(0o600)
    logger.info("Generated credential encryption key: %s", kf)
    return key


def encrypt_password(plaintext: str) -> str:
    """Encrypt a credential password for at-rest storage.

    Returns the plaintext unchanged (with a warning) if the cryptography
    package is not installed.
    """
    if not plaintext:
        return plaintext

    key = _get_or_create_key()
    if key is None:
        logger.warning(
            "cryptography package not installed — credential stored without encryption. "
            "Install it with: pip install 'familiar-agent[encryption]'"
        )
        return plaintext

    from cryptography.fernet import Fernet

    return Fernet(key).encrypt(plaintext.encode()).decode()


def decrypt_password(stored: str) -> str:
    """Decrypt a stored credential password.

    Falls back to returning the stored value unchanged when:
    - The value is not a Fernet token (plaintext migration case).
    - The cryptography package is not installed.
    - Decryption fails (wrong key or corrupted data — logs a warning).
    """
    if not stored:
        return stored

    # Fast-path: not a Fernet token → plaintext (migration or no-encryption case).
    if not stored.startswith(_FERNET_PREFIX):
        return stored

    key = _get_or_create_key()
    if key is None:
        return stored

    try:
        from cryptography.fernet import Fernet

        return Fernet(key).decrypt(stored.encode()).decode()
    except Exception:
        logger.warning(
            "Failed to decrypt stored credential — returning stored value as-is. "
            "If this persists, re-enter the password via /connect."
        )
        return stored
