# 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

"""
iMessage Channel

Send and receive iMessages via AppleScript (macOS only).
Requires the Mac to be running with Messages app configured.

This works by:
1. Monitoring a SQLite database for new messages
2. Using AppleScript to send messages

Note: This requires Full Disk Access permission for the script
to read the Messages database.

Setup:
1. System Preferences > Security & Privacy > Privacy > Full Disk Access
2. Add Terminal (or your Python interpreter) to the list
"""

import logging
import os
import sqlite3
import subprocess
import time
from pathlib import Path
from threading import Thread

logger = logging.getLogger(__name__)

# iMessage database location
IMESSAGE_DB = Path.home() / "Library" / "Messages" / "chat.db"


class iMessageChannel:
    """
    iMessage integration for Familiar (macOS only).

    Usage:
        channel = iMessageChannel(agent)
        channel.run()  # Starts listening for messages
    """

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

        Args:
            agent: The Familiar instance
            allowed_contacts: List of phone numbers/emails to respond to.
                            If None, responds to all contacts.
        """
        self.agent = agent
        self.allowed_contacts = allowed_contacts
        self._running = False
        self._last_message_id = None
        self._poll_interval = 2  # seconds

    def is_available(self) -> bool:
        """Check if iMessage is available (macOS with Messages app)."""
        import platform

        if platform.system() != "Darwin":
            return False
        return IMESSAGE_DB.exists()

    def send_message(self, recipient: str, text: str) -> bool:
        """
        Send an iMessage.

        Args:
            recipient: Phone number or email address
            text: Message text

        Returns:
            True if sent successfully
        """
        # Escape special characters for AppleScript
        text_escaped = text.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
        # Sanitize recipient to prevent AppleScript injection
        recipient_escaped = recipient.replace("\\", "\\\\").replace('"', '\\"')

        script = f'''
        tell application "Messages"
            set targetService to 1st account whose service type = iMessage
            set targetBuddy to participant "{recipient_escaped}" of targetService
            send "{text_escaped}" to targetBuddy
        end tell
        '''

        try:
            subprocess.run(
                ["osascript", "-e", script],
                check=True,
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
            )
            logger.info(f"Sent iMessage to {recipient}")
            return True
        except subprocess.CalledProcessError as e:
            logger.error(f"Failed to send iMessage: {e.stderr}")
            return False

    def _get_latest_message_id(self) -> int:
        """Get the ID of the most recent message."""
        try:
            conn = sqlite3.connect(str(IMESSAGE_DB))
            cursor = conn.cursor()
            cursor.execute("SELECT MAX(ROWID) FROM message")
            result = cursor.fetchone()
            conn.close()
            return result[0] if result[0] else 0
        except Exception as e:
            logger.error(f"Error reading message DB: {e}")
            return 0

    def _get_new_messages(self, since_id: int) -> list:
        """Get messages newer than the given ID."""
        try:
            conn = sqlite3.connect(str(IMESSAGE_DB))
            cursor = conn.cursor()

            # Query for new incoming messages
            query = """
                SELECT
                    m.ROWID,
                    m.text,
                    m.is_from_me,
                    m.date,
                    h.id as handle_id
                FROM message m
                LEFT JOIN handle h ON m.handle_id = h.ROWID
                WHERE m.ROWID > ?
                    AND m.is_from_me = 0
                    AND m.text IS NOT NULL
                ORDER BY m.ROWID ASC
            """

            cursor.execute(query, (since_id,))
            messages = cursor.fetchall()
            conn.close()

            return [
                {
                    "id": row[0],
                    "text": row[1],
                    "is_from_me": row[2],
                    "date": row[3],
                    "sender": row[4],
                }
                for row in messages
            ]
        except Exception as e:
            logger.error(f"Error fetching messages: {e}")
            return []

    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, message: dict):
        """Process an incoming message."""
        sender = message["sender"]
        text = message["text"]

        # ── Owner PIN verification ──
        owner_id = os.environ.get("OWNER_IMESSAGE_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:
                    self.send_message(sender, lockout_msg)
                    return
                if verify_owner_pin(text.strip()):
                    record_pin_attempt(sender, success=True)
                    claim_ownership(sender, "imessage", 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, "imessage")
                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} (not in allowed list)")
            return

        logger.info(f"iMessage 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:
                for chunk in self._split_message(response):
                    self.send_message(sender, chunk)
                return

        try:
            # Process through agent
            response = self.agent.chat(text, user_id=sender, channel="imessage")

            # Send response
            if response:
                # Split long messages (iMessage has limits)
                chunks = self._split_message(response, max_length=2000)
                for chunk in chunks:
                    self.send_message(sender, chunk)
                    if len(chunks) > 1:
                        time.sleep(0.5)  # Small delay between chunks

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

    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, "imessage")

        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"
                "/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, "imessage")
            return "Conversation cleared."

        return None

    def _split_message(self, text: str, max_length: int = 2000) -> list:
        """Split a long message into chunks."""
        if len(text) <= max_length:
            return [text]

        chunks = []
        while text:
            if len(text) <= max_length:
                chunks.append(text)
                break

            # Find a good break point
            break_point = text.rfind("\n", 0, max_length)
            if break_point == -1:
                break_point = text.rfind(" ", 0, max_length)
            if break_point == -1:
                break_point = max_length

            chunks.append(text[:break_point])
            text = text[break_point:].lstrip()

        return chunks

    def _poll_loop(self):
        """Main polling loop for new messages."""
        while self._running:
            try:
                messages = self._get_new_messages(self._last_message_id)

                for msg in messages:
                    self._last_message_id = msg["id"]
                    self._handle_message(msg)

            except Exception as e:
                logger.error(f"Error in poll loop: {e}")

            time.sleep(self._poll_interval)

    def run(self):
        """Start the iMessage channel."""
        if not self.is_available():
            logger.error("iMessage not available (requires macOS with Messages app)")
            return

        logger.info("Starting iMessage channel...")

        # Get current message ID to avoid processing old messages
        self._last_message_id = self._get_latest_message_id()
        self._running = True

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

        # Wire scheduler delivery → iMessage
        if self.agent.scheduler:
            owner_contact = os.environ.get("OWNER_IMESSAGE_ID", "")
            if owner_contact:
                _self = self

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

                self.agent.scheduler.set_delivery_callback(_deliver_to_imessage)
                logger.info(f"Scheduler delivery → iMessage ({owner_contact})")

        print("📱 iMessage channel active")
        print("   Monitoring for new messages...")
        if self.allowed_contacts:
            print(f"   Responding to: {', '.join(self.allowed_contacts)}")
        else:
            print("   Responding to: all contacts")

        # Start polling
        self._poll_loop()

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

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


def send_imessage(data: dict) -> str:
    """Tool function to send an iMessage."""
    recipient = data.get("to", "")
    message = data.get("message", "")

    if not recipient or not message:
        return "Please provide 'to' and 'message'"

    import platform

    if platform.system() != "Darwin":
        return "❌ iMessage only available on macOS"

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


# Tool definition for skill registration
TOOLS = [
    {
        "name": "send_imessage",
        "description": "Send an iMessage (macOS only)",
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {"type": "string", "description": "Recipient phone number or email"},
                "message": {"type": "string", "description": "Message to send"},
            },
            "required": ["to", "message"],
        },
        "handler": send_imessage,
        "category": "messaging",
    }
]
