"""Telegram Bot long-polling listener — runs inside the daemon."""

import asyncio
import os
import re
import tempfile
from pathlib import Path

import httpx
import structlog

from fliiq.runtime.agent.config import AgentConfig
from fliiq.runtime.agent.loop import agent_loop
from fliiq.runtime.agent.prompt import assemble_agent_prompt
from fliiq.runtime.agent.setup import setup_agent_resources
from fliiq.runtime.llm.providers import LLMConfig, create_llm, resolve_model_alias

log = structlog.get_logger()

TELEGRAM_API_BASE = "https://api.telegram.org"
OPENAI_TTS_URL = "https://api.openai.com/v1/audio/speech"
POLL_TIMEOUT = 30  # seconds — Telegram holds connection open
MAX_MESSAGE_LEN = 4096  # Telegram's per-message limit
DEFAULT_FLIIQ_VOICE = "onyx"  # deep English male
BACKOFF_BASE = 2
BACKOFF_MAX = 60


_RE_MODEL_RESET = re.compile(r"^/model\s+(reset|default)\b", re.IGNORECASE)
_RE_MODEL_SLASH = re.compile(r"^/model\s+(\S+)\s*(.*)", re.IGNORECASE | re.DOTALL)
_RE_MODEL_NATURAL = re.compile(
    r"^use\s+(?:the\s+model\s+)?(\S+)\s+(.+)", re.IGNORECASE | re.DOTALL,
)

_MODEL_RESET_SENTINEL = "__reset__"


def _parse_model_directive(text: str) -> tuple[str | None, str]:
    """Parse model override directive from message text.

    Returns (model_name_or_reset_sentinel, remaining_text).
    If no directive found, returns (None, original_text).
    """
    m = _RE_MODEL_RESET.match(text)
    if m:
        return _MODEL_RESET_SENTINEL, ""

    m = _RE_MODEL_SLASH.match(text)
    if m:
        return m.group(1), m.group(2).strip()

    m = _RE_MODEL_NATURAL.match(text)
    if m:
        return m.group(1), m.group(2).strip()

    return None, text


def _agent_sent_telegram(messages: list[dict], chat_id: int) -> bool:
    """Check if agent already sent a message to this chat via send_telegram tool."""
    for msg in messages:
        if msg.get("role") != "assistant":
            continue
        content = msg.get("content")
        if not isinstance(content, list):
            continue
        for block in content:
            if (
                block.get("type") == "tool_use"
                and block.get("name") == "send_telegram"
                and block.get("input", {}).get("chat_id") == chat_id
            ):
                return True
    return False


class TelegramListener:
    """Long-polls Telegram getUpdates and routes messages through agent_loop."""

    def __init__(
        self,
        bot_token: str,
        llm_config: LLMConfig,
        project_root: Path,
        allowed_chat_ids: set[int] | None = None,
    ):
        self._token = bot_token
        self._llm_config = llm_config
        self._project_root = project_root
        self._allowed_chat_ids = allowed_chat_ids
        self._offset = 0
        self._running = False
        self._sessions: dict[tuple[int, int | None], dict] = {}  # (chat_id, thread_id) → session

    @property
    def api_base(self) -> str:
        return f"{TELEGRAM_API_BASE}/bot{self._token}"

    async def send_message(
        self, chat_id: int, text: str, message_thread_id: int | None = None,
    ) -> None:
        """Send a text message. Splits if >4096 chars."""
        chunks = _split_message(text)
        payload_base: dict = {"chat_id": chat_id, "parse_mode": "Markdown"}
        if message_thread_id is not None:
            payload_base["message_thread_id"] = message_thread_id
        async with httpx.AsyncClient(timeout=15) as client:
            for chunk in chunks:
                resp = await client.post(
                    f"{self.api_base}/sendMessage",
                    json={**payload_base, "text": chunk},
                )
                if resp.status_code != 200:
                    # Retry without Markdown if parse error
                    if resp.status_code == 400 and "parse" in resp.text.lower():
                        plain = {k: v for k, v in payload_base.items() if k != "parse_mode"}
                        await client.post(
                            f"{self.api_base}/sendMessage",
                            json={**plain, "text": chunk},
                        )
                    else:
                        log.warning(
                            "telegram_send_failed",
                            chat_id=chat_id,
                            status=resp.status_code,
                            error=resp.text[:200],
                        )

    async def _poll_updates(self, client: httpx.AsyncClient) -> list[dict]:
        """Long-poll getUpdates. Returns list of Update objects."""
        resp = await client.get(
            f"{self.api_base}/getUpdates",
            params={"offset": self._offset, "timeout": POLL_TIMEOUT},
            timeout=POLL_TIMEOUT + 10,  # httpx timeout > Telegram timeout
        )
        if resp.status_code != 200:
            log.warning("telegram_poll_error", status=resp.status_code)
            return []

        data = resp.json()
        updates = data.get("result", [])
        if updates:
            self._offset = updates[-1]["update_id"] + 1
        return updates

    async def _get_or_create_session(
        self, chat_id: int, message_thread_id: int | None = None,
    ) -> dict:
        """Get or create a per-chat/topic session with agent resources."""
        key = (chat_id, message_thread_id)
        if key not in self._sessions:
            def _noop_ask_user(question: str) -> str:
                return "Not available — this is a Telegram bot session."

            def _noop_ask_user_choice(question: str, options: list[dict], recommended: int) -> str:
                if options and recommended and 1 <= recommended <= len(options):
                    picked = options[recommended - 1]
                    return f"Auto-selected: {picked['label']}"
                return "Auto-selected first option"

            resources = await setup_agent_resources(
                mode="autonomous",
                llm_config=self._llm_config,
                project_root=self._project_root,
                ask_user_handler=_noop_ask_user,
                ask_user_choice_handler=_noop_ask_user_choice,
            )

            self._sessions[key] = {
                "messages": [],
                "resources": resources,
                "model_override": None,
                "model_override_display": None,
            }

        return self._sessions[key]

    async def _download_voice(self, file_id: str) -> Path:
        """Download a Telegram voice message to a temp OGG file."""
        async with httpx.AsyncClient(timeout=30) as client:
            resp = await client.get(
                f"{self.api_base}/getFile",
                params={"file_id": file_id},
            )
            if resp.status_code != 200:
                raise ValueError(f"Telegram getFile failed ({resp.status_code}): {resp.text[:200]}")

            file_path = resp.json().get("result", {}).get("file_path")
            if not file_path:
                raise ValueError("Telegram getFile returned no file_path.")

            download_url = f"https://api.telegram.org/file/bot{self._token}/{file_path}"
            file_resp = await client.get(download_url)
            if file_resp.status_code != 200:
                raise ValueError(f"Telegram file download failed ({file_resp.status_code})")

        tmp = tempfile.NamedTemporaryFile(suffix=".ogg", delete=False)
        tmp.write(file_resp.content)
        tmp.close()
        return Path(tmp.name)

    async def _transcribe_voice(self, ogg_path: Path) -> str:
        """Transcribe an OGG voice file to text using the speech_to_text skill."""
        from fliiq.data.skills.core.speech_to_text.main import handler as stt_handler

        result = await stt_handler({"audio_file": str(ogg_path)})
        return result["text"]

    async def _synthesize_voice(self, text: str) -> Path:
        """Generate speech from text using OpenAI TTS API. Returns path to opus file."""
        api_key = os.environ.get("OPENAI_API_KEY")
        if not api_key:
            raise ValueError("OPENAI_API_KEY not set — cannot generate voice reply.")

        voice = os.environ.get("FLIIQ_VOICE_ID", DEFAULT_FLIIQ_VOICE)
        headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
        payload = {
            "model": "tts-1",
            "input": text,
            "voice": voice,
            "response_format": "opus",
        }

        async with httpx.AsyncClient(timeout=60) as client:
            resp = await client.post(OPENAI_TTS_URL, headers=headers, json=payload)

        if resp.status_code != 200:
            raise ValueError(f"OpenAI TTS error ({resp.status_code}): {resp.text[:300]}")

        tmp = tempfile.NamedTemporaryFile(suffix=".ogg", delete=False)
        tmp.write(resp.content)
        tmp.close()
        return Path(tmp.name)

    async def _send_voice_reply(
        self, chat_id: int, text: str, message_thread_id: int | None = None,
    ) -> None:
        """Generate TTS and send as a Telegram voice bubble. Best-effort — text reply already sent."""
        try:
            audio_path = await self._synthesize_voice(text)
            try:
                data: dict = {"chat_id": chat_id}
                if message_thread_id is not None:
                    data["message_thread_id"] = message_thread_id
                async with httpx.AsyncClient(timeout=30) as client:
                    with open(audio_path, "rb") as f:
                        resp = await client.post(
                            f"{self.api_base}/sendVoice",
                            data=data,
                            files={"voice": (audio_path.name, f, "audio/ogg")},
                        )
                    if resp.status_code != 200:
                        log.warning("telegram_voice_reply_send_failed", status=resp.status_code)
            finally:
                try:
                    audio_path.unlink()
                except OSError:
                    pass
        except Exception as e:
            log.warning("telegram_voice_reply_error", error=str(e))

    async def _send_chat_action(
        self, chat_id: int, action: str = "typing", message_thread_id: int | None = None,
    ) -> None:
        """Send a chat action (e.g. 'typing...' indicator). Best-effort."""
        try:
            payload: dict = {"chat_id": chat_id, "action": action}
            if message_thread_id is not None:
                payload["message_thread_id"] = message_thread_id
            async with httpx.AsyncClient(timeout=5) as client:
                await client.post(
                    f"{self.api_base}/sendChatAction",
                    json=payload,
                )
        except Exception:
            pass  # Non-critical — don't let indicator failure block processing

    async def _handle_message(
        self, chat_id: int, text: str, is_voice: bool = False,
        message_thread_id: int | None = None,
    ) -> None:
        """Process a single inbound message through agent_loop and reply."""
        session = await self._get_or_create_session(chat_id, message_thread_id)
        resources = session["resources"]
        messages = session["messages"]

        # Parse model override directive before tagging
        model_name, remaining = _parse_model_directive(text)

        if model_name == _MODEL_RESET_SENTINEL:
            session["model_override"] = None
            session["model_override_display"] = None
            await self.send_message(chat_id, "Model reset to default.", message_thread_id)
            return

        if model_name is not None:
            resolved = resolve_model_alias(model_name)
            session["model_override"] = resolved
            session["model_override_display"] = model_name
            if not remaining:
                await self.send_message(
                    chat_id,
                    f"Model set to {resolved}. Send your prompt next.",
                    message_thread_id,
                )
                return
            text = remaining

        voice_attr = ' type="voice"' if is_voice else ""
        thread_attr = f' message_thread_id="{message_thread_id}"' if message_thread_id else ""
        tagged = (
            f'<external_message source="telegram" chat_id="{chat_id}"{thread_attr}{voice_attr}>'
            f"\n{text}\n</external_message>"
        )
        messages.append({"role": "user", "content": tagged})

        # Enrich memory with prompt-relevant entities
        from fliiq.runtime.agent.setup import enrich_memory_context

        enriched_memory = enrich_memory_context(resources.memory_context, text, self._project_root)

        system = assemble_agent_prompt(
            soul=resources.soul,
            user_soul=resources.user_soul,
            skills=resources.skill_info if resources.skill_info else None,
            mode="autonomous",
            memory_context=enriched_memory,
            user_profile=resources.user_profile,
            fliiq_email=resources.fliiq_email,
        )

        config = AgentConfig(mode="autonomous")

        # Show "typing..." indicator while agent processes
        await self._send_chat_action(chat_id, "typing", message_thread_id)

        # Resolve LLM — use override if active, otherwise default
        override = session.get("model_override")
        if override:
            override_config = self._llm_config.model_copy(update={"model": override})
            llm = create_llm(override_config)
        else:
            llm = resources.llm

        result_messages: list[dict] = []
        try:
            result = await agent_loop(
                llm=llm,
                messages=messages,
                tools=resources.tools,
                config=config,
                system=system,
            )
            session["messages"] = result.messages
            result_messages = result.messages
            response = result.final_text or "(No response)"

            # Post-conversation memory extraction (non-fatal)
            try:
                if getattr(result, "iterations", 0) > 0:
                    from fliiq.runtime.memory.extractor import extract_and_save_memories

                    await extract_and_save_memories(resources.llm, result.messages, self._project_root)
            except Exception:
                pass
        except Exception as e:
            log.error("telegram_agent_error", chat_id=chat_id, error=str(e))
            response = f"Error processing your message: {e}"

        # Skip auto-reply if agent already sent to this chat via send_telegram tool
        already_sent = _agent_sent_telegram(result_messages, chat_id)
        if not already_sent:
            if override:
                display = session.get("model_override_display") or override
                response = f"[{display}] {response}"

            await self.send_message(chat_id, response, message_thread_id)

            if is_voice:
                await self._send_voice_reply(chat_id, response, message_thread_id)

    async def run(self) -> None:
        """Main loop: long-poll → process → reply. Runs until stop() is called."""
        self._running = True
        backoff = 0
        log.info("telegram_listener_running")

        async with httpx.AsyncClient() as client:
            while self._running:
                try:
                    updates = await self._poll_updates(client)
                    backoff = 0  # Reset on success

                    for update in updates:
                        msg = update.get("message", {})
                        chat_id = msg.get("chat", {}).get("id")
                        text = msg.get("text")
                        voice = msg.get("voice")
                        message_thread_id = msg.get("message_thread_id")

                        # Voice message: download and transcribe
                        is_voice = False
                        if voice and chat_id:
                            file_id = voice.get("file_id")
                            if not file_id:
                                continue
                            ogg_path = None
                            try:
                                ogg_path = await self._download_voice(file_id)
                                text = await self._transcribe_voice(ogg_path)
                                is_voice = True
                            except Exception as e:
                                log.error("telegram_voice_transcribe_error", chat_id=chat_id, error=str(e))
                                await self.send_message(
                                    chat_id, f"Could not process voice message: {e}",
                                    message_thread_id,
                                )
                                continue
                            finally:
                                if ogg_path:
                                    try:
                                        ogg_path.unlink()
                                    except OSError:
                                        pass

                        if not chat_id or not text:
                            continue

                        if self._allowed_chat_ids and chat_id not in self._allowed_chat_ids:
                            log.debug("telegram_chat_blocked", chat_id=chat_id)
                            await self.send_message(
                                chat_id,
                                "This Fliiq instance is not configured to respond to this chat.",
                            )
                            continue

                        try:
                            await self._handle_message(
                                chat_id, text, is_voice=is_voice,
                                message_thread_id=message_thread_id,
                            )
                        except Exception as e:
                            log.error("telegram_message_handler_error", chat_id=chat_id, error=str(e))

                except httpx.TimeoutException:
                    continue  # Normal — long poll timeout, retry immediately
                except Exception as e:
                    backoff = min(BACKOFF_BASE ** (backoff + 1), BACKOFF_MAX)
                    log.warning("telegram_poll_exception", error=str(e), backoff=backoff)
                    await asyncio.sleep(backoff)

        log.info("telegram_listener_stopped")

    async def stop(self) -> None:
        """Signal the run loop to exit."""
        self._running = False


def _split_message(text: str) -> list[str]:
    """Split text into chunks of MAX_MESSAGE_LEN or fewer."""
    if len(text) <= MAX_MESSAGE_LEN:
        return [text]

    chunks = []
    while text:
        if len(text) <= MAX_MESSAGE_LEN:
            chunks.append(text)
            break
        # Try to split at newline
        split_at = text.rfind("\n", 0, MAX_MESSAGE_LEN)
        if split_at == -1:
            split_at = MAX_MESSAGE_LEN
        chunks.append(text[:split_at])
        text = text[split_at:].lstrip("\n")
    return chunks


def parse_allowed_chat_ids() -> set[int] | None:
    """Parse TELEGRAM_ALLOWED_CHAT_IDS from env. Returns None if not set."""
    raw = os.environ.get("TELEGRAM_ALLOWED_CHAT_IDS", "").strip()
    if not raw:
        return None
    try:
        return {int(cid.strip()) for cid in raw.split(",") if cid.strip()}
    except ValueError:
        log.warning("telegram_invalid_allowed_chat_ids", raw=raw)
        return None
