"""
WhatsApp Channel Adapter — WhatsApp via Baileys (no Chromium).

Uses a Node.js subprocess running @whiskeysockets/baileys to bridge
WhatsApp messages to NeuralClaw.  Unlike the legacy whatsapp-web.js
adapter this does NOT require Puppeteer or Chromium, making it
suitable for lean Docker containers and CI environments.

Reference implementation: QRTrackingWhatsAppAdapter from neural-runtime-template.
"""

from __future__ import annotations

import asyncio
import io
import json
import logging
from pathlib import Path
from typing import Any, Callable, Coroutine

from neuralclaw.channels.protocol import ChannelAdapter, ChannelMessage

logger = logging.getLogger(__name__)


def render_qr_terminal(data: str, console: Any) -> None:
    """Render a QR code as ASCII art inside a Rich panel.

    *data* is the raw QR payload string emitted by the Baileys bridge.
    *console* is a ``rich.console.Console`` instance.
    """
    try:
        import qrcode  # type: ignore[import-untyped]
    except ImportError:
        console.print(f"[yellow]qrcode package not installed — raw QR data:[/yellow]\n{data}")
        return

    qr = qrcode.QRCode(border=2)
    qr.add_data(data)
    qr.make(fit=True)

    # Render to string using print_ascii
    buf = io.StringIO()
    qr.print_ascii(out=buf, invert=True)
    ascii_art = buf.getvalue()

    from rich.panel import Panel
    console.print(Panel(
        ascii_art,
        title="Scan with WhatsApp",
        subtitle="WhatsApp → Linked Devices → Link a Device",
        style="bold green",
        expand=False,
    ))


class BaileysWhatsAppAdapter(ChannelAdapter):
    """
    WhatsApp adapter using @whiskeysockets/baileys.

    Requires Node.js (>=18) and the baileys npm package::

        npm install @whiskeysockets/baileys

    Communication happens over stdin/stdout JSON lines with the
    embedded bridge script.  QR codes are emitted as ``qr`` events
    so the runtime can forward them to a dashboard or log.
    """

    name = "whatsapp-baileys"

    def __init__(
        self,
        auth_dir: str = ".baileys_auth",
        *,
        on_qr: Callable[[str], Any] | None = None,
    ) -> None:
        super().__init__()
        self._auth_dir = auth_dir
        self._on_qr = on_qr
        self._process: asyncio.subprocess.Process | None = None
        self._reader_task: asyncio.Task[None] | None = None
        self._connected = asyncio.Event()

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    async def start(self) -> None:
        """Start the Baileys bridge subprocess."""
        script = self._get_bridge_script()

        self._process = await asyncio.create_subprocess_exec(
            "node", "-e", script,
            stdin=asyncio.subprocess.PIPE,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
        )

        self._reader_task = asyncio.create_task(self._read_loop())
        logger.info("[WhatsApp-Baileys] Bridge started — waiting for QR or auth")

    async def stop(self) -> None:
        """Gracefully stop the Baileys bridge."""
        if self._process:
            self._process.terminate()
            try:
                await asyncio.wait_for(self._process.wait(), timeout=5)
            except asyncio.TimeoutError:
                self._process.kill()
        if self._reader_task:
            self._reader_task.cancel()
            try:
                await self._reader_task
            except asyncio.CancelledError:
                pass
        logger.info("[WhatsApp-Baileys] Bridge stopped")

    # ------------------------------------------------------------------
    # Sending
    # ------------------------------------------------------------------

    async def send(self, channel_id: str, content: str, **kwargs: Any) -> None:
        """Send a text message to a WhatsApp JID."""
        if not self._process or not self._process.stdin:
            raise RuntimeError("Baileys bridge is not running")

        payload = json.dumps({
            "type": "send",
            "to": channel_id,
            "content": content,
        })
        self._process.stdin.write(f"{payload}\n".encode())
        await self._process.stdin.drain()

    # ------------------------------------------------------------------
    # Reading
    # ------------------------------------------------------------------

    async def _read_loop(self) -> None:
        """Read JSON-line events from the bridge stdout."""
        assert self._process and self._process.stdout

        while True:
            try:
                line = await self._process.stdout.readline()
                if not line:
                    break

                data = json.loads(line.decode().strip())
                event_type = data.get("type")

                if event_type == "message":
                    msg = ChannelMessage(
                        content=data.get("content", ""),
                        author_id=data.get("from", "unknown"),
                        author_name=data.get("name", "Unknown"),
                        channel_id=data.get("chat_id", ""),
                        metadata={"platform": "whatsapp"},
                    )
                    await self._dispatch(msg)

                elif event_type == "qr":
                    qr_data = data.get("data", "")
                    logger.info("[WhatsApp-Baileys] QR code received")
                    if self._on_qr:
                        result = self._on_qr(qr_data)
                        if asyncio.iscoroutine(result):
                            await result

                elif event_type == "connected":
                    self._connected.set()
                    logger.info("[WhatsApp-Baileys] Connected")

                elif event_type == "disconnected":
                    self._connected.clear()
                    logger.warning("[WhatsApp-Baileys] Disconnected: %s", data.get("reason"))

            except json.JSONDecodeError:
                continue
            except asyncio.CancelledError:
                break
            except Exception as exc:
                logger.error("[WhatsApp-Baileys] Read error: %s", exc)
                await asyncio.sleep(1)

    # ------------------------------------------------------------------
    # Connectivity check
    # ------------------------------------------------------------------

    async def test_connection(self) -> tuple[bool, str]:
        """Check if WhatsApp auth files exist from a previous pairing."""
        auth_path = Path(self._auth_dir)
        if not auth_path.exists():
            return False, "auth directory does not exist — run neuralclaw channels connect whatsapp"
        creds = auth_path / "creds.json"
        if creds.exists():
            return True, f"auth files found in {self._auth_dir}"
        return False, "not paired — run neuralclaw channels connect whatsapp"

    # ------------------------------------------------------------------
    # Bridge script
    # ------------------------------------------------------------------

    def _get_bridge_script(self) -> str:
        """Return the embedded Node.js Baileys bridge."""
        return """
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = require('@whiskeysockets/baileys');
const { Boom } = require('@hapi/boom');
const fs = require('fs');

const AUTH_DIR = '__AUTH_DIR__';

async function start() {
    const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);

    const sock = makeWASocket({
        auth: state,
        printQRInTerminal: false,
    });

    sock.ev.on('creds.update', saveCreds);

    sock.ev.on('connection.update', (update) => {
        const { connection, lastDisconnect, qr } = update;
        if (qr) {
            console.log(JSON.stringify({ type: 'qr', data: qr }));
        }
        if (connection === 'open') {
            console.log(JSON.stringify({ type: 'connected' }));
        }
        if (connection === 'close') {
            const reason = new Boom(lastDisconnect?.error)?.output?.statusCode;
            console.log(JSON.stringify({ type: 'disconnected', reason: String(reason) }));
            if (reason !== DisconnectReason.loggedOut) {
                start();
            }
        }
    });

    sock.ev.on('messages.upsert', async ({ messages }) => {
        for (const msg of messages) {
            if (!msg.message || msg.key.fromMe) continue;
            const text = msg.message.conversation
                || msg.message.extendedTextMessage?.text
                || '';
            if (!text) continue;
            const jid = msg.key.remoteJid || '';
            let name = msg.pushName || 'Unknown';
            console.log(JSON.stringify({
                type: 'message',
                content: text,
                from: jid,
                name: name,
                chat_id: jid,
                timestamp: msg.messageTimestamp,
            }));
        }
    });

    process.stdin.on('data', async (data) => {
        try {
            const cmd = JSON.parse(data.toString().trim());
            if (cmd.type === 'send') {
                await sock.sendMessage(cmd.to, { text: cmd.content });
            }
        } catch (e) {
            console.error('stdin parse error:', e.message);
        }
    });
}

start().catch(err => {
    console.error('Fatal:', err);
    process.exit(1);
});
""".replace("__AUTH_DIR__", self._auth_dir.replace("\\", "\\\\").replace("'", "\\'"))
