#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Jiri Vyskocil
# SPDX-License-Identifier: Apache-2.0
# terok:container — this file is deployed into task containers, not used on the host.

"""Show the providers that each agent can use in this task.

The script reads ``~/.terok/agents.json``. The ``terok-agents`` command writes
this readiness manifest for each task. If the manifest is missing or invalid,
the script creates it again from the current container state.

By default, the script groups ready pairs by agent. The ``--all`` option also
shows pairs that are not ready and providers that are locked. A pair is not
ready if the protocol is not supported or the provider is not authenticated.
A locked provider is not authenticated. Run ``terok auth <provider>`` on the
host to unlock it. The default view shows this command in a short hint.

Agents are command-line tools and are cyan. This color is the same as in
``hilfe``. Providers are LLM services and are magenta. This script uses only
the standard library because Terok is not installed in task containers.
"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from collections import defaultdict
from pathlib import Path

MANIFEST = Path(os.environ.get("HOME", "/home/dev")) / ".terok" / "agents.json"

_CYAN = "\033[36m"
_MAGENTA = "\033[35m"
_DIM = "\033[2m"
_BOLD = "\033[1m"
_RESET = "\033[0m"


def main(argv: list[str]) -> int:
    """Show the manifest. Use ``--all`` to include pairs that are not ready."""
    show_all = "--all" in argv[1:]
    manifest = _read_manifest(MANIFEST)
    if manifest is None:
        diagnostic = _regenerate_manifest()
        manifest = _read_manifest(MANIFEST)
    else:
        diagnostic = ""
    if manifest is None:
        print(
            f"Terok could not create a valid readiness manifest at {MANIFEST}.",
            file=sys.stderr,
        )
        if diagnostic:
            print(diagnostic, file=sys.stderr)
        print(
            "Run terok-agents to see the error. If the error continues, rebuild the "
            "agent image for the project on the host.",
            file=sys.stderr,
        )
        return 1

    ready: dict[str, list[str]] = defaultdict(list)
    blocked: dict[str, list[str]] = defaultdict(list)
    for pair in manifest.get("pairs", []):
        bucket = ready if pair.get("ready") else blocked
        bucket[pair["agent"]].append(pair["provider"])
    locked = _locked_providers(manifest)

    if not ready and not blocked:
        print("No (agent, provider) pairs - authenticate a provider on the host.", file=sys.stderr)
        _print_locked(locked)
        return 0

    agents = sorted(ready if not show_all else {**ready, **blocked})
    width = max((len(a) for a in agents), default=0)
    print(f"{_BOLD}Ready agent → providers:{_RESET}")
    for agent in sorted(ready):
        names = ", ".join(f"{_MAGENTA}{p}{_RESET}" for p in sorted(set(ready[agent])))
        print(f"  {_CYAN}{agent:<{width}}{_RESET} {_DIM}→{_RESET} {names}")
    if show_all:
        print(f"\n{_BOLD}{_DIM}Not ready (protocol mismatch or unauthenticated):{_RESET}")
        for agent in sorted(blocked):
            names = ", ".join(sorted(set(blocked[agent])))
            print(f"  {_DIM}{agent:<{width}}  {names}{_RESET}")
        _print_locked(locked)
    else:
        print(f"\n{_DIM}Also list non-ready providers:{_RESET} providers --all")
        _print_locked_hint(locked)
        print(f"\n{_DIM}Use:{_RESET} {_CYAN}<agent>{_RESET} --provider {_MAGENTA}<name>{_RESET}")
    return 0


def _read_manifest(path: Path) -> dict | None:
    """Return a valid readiness manifest, or return ``None``."""
    try:
        manifest = json.loads(path.read_text(encoding="utf-8"))
    except (OSError, ValueError):
        return None
    if not isinstance(manifest, dict):
        return None
    pairs = manifest.get("pairs")
    if not isinstance(pairs, list):
        return None
    if any(
        not isinstance(pair, dict)
        or not isinstance(pair.get("agent"), str)
        or not isinstance(pair.get("provider"), str)
        for pair in pairs
    ):
        return None
    protocols = manifest.get("protocols")
    if not isinstance(protocols, list):
        return None
    if any(
        not isinstance(row, dict)
        or not isinstance(row.get("protocol"), str)
        or not _is_string_list(row.get("candidates"))
        or not _is_string_list(row.get("authenticated"))
        for row in protocols
    ):
        return None
    return manifest


def _is_string_list(value: object) -> bool:
    """Return whether *value* is a list that contains only strings."""
    return isinstance(value, list) and all(isinstance(item, str) for item in value)


def _regenerate_manifest() -> str:
    """Create the readiness manifest again and return diagnostic text."""
    try:
        result = subprocess.run(  # nosec B603, B607 - fixed L1 command, no shell
            ["terok-agents"],
            check=False,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            text=True,
        )
    except OSError as exc:
        return f"Could not run terok-agents: {exc}"
    stderr = result.stderr.strip()
    if result.returncode:
        status = f"terok-agents exited with status {result.returncode}."
        return f"{status} {stderr}" if stderr else status
    return stderr


def _locked_providers(manifest: dict) -> list[tuple[str, list[str]]]:
    """Return ``(protocol, providers)`` rows for the not-yet-authenticated candidates.

    Derived from the manifest's ``protocols`` view: per wire protocol in play,
    the candidate providers minus the authenticated ones.  Protocols whose
    candidates are all authenticated drop out — there is nothing to unlock.
    """
    rows: list[tuple[str, list[str]]] = []
    for row in manifest.get("protocols", []):
        if not isinstance(row, dict):
            continue
        authed = set(row.get("authenticated") or [])
        candidates = [p for p in row.get("candidates") or [] if p not in authed]
        if candidates:
            rows.append((str(row.get("protocol")), candidates))
    return rows


def _print_locked(locked: list[tuple[str, list[str]]]) -> None:
    """Print the locked-providers section: what to authenticate on the host.

    One row per wire protocol (matching the ``hilfe`` providers section, so the
    protocol column links a dimmed agent to the providers that would enable
    it), headed by the unlock command.  Prints nothing when nothing is locked.
    """
    if not locked:
        return
    print(
        f"\n{_BOLD}Locked providers{_RESET}"
        f"{_DIM} - unlock on the host:{_RESET} terok auth {_MAGENTA}<provider>{_RESET}"
    )
    width = max(len(protocol) for protocol, _ in locked)
    for protocol, providers in locked:
        print(f"  {_DIM}{protocol:<{width}}  {', '.join(providers)}{_RESET}")


def _print_locked_hint(locked: list[tuple[str, list[str]]]) -> None:
    """Print the default view's one-line pointer at the locked providers."""
    if not locked:
        return
    count = len({provider for _, providers in locked for provider in providers})
    noun = "provider" if count == 1 else "providers"
    print(
        f"{_DIM}{count} locked {noun} - unlock on the host:{_RESET} "
        f"terok auth {_MAGENTA}<provider>{_RESET}"
    )


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))
