"""SQLCipher encryption integration for NEUROVAULT v0.2."""

from __future__ import annotations

import hashlib
import logging
import os
import platform
import sqlite3
import subprocess
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)


def _sql_quote(value: str) -> str:
    """Return a safely escaped SQL string literal for SQLCipher PRAGMAs."""
    return "'" + value.replace("'", "''") + "'"


class EncryptionManager:
    """Manages database encryption lifecycle."""

    KDF_ITERATIONS = 256000
    CIPHER_PAGE_SIZE = 4096
    SALT_SIZE = 32

    def __init__(self, vault_dir: Path, vault_name: str = "default"):
        self._vault_dir = Path(vault_dir)
        self._vault_name = vault_name
        self._salt_path = self._vault_dir / ".salt"
        self._key_cache: str | None = None

    def derive_key(self, passphrase: str) -> str:
        """Derive database key from passphrase using PBKDF2-HMAC-SHA256."""
        salt = self._get_or_create_salt()
        dk = hashlib.pbkdf2_hmac(
            "sha256",
            passphrase.encode("utf-8"),
            salt,
            self.KDF_ITERATIONS,
        )
        self._key_cache = dk.hex()
        return self._key_cache

    def open_encrypted(self, db_path: str | Path, key: str) -> Any:
        """Open an encrypted SQLCipher database."""
        try:
            from pysqlcipher3 import dbapi2 as sqlcipher
        except ImportError as exc:
            raise ImportError(
                "Encryption requires pysqlcipher3. Install with: pip install neurovault[encryption]"
            ) from exc

        conn = sqlcipher.connect(str(db_path))
        conn.execute(f"PRAGMA key = {_sql_quote(key)}")
        conn.execute(f"PRAGMA cipher_page_size = {self.CIPHER_PAGE_SIZE}")
        conn.execute(f"PRAGMA kdf_iter = {self.KDF_ITERATIONS}")
        conn.execute("PRAGMA cipher_compatibility = 4")
        conn.execute("PRAGMA journal_mode = WAL")
        conn.execute("PRAGMA foreign_keys = ON")
        conn.row_factory = sqlcipher.Row

        try:
            conn.execute("SELECT count(*) FROM sqlite_master")
        except Exception as exc:
            conn.close()
            raise ValueError(f"Invalid encryption key: {exc}") from exc

        return conn

    def open_plaintext(self, db_path: str | Path) -> sqlite3.Connection:
        """Open a standard unencrypted SQLite database."""
        conn = sqlite3.connect(str(db_path))
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode = WAL")
        conn.execute("PRAGMA foreign_keys = ON")
        return conn

    def migrate_to_encrypted(
        self,
        plaintext_path: str | Path,
        encrypted_path: str | Path,
        key: str,
    ) -> bool:
        """Migrate a plaintext SQLite database to SQLCipher."""
        try:
            from pysqlcipher3 import dbapi2 as sqlcipher
        except ImportError:
            logger.error("Cannot migrate: pysqlcipher3 not installed")
            return False

        try:
            src = sqlcipher.connect(str(plaintext_path))
            src.execute(
                f"ATTACH DATABASE {_sql_quote(str(encrypted_path))} AS encrypted KEY {_sql_quote(key)}"
            )
            src.execute(f"PRAGMA encrypted.cipher_page_size = {self.CIPHER_PAGE_SIZE}")
            src.execute(f"PRAGMA encrypted.kdf_iter = {self.KDF_ITERATIONS}")
            src.execute("SELECT sqlcipher_export('encrypted')")
            src.execute("DETACH DATABASE encrypted")
            logger.info("Database migrated to encrypted format: %s", encrypted_path)
            return True
        except Exception as exc:
            logger.error("Encryption migration failed: %s", exc)
            return False
        finally:
            try:
                src.close()
            except Exception as exc:
                logger.debug("Failed to close SQLCipher source handle: %s", exc)

    def get_key(self, config: dict[str, Any]) -> str | None:
        """Retrieve encryption key based on configured source."""
        if self._key_cache:
            return self._key_cache

        env_key = os.environ.get("NEUROVAULT_DB_KEY")
        if env_key:
            return env_key

        source = str(config.get("key_source", "keychain"))
        if source == "keychain":
            return self._get_from_keychain() or self._get_from_keyring() or self._get_from_file()
        if source == "keyring":
            return self._get_from_keyring() or self._get_from_file()
        if source == "file":
            return self._get_from_file()
        if source == "prompt":
            return None
        return self._get_from_keychain() or self._get_from_keyring() or self._get_from_file()

    def store_key(self, key: str, source: str = "keychain") -> bool:
        """Store encryption key in configured backend."""
        if source == "keychain":
            return self._store_to_keychain(key) or self._store_to_keyring(key) or self._store_to_file(key)
        if source == "keyring":
            return self._store_to_keyring(key) or self._store_to_file(key)
        return self._store_to_file(key)

    def _get_or_create_salt(self) -> bytes:
        if self._salt_path.exists():
            return self._salt_path.read_bytes()
        salt = os.urandom(self.SALT_SIZE)
        self._salt_path.parent.mkdir(parents=True, exist_ok=True)
        self._salt_path.write_bytes(salt)
        try:
            os.chmod(self._salt_path, 0o600)
        except Exception as exc:
            logger.debug("Unable to chmod salt file %s: %s", self._salt_path, exc)
        return salt

    def _get_from_keychain(self) -> str | None:
        if platform.system() != "Darwin":
            return None
        try:
            result = subprocess.run(
                ["security", "find-generic-password", "-a", self._vault_name, "-s", "neurovault", "-w"],
                capture_output=True,
                text=True,
                check=True,
            )
            return result.stdout.strip()
        except Exception as exc:
            logger.debug("Keychain lookup failed for %s: %s", self._vault_name, exc)
            return None

    def _store_to_keychain(self, key: str) -> bool:
        if platform.system() != "Darwin":
            return False
        try:
            subprocess.run(
                ["security", "add-generic-password", "-a", self._vault_name, "-s", "neurovault", "-w", key, "-U"],
                capture_output=True,
                check=True,
            )
            return True
        except Exception as exc:
            logger.debug("Keychain store failed for %s: %s", self._vault_name, exc)
            return False

    def _get_from_keyring(self) -> str | None:
        try:
            import keyring  # type: ignore
        except Exception:
            return None
        try:
            value = keyring.get_password("neurovault", self._vault_name)
            return value.strip() if isinstance(value, str) and value.strip() else None
        except Exception:
            return None

    def _store_to_keyring(self, key: str) -> bool:
        try:
            import keyring  # type: ignore
        except Exception:
            return False
        try:
            keyring.set_password("neurovault", self._vault_name, key)
            return True
        except Exception:
            return False

    def _get_from_file(self) -> str | None:
        key_path = self._vault_dir / ".key"
        return key_path.read_text().strip() if key_path.exists() else None

    def _store_to_file(self, key: str) -> bool:
        key_path = self._vault_dir / ".key"
        key_path.parent.mkdir(parents=True, exist_ok=True)
        key_path.write_text(key)
        try:
            os.chmod(key_path, 0o600)
        except Exception as exc:
            logger.debug("Unable to chmod key file %s: %s", key_path, exc)
        return True
