"""obol config - manage CLI configuration."""
import base64
import hashlib
import json
import os

from obol.cli._base import output, error, secure_file, add_format_arg


CONFIG_PATH = os.path.expanduser("~/.obol/config.json")

_KEY_PREFIX_V2 = "enc2:"


def _get_machine_gcm_key() -> bytes:
    """Derive AES-256-GCM key from machine identity via HKDF."""
    identity = ""
    try:
        with open("/etc/machine-id") as f:
            identity = f.read().strip()
    except Exception:
        pass
    if not identity:
        import socket
        identity = f"{socket.gethostname()}:{os.environ.get('USER', os.environ.get('USERNAME', 'unknown'))}"
    try:
        from cryptography.hazmat.primitives.hashes import SHA256
        from cryptography.hazmat.primitives.kdf.hkdf import HKDF
        return HKDF(
            algorithm=SHA256(), length=32,
            salt=b"obol-config-aes256-v1", info=b"config-encryption",
        ).derive(identity.encode())
    except ImportError:
        # Fallback: raw SHA-256 (same as legacy but 32 bytes)
        return hashlib.sha256(f"obol-config-v1:{identity}".encode()).digest()


def _encrypt_api_key(api_key: str) -> str:
    """Encrypt API key for config storage using AES-256-GCM."""
    if not api_key or api_key.startswith(_KEY_PREFIX_V2):
        return api_key
    try:
        from cryptography.hazmat.primitives.ciphers.aead import AESGCM
        key = _get_machine_gcm_key()
        aesgcm = AESGCM(key)
        nonce = os.urandom(12)
        ct = aesgcm.encrypt(nonce, api_key.encode(), None)
        encoded = base64.urlsafe_b64encode(nonce + ct).decode()
        return _KEY_PREFIX_V2 + encoded
    except ImportError:
        return api_key  # No cryptography lib, store plaintext


def _decrypt_api_key(value: str) -> str:
    """Decrypt API key from config storage (AES-256-GCM)."""
    if not value:
        return value
    # AES-256-GCM format
    if value.startswith(_KEY_PREFIX_V2):
        try:
            from cryptography.hazmat.primitives.ciphers.aead import AESGCM
            key = _get_machine_gcm_key()
            raw = base64.urlsafe_b64decode(value[len(_KEY_PREFIX_V2):])
            nonce = raw[:12]
            ct_with_tag = raw[12:]
            return AESGCM(key).decrypt(nonce, ct_with_tag, None).decode()
        except Exception:
            return value  # Can't decrypt, return as-is
    return value  # Plaintext (no cryptography lib)

CURRENT_CONFIG_VERSION = 2

VALID_KEYS = {
    "api_key": "Obol API key (ob_live_xxx)",
    "api_url": "API base URL",
    "format": "Default output format (text or json)",
    "enforcement_mode": "Enforcement mode (audit, alert, enforce)",
    "allowed_destinations": "Allowlisted domains (JSON array)",
    "offline_policy": "Behavior when API unreachable (allow_low, block_high, block_all)",
    "rule_update_interval": "Detection rule pull interval in minutes (default: 60)",
    "local_baseline": "Enable local behavioral baseline (true/false)",
    "notify": "Desktop notifications (true/false)",
}

VALID_VALUES = {
    "enforcement_mode": ("audit", "alert", "enforce"),
    "format": ("text", "json"),
    "offline_policy": ("allow_low", "block_high", "block_all"),
    "local_baseline": ("true", "false"),
    "notify": ("true", "false"),
}


# --- Config Migration System ---

def _migrate_v1_to_v2(config: dict) -> dict:
    """Migrate from v1 (no version field) to v2.

    v2 adds: config_version, offline_policy, local_baseline defaults.
    """
    config["config_version"] = 2
    config.setdefault("offline_policy", "allow_low")
    config.setdefault("local_baseline", "true")
    config.setdefault("notify", "true")
    return config


# Ordered list of migrations: (from_version, to_version, function)
_MIGRATIONS = [
    (1, 2, _migrate_v1_to_v2),
]


def _apply_migrations(config: dict) -> tuple[dict, bool]:
    """Apply any pending migrations. Returns (config, was_migrated)."""
    version = config.get("config_version", 1)
    migrated = False

    for from_v, to_v, migrate_fn in _MIGRATIONS:
        if version == from_v:
            config = migrate_fn(config)
            version = to_v
            migrated = True

    return config, migrated


def _load_config() -> dict:
    if os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH) as f:
            config = json.load(f)
        # Transparently decrypt api_key
        if "api_key" in config:
            config["api_key"] = _decrypt_api_key(config["api_key"])
        # Validate enforcement_mode (fail-safe: invalid values default to "audit")
        if config.get("enforcement_mode") not in ("audit", "alert", "enforce"):
            config["enforcement_mode"] = "audit"
        # Apply migrations if needed
        config, migrated = _apply_migrations(config)
        if migrated:
            _save_config(config)
        return config
    return {"config_version": CURRENT_CONFIG_VERSION}


def _save_config(config: dict):
    os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
    # Ensure version is always present
    save_config = dict(config)
    save_config.setdefault("config_version", CURRENT_CONFIG_VERSION)
    if "api_key" in save_config:
        save_config["api_key"] = _encrypt_api_key(save_config["api_key"])
    # Atomic write: write to temp file then rename (prevents corruption)
    tmp_path = CONFIG_PATH + ".tmp"
    fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    try:
        with os.fdopen(fd, "w") as f:
            json.dump(save_config, f, indent=2)
        os.replace(tmp_path, CONFIG_PATH)
    except Exception:
        try:
            os.unlink(tmp_path)
        except OSError:
            pass
        raise
    # Ensure final file has correct permissions
    secure_file(CONFIG_PATH)


def cmd_config(args):
    fmt = getattr(args, "format", "text")
    sub = getattr(args, "config_cmd", None)

    if sub == "set":
        key = args.key
        value = args.value

        if key not in VALID_KEYS:
            error(f"Unknown config key: {key}. Valid: {', '.join(VALID_KEYS.keys())}")

        if key in VALID_VALUES and value not in VALID_VALUES[key]:
            error(f"Invalid value for {key}: {value}. Valid: {', '.join(VALID_VALUES[key])}")

        config = _load_config()
        config[key] = value
        _save_config(config)

        # Mask API key in output
        display_value = value
        if key == "api_key" and len(value) > 10:
            display_value = value[:10] + "..." + value[-4:]

        print(f"Set {key} = {display_value}")
        return

    # Default: show
    config = _load_config()

    if fmt == "json":
        # Mask api_key
        safe = dict(config)
        if "api_key" in safe:
            k = safe["api_key"]
            safe["api_key"] = k[:10] + "..." + k[-4:] if len(k) > 10 else "***"
        output(safe, fmt="json")
        return

    if not config:
        print("No configuration set.")
        print()
        print("Available settings:")
        for key, desc in VALID_KEYS.items():
            env = {"api_key": "OBOL_API_KEY", "api_url": "OBOL_API_URL"}.get(key, "")
            env_str = f" (env: {env})" if env else ""
            print(f"  {key:<12} {desc}{env_str}")
        print()
        print("Set with: obol config set <key> <value>")
        return

    # Internal keys hidden from display
    _INTERNAL_KEYS = {"config_version", "hook_handler_hash", "agent_ids", "admin_secret"}

    print("Current configuration:")
    for key, value in sorted(config.items()):
        if key in _INTERNAL_KEYS:
            continue
        display = value
        if key == "api_key" and len(str(value)) > 10:
            display = str(value)[:10] + "..." + str(value)[-4:]
        print(f"  {key:<22} {display}")


def setup_parser(subparsers):
    """Add config subparser."""
    parser = subparsers.add_parser("config", help="CLI configuration")
    parser.set_defaults(func=cmd_config, config_cmd=None)
    add_format_arg(parser)

    sub = parser.add_subparsers(dest="config_cmd")

    sub.add_parser("show", help="Show current config")

    set_p = sub.add_parser("set", help="Set a config value")
    set_p.add_argument("key", help="Config key")
    set_p.add_argument("value", help="Config value")
