"""
Encrypted secrets store with master password protection.

The secrets are stored in an encrypted file, with the encryption key derived
from a master password using Argon2id. The master password can be changed
without re-encrypting all secrets.
"""
import json
import os
import stat
from pathlib import Path
from typing import Dict, Optional, Any

from cryptography.fernet import InvalidToken

from .crypto import (
    generate_salt,
    derive_key_from_password,
    encrypt_data,
    decrypt_data,
    create_verification_hash,
    verify_password,
)


class SecretsStoreError(Exception):
    """Base exception for secrets store errors."""
    pass


class SecretsStoreLocked(SecretsStoreError):
    """Raised when trying to access secrets while store is locked."""
    pass


class InvalidMasterPassword(SecretsStoreError):
    """Raised when master password is incorrect."""
    pass


class SecretsStore:
    """
    Encrypted secrets store with master password protection.

    Usage:
        # Initialize (first time)
        store = SecretsStore()
        store.initialize("my-secure-password")

        # Unlock
        store = SecretsStore()
        store.unlock("my-secure-password")

        # Access secrets
        api_key = store.get("ANTHROPIC_API_KEY")
        store.set("NEW_SECRET", "secret-value")

        # Change master password
        store.change_master_password("my-secure-password", "new-password")

        # Lock when done
        store.lock()
    """

    def __init__(self, data_dir: Optional[Path] = None):
        """
        Initialize secrets store.

        Args:
            data_dir: Directory for secrets files (defaults to backend/data)
        """
        if data_dir is None:
            # Default to backend/data
            backend_root = Path(__file__).parent.parent.parent
            data_dir = backend_root / "data"

        self.data_dir = Path(data_dir)
        self.data_dir.mkdir(parents=True, exist_ok=True)

        self.master_key_file = self.data_dir / ".master.key"
        self.secrets_file = self.data_dir / "secrets.enc"

        # Runtime state
        self._encryption_key: Optional[bytes] = None
        self._secrets: Optional[Dict[str, Any]] = None

    @property
    def is_initialized(self) -> bool:
        """Check if the secrets store has been initialized."""
        return self.master_key_file.exists()

    @property
    def is_unlocked(self) -> bool:
        """Check if the secrets store is currently unlocked."""
        return self._encryption_key is not None

    def initialize(self, master_password: str) -> None:
        """
        Initialize the secrets store with a master password.

        This should only be called once when setting up the store.

        Args:
            master_password: The master password to protect the secrets

        Raises:
            SecretsStoreError: If store is already initialized
        """
        if self.is_initialized:
            raise SecretsStoreError(
                "Secrets store is already initialized. "
                "Use unlock() to access it or change_master_password() to change the password."
            )

        # Generate a new random salt
        salt = generate_salt()

        # Derive encryption key
        encryption_key = derive_key_from_password(master_password, salt)

        # Create verification hash
        verification_hash = create_verification_hash(master_password, salt)

        # Store salt and verification hash
        master_key_data = {
            "salt": salt.hex(),
            "verification": verification_hash,
        }

        self.master_key_file.write_text(json.dumps(master_key_data, indent=2))

        # Set restrictive permissions (owner read/write only)
        os.chmod(self.master_key_file, stat.S_IRUSR | stat.S_IWUSR)

        # Initialize empty secrets
        self._encryption_key = encryption_key
        self._secrets = {}
        self._save_secrets()

        print(f"✓ Secrets store initialized at {self.secrets_file}")

    def unlock(self, master_password: str) -> None:
        """
        Unlock the secrets store with the master password.

        Args:
            master_password: The master password

        Raises:
            SecretsStoreError: If store is not initialized
            InvalidMasterPassword: If password is incorrect
        """
        if not self.is_initialized:
            raise SecretsStoreError(
                "Secrets store is not initialized. Call initialize() first."
            )

        # Load salt and verification hash
        master_key_data = json.loads(self.master_key_file.read_text())
        salt = bytes.fromhex(master_key_data["salt"])
        verification_hash = master_key_data["verification"]

        # Verify password
        if not verify_password(master_password, salt, verification_hash):
            raise InvalidMasterPassword("Invalid master password")

        # Derive encryption key
        encryption_key = derive_key_from_password(master_password, salt)

        # Load and decrypt secrets
        if self.secrets_file.exists():
            try:
                encrypted_data = self.secrets_file.read_bytes()
                decrypted_data = decrypt_data(encrypted_data, encryption_key)
                self._secrets = json.loads(decrypted_data.decode('utf-8'))
            except InvalidToken:
                raise InvalidMasterPassword("Failed to decrypt secrets (invalid password)")
            except json.JSONDecodeError as e:
                raise SecretsStoreError(f"Corrupted secrets file: {e}")
        else:
            # No secrets file yet (fresh initialization)
            self._secrets = {}

        self._encryption_key = encryption_key
        print(f"✓ Secrets store unlocked ({len(self._secrets)} secrets)")

    def lock(self) -> None:
        """Lock the secrets store (clear decrypted data from memory)."""
        self._encryption_key = None
        self._secrets = None

    def get(self, key: str, default: Any = None) -> Any:
        """
        Get a secret value.

        Args:
            key: Secret key name
            default: Default value if key doesn't exist

        Returns:
            Secret value or default

        Raises:
            SecretsStoreLocked: If store is locked
        """
        if not self.is_unlocked:
            raise SecretsStoreLocked("Secrets store is locked. Call unlock() first.")

        return self._secrets.get(key, default)

    def set(self, key: str, value: Any) -> None:
        """
        Set a secret value.

        Args:
            key: Secret key name
            value: Secret value (must be JSON-serializable)

        Raises:
            SecretsStoreLocked: If store is locked
        """
        if not self.is_unlocked:
            raise SecretsStoreLocked("Secrets store is locked. Call unlock() first.")

        self._secrets[key] = value
        self._save_secrets()

    def delete(self, key: str) -> bool:
        """
        Delete a secret.

        Args:
            key: Secret key name

        Returns:
            True if secret existed and was deleted, False otherwise

        Raises:
            SecretsStoreLocked: If store is locked
        """
        if not self.is_unlocked:
            raise SecretsStoreLocked("Secrets store is locked. Call unlock() first.")

        if key in self._secrets:
            del self._secrets[key]
            self._save_secrets()
            return True
        return False

    def list_keys(self) -> list[str]:
        """
        List all secret keys.

        Returns:
            List of secret keys

        Raises:
            SecretsStoreLocked: If store is locked
        """
        if not self.is_unlocked:
            raise SecretsStoreLocked("Secrets store is locked. Call unlock() first.")

        return list(self._secrets.keys())

    def change_master_password(self, current_password: str, new_password: str) -> None:
        """
        Change the master password.

        This re-encrypts the secrets with a new key derived from the new password.

        Args:
            current_password: Current master password
            new_password: New master password

        Raises:
            InvalidMasterPassword: If current password is incorrect
        """
        # ALWAYS verify current password before changing
        # Load salt and verification hash
        if not self.is_initialized:
            raise SecretsStoreError(
                "Secrets store is not initialized. Call initialize() first."
            )

        master_key_data = json.loads(self.master_key_file.read_text())
        salt = bytes.fromhex(master_key_data["salt"])
        verification_hash = master_key_data["verification"]

        # Verify current password is correct
        if not verify_password(current_password, salt, verification_hash):
            raise InvalidMasterPassword("Invalid current password")

        # Unlock if needed to access secrets
        was_unlocked = self.is_unlocked
        if not was_unlocked:
            # Store is locked, so unlock with current password
            # (we already verified it above, so this will succeed)
            encryption_key = derive_key_from_password(current_password, salt)

            # Load and decrypt secrets
            if self.secrets_file.exists():
                encrypted_data = self.secrets_file.read_bytes()
                decrypted_data = decrypt_data(encrypted_data, encryption_key)
                self._secrets = json.loads(decrypted_data.decode('utf-8'))
            else:
                self._secrets = {}

            self._encryption_key = encryption_key

        # Generate new salt
        new_salt = generate_salt()

        # Derive new encryption key
        new_encryption_key = derive_key_from_password(new_password, new_salt)

        # Create new verification hash
        new_verification_hash = create_verification_hash(new_password, new_salt)

        # Update master key file
        master_key_data = {
            "salt": new_salt.hex(),
            "verification": new_verification_hash,
        }
        self.master_key_file.write_text(json.dumps(master_key_data, indent=2))
        os.chmod(self.master_key_file, stat.S_IRUSR | stat.S_IWUSR)

        # Re-encrypt secrets with new key
        old_key = self._encryption_key
        self._encryption_key = new_encryption_key
        self._save_secrets()

        print("✓ Master password changed successfully")

        # Lock if it wasn't unlocked before
        if not was_unlocked:
            self.lock()

    def _save_secrets(self) -> None:
        """Save secrets to encrypted file."""
        if not self.is_unlocked:
            raise SecretsStoreLocked("Cannot save while locked")

        # Serialize secrets to JSON
        secrets_json = json.dumps(self._secrets, indent=2)
        secrets_bytes = secrets_json.encode('utf-8')

        # Encrypt
        encrypted_data = encrypt_data(secrets_bytes, self._encryption_key)

        # Write to file
        self.secrets_file.write_bytes(encrypted_data)

        # Set restrictive permissions
        os.chmod(self.secrets_file, stat.S_IRUSR | stat.S_IWUSR)

    def export_encrypted(self, output_path: Path) -> None:
        """
        Export encrypted secrets to a file (for backup).

        Args:
            output_path: Path to export file

        Raises:
            SecretsStoreError: If secrets file doesn't exist
        """
        if not self.secrets_file.exists():
            raise SecretsStoreError("No secrets to export")

        import shutil
        shutil.copy2(self.secrets_file, output_path)
        print(f"✓ Encrypted secrets exported to {output_path}")

    def import_encrypted(self, input_path: Path, master_password: str) -> None:
        """
        Import encrypted secrets from a file.

        This will verify the password can decrypt the import before replacing
        the current secrets.

        Args:
            input_path: Path to import file
            master_password: Master password for the current store

        Raises:
            InvalidMasterPassword: If password doesn't work with import
        """
        if not self.is_unlocked:
            self.unlock(master_password)

        # Try to decrypt the imported file with current key
        try:
            encrypted_data = Path(input_path).read_bytes()
            decrypted_data = decrypt_data(encrypted_data, self._encryption_key)
            imported_secrets = json.loads(decrypted_data.decode('utf-8'))
        except InvalidToken:
            raise InvalidMasterPassword(
                "Cannot decrypt imported secrets with current master password"
            )
        except json.JSONDecodeError as e:
            raise SecretsStoreError(f"Corrupted import file: {e}")

        # Replace secrets
        self._secrets = imported_secrets
        self._save_secrets()

        print(f"✓ Imported {len(self._secrets)} secrets from {input_path}")
