"""Resolve MCP service URLs, the bearer token and the bot tag.

Cerbero MCP V2 (a single FastAPI image fronting Deribit, Hyperliquid,
Macro, Sentiment and friends) is deployed on a dedicated VPS and reached
through the public gateway at ``https://cerbero-mcp.tielogic.xyz``. The
server decides the upstream environment (testnet vs mainnet) entirely
from the bearer token attached to each request — Cerbero Bite does not
have to be told which is which: swapping the token in ``.env`` is enough
to switch environments.

The resolver supports the following layers of override:

1. Per-service URL env vars (``CERBERO_BITE_MCP_DERIBIT_URL``,
   ``CERBERO_BITE_MCP_HYPERLIQUID_URL``, ``CERBERO_BITE_MCP_MACRO_URL``,
   ``CERBERO_BITE_MCP_SENTIMENT_URL``). Useful for local dev when the
   bot must talk to a same-host MCP server (``http://localhost:9000``)
   instead of the public gateway.
2. ``CERBERO_BITE_MCP_TOKEN`` env var: the bearer token used on every
   request. The token's value is *never* logged.
3. ``CERBERO_BITE_MCP_BOT_TAG`` env var: identifier sent on the
   ``X-Bot-Tag`` header (default ``BOT__CERBERO_BITE``). Must be a
   non-empty string of at most 64 characters.
"""

from __future__ import annotations

import os
from dataclasses import dataclass

from cerbero_bite.clients._base import DEFAULT_BOT_TAG

__all__ = [
    "DEFAULT_BOT_TAG",
    "DEFAULT_ENDPOINTS",
    "MCP_SERVICES",
    "McpEndpoints",
    "load_bot_tag",
    "load_endpoints",
    "load_token",
]


# Service identifier → (default Docker DNS host, default port, env var name)
#
# Telegram and Portfolio used to be shared MCP services; both are now
# in-process per bot (Telegram → public Bot API, Portfolio → aggregator
# over Deribit + Hyperliquid + Macro). They are no longer listed here.
MCP_SERVICES: dict[str, tuple[str, int, str]] = {
    "deribit": ("mcp-deribit", 9011, "CERBERO_BITE_MCP_DERIBIT_URL"),
    "hyperliquid": ("mcp-hyperliquid", 9012, "CERBERO_BITE_MCP_HYPERLIQUID_URL"),
    "macro": ("mcp-macro", 9013, "CERBERO_BITE_MCP_MACRO_URL"),
    "sentiment": ("mcp-sentiment", 9014, "CERBERO_BITE_MCP_SENTIMENT_URL"),
}


def _default_url(host: str, port: int) -> str:
    return f"http://{host}:{port}"


DEFAULT_ENDPOINTS: dict[str, str] = {
    name: _default_url(host, port) for name, (host, port, _) in MCP_SERVICES.items()
}


# Cerbero MCP V2 unified interface (``/mcp``). The data tools
# ``get_instruments`` / ``get_historical`` / ``get_indicators`` live here
# ONLY — they were removed from the per-exchange routers in MCP V2. The
# caller passes ``exchange="deribit"`` in the body to target one venue.
_UNIFIED_ENV = "CERBERO_BITE_MCP_UNIFIED_URL"
_DEFAULT_UNIFIED_URL = "http://cerbero-mcp:9000/mcp"


@dataclass(frozen=True)
class McpEndpoints:
    """Resolved per-service URLs."""

    deribit: str
    hyperliquid: str
    macro: str
    sentiment: str
    unified: str = _DEFAULT_UNIFIED_URL

    def for_service(self, name: str) -> str:
        try:
            return getattr(self, name)  # type: ignore[no-any-return]
        except AttributeError as exc:
            raise KeyError(f"unknown MCP service '{name}'") from exc


def load_endpoints(env: dict[str, str] | None = None) -> McpEndpoints:
    """Build an :class:`McpEndpoints` honouring env-var overrides."""
    e = env if env is not None else os.environ
    resolved: dict[str, str] = {}
    for name, (host, port, env_var) in MCP_SERVICES.items():
        override = e.get(env_var)
        resolved[name] = override.rstrip("/") if override else _default_url(host, port)
    unified_override = e.get(_UNIFIED_ENV)
    resolved["unified"] = (
        unified_override.rstrip("/") if unified_override else _DEFAULT_UNIFIED_URL
    )
    return McpEndpoints(**resolved)


_TOKEN_ENV = "CERBERO_BITE_MCP_TOKEN"
_BOT_TAG_ENV = "CERBERO_BITE_MCP_BOT_TAG"
_BOT_TAG_MAX_LEN = 64


def load_token(
    *,
    value: str | None = None,
    env: dict[str, str] | None = None,
) -> str:
    """Return the MCP bearer token, stripped of surrounding whitespace.

    Resolution order:
      1. explicit ``value`` argument (e.g. from a CLI flag);
      2. ``CERBERO_BITE_MCP_TOKEN`` env var.
    """
    if value is not None:
        token = value.strip()
        if not token:
            raise ValueError("explicit MCP token is empty")
        return token
    e = env if env is not None else os.environ
    raw = e.get(_TOKEN_ENV, "")
    token = raw.strip()
    if not token:
        raise ValueError(
            f"{_TOKEN_ENV} is unset or empty; set it in .env to the testnet or "
            "mainnet bearer issued by Cerbero MCP"
        )
    return token


def load_bot_tag(
    *,
    value: str | None = None,
    env: dict[str, str] | None = None,
) -> str:
    """Return the ``X-Bot-Tag`` value, with the project default as fallback.

    Resolution order:
      1. explicit ``value`` argument;
      2. ``CERBERO_BITE_MCP_BOT_TAG`` env var;
      3. :data:`DEFAULT_BOT_TAG` (``"BOT__CERBERO_BITE"``).
    """
    raw = value if value is not None else (env if env is not None else os.environ).get(
        _BOT_TAG_ENV, ""
    )
    cleaned = raw.strip() if raw else ""
    if not cleaned:
        return DEFAULT_BOT_TAG
    if len(cleaned) > _BOT_TAG_MAX_LEN:
        raise ValueError(
            f"{_BOT_TAG_ENV} exceeds {_BOT_TAG_MAX_LEN} characters: {cleaned!r}"
        )
    return cleaned
