import asyncio
import json
import logging
import os
import shutil
import subprocess
from collections.abc import Awaitable, Callable
from contextlib import suppress
from pathlib import Path
from typing import cast

import websockets
from pydantic_ai import Agent

from app.agent.deps import Deps
from app.channels.whatsapp.handlers import (
    _process_message,
    _user_id_for_sender,
    create_message_handler,
)
from app.context import Context
from app.context.state import State
from app.settings import Contacts

logger = logging.getLogger(__name__)

SendJson = Callable[[dict[str, object]], Awaitable[None]]


def _resolve_executable(names: list[str]) -> str | None:
    for name in names:
        resolved = shutil.which(name)
        if resolved:
            return resolved
    return None


def _resolve_npm_executable() -> str | None:
    candidates = ["npm"]
    if os.name == "nt":
        candidates = ["npm.cmd", "npm.exe", "npm"]
    return _resolve_executable(candidates)


def _resolve_node_executable() -> str | None:
    candidates = ["node"]
    if os.name == "nt":
        candidates = ["node.exe", "node"]
    return _resolve_executable(candidates)


def _ensure_bridge_dependencies(bridge_dir: Path) -> None:
    node_modules_dir = bridge_dir / "node_modules"
    if node_modules_dir.exists():
        return

    npm_executable = _resolve_npm_executable()
    if npm_executable is None:
        raise RuntimeError(
            "npm não encontrado no PATH. "
            + "Instale o Node.js (incluindo npm) ou adicione "
            + "o executável do npm ao PATH do sistema."
        )

    logger.info("Instalando dependências do bridge WhatsApp...")
    try:
        subprocess.run([npm_executable, "install"], cwd=str(bridge_dir), check=True)
    except subprocess.CalledProcessError as error:
        raise RuntimeError(
            f"Falha ao instalar dependências do bridge em {bridge_dir}. "
            + f"Saída de erro: {error}"
        ) from error


def _reset_bridge_session(bridge_dir: Path) -> None:
    session_dir = bridge_dir / "session"
    if not session_dir.exists():
        logger.info("Sessão do WhatsApp não encontrada; novo login já será solicitado")
        return

    shutil.rmtree(session_dir)
    logger.info("Sessão do WhatsApp removida; escaneie o QR para novo login")


def _start_bridge_process(bridge_dir: Path, port: int) -> subprocess.Popen[bytes]:
    node_executable = _resolve_node_executable()
    if node_executable is None:
        raise RuntimeError(
            "node não encontrado no PATH. "
            + "Instale o Node.js ou adicione o executável do node ao PATH do sistema."
        )

    env = os.environ.copy()
    env.setdefault("WHATSAPP_BRIDGE_PORT", str(port))

    process: subprocess.Popen[bytes] = subprocess.Popen(
        [node_executable, "index.js"],
        cwd=str(bridge_dir),
        env=env,
    )
    return process


async def run_whatsapp_bot(
    ctx: Context, agent_state: State[Agent[Deps, str]], relogin: bool = False
) -> None:
    try:
        await _run(ctx, agent_state, relogin)
    except KeyboardInterrupt:
        logger.info("Encerrando canal WhatsApp por interrupção do usuário")


async def _run(ctx: Context, agent_state: State[Agent[Deps, str]], relogin: bool) -> None:
    bridge_dir = Path(__file__).parent / "bridge"
    port = int(os.environ.get("WHATSAPP_BRIDGE_PORT", "3001"))
    bridge_url = f"ws://127.0.0.1:{port}"

    _ensure_bridge_dependencies(bridge_dir)
    if relogin:
        _reset_bridge_session(bridge_dir)
    process = _start_bridge_process(bridge_dir, port)

    message_handler = create_message_handler(ctx=ctx, agent_state=agent_state)
    current_send_json: SendJson | None = None

    async def _notify_whatsapp_authorized(jid: str) -> None:
        if current_send_json is None:
            logger.warning("Sem conexão ativa para processar fila de %s", jid)
            return

        contacts = ctx.contacts.get()
        entry = contacts.whatsapp.get(jid)
        if entry is None or not entry.pending_messages:
            return

        pending = entry.pending_messages
        ctx.contacts.set(
            contacts.model_copy(
                update={
                    "whatsapp": {
                        **contacts.whatsapp,
                        jid: entry.model_copy(update={"pending_messages": []}),
                    }
                }
            )
        )

        numbered = "\n".join(f"{i + 1}. {msg}" for i, msg in enumerate(pending))
        user_input = (
            f"[Sistema: este usuário acabou de ser autorizado."
            f" Antes da autorização, enviou as seguintes mensagens:\n{numbered}]"
        )

        user_id = _user_id_for_sender(jid)
        await _process_message(
            ctx=ctx,
            agent=agent_state.get(),
            user_input=user_input,
            user_id=user_id,
            sender=jid,
            send_json=current_send_json,
        )

    known_wa_authorized: set[str] = {
        c.jid for c in ctx.contacts.get().whatsapp_contacts if c.authorized
    }

    def _on_contacts_change(new_contacts: Contacts) -> None:
        nonlocal known_wa_authorized
        new_authorized = {c.jid for c in new_contacts.whatsapp_contacts if c.authorized}
        newly = new_authorized - known_wa_authorized
        known_wa_authorized = new_authorized
        for jid in newly:
            asyncio.create_task(_notify_whatsapp_authorized(jid))

    ctx.contacts.subscribe(_on_contacts_change)

    tasks: set[asyncio.Task[None]] = set()

    try:
        while True:
            if process.poll() is not None:
                raise RuntimeError("Bridge WhatsApp finalizado inesperadamente")

            try:
                async with websockets.connect(bridge_url) as websocket:
                    logger.info("Conectado ao bridge WhatsApp em %s", bridge_url)
                    send_lock = asyncio.Lock()

                    async def send_json(payload: dict[str, object]) -> None:
                        async with send_lock:
                            await websocket.send(json.dumps(payload, ensure_ascii=False))

                    current_send_json = send_json

                    async for raw_message in websocket:
                        raw_text = (
                            raw_message.decode("utf-8")
                            if isinstance(raw_message, bytes)
                            else str(raw_message)
                        )
                        try:
                            raw_payload = cast(object, json.loads(raw_text))
                        except json.JSONDecodeError:
                            logger.warning("Mensagem JSON inválida recebida do bridge")
                            continue

                        if not isinstance(raw_payload, dict):
                            continue

                        payload_dict = cast(dict[object, object], raw_payload)
                        payload_obj: dict[str, object] = {
                            str(key): value for key, value in payload_dict.items()
                        }

                        task = asyncio.create_task(message_handler(payload_obj, send_json))
                        tasks.add(task)
                        task.add_done_callback(tasks.discard)
            except OSError:
                current_send_json = None
                logger.info("Bridge ainda não disponível, tentando novamente...")
                await asyncio.sleep(1)
            except websockets.ConnectionClosed:
                current_send_json = None
                logger.warning("Conexão com bridge encerrada. Reconectando...")
                await asyncio.sleep(1)
    finally:
        for task in list(tasks):
            task.cancel()
        for task in list(tasks):
            with suppress(asyncio.CancelledError):
                await task

        if process.poll() is None:
            process.terminate()
            try:
                process.wait(timeout=10)
            except subprocess.TimeoutExpired:
                process.kill()
