"""Persistent credential store at ~/.alice/credentials.json.

Credentials are encrypted at rest using Fernet (AES-128-CBC + HMAC-SHA256).
The encryption key is stored in the OS keychain (macOS Keychain, Windows
Credential Manager) via the ``keyring`` library. On systems without a
supported keychain, the key falls back to ``~/.alice/.key`` (0o600).

File format when encrypted::

    {"v": 1, "enc": "<fernet-token>"}

Plain JSON files written by older versions are transparently migrated to
the encrypted format on first read.
"""

from __future__ import annotations

import contextlib
import json
import os
from dataclasses import asdict, dataclass
from pathlib import Path

_STORE_DIR = Path.home() / ".alice"
_STORE_FILE = _STORE_DIR / "credentials.json"
_KEY_FILE = _STORE_DIR / ".key"

_KEYRING_SERVICE = "alice-cli"
_KEYRING_USERNAME = "fernet-key"


@dataclass
class StoredCredentials:
    """Credentials persisted between CLI sessions."""

    profile: str | None = None
    region: str | None = None
    bedrock_access_key: str | None = None
    bedrock_secret_key: str | None = None
    bedrock_bearer_token: str | None = None
    inference_profile_arns: dict[str, str] | None = None


# ── encryption helpers ────────────────────────────────────────────────────────


def _get_or_create_key() -> bytes:
    """Return the Fernet key, creating and persisting it on first use.

    Priority:
    1. OS keychain via ``keyring`` (most secure)
    2. Key file ``~/.alice/.key`` (fallback for headless / CI environments)
    """
    # Try OS keychain first
    try:
        import keyring  # type: ignore[import-untyped,unused-ignore]
        from keyring.errors import NoKeyringError  # type: ignore[import-untyped,unused-ignore]

        try:
            existing = keyring.get_password(_KEYRING_SERVICE, _KEYRING_USERNAME)
            if existing:
                return existing.encode()  # type: ignore[no-any-return,return-value,unused-ignore]
            # No key yet — generate and store
            from cryptography.fernet import Fernet

            new_key = Fernet.generate_key()
            keyring.set_password(_KEYRING_SERVICE, _KEYRING_USERNAME, new_key.decode())
            return new_key  # type: ignore[no-any-return,return-value,unused-ignore]
        except NoKeyringError:
            pass
        except Exception:  # noqa: BLE001 — keyring can raise many things on misconfigured systems
            pass
    except ImportError:
        pass

    # Fallback: key file
    if _KEY_FILE.exists():
        return _KEY_FILE.read_bytes().strip()

    from cryptography.fernet import Fernet

    new_key = Fernet.generate_key()
    _STORE_DIR.mkdir(parents=True, exist_ok=True)
    _KEY_FILE.write_bytes(new_key + b"\n")
    os.chmod(_KEY_FILE, 0o600)
    return new_key  # type: ignore[no-any-return,return-value,unused-ignore]


def _encrypt_json(data: dict) -> str:  # type: ignore[type-arg]
    """Return an encrypted envelope ``{"v": 1, "enc": "<token>"}``."""
    from cryptography.fernet import Fernet

    key = _get_or_create_key()
    token = Fernet(key).encrypt(json.dumps(data).encode()).decode()
    return json.dumps({"v": 1, "enc": token})


def _decrypt_json(raw: str) -> dict:  # type: ignore[type-arg]
    """Decrypt an encrypted envelope and return the inner dict."""
    from cryptography.fernet import Fernet, InvalidToken

    envelope = json.loads(raw)
    key = _get_or_create_key()
    try:
        plaintext = Fernet(key).decrypt(envelope["enc"].encode()).decode()
    except InvalidToken as exc:
        raise ValueError("Credential file is corrupted or the encryption key has changed.") from exc
    return json.loads(plaintext)  # type: ignore[no-any-return]


def _is_encrypted(data: dict) -> bool:  # type: ignore[type-arg]
    """Return True if ``data`` is an encrypted envelope."""
    return data.get("v") == 1 and "enc" in data


# ── public helpers ────────────────────────────────────────────────────────────


def _is_nonblank_str(value: object) -> bool:
    return isinstance(value, str) and value.strip() != ""


def _validate_arns(value: object) -> dict[str, str] | None:
    if not isinstance(value, dict):
        return None
    return {k: v for k, v in value.items() if _is_nonblank_str(k) and _is_nonblank_str(v)} or None


def _parse_credential_dict(d: dict) -> StoredCredentials:  # type: ignore[type-arg]
    return StoredCredentials(
        profile=d.get("profile") if _is_nonblank_str(d.get("profile")) else None,
        region=d.get("region") if _is_nonblank_str(d.get("region")) else None,
        bedrock_access_key=d.get("bedrock_access_key")
        if _is_nonblank_str(d.get("bedrock_access_key"))
        else None,
        bedrock_secret_key=d.get("bedrock_secret_key")
        if _is_nonblank_str(d.get("bedrock_secret_key"))
        else None,
        bedrock_bearer_token=d.get("bedrock_bearer_token")
        if _is_nonblank_str(d.get("bedrock_bearer_token"))
        else None,
        inference_profile_arns=_validate_arns(d.get("inference_profile_arns")),
    )


def load_stored_credentials() -> StoredCredentials:
    """Load credentials from disk, returning defaults if the file is missing or corrupt.

    Transparently migrates plain-JSON files to encrypted format on first read.
    """
    if not _STORE_FILE.exists():
        return StoredCredentials()
    try:
        raw = _STORE_FILE.read_text()
        data = json.loads(raw)
        if not isinstance(data, dict):
            return StoredCredentials()

        if _is_encrypted(data):
            # Encrypted format — decrypt
            try:
                inner = _decrypt_json(raw)
            except (ValueError, KeyError):
                return StoredCredentials()
            return _parse_credential_dict(inner)
        else:
            # Plain JSON (legacy) — parse and migrate to encrypted
            creds = _parse_credential_dict(data)
            with contextlib.suppress(Exception):  # migration is best-effort
                _write_encrypted(creds)
            return creds
    except (json.JSONDecodeError, OSError):
        return StoredCredentials()


def _write_encrypted(creds: StoredCredentials) -> None:
    """Write credentials to disk in encrypted format."""
    _STORE_DIR.mkdir(parents=True, exist_ok=True)
    os.chmod(_STORE_DIR, 0o700)
    encrypted = _encrypt_json(asdict(creds))
    _STORE_FILE.write_text(encrypted + "\n")
    os.chmod(_STORE_FILE, 0o600)


def save_stored_credentials(creds: StoredCredentials) -> Path:
    """Write credentials to disk after validation. Returns the path written.

    Raises ``ValueError`` if bedrock_secret_key is blank/empty.
    """
    if creds.bedrock_secret_key is not None and not creds.bedrock_secret_key.strip():
        raise ValueError("Cannot store a blank Bedrock secret key.")
    if creds.bedrock_access_key is not None and not creds.bedrock_access_key.strip():
        creds.bedrock_access_key = None

    _write_encrypted(creds)
    return _STORE_FILE


def clear_stored_credentials() -> None:
    """Remove the stored credentials file if it exists."""
    _STORE_FILE.unlink(missing_ok=True)
