"""Peer-side config: where to find the leader + credentials.

Stored at ~/.config/maxim/peer.yml on POSIX, %APPDATA%\\maxim\\peer.yml on
Windows. Mirrors the layout of the leader's ~/.config/maxim/api_key file.

Fields:
  url          — leader's OpenAI-compatible endpoint (e.g., tunnel URL or LAN IP)
  api_key      — Bearer token shared by the leader
  model        — optional; model name to send (defaults to whatever /v1/models returns)
  is_cloud     — optional; forces MAXIM_MAX_CLOUD_LANES=1 for public URLs

`build_primary_router` consults this file at startup and uses its values
as defaults for the large-lane remote config. Env vars (MAXIM_LANE_LARGE_*)
still override these — so users can do per-session tweaks without touching
the file.
"""

from __future__ import annotations

import os
import platform
from dataclasses import dataclass
from pathlib import Path


def peer_config_path() -> Path:
    """Return the platform-appropriate peer config path."""
    if platform.system() == "Windows":
        appdata = os.environ.get("APPDATA")
        base = Path(appdata) if appdata else Path.home() / "AppData" / "Roaming"
        return base / "maxim" / "peer.yml"
    xdg = os.environ.get("XDG_CONFIG_HOME")
    base = Path(xdg) if xdg else Path.home() / ".config"
    return base / "maxim" / "peer.yml"


@dataclass(frozen=True)
class PeerConfig:
    url: str
    api_key: str
    model: str | None = None
    is_cloud: bool = False

    def to_yaml(self) -> str:
        """Minimal YAML serialization (stable field order, no PyYAML dep)."""
        lines = [
            f"url: {self.url}",
            f"api_key: {self.api_key}",
        ]
        if self.model:
            lines.append(f"model: {self.model}")
        if self.is_cloud:
            lines.append("is_cloud: true")
        return "\n".join(lines) + "\n"


def read_peer_config() -> PeerConfig | None:
    """Load the peer config, or None if missing / malformed."""
    path = peer_config_path()
    if not path.is_file():
        return None
    try:
        content = path.read_text()
    except OSError:
        return None
    fields: dict[str, str] = {}
    for line in content.splitlines():
        stripped = line.strip()
        if not stripped or stripped.startswith("#"):
            continue
        key, _, value = stripped.partition(":")
        fields[key.strip()] = value.strip()
    url = fields.get("url", "")
    api_key = fields.get("api_key", "")
    if not url or not api_key:
        return None
    is_cloud = fields.get("is_cloud", "").lower() in ("true", "1", "yes")
    model = fields.get("model") or None
    return PeerConfig(url=url, api_key=api_key, model=model, is_cloud=is_cloud)


def write_peer_config(cfg: PeerConfig) -> Path:
    """Persist a peer config with 0600 perms on POSIX.

    **Plan 4 C3.1 fold (architecture review A4):** routes through
    :func:`maxim.utils.atomic_io.atomic_write_secret` for the same
    reason :func:`maxim.peer.mesh_config.write_mesh_config` does —
    ``peer.yml::api_key`` is a secret, and the C2 invariant
    ("credential-bearing files use ``atomic_write_secret``") applies.
    Pre-C3.1 this used plain ``path.write_text`` + explicit
    ``os.chmod`` which was a latent inconsistency from before the
    C2 invariant landed. The architecture lens reviewer flagged
    leaving it inconsistent as a footgun for the next C3 reviewer
    (two credential-bearing writers with two different patterns).

    On first write, ``atomic_write_secret`` no-ops the mode preserve
    (no existing file). The explicit ``os.chmod(0o600)`` runs
    afterwards. On rewrites, ``atomic_write_secret`` preserves the
    existing mode bits and the explicit chmod is a no-op assuming
    the file is already at 0o600 (operator may have widened it
    intentionally for a shared-group setup, in which case
    ``preserve_mode=True`` keeps their choice and the chmod here
    re-tightens — minor disagreement with operator intent that
    matches the pre-fold behavior, so no behavior regression).
    """
    from maxim.utils.atomic_io import atomic_write_secret

    path = peer_config_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    is_new = not path.is_file()
    atomic_write_secret(str(path), cfg.to_yaml())
    if is_new and platform.system() != "Windows":
        try:
            os.chmod(path, 0o600)
        except OSError:
            # Same best-effort + soft-failure mode as
            # mesh_config.write_mesh_config. The fold there added a
            # logger.warning; we mirror that here for consistency.
            import logging

            logging.getLogger(__name__).warning(
                "write_peer_config: chmod 0o600 on first write failed for %s. "
                "API key may be readable to other users on this system.",
                path,
            )
    return path


def delete_peer_config() -> bool:
    """Remove the peer config file. Returns True if a file was deleted."""
    path = peer_config_path()
    if not path.is_file():
        return False
    try:
        path.unlink()
        return True
    except OSError:
        return False


def _setdefault_nonempty(key: str, value: str) -> None:
    """Like ``os.environ.setdefault`` but treats an existing *empty* value as
    absent.  An empty URL or API key is never intentional — it usually means a
    previous session or shell snippet exported the var without a value, which
    would silently shadow the peer config.
    """
    if not os.environ.get(key, "").strip():
        os.environ[key] = value


def apply_peer_config_to_env(cfg: PeerConfig) -> None:
    """Set MAXIM_LANE_LARGE_* env vars from a peer config, without overwriting
    any that are already set (env wins over file for per-session overrides).

    Note: the variable name family is keyed off the canonical lane tier name
    (``large``). Earlier code wrote ``MAXIM_LANE_INFER_*`` as a legacy alias;
    that fallback was removed in v1.0.0 along with the matching read code.

    Also registers the ``leader`` HTTP endpoint in ``maxim.utils.http`` so
    downstream callers (probes, doctor, proxy, peer CLI) reference it by
    name instead of building urllib requests from scratch. The registration
    is idempotent and keys auth off the current ``MAXIM_LANE_LARGE_REMOTE_API_KEY``
    env var via a late-bound ``auth_provider`` — cluster-key rotation doesn't
    need to touch call sites.
    """
    _setdefault_nonempty("MAXIM_LANE_LARGE_REMOTE_URL", cfg.url)
    _setdefault_nonempty("MAXIM_LANE_LARGE_REMOTE_API_KEY", cfg.api_key)
    if cfg.model:
        _setdefault_nonempty("MAXIM_LANE_LARGE_REMOTE_MODEL", cfg.model)
    # Peer connections are to your own infrastructure — even if the URL
    # resolves to a public IP (Cloudflare tunnel), it's not a cloud
    # provider. Mark it as peer-owned so lane_backends treats it as
    # "self-hosted" (no cloud gate, no redaction policy required).
    os.environ.setdefault("MAXIM_MAX_CLOUD_LANES", "1")

    register_leader_endpoint(cfg.url)


def register_leader_endpoint(base_url: str) -> None:
    """Register the ``leader`` HTTP endpoint so callers can use
    ``http.get("leader", "/v1/models")`` instead of hand-rolled urllib.

    Idempotent — later calls overwrite the registration, which is correct
    if the peer config is reloaded with a new URL.
    """
    if not base_url:
        return
    from maxim.utils import http as _http

    def _leader_auth() -> str | None:
        return os.environ.get("MAXIM_LANE_LARGE_REMOTE_API_KEY") or None

    _http.register_endpoint(
        _http.HTTPEndpoint(
            name="leader",
            base_url=base_url.rstrip("/"),
            default_headers={"User-Agent": _http.DEFAULT_USER_AGENT},
            auth_provider=_leader_auth,
            timeouts=_http.TimeoutPolicy(connect_s=3.0, read_s=60.0, total_s=120.0),
            max_pool_connections=_http.DEFAULT_POOL_PER_ENDPOINT,
            internal=True,
        )
    )


def truncate_key(key: str, *, keep: int = 6) -> str:
    if len(key) <= keep * 2 + 1:
        return key
    return f"{key[:keep]}…{key[-keep:]}"


__all__ = [
    "PeerConfig",
    "peer_config_path",
    "read_peer_config",
    "write_peer_config",
    "delete_peer_config",
    "apply_peer_config_to_env",
    "register_leader_endpoint",
    "truncate_key",
]
