"""Telegram Bot API client for the orc agent communication channel.

Agents use this module to read and write messages instead of ``.orc/chat.md``.
The bot token and target chat ID are loaded from a ``.env`` file
(auto-discovered from the current working directory upward).

Required environment variables (optional — Telegram is disabled when absent)::

    COLONY_TELEGRAM_TOKEN   – Bot token from @BotFather
    COLONY_TELEGRAM_CHAT_ID – Target chat/group/channel ID (numeric or @handle)

When Telegram is not configured:

* ``_send_message`` writes only to the local log (no HTTP call).
* ``_get_messages`` returns only the local log (no incoming updates fetched).
* Hard-blocked agents cannot be resolved via human reply; the dispatcher will
  log a warning and exit rather than waiting indefinitely.

Message format::

    [agent_name](exit_state) YYYY-MM-DDTHH:MM:SSZ: <message text>

Example::

    [planner](ready) 2026-03-01T10:00:00Z: Created plan 0002-add-resource-system.md.

Local log
---------
Because the Telegram Bot API's ``getUpdates`` only returns *incoming* messages
(not messages the bot itself sends), outbound ``send_message`` calls are also
appended to a local JSONL log at ``{LOG_DIR}/chat.log`` (i.e. inside the
project's log directory, configured via ``orc-log-dir`` in ``config.yaml``,
defaulting to ``.orc/logs/``).  ``_get_messages`` merges both sources
so the state machine always sees the full history.
"""

from __future__ import annotations

import json
import os
from datetime import UTC, datetime
from pathlib import Path

import certifi
import httpx
import structlog
from dotenv import load_dotenv

# Pure message parsing helpers — no side effects, no global state.
# Re-exported here so callers can use ``from orc import telegram as tg``
# and call ``tg.parse_agent_id`` / ``tg.KNOWN_ROLES`` etc. as before.
from orc.messaging.messages import (  # noqa: F401
    _AGENT_ID_RE,
    _MSG_RE,
    INFORMATIONAL_STATES,
    KNOWN_ROLES,
    ChatMessage,
    format_agent_message,
    is_agent_message,
    make_agent_id,
    messages_to_text,
    parse_agent_id,
)

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

load_dotenv()  # auto-discovers .env from CWD upward

logger = structlog.get_logger(__name__)

_TOKEN = os.environ.get("COLONY_TELEGRAM_TOKEN")
_CHAT_ID = os.environ.get("COLONY_TELEGRAM_CHAT_ID")
_API_BASE = f"https://api.telegram.org/bot{_TOKEN}"

# Sentinel — resolved lazily on first use so that config.init() has been
# called before we compute the path.  Tests may patch this directly.
_LOG_FILE: Path | None = None


def _get_log_file() -> Path:
    """Return the chat log path, creating the file if it does not yet exist."""
    global _LOG_FILE
    if _LOG_FILE is None:
        from orc import config  # import here to avoid circular-import at module level

        _LOG_FILE = config.get().log_dir / "chat.log"
    _LOG_FILE.touch(exist_ok=True)
    return _LOG_FILE


# Use certifi's bundled CA certs so we don't depend on system cert paths
# (which may be absent inside containers).
_CA_BUNDLE = certifi.where()


def _is_configured() -> bool:
    """Return True if both Telegram env vars are present."""
    return bool(_TOKEN and _CHAT_ID)


def _require_config() -> None:
    """Raise a clear error if the bot token or chat ID is not configured."""
    if not _TOKEN:
        raise OSError(
            "COLONY_TELEGRAM_TOKEN is not set. "
            "Copy .env.example to .env and fill in your bot token."
        )
    if not _CHAT_ID:
        raise OSError(
            "COLONY_TELEGRAM_CHAT_ID is not set. "
            "Copy .env.example to .env and fill in your chat ID."
        )


def _append_to_log(text: str) -> None:
    """Append a message to the local chat.log as a JSONL entry."""
    entry = {
        "text": text,
        "date": int(datetime.now(UTC).timestamp()),
        "from": {"username": "bot", "first_name": "bot"},
    }
    with _get_log_file().open("a") as fh:
        fh.write(json.dumps(entry) + "\n")


def _parse_chat_message(raw: dict[str, object]) -> ChatMessage:
    """Convert a raw dict (from local log or Telegram API) to a :class:`ChatMessage`."""
    sender = raw.get("from") or {}
    if not isinstance(sender, dict):
        sender = {}
    name: str = (
        str(sender.get("username") or "") or str(sender.get("first_name") or "") or "unknown"
    )
    return ChatMessage(
        text=str(raw.get("text") or ""),
        date=int(raw.get("date") or 0),  # type: ignore[call-overload]
        sender_name=name,
    )


def _read_log() -> list[ChatMessage]:
    """Return all entries from the local chat.log."""
    log_file = _get_log_file()
    msgs: list[ChatMessage] = []
    for line in log_file.read_text().splitlines():
        line = line.strip()
        if not line:
            continue
        try:
            raw: dict[str, object] = json.loads(line)
            msgs.append(_parse_chat_message(raw))
        except json.JSONDecodeError:
            logger.debug("telegram: skipping malformed JSONL line", line=line, exc_info=True)
    return msgs


def _get_telegram_updates(limit: int = 100) -> list[ChatMessage]:
    """Fetch incoming messages via getUpdates (human/external messages only)."""
    _require_config()
    with httpx.Client(timeout=15, verify=_CA_BUNDLE) as client:
        resp = client.get(
            f"{_API_BASE}/getUpdates",
            params={"limit": limit, "allowed_updates": ["message"]},
        )
        resp.raise_for_status()
        data: dict[str, object] = resp.json()
        result = data.get("result") or []
        if not isinstance(result, list):
            return []
        messages: list[ChatMessage] = []
        for update in result:
            if isinstance(update, dict):
                msg = update.get("message")
                if isinstance(msg, dict):
                    messages.append(_parse_chat_message(msg))
        return messages


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def _send_message(text: str) -> None:
    """Post *text* to the Telegram chat and record it in the local log.

    The local log write always happens so the local state machine has the
    message even when Telegram is not configured or the API call fails.
    Raises ``httpx.HTTPStatusError`` on Telegram API errors.
    """
    _append_to_log(text)
    if not _is_configured():
        return
    with httpx.Client(timeout=15, verify=_CA_BUNDLE) as client:
        resp = client.post(
            f"{_API_BASE}/sendMessage",
            json={"chat_id": _CHAT_ID, "text": text},
        )
        resp.raise_for_status()


def _get_messages(limit: int = 100) -> list[ChatMessage]:
    """Return the merged message history from the local log and Telegram updates.

    The local log contains all bot-sent messages (agent exit states).
    Telegram updates contain incoming messages from humans or other bots.
    The merged list is sorted by timestamp so the state machine always sees
    events in the correct order.
    """
    local = _read_log()
    if not _is_configured():
        return local
    try:
        remote = _get_telegram_updates(limit)
    except (OSError, Exception):
        logger.debug("get_messages: failed to fetch Telegram updates", exc_info=True)
        remote = []

    seen_texts = {m.text for m in local}
    merged: list[ChatMessage] = list(local)
    for msg in remote:
        if msg.text not in seen_texts:
            merged.append(msg)

    merged.sort(key=lambda m: m.date)
    return merged


class TelegramMessagingService:
    """Sole public API for Telegram messaging.

    Encapsulates all I/O with Telegram and the local chat log.
    Has no inputs other than the env vars (for the Telegram link) and the
    orc config (for the chat.log path).
    """

    def is_configured(self) -> bool:
        """Return True if both Telegram env vars are present."""
        return _is_configured()

    def send_message(self, text: str) -> None:
        """Post *text* to the Telegram chat and record it in the local log."""
        _send_message(text)

    def get_messages(self, limit: int = 100) -> list[ChatMessage]:
        """Return the merged message history from the local log and Telegram updates."""
        return _get_messages(limit)

    def post_boot_message(self, agent_id: str, body: str) -> None:
        """Format and send a boot message to Telegram."""
        _send_message(format_agent_message(agent_id, "boot", body))
