"""MCP server (stdio) exposing the read-only HA tools in tools.py.

Every tool is declared with `read_only_hint=True` / `destructive_hint=False`
— but the guarantee is structural, not advisory: this process has no code
path that writes to HA (see backend.py).

Errors never propagate as exceptions: a missing token, an unreachable
instance or an unknown entity comes back as `{"error": "..."}` inside a
normal tool result, so one bad call cannot take the server down mid-session.

Logging goes to stderr only — stdout is the JSON-RPC channel.
"""
import logging
import sys

from mcp.server.mcpserver import MCPServer
from mcp.types import ToolAnnotations

from . import tools
from .backend import DEFAULT_TIMEOUT, BackendError, LiveBackend
from .config import DEFAULT_INSTANCE, ConfigError, get_instance, repo_root

logging.basicConfig(stream=sys.stderr, level=logging.INFO, format="ha-mcp: %(message)s")
log = logging.getLogger("ha-mcp")

READ_ONLY = ToolAnnotations(read_only_hint=True, destructive_hint=False, open_world_hint=True)

mcp = MCPServer(
    name="ha-mcp",
    version="0.1.0",
    instructions=(
        "Read-only access to the Home Assistant instances defined in "
        "services/home-assistant/instances.yaml. Default instance: 'ken' (the "
        "canonical home instance); pass `instance` to target another one. "
        "These tools cannot change anything in Home Assistant — to change an "
        "automation, edit services/home-assistant/config/<instance>/ in the repo "
        "and deploy with scripts/ha/deploy.sh (see services/home-assistant/DESIGN.md). "
        "Entities reported with unavailable=true are dead: their automations "
        "silently do not fire."
    ),
)


def _backend(instance):
    name, cfg = get_instance(instance, repo_root())
    return LiveBackend(name, cfg, repo_root(), timeout=DEFAULT_TIMEOUT)


def _call(fn, instance, **kwargs):
    """Run one tool, turning any failure into a readable result payload."""
    try:
        backend = _backend(instance)
        return fn(backend, **kwargs)
    except (BackendError, ConfigError) as exc:
        return {"error": str(exc), "instance": instance or DEFAULT_INSTANCE}
    except Exception as exc:  # noqa: BLE001 — the server must survive any tool
        log.exception("unhandled error in %s", getattr(fn, "__name__", fn))
        return {
            "error": f"{type(exc).__name__}: {exc}",
            "instance": instance or DEFAULT_INSTANCE,
        }


@mcp.tool(annotations=READ_ONLY)
def list_entities(domain: str | None = None, area: str | None = None,
                  limit: int = tools.DEFAULT_ENTITY_LIMIT,
                  instance: str | None = None) -> dict:
    """List Home Assistant entities with state and area.

    Filter by `domain` (e.g. "sensor", "light", "binary_sensor") and/or `area`
    (area name or area_id, e.g. "Salon"). Entities that are dead are flagged
    with unavailable=true and unavailable_since.
    """
    return _call(tools.list_entities, instance, domain=domain, area=area, limit=limit)


@mcp.tool(annotations=READ_ONLY)
def get_state(entity_id: str, instance: str | None = None) -> dict:
    """Full state of one entity, including every attribute and its area."""
    return _call(tools.get_state, instance, entity_id=entity_id)


@mcp.tool(annotations=READ_ONLY)
def get_areas(instance: str | None = None) -> dict:
    """List areas (rooms) with the number of entities in each, dead ones counted separately."""
    return _call(tools.get_areas, instance)


@mcp.tool(annotations=READ_ONLY)
def find_entities_by_description(text: str, instance: str | None = None) -> dict:
    """Find entities from a loose description, in Polish or English.

    Matches entity_id, friendly_name and area with substring/prefix matching
    plus a PL->EN synonym table (e.g. "czujnik temperatury salon" ->
    sensor.thsalon_temperature). Returns up to 20 hits, each with the reason
    it matched.
    """
    return _call(tools.find_entities_by_description, instance, text=text)


@mcp.tool(annotations=READ_ONLY)
def read_automation(id_or_alias: str, instance: str | None = None) -> dict:
    """Read one automation's YAML from the repo, annotated with its live state.

    Accepts a numeric automation id or (part of) its alias. The content comes
    from services/home-assistant/config/<instance>/automations/ — the
    reviewable source of truth — not from the HA API; last_triggered and the
    on/off state are read from the live instance when it is reachable.
    """
    return _call(tools.read_automation, instance, id_or_alias=id_or_alias)


@mcp.tool(annotations=READ_ONLY)
def list_automations(filter: str | None = None, instance: str | None = None) -> dict:
    """List automations (id, alias, state, last_triggered), optional substring filter."""
    return _call(tools.list_automations, instance, filter=filter)


@mcp.tool(annotations=READ_ONLY)
def instance_status(instance: str | None = None) -> dict:
    """Reachability, HA version, entity/automation counts and how many entities are dead.

    Instances marked `status: offline` in instances.yaml (chelsty-ha, LTE site)
    report that status directly instead of burning a network timeout.
    """
    return _call(tools.instance_status, instance)


def main():
    log.info("starting stdio server (read-only), repo=%s", repo_root())
    mcp.run("stdio")


if __name__ == "__main__":
    main()
