"""
store.py — Encrypted persistent config backed by a single JSON file on disk.

Rules:
  • ai_api_key and telegram_token are always stored encrypted; decrypted transparently on read.
  • The Fernet symmetric key lives in ENCRYPTION_KEY_FILE (mode 0o600).
  • Atomic writes via temp-file + rename to prevent corruption.
  • Module-level singleton: the file is parsed only once per process.
  • exists() returns True only when setup_complete == True.
"""
from __future__ import annotations

import json
import os
import stat
import tempfile
import threading
from pathlib import Path
from typing import Any

from cryptography.fernet import Fernet, InvalidToken

from .config import CONFIG_FILE, DATA_DIR, ENCRYPTION_KEY_FILE


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

def _ensure_dirs() -> None:
    DATA_DIR.mkdir(parents=True, exist_ok=True)


def _load_or_create_fernet() -> Fernet:
    """Return a Fernet instance, generating + persisting the key if needed."""
    _ensure_dirs()
    if ENCRYPTION_KEY_FILE.exists():
        try:
            key = ENCRYPTION_KEY_FILE.read_bytes().strip()
            # Validate the key is a proper Fernet key
            return Fernet(key)
        except (ValueError, Exception):
            # Key is corrupted — generate a new one (user will need to re-init)
            pass
    key = Fernet.generate_key()
    # Write atomically
    tmp = ENCRYPTION_KEY_FILE.with_suffix(".tmp")
    tmp.write_bytes(key)
    tmp.rename(ENCRYPTION_KEY_FILE)
    # chmod 600 — owner read/write only
    try:
        ENCRYPTION_KEY_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
    except OSError:
        pass  # Windows may not support this; not fatal
    return Fernet(key)


def _read_raw() -> dict:
    if not CONFIG_FILE.exists():
        return {}
    try:
        text = CONFIG_FILE.read_text(encoding="utf-8")
        if not text.strip():
            return {}
        return json.loads(text)
    except (json.JSONDecodeError, OSError, UnicodeDecodeError):
        # Attempt to recover from backup
        backup = CONFIG_FILE.with_suffix(".bak")
        if backup.exists():
            try:
                return json.loads(backup.read_text(encoding="utf-8"))
            except Exception:
                pass
        return {}


def _write_raw(data: dict) -> None:
    """Atomic write: write to temp file then rename to prevent partial writes."""
    _ensure_dirs()
    serialized = json.dumps(data, indent=2, ensure_ascii=False)
    # Write backup of old file first
    if CONFIG_FILE.exists():
        try:
            CONFIG_FILE.replace(CONFIG_FILE.with_suffix(".bak"))
        except OSError:
            pass
    # Atomic write
    tmp_fd, tmp_path = tempfile.mkstemp(
        dir=DATA_DIR, prefix="config_", suffix=".json.tmp"
    )
    try:
        with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
            f.write(serialized)
            f.flush()
            os.fsync(f.fileno())
        Path(tmp_path).replace(CONFIG_FILE)
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise


# ---------------------------------------------------------------------------
# ConfigStore
# ---------------------------------------------------------------------------

class ConfigStore:
    """
    Thread-safe wrapper around the single JSON config file.

    Sensitive fields (ai_api_key, telegram_token) are automatically
    encrypted on write and decrypted on read using Fernet symmetric encryption.

    Usage:
        store = ConfigStore()
        store.set("telegram_token", "123:ABC")
        token = store.get("telegram_token")
        store.set("ai_api_key", "sk-...")   # auto-encrypted
        key = store.get("ai_api_key")       # auto-decrypted
    """

    _ENCRYPTED_FIELDS = {"ai_api_key", "telegram_token"}

    def __init__(self) -> None:
        self._fernet: Fernet = _load_or_create_fernet()
        self._cache: dict | None = None
        self._lock = threading.RLock()

    # ------------------------------------------------------------------
    # Private
    # ------------------------------------------------------------------

    def _load(self) -> dict:
        with self._lock:
            if self._cache is None:
                self._cache = _read_raw()
            return self._cache

    def _save(self) -> None:
        with self._lock:
            _write_raw(self._cache or {})

    def _encrypt(self, value: str) -> str:
        return self._fernet.encrypt(value.encode("utf-8")).decode("ascii")

    def _decrypt(self, value: str) -> str:
        try:
            return self._fernet.decrypt(value.encode("ascii")).decode("utf-8")
        except (InvalidToken, ValueError, Exception):
            # Value may already be plaintext (manual edit / migration).
            # Return as-is rather than crashing.
            return value

    @staticmethod
    def _looks_encrypted(value: str) -> bool:
        """Fernet tokens are base64url and start with 'gAAAAA'."""
        return isinstance(value, str) and value.startswith("gAAAAA") and len(value) > 20

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def get(self, key: str, default: Any = None) -> Any:
        """Return the value for *key*, decrypting sensitive fields transparently."""
        data = self._load()
        value = data.get(key, default)
        if key in self._ENCRYPTED_FIELDS and isinstance(value, str) and self._looks_encrypted(value):
            return self._decrypt(value)
        return value

    def set(self, key: str, value: Any) -> None:
        """Persist *key* → *value*, encrypting sensitive fields automatically."""
        with self._lock:
            data = self._load()
            if key in self._ENCRYPTED_FIELDS and isinstance(value, str) and not self._looks_encrypted(value):
                value = self._encrypt(value)
            data[key] = value
            self._save()

    def delete(self, key: str) -> None:
        """Remove a key from the config."""
        with self._lock:
            data = self._load()
            data.pop(key, None)
            self._save()

    def all(self) -> dict:
        """
        Return a copy of the entire config dict with sensitive fields decrypted.
        Safe for internal use; never expose raw encrypted values to untrusted callers.
        """
        with self._lock:
            data = dict(self._load())
        for field in self._ENCRYPTED_FIELDS:
            if field in data and isinstance(data[field], str) and self._looks_encrypted(data[field]):
                data[field] = self._decrypt(data[field])
        return data

    def exists(self) -> bool:
        """True only when setup has been completed successfully."""
        return bool(self._load().get("setup_complete", False))

    def reset(self) -> None:
        """Wipe the config (used by re-init)."""
        with self._lock:
            self._cache = {}
            self._save()

    def invalidate_cache(self) -> None:
        """Force a re-read from disk on next access (useful after external modification)."""
        with self._lock:
            self._cache = None


# ---------------------------------------------------------------------------
# Module-level singleton
# ---------------------------------------------------------------------------

_store: ConfigStore | None = None
_store_lock = threading.Lock()


def get_store() -> ConfigStore:
    """Return the process-wide singleton ConfigStore (thread-safe)."""
    global _store
    if _store is None:
        with _store_lock:
            if _store is None:
                _store = ConfigStore()
    return _store
