from __future__ import annotations

import json
import os
import tempfile
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Mapping, Optional
from urllib.parse import urlparse

CONFIG_DIRNAME = ".instavm"
CONFIG_FILENAME = "config.json"
SCHEMA_VERSION = 1

DEFAULT_BASE_URL = "https://api.instavm.io"
DEFAULT_DOCS_URL = "https://docs.instavm.io"
DEFAULT_BILLING_URL = "https://dashboard.instavm.io/billing"
DEFAULT_SSH_HOST = "instavm.dev"


class ConfigError(RuntimeError):
    """Raised when CLI config cannot be loaded or saved safely."""


@dataclass(frozen=True)
class RuntimeConfig:
    api_key: Optional[str]
    base_url: str
    ssh_host: str
    api_key_source: str
    base_url_source: str
    ssh_host_source: str
    config_path: Path
    config: dict[str, Any]


def get_home_dir() -> Path:
    if os.name == "nt":
        home = os.environ.get("USERPROFILE")
        if home:
            return Path(home)
    return Path.home()


def get_config_dir(home: Optional[Path] = None) -> Path:
    return (home or get_home_dir()) / CONFIG_DIRNAME


def get_config_path(home: Optional[Path] = None) -> Path:
    return get_config_dir(home) / CONFIG_FILENAME


def default_config() -> dict[str, Any]:
    return {
        "schema_version": SCHEMA_VERSION,
        "active_profile": "default",
        "profiles": {
            "default": {
                "auth": {
                    "type": "api_key",
                    "api_key": None,
                },
            },
        },
    }


def _normalize_profile(profile: Mapping[str, Any]) -> dict[str, Any]:
    normalized = dict(profile)
    auth = profile.get("auth")
    normalized_auth = dict(auth) if isinstance(auth, Mapping) else {}
    auth_type = str(normalized_auth.get("type") or "api_key").strip() or "api_key"
    raw_api_key = normalized_auth.get("api_key")
    api_key = None if raw_api_key is None else str(raw_api_key).strip() or None
    normalized_auth["type"] = auth_type
    normalized_auth["api_key"] = api_key
    normalized["auth"] = normalized_auth

    for field in ("base_url", "ssh_host"):
        raw_value = normalized.get(field)
        if raw_value is None:
            continue
        value = str(raw_value).strip()
        if value:
            normalized[field] = value
        else:
            normalized.pop(field, None)
    return normalized


def normalize_config(raw_config: Optional[Mapping[str, Any]]) -> dict[str, Any]:
    config = default_config()
    if not isinstance(raw_config, Mapping):
        return config

    schema_version = raw_config.get("schema_version")
    if isinstance(schema_version, int):
        config["schema_version"] = schema_version

    profiles = raw_config.get("profiles")
    if isinstance(profiles, Mapping):
        config["profiles"] = {}
        for name, profile in profiles.items():
            if isinstance(name, str) and isinstance(profile, Mapping):
                config["profiles"][name] = _normalize_profile(profile)

    if "default" not in config["profiles"]:
        config["profiles"]["default"] = default_config()["profiles"]["default"]

    active_profile = raw_config.get("active_profile")
    if isinstance(active_profile, str) and active_profile in config["profiles"]:
        config["active_profile"] = active_profile

    return config


def load_config(path: Optional[Path] = None) -> dict[str, Any]:
    config_path = path or get_config_path()
    if not config_path.exists():
        return default_config()

    try:
        payload = json.loads(config_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        raise ConfigError(f"Invalid config JSON in {config_path}") from exc
    except OSError as exc:
        raise ConfigError(f"Unable to read config from {config_path}") from exc
    return normalize_config(payload)


def _ensure_posix_permissions(path: Path, mode: int) -> None:
    if os.name != "nt":
        path.chmod(mode)


def save_config(config: Mapping[str, Any], path: Optional[Path] = None) -> Path:
    config_path = path or get_config_path()
    config_dir = config_path.parent
    try:
        config_dir.mkdir(parents=True, exist_ok=True)
        _ensure_posix_permissions(config_dir, 0o700)

        normalized = normalize_config(config)
        fd, temp_name = tempfile.mkstemp(prefix=f".{config_path.name}.", dir=str(config_dir))
        temp_path = Path(temp_name)
        try:
            with os.fdopen(fd, "w", encoding="utf-8") as handle:
                json.dump(normalized, handle, indent=2, sort_keys=True)
                handle.write("\n")
            _ensure_posix_permissions(temp_path, 0o600)
            os.replace(temp_path, config_path)
            _ensure_posix_permissions(config_path, 0o600)
        finally:
            if temp_path.exists():
                temp_path.unlink()
    except OSError as exc:
        raise ConfigError(f"Unable to write config to {config_path}") from exc
    return config_path


def get_active_profile(config: Mapping[str, Any]) -> dict[str, Any]:
    active_profile = config.get("active_profile") if isinstance(config, Mapping) else None
    profiles = config.get("profiles") if isinstance(config, Mapping) else None
    if isinstance(active_profile, str) and isinstance(profiles, Mapping):
        profile = profiles.get(active_profile)
        if isinstance(profile, Mapping):
            return dict(profile)
    return default_config()["profiles"]["default"]


def redact_secret(secret: Optional[str]) -> Optional[str]:
    if not secret:
        return None
    value = secret.strip()
    if len(value) <= 8:
        return "*" * len(value)
    return f"{value[:8]}...{value[-4:]}"


def derive_ssh_host(base_url: str) -> str:
    try:
        parsed = urlparse(base_url)
    except ValueError:
        return DEFAULT_SSH_HOST

    host = (parsed.hostname or "").strip().lower()
    if not host:
        return DEFAULT_SSH_HOST
    if host in {"localhost", "127.0.0.1"} or host.startswith("localhost:"):
        return host
    if "staging" in host and "instavm" in host:
        return "staging.instavm.dev"
    if host.startswith("api.") and "instavm" in host:
        return DEFAULT_SSH_HOST
    if host.startswith("staging.api.") and "instavm" in host:
        return "staging.instavm.dev"
    if "instavm" in host:
        return DEFAULT_SSH_HOST
    return host


def resolve_runtime_config(
    *,
    api_key: Optional[str] = None,
    base_url: Optional[str] = None,
    ssh_host: Optional[str] = None,
    env: Optional[Mapping[str, str]] = None,
    config: Optional[Mapping[str, Any]] = None,
    config_path: Optional[Path] = None,
) -> RuntimeConfig:
    env_map = env or os.environ
    resolved_path = config_path or get_config_path()
    loaded_config = normalize_config(config or load_config(resolved_path))
    profile = get_active_profile(loaded_config)
    auth = profile.get("auth") if isinstance(profile, Mapping) else {}
    stored_key = auth.get("api_key") if isinstance(auth, Mapping) else None
    stored_base_url = profile.get("base_url") if isinstance(profile, Mapping) else None
    stored_ssh_host = profile.get("ssh_host") if isinstance(profile, Mapping) else None

    if api_key:
        effective_api_key = api_key.strip()
        api_key_source = "flag"
    elif env_map.get("INSTAVM_API_KEY"):
        effective_api_key = str(env_map["INSTAVM_API_KEY"]).strip()
        api_key_source = "env"
    else:
        effective_api_key = str(stored_key).strip() if stored_key else None
        api_key_source = "config" if effective_api_key else "missing"

    if base_url:
        effective_base_url = base_url.strip()
        base_url_source = "flag"
    elif env_map.get("INSTAVM_BASE_URL"):
        effective_base_url = str(env_map["INSTAVM_BASE_URL"]).strip()
        base_url_source = "env"
    elif stored_base_url:
        effective_base_url = str(stored_base_url).strip()
        base_url_source = "config"
    else:
        effective_base_url = DEFAULT_BASE_URL
        base_url_source = "default"

    if ssh_host:
        effective_ssh_host = ssh_host.strip()
        ssh_host_source = "flag"
    elif env_map.get("INSTAVM_SSH_HOST"):
        effective_ssh_host = str(env_map["INSTAVM_SSH_HOST"]).strip()
        ssh_host_source = "env"
    elif stored_ssh_host:
        effective_ssh_host = str(stored_ssh_host).strip()
        ssh_host_source = "config"
    else:
        effective_ssh_host = derive_ssh_host(effective_base_url)
        ssh_host_source = "derived"

    return RuntimeConfig(
        api_key=effective_api_key or None,
        base_url=effective_base_url,
        ssh_host=effective_ssh_host,
        api_key_source=api_key_source,
        base_url_source=base_url_source,
        ssh_host_source=ssh_host_source,
        config_path=resolved_path,
        config=loaded_config,
    )


def update_profile_settings(
    *,
    api_key: Optional[str] = None,
    clear_api_key: bool = False,
    base_url: Optional[str] = None,
    ssh_host: Optional[str] = None,
    config_path: Optional[Path] = None,
) -> Path:
    resolved_path = config_path or get_config_path()
    config = load_config(resolved_path)
    active_profile_name = config.get("active_profile", "default")
    profiles = config.setdefault("profiles", {})
    profile = dict(profiles.get(active_profile_name) or {})
    auth = dict(profile.get("auth") or {})
    auth["type"] = str(auth.get("type") or "api_key")

    if clear_api_key:
        auth["api_key"] = None
    elif api_key is not None:
        auth["api_key"] = api_key.strip() or None

    if base_url is not None:
        cleaned_base_url = base_url.strip()
        if cleaned_base_url:
            profile["base_url"] = cleaned_base_url
        else:
            profile.pop("base_url", None)

    if ssh_host is not None:
        cleaned_ssh_host = ssh_host.strip()
        if cleaned_ssh_host:
            profile["ssh_host"] = cleaned_ssh_host
        else:
            profile.pop("ssh_host", None)

    profile["auth"] = auth
    profiles[active_profile_name] = profile
    config["profiles"] = profiles
    return save_config(config, resolved_path)
