# 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

"""
Signal Channel

Connect to Signal via signal-cli.

signal-cli is an unofficial command-line client for Signal.
It can be used to send/receive messages programmatically.

Setup:
1. Install signal-cli:
   - macOS: brew install signal-cli
   - Linux: https://github.com/AsamK/signal-cli/releases

2. Register/link your phone number:
   signal-cli -u +1YOURPHONE register
   signal-cli -u +1YOURPHONE verify CODE

   Or link to existing Signal account:
   signal-cli link -n "Familiar"

3. Set SIGNAL_PHONE environment variable

The channel monitors for incoming messages via signal-cli daemon mode.
"""

import json
import logging
import os
import subprocess
from threading import Thread

logger = logging.getLogger(__name__)

# Signal configuration
SIGNAL_PHONE = os.environ.get("SIGNAL_PHONE", "")
SIGNAL_CLI = os.environ.get("SIGNAL_CLI_PATH", "signal-cli")


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


class SignalChannel(ConnectMixin):
    """
    Signal integration via signal-cli.

    Usage:
        channel = SignalChannel(agent)
        channel.run()
    """

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

    def __init__(self, agent, phone: str = None, allowed_contacts: list = None):
        """
        Initialize Signal channel.

        Args:
            agent: The Familiar instance
            phone: Your Signal phone number (with country code)
            allowed_contacts: List of phone numbers to respond to.
        """
        self.agent = agent
        self.phone = phone or SIGNAL_PHONE
        self.allowed_contacts = allowed_contacts
        self._running = False
        self._process = None
        self._loop = None  # Event loop for async wizard methods

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

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

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

    def is_available(self) -> bool:
        """Check if signal-cli is installed and configured."""
        try:
            result = subprocess.run([SIGNAL_CLI, "--version"], capture_output=True, timeout=5)
            return result.returncode == 0
        except (subprocess.SubprocessError, FileNotFoundError, OSError):
            return False

    def _run_async(self, coro):
        """Run an async coroutine from sync signal-cli handler."""
        import asyncio

        if self._loop and self._loop.is_running():
            future = asyncio.run_coroutine_threadsafe(coro, self._loop)
            future.result(timeout=120)
        else:
            asyncio.run(coro)

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

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

        if cmd == "connect":
            if session.trust_level != TrustLevel.OWNER:
                return "🔒 /connect requires OWNER trust level."
            connect_args = args.split() if args else []
            self._run_async(self.handle_connect_command(sender, connect_args))
            return ""  # wizard_send handles the response

        if cmd == "browse":
            if session.trust_level != TrustLevel.OWNER:
                return "🔒 /browse requires OWNER trust level."
            self._run_async(self.handle_browse_command(sender))
            return ""

        if cmd == "search":
            if session.trust_level != TrustLevel.OWNER:
                return "🔒 /search requires OWNER trust level."
            search_args = args.split() if args else []
            self._run_async(self.handle_search_command(sender, search_args))
            return ""

        if cmd in ("help", "start"):
            status = self.agent.get_status()
            return (
                "Familiar Commands\n\n"
                "/status - System status\n"
                "/trust - Your trust level\n"
                "/budget - Spending status\n"
                "/caps - Your capabilities\n"
                "/model - Show/switch provider\n"
                "/connect - Connect services\n"
                "/browse - Browse connected services\n"
                "/search <service> <query> - Search a service\n"
                "/remember <key> <value> - Store memory\n"
                "/recall <query> - Search memories\n"
                "/clear - Clear conversation\n"
                f"\nModel: {status['provider']}"
            )

        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']}"
            )

        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 = "ok "
                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"[{'|' * int(pct / 10)}{' ' * (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)}\nUsage: /model <n>"
            result = self.agent.switch_provider(args)
            return f"OK: {result}"

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

            if not session.has_capability(Capability.WRITE_MEMORY):
                return "No 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 "No 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, "signal")
            return "Conversation cleared."

        return None

    def send_message(self, recipient: str, text: str) -> bool:
        """
        Send a Signal message.

        Args:
            recipient: Phone number with country code
            text: Message text

        Returns:
            True if sent successfully
        """
        if not self.phone:
            logger.error("SIGNAL_PHONE not configured")
            return False

        try:
            result = subprocess.run(
                [SIGNAL_CLI, "-u", self.phone, "send", "-m", text, recipient],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                timeout=30,
            )

            if result.returncode == 0:
                logger.info(f"Signal message sent to {recipient}")
                return True
            else:
                logger.error(f"Signal send failed: {result.stderr}")
                return False

        except subprocess.TimeoutExpired:
            logger.error("Signal send timed out")
            return False
        except Exception as e:
            logger.error(f"Signal send error: {e}")
            return False

    def _should_respond(self, sender: str) -> bool:
        """Check if we should respond to this sender."""
        if self.allowed_contacts is None:
            return True
        return sender in self.allowed_contacts

    def _handle_message(self, data: dict):
        """Process an incoming Signal message."""
        envelope = data.get("envelope", {})
        sender = envelope.get("source", "")

        # Get message content
        data_message = envelope.get("dataMessage", {})
        text = data_message.get("message", "")

        if not text:
            return

        # ── Owner PIN verification ──
        owner_id = os.environ.get("OWNER_SIGNAL_ID")
        pin_hash = os.environ.get("OWNER_PIN_HASH")
        if not owner_id and pin_hash:
            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:
                    self.send_message(sender, lockout_msg)
                    return
                if verify_owner_pin(text.strip()):
                    record_pin_attempt(sender, success=True)
                    claim_ownership(sender, "signal", self.agent.sessions)
                    self.send_message(sender, "🔐 Owner verified. Full access granted.")
                    return
                elif text.strip().isdigit() and 4 <= len(text.strip()) <= 8:
                    record_pin_attempt(sender, success=False)
                    self.send_message(sender, "❌ Incorrect PIN.")
                    return
                else:
                    self.send_message(sender, "🔒 Enter your owner PIN to claim this bot.")
                    return
            except ImportError:
                pass

        # 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, "signal")
                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:
                pass

        if not self._should_respond(sender):
            logger.debug(f"Ignoring message from {sender}")
            return

        logger.info(f"Signal 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 ""
            response = self._handle_command(cmd, args, sender)
            if response is not None:
                self.send_message(sender, response)
                return

        # Intercept messages during wizard flows (ConnectMixin)
        try:
            import asyncio as _aio

            if self._loop and self._loop.is_running():
                fut = _aio.run_coroutine_threadsafe(
                    self.handle_wizard_message(sender, text), self._loop
                )
                if fut.result(timeout=30):
                    return
            else:
                if _aio.run(self.handle_wizard_message(sender, text)):
                    return
        except Exception:
            pass

        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, "signal")
                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": "signal",
                            },
                        )
                        from familiar.core.agent import _capture_context_from_result

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

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

            # 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, "signal")
                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 ""
                    self.send_message(
                        sender, flag + preview + "\nReply Y to confirm or N to cancel."
                    )
                else:
                    self.send_message(sender, "⚠️ Confirmation expired. Please try again.")
            elif response:
                self.send_message(sender, response)

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

    def run(self):
        """Start the Signal channel (blocking)."""
        if not self.phone:
            logger.error("SIGNAL_PHONE not set")
            print("❌ Set SIGNAL_PHONE environment variable")
            return

        if not self.is_available():
            logger.error("signal-cli not available")
            print("❌ signal-cli not installed or not in PATH")
            return

        logger.info("Starting Signal channel...")
        print(f"📱 Signal channel active for {self.phone}")

        # Start scheduler (backup tasks, proactive briefings)
        if hasattr(self, "agent") and hasattr(self.agent, "start_scheduler"):
            self.agent.start_scheduler()

        # Wire scheduler delivery → Signal
        if self.agent.scheduler:
            owner_num = os.environ.get("OWNER_SIGNAL_ID", self.phone or "")
            if owner_num:
                _self = self

                def _deliver_to_signal(message, channel, chat_id):
                    target = chat_id or owner_num
                    if target:
                        _self.send_message(target, f"[Scheduled] {message}")

                self.agent.scheduler.set_delivery_callback(_deliver_to_signal)
                logger.info(f"Scheduler delivery → Signal ({owner_num})")

        self._running = True

        # Use signal-cli in JSON-RPC mode
        try:
            self._process = subprocess.Popen(
                [SIGNAL_CLI, "-u", self.phone, "jsonRpc"],
                stdin=subprocess.PIPE,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                bufsize=1,
            )

            # Read incoming messages
            for line in self._process.stdout:
                if not self._running:
                    break

                try:
                    data = json.loads(line.strip())

                    # Check if it's a receive notification
                    if data.get("method") == "receive":
                        for msg in data.get("params", {}).get("envelope", []):
                            self._handle_message({"envelope": msg})

                except json.JSONDecodeError:
                    continue
                except Exception as e:
                    logger.error(f"Error processing line: {e}")

        except Exception as e:
            logger.error(f"Signal channel error: {e}")

        finally:
            if self._process:
                self._process.terminate()

    def run_daemon(self):
        """
        Alternative: Run using signal-cli daemon mode.
        This is more reliable for long-running operation.
        """
        if not self.phone:
            print("❌ Set SIGNAL_PHONE environment variable")
            return

        print(f"📱 Starting Signal daemon for {self.phone}...")
        print("   Messages will be received and processed automatically.")

        self._running = True

        # Start daemon
        try:
            self._process = subprocess.Popen(
                [SIGNAL_CLI, "-u", self.phone, "daemon", "--json"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )

            # Process output
            for line in self._process.stdout:
                if not self._running:
                    break

                try:
                    data = json.loads(line.strip())
                    self._handle_message(data)
                except json.JSONDecodeError:
                    continue

        except Exception as e:
            logger.error(f"Signal daemon error: {e}")

        finally:
            if self._process:
                self._process.terminate()

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

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


def send_signal(data: dict) -> str:
    """Tool function to send a Signal 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'"

    if not SIGNAL_PHONE:
        return "❌ SIGNAL_PHONE environment variable not set"

    channel = SignalChannel(None)
    if not channel.is_available():
        return "❌ signal-cli not installed"

    if channel.send_message(recipient, message):
        return f"✅ Signal message sent to {recipient}"
    else:
        return "❌ Failed to send Signal message"


# Tool definition
TOOLS = [
    {
        "name": "send_signal",
        "description": "Send a Signal message. Requires signal-cli to be installed and configured.",
        "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_signal,
        "category": "messaging",
    }
]
