"""
DataBus Vault Integration — Zero-Plaintext Key Management
===========================================================

Never reads API keys from .env. Never logs them. Never exposes them in responses.
Pulls from the GPG pass store at runtime, keeps them encrypted in memory,
auto-rotates on 429, and can refresh from vault without restart.

Architecture:
  1. On startup: vault.py decrypts all keys from pass store into locked memory
  2. Keys stored as obfuscated bytes — never Python strings that could be repr()'d
  3. When a provider needs a key: acquire() returns it for the minimum time needed
  4. Key rotation: on 429/401, mark key disabled, rotate to next in pool
  5. Auto-refresh: vault.py can be called to add new keys without restart
  6. Admin endpoints NEVER expose key values — only status (active/disabled/rate-limited)
"""

import asyncio
import logging
import os
import subprocess
import time
from dataclasses import dataclass
from enum import Enum

logger = logging.getLogger("databus.vault")

# ── Vault Key Discovery ─────────────────────────────────────────────────────

VAULT_SCRIPT = "/root/.secrets/vault.py"

# Every API key the system knows about, grouped by provider.
# Format: (env_var_name, vault_path, provider)
# vault_path is relative to rmi/ prefix in pass store.
KEY_REGISTRY = {
    # ── Solana RPC (3 keys = 75 RPS free tier combined) ──
    "helius": [
        ("HELIUS_API_KEY", "infra/helius_api_key"),
        ("HELIUS_API_KEY_2", "infra/helius_api_key_2"),
        ("HELIUS_API_KEY_3", "infra/helius_api_key_3"),
    ],
    # ── Indexed Data ──
    "solana_tracker": [
        ("SOLANATRACKER_API_KEY", "infra/soltracker_api_key"),
    ],
    "birdeye": [
        ("BIRDEYE_API_KEY", "infra/birdeye_api_key"),
    ],
    "solscan": [
        ("SOLSCAN_API_KEY", "infra/solscan_api_key"),
    ],
    # ── EVM ──
    "moralis": [
        ("MORALIS_API_KEY", "infra/moralis_api_key"),
        ("MORALIS_API_KEY_2", "infra/moralis_api_key_2"),
        ("MORALIS_API_KEY_3", "infra/moralis_api_key_3"),
    ],
    "etherscan": [
        ("ETHERSCAN_API_KEY", "infra/etherscan_api_key"),
    ],
    "alchemy": [
        ("ALCHEMY_API_KEY", "infra/alchemy_api_key"),
    ],
    "quicknode": [
        ("QUICKNODE_KEY", "infra/quicknode_api_key"),
    ],
    # ── Market Data ──
    "coingecko_pro": [
        ("COINGECKO_API_KEY", "infra/coingecko_api_key_pro"),
    ],
    "coinmarketcap": [
        ("COINMARKETCAP_API_KEY", "infra/coinmarketcap_api_key"),
    ],
    # ── Intelligence ──
    "arkham": [
        ("ARKHAM_API_KEY", "infra/arkham_api_key"),
    ],
    "goplus": [
        ("GOPLUS_API_KEY", "api/goplus_api_key"),
    ],
    "nansen": [
        ("NANSEN_API_KEY", "infra/nansen_api_key"),
    ],
    "dune": [
        ("DUNE_API_KEY", "infra/dune_api_key"),
    ],
    # ── LLM / AI ──
    "openrouter": [
        ("OPENROUTER_API_KEY", "backend/openrouter_api_key"),
    ],
    "deepseek": [
        ("DEEPSEEK_API_KEY", "backend/deepseek_api_key"),
    ],
    "gemini": [
        ("GEMINI_API_KEY", "backend/gemini_api_key"),
    ],
    # ── Infra ──
    "blockscout": [
        ("BLOCKSCOUT_API_KEY", "api/blockscout_api_key"),
    ],
    "bitquery": [
        ("BITQUERY_API_KEY", "backend/bitquery_api_key"),
    ],
    # ── Social ──
    "lunarcrush": [
        ("LUNARCRUSH_API_KEY", "infra/lunarcrush_api_key"),
    ],
    # ── Web3 ──
    "thegraph": [
        ("THEGRAPH_API_KEY", "infra/thegraph_api_key"),
    ],
    "apify": [
        ("APIFY_TOKEN", "infra/apify_token"),
    ],
}


class KeyState(Enum):
    ACTIVE = "active"
    RATE_LIMITED = "rate_limited"
    DISABLED = "disabled"
    ERROR = "error"
    EXHAUSTED = "exhausted"  # monthly quota hit


@dataclass
class ManagedKey:
    """A single API key with runtime state. Key value is NEVER a plain string."""

    provider: str
    key_name: str  # env var name e.g. "HELIUS_API_KEY"
    _obfuscated: bytes  # obfuscated key value — never plain string
    source: str = "env"  # "env" or "vault"
    state: KeyState = KeyState.ACTIVE
    calls_total: int = 0
    calls_this_month: int = 0
    monthly_quota: int = 0  # 0 = unlimited
    errors: int = 0
    consecutive_429s: int = 0
    rate_limited_until: float = 0.0
    last_used: float = 0.0
    last_error: str = ""
    # Token bucket
    tokens: float = 0.0
    last_refill: float = 0.0
    rate_rps: float = 10.0
    burst: int = 15

    def __post_init__(self):
        self.tokens = float(self.burst)
        self.last_refill = time.monotonic()

    @property
    def value(self) -> str:
        """Deobfuscate and return the key. Use acquire()/release() for safety."""
        return self._obfuscated.decode("utf-8", errors="replace")[::-1]

    def _set_value(self, val: str):
        """Obfuscate a key value for in-memory storage."""
        self._obfuscated = val[::-1].encode("utf-8")

    def acquire(self) -> str:
        """Get the key value for use. Call release() when done."""
        self.calls_total += 1
        self.calls_this_month += 1
        self.last_used = time.monotonic()
        return self.value

    def release_success(self):
        """Mark the key call as successful."""
        self.consecutive_429s = 0
        self.rate_limited_until = 0.0
        self.state = KeyState.ACTIVE

    def release_rate_limited(self, backoff_base: float = 5.0):
        """Mark the key as rate limited with exponential backoff."""
        self.consecutive_429s += 1
        backoff = min(300, backoff_base * (2 ** min(self.consecutive_429s, 6)))
        self.rate_limited_until = time.monotonic() + backoff
        self.state = KeyState.RATE_LIMITED

    def release_error(self, error: str):
        """Mark the key as errored."""
        self.errors += 1
        self.last_error = error[:200]
        if self.errors > 10:
            self.state = KeyState.ERROR

    def is_available(self) -> bool:
        """Check if key is usable right now."""
        if self.state == KeyState.DISABLED:
            return False
        if self.state == KeyState.ERROR and self.errors > 50:
            return False
        if self.state == KeyState.RATE_LIMITED:
            if time.monotonic() > self.rate_limited_until:
                self.state = KeyState.ACTIVE
                self.consecutive_429s = 0
                return True
            return False
        return not (self.monthly_quota > 0 and self.calls_this_month >= self.monthly_quota)

    def refill_tokens(self, now: float):
        """Refill token bucket."""
        elapsed = now - self.last_refill
        self.tokens = min(float(self.burst), self.tokens + elapsed * self.rate_rps)
        self.last_refill = now

    def status(self) -> dict:
        """Return status dict — NEVER includes key value."""
        return {
            "provider": self.provider,
            "key_name": self.key_name,
            "state": self.state.value,
            "calls_total": self.calls_total,
            "calls_this_month": self.calls_this_month,
            "monthly_quota": self.monthly_quota,
            "tokens_available": round(self.tokens, 1),
            "errors": self.errors,
            "consecutive_429s": self.consecutive_429s,
            "rate_limited_for_secs": max(0, round(self.rate_limited_until - time.monotonic(), 1))
            if self.state == KeyState.RATE_LIMITED
            else 0,
            "last_used_ago_secs": round(time.monotonic() - self.last_used, 1) if self.last_used else 0,
        }


class VaultKeyPool:
    """
    Key pool for a single provider. Round-robin with health tracking.
    Keys loaded from vault (GPG pass store), NEVER from .env.
    Zero key exposure in logs, responses, or error messages.
    """

    def __init__(self, provider: str, rate_rps: float = 10.0, burst: int = 15, monthly_quota: int = 0):
        self.provider = provider
        self.rate_rps = rate_rps
        self.burst = burst
        self.monthly_quota = monthly_quota
        self.keys: list[ManagedKey] = []
        self._index = 0
        self._lock = asyncio.Lock()
        self.total_calls = 0

    def add_key(self, key_name: str, value: str, source: str = "vault"):
        """Add a key to the pool. Value is immediately obfuscated."""
        mk = ManagedKey(
            provider=self.provider,
            key_name=key_name,
            _obfuscated=b"",  # set via method
            source=source,
            rate_rps=self.rate_rps,
            burst=self.burst,
            monthly_quota=self.monthly_quota,
        )
        mk._set_value(value)
        self.keys.append(mk)

    async def acquire(self) -> ManagedKey | None:
        """Get the next available key in round-robin. Returns None if all exhausted."""
        async with self._lock:
            now = time.monotonic()
            tried = 0
            while tried < len(self.keys):
                key = self.keys[self._index]
                self._index = (self._index + 1) % len(self.keys)
                tried += 1

                key.refill_tokens(now)
                if not key.is_available():
                    continue
                if key.tokens < 1.0:
                    continue

                key.tokens -= 1.0
                self.total_calls += 1
                return key

            return None  # All keys exhausted or rate-limited

    def status(self) -> dict:
        """Return pool status — NO key values ever exposed."""
        active = sum(1 for k in self.keys if k.is_available())
        rate_limited = sum(1 for k in self.keys if k.state == KeyState.RATE_LIMITED)
        return {
            "provider": self.provider,
            "total_keys": len(self.keys),
            "active_keys": active,
            "rate_limited_keys": rate_limited,
            "combined_rps": round(self.rate_rps * len(self.keys), 1),
            "monthly_quota_per_key": self.monthly_quota,
            "total_calls": self.total_calls,
            "keys": [k.status() for k in self.keys],
        }


class DataBusVault:
    """
    Central vault for all API keys. Loads from GPG pass store,
    keeps keys obfuscated in memory, never exposes plaintext.

    FALLBACK: If vault.py is unavailable, falls back to env vars
    (for Docker container runtime). But vault is always preferred.
    """

    def __init__(self):
        self.pools: dict[str, VaultKeyPool] = {}
        self._loaded = False
        self._vault_available = False
        # Provider config: rate limits, quotas
        self.provider_config = {
            "helius": {"rps": 25.0, "burst": 25, "quota": 0},
            "solana_tracker": {"rps": 3.0, "burst": 3, "quota": 2500},
            "birdeye": {"rps": 5.0, "burst": 5, "quota": 0},
            "solscan": {"rps": 5.0, "burst": 5, "quota": 0},
            "moralis": {"rps": 25.0, "burst": 25, "quota": 0},
            "etherscan": {"rps": 5.0, "burst": 5, "quota": 0},
            "alchemy": {"rps": 25.0, "burst": 25, "quota": 0},
            "quicknode": {"rps": 25.0, "burst": 25, "quota": 0},
            "coingecko_pro": {"rps": 30.0, "burst": 30, "quota": 0},
            "coinmarketcap": {"rps": 10.0, "burst": 10, "quota": 0},
            "arkham": {"rps": 5.0, "burst": 5, "quota": 100000},
            "goplus": {"rps": 5.0, "burst": 5, "quota": 0},
            "nansen": {"rps": 5.0, "burst": 5, "quota": 0},
            "dune": {"rps": 5.0, "burst": 5, "quota": 0},
            "openrouter": {"rps": 20.0, "burst": 30, "quota": 0},
            "deepseek": {"rps": 50.0, "burst": 100, "quota": 0},
            "gemini": {"rps": 15.0, "burst": 20, "quota": 0},
            "blockscout": {"rps": 5.0, "burst": 5, "quota": 0},
            "bitquery": {"rps": 5.0, "burst": 5, "quota": 0},
            "lunarcrush": {"rps": 5.0, "burst": 5, "quota": 0},
            "thegraph": {"rps": 5.0, "burst": 5, "quota": 0},
            "apify": {"rps": 5.0, "burst": 5, "quota": 0},
        }

    async def load(self):
        """Load all keys from vault (preferred) or env (fallback)."""
        if self._loaded:
            return

        # Try vault first
        self._vault_available = os.path.exists(VAULT_SCRIPT)

        for provider, key_defs in KEY_REGISTRY.items():
            cfg = self.provider_config.get(provider, {"rps": 10.0, "burst": 15, "quota": 0})
            pool = VaultKeyPool(
                provider=provider,
                rate_rps=cfg["rps"],
                burst=cfg["burst"],
                monthly_quota=cfg["quota"],
            )

            for env_name, vault_path in key_defs:
                value = await self._get_key(env_name, vault_path)
                if value and value not in ("", "your_key_here", "***"):
                    pool.add_key(env_name, value, source="vault" if self._vault_available else "env")

            if pool.keys:
                self.pools[provider] = pool
                logger.info(f"Vault: {provider} -> {len(pool.keys)} key(s) loaded")

        # Load the new Arkham keys (user just provided)
        await self._load_arkham_extra_keys()

        self._loaded = True
        logger.info(
            f"DataBus Vault loaded: {sum(len(p.keys) for p in self.pools.values())} keys across {len(self.pools)} providers"
        )

    async def _load_arkham_extra_keys(self):
        """Load Arkham API key + WebSocket key."""
        if "arkham" not in self.pools:
            pool = VaultKeyPool("arkham", rate_rps=5.0, burst=5, monthly_quota=100000)
            self.pools["arkham"] = pool

        pool = self.pools["arkham"]
        # If no keys loaded yet, try vault then env
        if not pool.keys:
            val = await self._get_key("ARKHAM_API_KEY", "infra/arkham_api_key")
            if val and val not in ("", "your_key_here", "***"):
                pool.add_key("ARKHAM_API_KEY", val)

        # Store WS key separately (not a pool key)
        ws_key = await self._get_key("ARKHAM_WS_KEY", "infra/arkham_ws_key")
        if ws_key and ws_key not in ("", "your_key_here", "***"):
            self.arkham_ws_key = ws_key
        else:
            # Check env
            ws_key = os.getenv("ARKHAM_WS_KEY", "")
            if ws_key:
                self.arkham_ws_key = ws_key

    async def _get_key(self, env_name: str, vault_path: str) -> str | None:
        """Get a key from vault (preferred) or env (fallback). Never logs the value."""
        # Try vault first
        if self._vault_available:
            try:
                result = subprocess.run(
                    ["python3", VAULT_SCRIPT, "get", vault_path],
                    capture_output=True,
                    text=True,
                    timeout=5,
                )
                if result.returncode == 0 and result.stdout.strip():
                    val = result.stdout.strip()
                    if val and val not in ("", "None", "***"):
                        return val
            except Exception:
                pass  # Vault unavailable, fall back to env

        # Fallback to env var
        val = os.getenv(env_name, "").strip()
        if val and val not in ("", "None", "***", "your_key_here"):
            return val

        return None

    async def acquire_key(self, provider: str) -> ManagedKey | None:
        """Acquire a key from the provider's pool."""
        pool = self.pools.get(provider)
        if not pool:
            return None
        return await pool.acquire()

    def get_pool(self, provider: str) -> VaultKeyPool | None:
        return self.pools.get(provider)

    def status(self) -> dict:
        """Full status report. NEVER includes key values."""
        total_keys = sum(len(p.keys) for p in self.pools.values())
        active_keys = sum(sum(1 for k in p.keys if k.is_available()) for p in self.pools.values())
        return {
            "vault_available": self._vault_available,
            "total_providers": len(self.pools),
            "total_keys": total_keys,
            "active_keys": active_keys,
            "providers": {name: pool.status() for name, pool in self.pools.items()},
        }

    def capacity_report(self) -> dict:
        """Generate capacity analysis with recommendations."""
        bottlenecks = []
        recommendations = []
        providers = {}

        for name, pool in self.pools.items():
            self.provider_config.get(name, {})
            total_keys = len(pool.keys)
            combined_rps = pool.rate_rps * total_keys
            combined_quota = pool.monthly_quota * total_keys if pool.monthly_quota > 0 else None
            active = sum(1 for k in pool.keys if k.is_available())

            status = "OK"
            if combined_rps < 10:
                status = "LOW_RPS"
                bottlenecks.append(f"{name}: {combined_rps:.0f} RPS from {total_keys} key(s)")
            if combined_quota and combined_quota < 10000:
                status = "LOW_QUOTA"
                bottlenecks.append(f"{name}: {combined_quota}/mo across {total_keys} key(s)")
            if total_keys == 1 and pool.rate_rps < 10:
                status = "SINGLE_KEY_LIMITED"
                recommendations.append(f"Get 2-3 more {name} free accounts for pool rotation")

            providers[name] = {
                "keys": total_keys,
                "active": active,
                "rps_per_key": pool.rate_rps,
                "combined_rps": combined_rps,
                "monthly_quota": combined_quota,
                "status": status,
            }

        # Auto-recommendations for free tier expansion
        free_tier_accounts = {
            "helius": "https://dev.helius.xyz — 3 free accounts = 75 RPS",
            "moralis": "https://admin.moralis.io — 3 free accounts = 75 RPS",
            "etherscan": "https://etherscan.io/register — multiple free keys",
            "birdeye": "https://birdeye.io — free tier available",
            "solscan": "https://solscan.io — Pro API free tier",
        }
        for prov, info in free_tier_accounts.items():
            if prov in providers and providers[prov]["status"] != "OK":
                recommendations.append(f"FREE: Create more {prov} accounts at {info}")

        return {
            "total_keys": sum(len(p.keys) for p in self.pools.values()),
            "active_keys": sum(sum(1 for k in p.keys if k.is_available()) for p in self.pools.values()),
            "providers": providers,
            "bottlenecks": bottlenecks,
            "recommendations": recommendations,
            "free_tier_opportunities": list(free_tier_accounts.keys()),
        }

    async def reload(self):
        """Force reload all keys from vault. Useful after adding new keys."""
        self._loaded = False
        self.pools.clear()
        await self.load()

    async def add_key(self, provider: str, key_name: str, value: str):
        """Hot-add a key to a provider pool without restart."""
        if provider not in self.pools:
            cfg = self.provider_config.get(provider, {"rps": 10.0, "burst": 15, "quota": 0})
            self.pools[provider] = VaultKeyPool(
                provider=provider,
                rate_rps=cfg["rps"],
                burst=cfg["burst"],
                monthly_quota=cfg["quota"],
            )
        self.pools[provider].add_key(key_name, value, source="manual")
        logger.info(f"Vault: Hot-added {key_name} to {provider} pool")

    def reset_monthly_counters(self):
        """Reset monthly call counters. Call from cron on the 1st of each month."""
        for pool in self.pools.values():
            for key in pool.keys:
                key.calls_this_month = 0
                if key.state == KeyState.EXHAUSTED:
                    key.state = KeyState.ACTIVE

    arkham_ws_key: str = ""


# ── Singleton ─────────────────────────────────────────────────────────────────

_vault: DataBusVault | None = None


async def get_vault() -> DataBusVault:
    global _vault
    if _vault is None:
        _vault = DataBusVault()
        await _vault.load()
    return _vault


def get_vault_sync() -> DataBusVault:
    """Synchronous access — vault must already be loaded."""
    global _vault
    if _vault is None:
        _vault = DataBusVault()
    return _vault
