import os
import platform
import keyring
import json
import re
import time
from pathlib import Path
from typing import Optional, Dict, Any


def armorer_config_dir_override() -> Optional[Path]:
    for env_name in ("ARMORER_CONFIG_DIR", "CONFIG_DIR"):
        raw = str(os.getenv(env_name) or "").strip()
        if raw:
            return Path(raw).expanduser()
    return None


def resolve_armorer_config_dir(default: Optional[Path] = None) -> Path:
    override = armorer_config_dir_override()
    if override is not None:
        return override
    if default is not None:
        return Path(default).expanduser()
    return Path.home() / ".armorer"


class ConfigManager:
    """Manages configuration via vault/keyring (for secrets) and .env (for non-secrets)."""
    
    SERVICE_NAME = "armorer"
    VAULT_FILE = Path.home() / ".armorer" / "vault.json"
    CONFIG_DIR = Path.home() / ".armorer"
    ARMORER_TELEGRAM_BOT_TOKEN_KEY = "ARMORER_TELEGRAM_BOT_TOKEN"
    LEGACY_TELEGRAM_BOT_TOKEN_KEY = "TELEGRAM_BOT_TOKEN"

    def __init__(self, env_path: str = ".env"):
        self.env_path = Path(env_path)
        class_config_dir = Path(type(self).CONFIG_DIR)
        class_vault_file = Path(type(self).VAULT_FILE)
        config_dir_override = armorer_config_dir_override()
        self.CONFIG_DIR = resolve_armorer_config_dir(class_config_dir)
        self.VAULT_FILE = (
            self.CONFIG_DIR / class_vault_file.name
            if config_dir_override
            else class_vault_file.expanduser()
        )
        # Create .env if it doesn't exist
        if not self.env_path.exists():
            with open(self.env_path, "w") as f:
                f.write("# Armorer Configuration\n")
        
        self.VAULT_FILE.parent.mkdir(parents=True, exist_ok=True)
        if not self.VAULT_FILE.exists():
            with open(self.VAULT_FILE, "w") as f:
                json.dump({"keys": {}, "secrets": {}}, f)

        # Detect Docker environment
        self.is_docker = os.path.exists('/.dockerenv')

        # Vault (vault.json) is the canonical secret store on all platforms.
        # Keyring is disabled everywhere to avoid macOS Keychain popups on
        # the host and EncryptedKeyring password prompts inside Docker.
        self.use_keyring = False

        # One-time hygiene: move legacy secrets out of .env into the vault.
        self._migrate_env_secrets_to_vault()

    def _ensure_vault_shape(self, data: Dict[str, Any]) -> Dict[str, Any]:
        if not isinstance(data, dict):
            data = {}
        if not isinstance(data.get("keys"), dict):
            data["keys"] = {}
        if not isinstance(data.get("secrets"), dict):
            data["secrets"] = {}
        return data

    def _load_vault(self) -> Dict[str, Any]:
        try:
            with open(self.VAULT_FILE, "r") as f:
                raw = json.load(f)
                return self._ensure_vault_shape(raw)
        except Exception:
            return {"keys": {}, "secrets": {}}

    def _save_vault(self, data: Dict[str, Any]):
        payload = self._ensure_vault_shape(data)
        with open(self.VAULT_FILE, "w") as f:
            json.dump(payload, f, indent=4)
        try:
            os.chmod(self.VAULT_FILE, 0o600)
        except Exception:
            pass

    def _migrate_env_secrets_to_vault(self):
        """Move any legacy secrets from .env into vault storage."""
        config = self.load_env()
        if not config:
            return

        vault = self._load_vault()
        vault_changed = False
        env_changed = False

        for key, value in list(config.items()):
            if not self.is_secret(key):
                continue
            if not value:
                continue

            if self.use_keyring:
                try:
                    keyring.set_password(self.SERVICE_NAME, key, value)
                except Exception:
                    pass

            if vault["secrets"].get(key) != value:
                vault["secrets"][key] = value
                vault_changed = True

            del config[key]
            env_changed = True

        if vault_changed:
            self._save_vault(vault)
        if env_changed:
            self._write_env(config)

    def find_vault_key(self, search_key: str) -> Optional[str]:
        """
        Find a secret in the vault matching the search_key (fuzzy/alias match).
        Returns the actual secret value if found.
        """
        vault = self._load_vault()
        search_key_norm = search_key.upper()
        
        # 1. Check exact ID match in Vault metadata
        if search_key in vault["keys"]:
            return self.get_key(search_key)
        
        # 2. Search aliases in Vault metadata
        for key_id, meta in vault["keys"].items():
            if search_key_norm == key_id.upper():
                return self.get_key(key_id)
            if search_key_norm in [a.upper() for a in meta.get("aliases", [])]:
                return self.get_key(key_id)
        
        # 3. Fallback: Check Armorer's own environment/config for common matches
        return self.get_key(search_key)

    def save_to_vault(self, name: str, value: str, description: str = "", aliases: list = None):
        """Save a secret to the centralized vault."""
        # 1. Save secret
        self.set_key(name, value)
        
        # 2. Update Metadata
        vault = self._load_vault()
        vault["keys"][name] = {
            "description": description,
            "aliases": aliases or [],
            "last_updated": time.time()
        }
        self._save_vault(vault)

    def load_env(self) -> Dict[str, str]:
        """Loads environment variables from .env file."""
        config = {}
        if self.env_path.exists():
            with open(self.env_path, "r") as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith("#") and "=" in line:
                        key, value = line.split("=", 1)
                        config[key.strip()] = value.strip()
        return config
    
    def is_secret(self, key: str) -> bool:
        """Heuristic to determine if a key is a secret."""
        key_u = key.upper()
        return (
            key_u.endswith("_KEY")
            or key_u.endswith("_SECRET")
            or key_u.endswith("_TOKEN")
            or "PASSWORD" in key_u
        )

    def _read_keyring_secret(self, key: str) -> Optional[str]:
        if not self.use_keyring:
            return None
        try:
            val = keyring.get_password(self.SERVICE_NAME, key)
        except Exception:
            return None
        return str(val).strip() if val else None

    def _sync_keyring_secret(self, key: str, value: str) -> None:
        if not self.use_keyring or not value:
            return
        try:
            keyring.set_password(self.SERVICE_NAME, key, value)
        except Exception:
            pass

    def scoped_secret_key(self, scope: str, key: str) -> str:
        normalized_scope = re.sub(r"[^A-Z0-9]+", "_", str(scope or "").upper()).strip("_")
        normalized_key = re.sub(r"[^A-Z0-9_]+", "_", str(key or "").upper()).strip("_")
        if not normalized_scope or not normalized_key:
            raise ValueError("scope and key are required for scoped secrets")
        return f"{normalized_scope}__{normalized_key}"

    def get_scoped_secret(self, scope: str, key: str, fallback_to_global: bool = True) -> Optional[str]:
        scoped_key = self.scoped_secret_key(scope, key)
        value = self.get_key(scoped_key)
        if value:
            return value
        if fallback_to_global:
            return self.get_key(key)
        return None

    def save_scoped_secret(
        self,
        scope: str,
        key: str,
        value: str,
        description: str = "",
        aliases: list | None = None,
    ) -> str:
        scoped_key = self.scoped_secret_key(scope, key)
        scoped_aliases = list(dict.fromkeys([key, *(aliases or [])]))
        self.save_to_vault(scoped_key, value, description=description, aliases=scoped_aliases)
        return scoped_key

    def get_armorer_telegram_bot_token(self) -> Optional[str]:
        """Return Armorer's dedicated Telegram bot token.

        Falls back to the legacy shared TELEGRAM_BOT_TOKEN only when vault metadata
        indicates that the shared secret was originally saved for Armorer.
        """
        token = self.get_key(self.ARMORER_TELEGRAM_BOT_TOKEN_KEY)
        if token:
            return token

        legacy = self.get_key(self.LEGACY_TELEGRAM_BOT_TOKEN_KEY)
        if not legacy:
            return None

        vault = self._load_vault()
        legacy_meta = vault.get("keys", {}).get(self.LEGACY_TELEGRAM_BOT_TOKEN_KEY, {})
        description = str(legacy_meta.get("description") or "").strip().lower()
        aliases = [str(alias or "").strip().upper() for alias in legacy_meta.get("aliases", [])]
        if "armorer telegram bot token" not in description and self.ARMORER_TELEGRAM_BOT_TOKEN_KEY not in aliases:
            scoped_values = {
                str(value or "").strip()
                for key, value in (vault.get("secrets", {}) or {}).items()
                if str(key or "").strip().upper().endswith("__TELEGRAM_BOT_TOKEN")
                and str(value or "").strip()
            }
            if legacy in scoped_values:
                return None

        self.save_to_vault(
            self.ARMORER_TELEGRAM_BOT_TOKEN_KEY,
            legacy,
            description="Armorer Telegram Bot Token",
            aliases=[self.LEGACY_TELEGRAM_BOT_TOKEN_KEY],
        )
        return legacy

    def set_armorer_telegram_bot_token(self, value: str) -> None:
        token = str(value or "").strip()
        if token:
            self.save_to_vault(
                self.ARMORER_TELEGRAM_BOT_TOKEN_KEY,
                token,
                description="Armorer Telegram Bot Token",
                aliases=[self.LEGACY_TELEGRAM_BOT_TOKEN_KEY],
            )
        else:
            self.set_key(self.ARMORER_TELEGRAM_BOT_TOKEN_KEY, "")
        os.environ[self.ARMORER_TELEGRAM_BOT_TOKEN_KEY] = token

    def get_key(self, key: str) -> Optional[str]:
        """Get secret from keyring/vault; non-secrets from .env/environment."""
        if self.is_secret(key):
            vault = self._load_vault()
            vault_val = vault.get("secrets", {}).get(key)
            vault_text = str(vault_val).strip() if vault_val else None
            keyring_text = self._read_keyring_secret(key)

            # Prefer the file-backed vault as the canonical cross-platform source.
            if vault_text:
                if keyring_text != vault_text:
                    self._sync_keyring_secret(key, vault_text)
                return vault_text

            if keyring_text:
                vault["secrets"][key] = keyring_text
                self._save_vault(vault)
                return keyring_text
            return None

        config = self.load_env()
        val = config.get(key) or os.getenv(key)
        return val.strip() if val else None

    def set_key(self, key: str, value: str):
        """Store secrets in vault/keyring only; store non-secrets in .env."""
        if self.is_secret(key):
            if self.use_keyring:
                try:
                    keyring.set_password(self.SERVICE_NAME, key, value)
                except Exception as e:
                    print(f"Warning: Keyring failed ({e}).")

            vault = self._load_vault()
            vault["secrets"][key] = value
            self._save_vault(vault)

            # Remove legacy secret copy from .env if present.
            config = self.load_env()
            if key in config:
                del config[key]
                self._write_env(config)

            os.environ[key] = value
            return

        config = self.load_env()
        config[key] = value
        self._write_env(config)
        os.environ[key] = value

    def _write_env(self, config: Dict[str, str]):
        """Writes the configuration back to the .env file."""
        with open(self.env_path, "w") as f:
            for key, value in config.items():
                f.write(f"{key}={value}\n")
