# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
WhatsApp Channel

Connect to WhatsApp via whatsapp-web.js bridge.

This works by running a Node.js process that connects to WhatsApp Web
and communicates with Python via a local WebSocket.

Setup:
1. Install Node.js
2. Run the WhatsApp bridge: node whatsapp_bridge.js
3. Scan QR code with your phone
4. Start this channel

The bridge script (whatsapp_bridge.js) must be running separately.
"""

import asyncio
import json
import logging
import os
from pathlib import Path
from threading import Thread

logger = logging.getLogger(__name__)

# Bridge connection settings
BRIDGE_HOST = os.environ.get("WHATSAPP_BRIDGE_HOST", "localhost")
BRIDGE_PORT = int(os.environ.get("WHATSAPP_BRIDGE_PORT", "3001"))


from .connect_wizard import ConnectMixin  # noqa: E402
from .formatting import FormatMode  # noqa: E402


class WhatsAppChannel(ConnectMixin):
    """
    WhatsApp integration via whatsapp-web.js bridge.

    Usage:
        channel = WhatsAppChannel(agent)
        await channel.connect()
    """

    # ConnectMixin adapter properties
    format_mode = FormatMode.WHATSAPP
    supports_buttons = False
    supports_message_deletion = False

    def __init__(self, agent, allowed_numbers: list = None):
        """
        Initialize WhatsApp channel.

        Args:
            agent: The Familiar instance
            allowed_numbers: List of phone numbers to respond to (with country code).
                           If None, responds to all.
        """
        self.agent = agent
        self.allowed_numbers = allowed_numbers
        self.websocket = None
        self._running = False

    # ── ConnectMixin adapter methods ────────────────────────────────

    async def wizard_send(self, recipient_id: str, text: str) -> None:
        await self.send_message(recipient_id, text)

    async def wizard_delete_message(self, recipient_id: str, message_ref) -> bool:
        return False  # WhatsApp cannot delete messages

    async def connect(self):
        """Connect to the WhatsApp bridge."""
        try:
            import websockets
        except ImportError:
            raise RuntimeError("websockets not available. Check server logs.")

        url = f"ws://{BRIDGE_HOST}:{BRIDGE_PORT}"
        logger.info(f"Connecting to WhatsApp bridge at {url}")

        self._running = True

        while self._running:
            try:
                async with websockets.connect(url) as ws:
                    self.websocket = ws
                    logger.info("Connected to WhatsApp bridge")
                    print("📱 WhatsApp channel active")

                    await self._message_loop(ws)

            except Exception as e:
                logger.error(f"WhatsApp bridge error: {e}")
                if self._running:
                    logger.info("Reconnecting in 5s...")
                    await asyncio.sleep(5)

    async def _message_loop(self, ws):
        """Handle incoming messages from bridge."""
        async for raw in ws:
            try:
                data = json.loads(raw)
                msg_type = data.get("type")

                if msg_type == "message":
                    await self._handle_message(data)

                elif msg_type == "qr":
                    # QR code for authentication
                    print("\n" + "=" * 40)
                    print("Scan this QR code with WhatsApp:")
                    print(data.get("qr", ""))
                    print("=" * 40 + "\n")

                elif msg_type == "ready":
                    logger.info("WhatsApp client ready")
                    print("✅ WhatsApp authenticated and ready")

                elif msg_type == "disconnected":
                    logger.warning("WhatsApp disconnected")

            except Exception as e:
                logger.error(f"Error handling message: {e}")

    async def _handle_message(self, data: dict):
        """Process an incoming WhatsApp message."""
        sender = data.get("from", "")
        text = data.get("body", "")
        chat_id = data.get("chatId", sender)
        is_group = data.get("isGroup", False)

        # Skip group messages unless mentioned
        if is_group and not data.get("mentionedMe", False):
            return

        # ── Owner PIN verification ──
        owner_id = os.environ.get("OWNER_WHATSAPP_ID")
        pin_hash = os.environ.get("OWNER_PIN_HASH")
        if not owner_id and pin_hash and text:
            try:
                from ..core.security import (
                    check_pin_rate_limit,
                    claim_ownership,
                    record_pin_attempt,
                    verify_owner_pin,
                )

                allowed, lockout_msg = check_pin_rate_limit(sender)
                if not allowed:
                    await self.send_message(chat_id, lockout_msg)
                    return
                if verify_owner_pin(text.strip()):
                    record_pin_attempt(sender, success=True)
                    claim_ownership(sender, "whatsapp", self.agent.sessions)
                    await self.send_message(chat_id, "🔐 Owner verified. Full access granted.")
                    return
                elif text.strip().isdigit() and 4 <= len(text.strip()) <= 8:
                    record_pin_attempt(sender, success=False)
                    await self.send_message(chat_id, "❌ Incorrect PIN.")
                    return
                else:
                    await self.send_message(chat_id, "🔒 Enter your owner PIN to claim this bot.")
                    return
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
        # Auto-promote owner
        if owner_id and sender == owner_id:
            try:
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(sender, "whatsapp")
                if session.trust_level != TrustLevel.OWNER:
                    session.set_trust_level(TrustLevel.OWNER)
                    session.daily_budget = 50.0
                    self.agent.sessions.save_session(session)
            except ImportError as e:
                logger.debug("Optional dependency not available: %s", e)
        # Check allowed list
        if self.allowed_numbers and sender not in self.allowed_numbers:
            logger.debug(f"Ignoring message from {sender}")
            return

        logger.info(f"WhatsApp from {sender}: {text[:50]}...")

        # ── Command dispatcher ────────────────────────────────────────────────
        if text.startswith("/"):
            parts = text[1:].split(maxsplit=1)
            cmd = parts[0].lower()
            args = parts[1] if len(parts) > 1 else ""
            if cmd == "connect":
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(sender, "whatsapp")
                if session.trust_level != TrustLevel.OWNER:
                    await self.send_message(chat_id, "🔒 /connect requires OWNER trust level.")
                    return
                connect_args = args.split() if args else []
                await self.handle_connect_command(chat_id, connect_args)
                return
            if cmd == "browse":
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(sender, "whatsapp")
                if session.trust_level != TrustLevel.OWNER:
                    await self.send_message(chat_id, "🔒 /browse requires OWNER trust level.")
                    return
                await self.handle_browse_command(chat_id)
                return
            if cmd == "search":
                from ..core.security import TrustLevel

                session = self.agent.sessions.get_or_create_session(sender, "whatsapp")
                if session.trust_level != TrustLevel.OWNER:
                    await self.send_message(chat_id, "🔒 /search requires OWNER trust level.")
                    return
                search_args = args.split() if args else []
                await self.handle_search_command(chat_id, search_args)
                return
            response = await self._handle_command(cmd, args, sender, chat_id)
            if response is not None:
                await self.send_message(chat_id, response)
                return

        # Intercept messages during wizard flows (ConnectMixin)
        if await self.handle_wizard_message(chat_id, text):
            return

        try:
            # Process through agent
            # Check if user is responding to a pending confirmation
            if hasattr(self, "_pending_confirm") and sender in self._pending_confirm:
                token = self._pending_confirm.pop(sender)
                session = self.agent.sessions.get_or_create_session(sender, "whatsapp")
                pending = session.pending_confirmations.get(token)
                answer = text.strip().lower()
                if pending and answer.startswith("y"):
                    session.pending_confirmations.pop(token, None)
                    tool_input = dict(pending["tool_input"])
                    tool_input["_confirmed"] = True
                    try:
                        result = self.agent.tools.execute(
                            pending["tool_name"],
                            tool_input,
                            context={
                                "session": session,
                                "session_manager": self.agent.sessions,
                                "agent": self.agent,
                                "user_id": sender,
                                "channel": "whatsapp",
                            },
                        )
                        from familiar.core.agent import _capture_context_from_result

                        _capture_context_from_result(
                            pending["tool_name"],
                            tool_input,
                            result,
                            session,
                            self.agent.sessions,
                        )
                        await self.send_message(chat_id, result or "✅ Done.")
                    except Exception as _ce:
                        await self.send_message(chat_id, f"⚠️ Action failed: {_ce}")
                elif pending and answer.startswith("n"):
                    session.pending_confirmations.pop(token, None)
                    self.agent.sessions.save_session(session)
                    await self.send_message(chat_id, "❌ Cancelled.")
                else:
                    # Unrecognised reply — treat as new message, restore token
                    self._pending_confirm[sender] = token
                    await self.send_message(chat_id, "Reply *Y* to confirm or *N* to cancel.")
                return

            response = self.agent.chat(text, user_id=sender, channel="whatsapp")

            # Phase 2/4: intercept confirmation sentinel
            from familiar.core.confirmations import SENTINEL_PREFIX

            if response and response.startswith(SENTINEL_PREFIX):
                token = response[len(SENTINEL_PREFIX) :]
                session = self.agent.sessions.get_or_create_session(sender, "whatsapp")
                pending = session.pending_confirmations.get(token)
                if pending:
                    if not hasattr(self, "_pending_confirm"):
                        self._pending_confirm = {}
                    self._pending_confirm[sender] = token
                    preview = pending.get("preview", "This action is ready to execute.")
                    risk = pending.get("risk", "medium")
                    flag = "⚠️ " if risk == "high" else ""
                    await self.send_message(
                        chat_id, flag + preview + "\n\nReply *Y* to confirm or *N* to cancel."
                    )
                else:
                    await self.send_message(chat_id, "⚠️ Confirmation expired. Please try again.")
            elif response:
                await self.send_message(chat_id, response)

        except Exception as e:
            logger.error(f"Error processing message: {e}")
            await self.send_message(chat_id, "Sorry, I encountered an error.")

    async def _handle_command(self, cmd: str, args: str, sender: str, chat_id: str):
        """Handle slash commands. Returns response string or None to ignore."""
        from ..core.security import TrustLevel

        session = self.agent.sessions.get_or_create_session(sender, "whatsapp")

        if cmd == "start":
            await self.handle_start_command(chat_id)
            return None

        if cmd == "tutorial":
            await self.handle_tutorial_command(chat_id)
            return None

        if cmd in ("help",):
            status = self.agent.get_status()
            return (
                "🤖 *Familiar Commands*\n\n"
                "/start — Guided setup\n"
                "/tutorial — Replay onboarding\n"
                "/status — System status\n"
                "/trust — Your trust level\n"
                "/budget — Spending status\n"
                "/caps — Your capabilities\n"
                "/model — Show/switch provider\n"
                "/remember <key> <value> — Store memory\n"
                "/recall <query> — Search memories\n"
                "/clear — Clear conversation\n"
                "/help — This message\n\n"
                f"Model: {status['provider']}  Security: {status['security_mode']}"
            )

        elif cmd == "status":
            status = self.agent.get_status()
            return (
                f"📊 *Status*\n"
                f"Trust: {session.trust_level.value.upper()}\n"
                f"Budget: ${session.remaining_budget:.2f} left\n"
                f"Model: {status['provider']}\n"
                f"Memory: {status['memory_entries']} entries\n"
                f"Skills: {status['skills_loaded']}\n"
                f"Tasks: {status['scheduled_tasks']}"
            )

        elif cmd == "trust":
            levels = list(TrustLevel)
            lines = ["🔐 *Trust Level*", ""]
            for lvl in levels:
                if session.trust_level == lvl:
                    m = "▶"
                elif levels.index(session.trust_level) > levels.index(lvl):
                    m = "✅"
                else:
                    m = "⬜"
                lines.append(f"{m} {lvl.value.upper()}")
            if session.trust_level == TrustLevel.STRANGER:
                prog = f"{session.positive_interactions}/10 to KNOWN"
            elif session.trust_level == TrustLevel.KNOWN:
                prog = f"{session.positive_interactions}/50 to TRUSTED"
            else:
                prog = "Maximum level"
            lines += ["", f"Score: {session.trust_score:.1f}", f"Progress: {prog}"]
            return "\n".join(lines)

        elif cmd == "budget":
            pct = (
                min(100, session.spent_today / session.daily_budget * 100)
                if session.daily_budget > 0
                else 0
            )
            bar = f"[{chr(9608) * int(pct / 10)}{chr(9617) * (10 - int(pct / 10))}]"
            return (
                f"💰 *Budget*\n"
                f"Limit: ${session.daily_budget:.2f}\n"
                f"{bar} {pct:.1f}%\n"
                f"Spent: ${session.spent_today:.4f}\n"
                f"Remaining: ${session.remaining_budget:.4f}"
            )

        elif cmd == "caps":
            caps = sorted([c.value for c in session.capabilities])
            cap_list = "\n".join(f"✅ {c}" for c in caps) or "(none yet)"
            return f"🔑 *Capabilities ({len(caps)})*\n{cap_list}"

        elif cmd == "model":
            if not args:
                from ..core.providers import get_available_providers

                providers = get_available_providers()
                return f"Current: {self.agent.provider.name}\nAvailable: {', '.join(providers)}\n\nUsage: /model <provider>"
            result = self.agent.switch_provider(args)
            return f"✓ {result}"

        elif cmd == "remember":
            from ..core.security import Capability

            if not session.has_capability(Capability.WRITE_MEMORY):
                return "⚠️ You don't have write memory permission yet."
            parts = args.split(maxsplit=1)
            if len(parts) < 2:
                return "Usage: /remember <key> <value>"
            self.agent.remember(parts[0], parts[1])
            return f"✓ Remembered: {parts[0]}"

        elif cmd == "recall":
            from ..core.security import Capability

            if not session.has_capability(Capability.READ_MEMORY):
                return "⚠️ You don't have read memory permission yet."
            if not args:
                return "Usage: /recall <query>"
            results = self.agent.recall(args)
            if not results:
                return f"No memories matching '{args}'"
            return "\n".join(f"• {e.key}: {e.value}" for e in results[:10])

        elif cmd == "clear":
            self.agent.clear_history(sender, "whatsapp")
            return "🧹 Conversation cleared!"

        return None  # Not a recognised command — fall through to agent

    async def send_message(self, chat_id: str, text: str):
        """Send a WhatsApp message."""
        if not self.websocket:
            logger.error("Not connected to WhatsApp bridge")
            return False

        try:
            await self.websocket.send(
                json.dumps({"type": "send", "chatId": chat_id, "message": text})
            )
            return True
        except Exception as e:
            logger.error(f"Failed to send message: {e}")
            return False

    def run(self):
        """Start the WhatsApp channel (blocking)."""
        # Start scheduler (backup tasks, proactive briefings)
        if hasattr(self, "agent") and hasattr(self.agent, "start_scheduler"):
            self.agent.start_scheduler()

        # Wire scheduler delivery → WhatsApp
        if self.agent.scheduler:
            owner_num = os.environ.get("OWNER_WHATSAPP_ID", "")
            if owner_num:
                _self = self

                def _deliver_to_whatsapp(message, channel, chat_id):
                    target = chat_id or owner_num
                    if target and _self.websocket:
                        import asyncio as _aio

                        try:
                            loop = _aio.get_event_loop()
                            _aio.run_coroutine_threadsafe(
                                _self.send_message(target, f"📬 {message}"), loop
                            )
                        except Exception as e:
                            logger.error(f"WhatsApp delivery failed: {e}")

                self.agent.scheduler.set_delivery_callback(_deliver_to_whatsapp)
                logger.info(f"Scheduler delivery → WhatsApp (owner: {owner_num})")

        asyncio.run(self.connect())

    def run_async(self):
        """Start in background thread."""
        thread = Thread(target=self.run, daemon=True)
        thread.start()
        return thread

    def stop(self):
        """Stop the channel."""
        self._running = False


# Node.js bridge script (save as whatsapp_bridge.js)
BRIDGE_SCRIPT = """
/**
 * WhatsApp Bridge for Familiar
 *
 * Run: node whatsapp_bridge.js
 *
 * Requires:
 *   npm install whatsapp-web.js qrcode-terminal ws
 */

const { Client, LocalAuth } = require('whatsapp-web.js');
const qrcode = require('qrcode-terminal');
const WebSocket = require('ws');

const PORT = process.env.WHATSAPP_BRIDGE_PORT || 3001;

// WhatsApp client with persistent session
const client = new Client({
    authStrategy: new LocalAuth(),
    puppeteer: {
        headless: true,
        args: ['--no-sandbox']
    }
});

// WebSocket server for Python
const wss = new WebSocket.Server({ port: PORT });
let connections = [];

wss.on('connection', (ws) => {
    console.log('Python client connected');
    connections.push(ws);

    // Send ready status if already authenticated
    if (client.info) {
        ws.send(JSON.stringify({ type: 'ready', info: client.info }));
    }

    ws.on('message', async (data) => {
        try {
            const msg = JSON.parse(data);

            if (msg.type === 'send') {
                await client.sendMessage(msg.chatId, msg.message);
                console.log(`Sent message to ${msg.chatId}`);
            }
        } catch (e) {
            console.error('Error handling message:', e);
        }
    });

    ws.on('close', () => {
        connections = connections.filter(c => c !== ws);
        console.log('Python client disconnected');
    });
});

// Broadcast to all Python clients
function broadcast(data) {
    const msg = JSON.stringify(data);
    connections.forEach(ws => {
        if (ws.readyState === WebSocket.OPEN) {
            ws.send(msg);
        }
    });
}

// WhatsApp events
client.on('qr', (qr) => {
    console.log('QR Code received, scan with WhatsApp:');
    qrcode.generate(qr, { small: true });
    broadcast({ type: 'qr', qr: qr });
});

client.on('ready', () => {
    console.log('WhatsApp client ready!');
    broadcast({ type: 'ready', info: client.info });
});

client.on('message', async (msg) => {
    // Skip status messages
    if (msg.isStatus) return;

    const chat = await msg.getChat();

    broadcast({
        type: 'message',
        from: msg.from,
        body: msg.body,
        chatId: msg.from,
        isGroup: chat.isGroup,
        timestamp: msg.timestamp,
        mentionedMe: msg.mentionedIds?.includes(client.info.wid._serialized)
    });
});

client.on('disconnected', (reason) => {
    console.log('WhatsApp disconnected:', reason);
    broadcast({ type: 'disconnected', reason: reason });
});

// Start
console.log(`WhatsApp bridge starting on port ${PORT}...`);
client.initialize();
"""


def create_bridge_script(path: str = None) -> str:
    """Create the Node.js bridge script."""
    if path is None:
        path = Path.home() / ".familiar" / "whatsapp_bridge.js"

    path = Path(path)
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(BRIDGE_SCRIPT)

    return str(path)


def send_whatsapp(data: dict) -> str:
    """Tool function to send a WhatsApp message."""
    recipient = data.get("to", "")
    message = data.get("message", "")

    if not recipient or not message:
        return "Please provide 'to' (phone with country code) and 'message'"

    # Format phone number
    if not recipient.endswith("@c.us"):
        # Remove any non-digits
        phone = "".join(c for c in recipient if c.isdigit())
        recipient = f"{phone}@c.us"

    # Send via bridge (this is async, so we use a sync wrapper)
    import asyncio

    async def _send():
        try:
            import websockets

            url = f"ws://{BRIDGE_HOST}:{BRIDGE_PORT}"

            async with websockets.connect(url) as ws:
                await ws.send(json.dumps({"type": "send", "chatId": recipient, "message": message}))
                return True
        except Exception as e:
            logger.error(f"WhatsApp send error: {e}")
            return False

    try:
        loop = asyncio.get_event_loop()
    except RuntimeError:
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

    success = loop.run_until_complete(_send())

    if success:
        return f"✅ WhatsApp message sent to {recipient}"
    else:
        return "❌ Failed to send. Is the WhatsApp bridge running?"


# Tool definition
TOOLS = [
    {
        "name": "send_whatsapp",
        "description": "Send a WhatsApp message. Requires WhatsApp bridge running.",
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {
                    "type": "string",
                    "description": "Phone number with country code (e.g., +14155551234)",
                },
                "message": {"type": "string", "description": "Message to send"},
            },
            "required": ["to", "message"],
        },
        "handler": send_whatsapp,
        "category": "messaging",
    }
]
