#!/usr/bin/env python3
# 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

"""
Familiar - Main Entry Point

A standalone personal AI agent for Raspberry Pi (or any machine).
Supports Claude, GPT, and local models via Ollama.

Usage:
    # CLI mode
    python -m familiar

    # Telegram mode
    python -m familiar --telegram

    # Gateway mode (for multi-device mesh)
    python -m familiar --gateway

    # Initialize config
    python -m familiar --init
"""

import argparse
import asyncio
import logging
import os
import sys

logger = logging.getLogger(__name__)

# Add parent to path for imports
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))


def _load_dotenv():
    """Load .env from project dir or ~/.familiar/."""
    from dotenv import load_dotenv as _dotenv_load

    for env_path in [
        os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env"),
        os.path.expanduser("~/.familiar/.env"),
    ]:
        if os.path.exists(env_path):
            _dotenv_load(env_path, override=False)
            break


_load_dotenv()


def _check_dependencies(config=None):
    """
    Preflight dependency check. Runs every startup.
    Auto-installs missing required packages. Logs missing optional ones.
    """

    # Map: import_name → pip_package
    required = {
        "yaml": "pyyaml",
        "pydantic": "pydantic",
        "httpx": "httpx",
        "cryptography": "cryptography",
    }

    # Channel-specific
    enabled = os.environ.get("ENABLED_CHANNELS", "")
    if "telegram" in enabled or os.environ.get("TELEGRAM_BOT_TOKEN"):
        required["telegram"] = "python-telegram-bot>=21.0"
    if "discord" in enabled or os.environ.get("DISCORD_BOT_TOKEN"):
        required["discord"] = "discord.py>=2.3.0"

    # Provider-specific
    if os.environ.get("ANTHROPIC_API_KEY"):
        required["anthropic"] = "anthropic>=0.40.0"
    if os.environ.get("OPENAI_API_KEY") or os.environ.get("OLLAMA_MODEL"):
        required["openai"] = "openai>=1.0.0"

    # Check and collect missing
    missing = []
    for import_name, pip_name in required.items():
        try:
            __import__(import_name)
        except ImportError:
            missing.append(pip_name)

    if missing:
        print(f"  ⏳ Missing {len(missing)} required package(s): {', '.join(missing)}")
        print("     Auto-installing...")
        from familiar.core.deps import ensure_packages

        pairs = [(imp, pip) for imp, pip in required.items() if pip in missing]
        ok, failed = ensure_packages(pairs)
        if not ok:
            print(f"  ❌ Failed to install: {', '.join(failed)}")
            print(f"     pip install {' '.join(failed)}")
            print()
            print("     Then re-run: python -m familiar")
            sys.exit(1)
        print(f"  ✅ Installed: {', '.join(missing)}")

    # Optional: log at debug level, don't nag every startup
    optional = {
        "bs4": "beautifulsoup4 (web scraping skills)",
        "docx": "python-docx (document skills)",
        "PIL": "Pillow (image skills)",
        "whisper": "openai-whisper (voice transcription)",
        "pyttsx3": "pyttsx3 (text-to-speech)",
    }
    missing_opt = [desc for imp, desc in optional.items() if not _try_import(imp)]
    if missing_opt:
        logger.debug(f"Optional packages not installed: {', '.join(missing_opt)}")


def _try_import(name):
    try:
        __import__(name)
        return True
    except ImportError:
        return False


def _check_skill_health():
    """Log a clear summary of which optional skills are ready vs degraded."""
    from familiar.core.skills import SKILL_PREREQUISITES, check_skill_prerequisites

    failures = check_skill_prerequisites()
    all_skills = sorted(SKILL_PREREQUISITES.keys())

    if not failures:
        logger.info(f"Skill prerequisites: all {len(all_skills)} checked skills ready")
        return

    ready = [s for s in all_skills if s not in failures]
    degraded = sorted(failures.keys())

    logger.info(f"Skill prerequisites: {len(ready)} ready, {len(degraded)} degraded")
    for skill_name in degraded:
        for issue in failures[skill_name]:
            logger.warning(f"  {skill_name}: {issue}")


def setup_logging(verbose: bool = False):
    """Configure logging."""
    level = logging.DEBUG if verbose else logging.INFO
    logging.basicConfig(
        level=level, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%H:%M:%S"
    )


def main():
    parser = argparse.ArgumentParser(description="Familiar - Personal AI Assistant")

    parser.add_argument("--telegram", "-t", action="store_true", help="Run with Telegram interface")

    parser.add_argument("--discord", action="store_true", help="Run with Discord interface")

    parser.add_argument("--matrix", action="store_true", help="Run with Matrix interface")

    parser.add_argument(
        "--github", action="store_true", help="Run with GitHub interface (notification polling)"
    )

    parser.add_argument(
        "--slack", action="store_true", help="Run with Slack interface (Socket Mode)"
    )

    parser.add_argument("--dashboard", "-d", action="store_true", help="Run web dashboard")

    parser.add_argument(
        "--dashboard-port", type=int, default=5000, help="Dashboard port (default: 5000)"
    )

    parser.add_argument(
        "--gateway",
        "-g",
        action="store_true",
        help="Run as mesh gateway (other devices connect to this)",
    )

    parser.add_argument(
        "--gateway-port", type=int, default=18789, help="Gateway port (default: 18789)"
    )

    parser.add_argument("--init", action="store_true", help="Initialize default configuration")

    parser.add_argument(
        "--provider",
        "-p",
        type=str,
        default="ollama",
        help="LLM provider (ollama, claude, gpt, llama, mistral). Default: ollama",
    )

    parser.add_argument(
        "--onboard", action="store_true", help="Run the onboarding wizard (first-time setup)"
    )

    parser.add_argument(
        "--reconfigure",
        action="store_true",
        help="Re-run the onboarding wizard to update configuration",
    )

    parser.add_argument(
        "--onboard-tui",
        action="store_true",
        dest="onboard_tui",
        help="Run the onboarding wizard with a rich terminal UI (requires textual)",
    )

    parser.add_argument(
        "--setup-google",
        action="store_true",
        dest="setup_google",
        help="Connect Google Workspace (Calendar, Drive, Gmail, Contacts)",
    )

    parser.add_argument("--verbose", "-v", action="store_true", help="Verbose logging")

    parser.add_argument("--new-skill", type=str, metavar="NAME", help="Create a new skill template")

    parser.add_argument(
        "--show-key", action="store_true", help="Show the mesh secret key for connecting nodes"
    )

    args = parser.parse_args()

    setup_logging(args.verbose)

    # Handle --onboard-tui (rich terminal UI wizard)
    if getattr(args, "onboard_tui", False):
        from familiar.onboard_tui import main as tui_main

        tui_main()
        return

    # Handle --reconfigure (re-run wizard without auto-launch)
    if args.reconfigure:
        # PIN gate: require owner PIN if one has been set
        pin_hash = os.environ.get("OWNER_PIN_HASH")
        if pin_hash:
            import getpass

            from familiar.core.security import verify_owner_pin

            entered = getpass.getpass("  Enter owner PIN to reconfigure: ")
            if not verify_owner_pin(entered):
                print("  Incorrect PIN. Access denied.")
                return
        from familiar.onboard import run_onboard

        run_onboard(auto_launch=False)
        return

    # Handle --onboard (Moltbot-parity onboarding wizard)
    if args.onboard:
        from familiar.onboard import run_onboard

        run_onboard()

    # Handle --setup-google (Phase 3 Google onboarding)
    if getattr(args, "setup_google", False):
        from familiar.onboard.google_setup import run_google_setup

        run_google_setup()
        return

    # Handle --init
    if args.init:
        from familiar.core.config import CONFIG_FILE, save_default_config
        from familiar.core.mesh import MeshConfig

        save_default_config()

        # Initialize mesh config with secret key
        mesh_config = MeshConfig()
        mesh_config.save()

        print(f"\n✓ Config created at {CONFIG_FILE}")
        key = mesh_config.get("secret_key")
        print(f"\n🔑 Mesh secret key: {key[:8]}...{key[-4:]}")
        print("   Full key saved to: ~/.familiar/data/mesh/mesh_config.json")
        print("   (Save this for connecting nodes)")
        print("\nRequired environment variables:")
        print("  ANTHROPIC_API_KEY - for Claude")
        print("  OPENAI_API_KEY    - for GPT")
        print("  TELEGRAM_BOT_TOKEN - for Telegram mode")
        print("\nOr use Ollama for free local models:")
        print("  curl -fsSL https://ollama.ai/install.sh | sh")
        print("  ollama pull llama3.2")
        return

    # Handle --show-key
    if args.show_key:
        from familiar.core.mesh import MeshConfig

        config = MeshConfig()
        key = config.get("secret_key")
        print(f"\n🔑 Mesh secret key: {key[:8]}...{key[-4:]}")
        print(f"   Gateway: ws://localhost:{args.gateway_port}")
        print("\nConnect a node with:")
        print(
            f"   python -m familiar.node --gateway ws://YOUR_PI_IP:{args.gateway_port} --key <SECRET_KEY>"
        )
        print("   (Get key from: ~/.familiar/data/mesh/mesh_config.json)")
        return

    # Handle --new-skill
    if args.new_skill:
        from familiar.core.config import SKILLS_DIR
        from familiar.core.skills import create_skill_template

        path = create_skill_template(SKILLS_DIR, args.new_skill)
        print(f"\n✓ Created skill template at: {path}")
        print("Edit the files to add your custom functionality.")
        return

    # Load config and create agent
    from familiar.core.agent import Agent
    from familiar.core.config import CONFIG_FILE, load_config

    # ── First-run detection ──
    if not CONFIG_FILE.exists():
        print()
        print("🦔 Welcome to Familiar!")
        print("   First run detected.")
        print()

        # Offer the onboarding wizard
        try:
            answer = input("   Run the setup wizard? [Y/n]: ").strip().lower()
        except (EOFError, KeyboardInterrupt):
            answer = "n"

        if answer != "n":
            from familiar.onboard import run_onboard

            run_onboard()
            # Reload config after onboard
            if not CONFIG_FILE.exists():
                return
        else:
            # Silent init for experienced users
            from familiar.core.config import save_default_config
            from familiar.core.mesh import MeshConfig

            save_default_config()
            mesh_config = MeshConfig()
            mesh_config.save()

            print(f"   ✓ Config created at {CONFIG_FILE}")
            print("   ✓ Encryption keys generated")
            print()

            # Detect available providers and advise
            has_anthropic = bool(os.environ.get("ANTHROPIC_API_KEY"))
            has_openai = bool(os.environ.get("OPENAI_API_KEY"))
            has_ollama = False
            try:
                import subprocess

                result = subprocess.run(["ollama", "list"], capture_output=True, timeout=5)
                has_ollama = result.returncode == 0
            except (FileNotFoundError, subprocess.TimeoutExpired):
                pass

            if has_anthropic:
                print("   ✓ ANTHROPIC_API_KEY detected — using Claude")
            elif has_openai:
                print("   ✓ OPENAI_API_KEY detected — using GPT")
            elif has_ollama:
                print("   ✓ Ollama detected — using local models (free)")
            else:
                print("   ⚠  No LLM provider found. Pick one:")
                print()
                print("   Option 1 — Ollama (free, local, private):")
                print("     curl -fsSL https://ollama.ai/install.sh | sh")
                print("     ollama pull llama3.2")
                print()
                print("   Option 2 — Claude (cloud, best quality):")
                print("     export ANTHROPIC_API_KEY='sk-ant-...'")
                print()
                print("   Option 3 — GPT (cloud):")
                print("     export OPENAI_API_KEY='sk-...'")
                print()
                print("   Then re-run: python -m familiar")
                return

            print()

    # ── Config completeness check ────────────────────────────────
    if CONFIG_FILE.exists():
        from familiar.onboard_engine import OnboardingEngine

        completeness = OnboardingEngine.validate_config_completeness()
        if not completeness.success:
            for w in completeness.warnings:
                print(f"  ⚠  {w}")
            print()
            print("  Run: python -m familiar --reconfigure")
            print()

    config = load_config()

    # ── Preflight: check dependencies ──────────────────────────────
    _check_dependencies(config)
    _check_skill_health()

    # Override provider if specified
    if args.provider:
        config.llm.default_provider = args.provider

    # Create agent
    agent = Agent(config)

    # Run web dashboard
    if args.dashboard:
        from familiar.dashboard import run_dashboard

        gateway = None
        if args.gateway:
            from familiar.core.mesh import MeshGateway

            gateway = MeshGateway(agent, port=args.gateway_port)
            # Start gateway in background
            import threading

            def start_gateway():
                asyncio.run(gateway.start())

            t = threading.Thread(target=start_gateway, daemon=True)
            t.start()

        run_dashboard(agent, gateway, port=args.dashboard_port)
        return

    # Run in gateway mode
    if args.gateway:
        from familiar.core.mesh import MeshConfig, MeshGateway

        mesh_config = MeshConfig()
        key = mesh_config.get("secret_key")
        print(f"\n🌐 Starting mesh gateway on port {args.gateway_port}")
        print(f"🔑 Secret key: {key[:8]}...{key[-4:]}")
        print("\nConnect nodes with:")
        print(
            f"   python -m familiar.node --gateway ws://YOUR_IP:{args.gateway_port} --key <SECRET_KEY>"
        )
        print("   (Get key from: ~/.familiar/data/mesh/mesh_config.json)")
        print()

        async def run():
            gateway = MeshGateway(agent, port=args.gateway_port)
            await gateway.start()

            # Also start Telegram if enabled
            if args.telegram or config.channels.telegram_enabled:
                # Run telegram in background
                # (simplified - real impl would be more robust)
                pass

            # Keep running
            try:
                while True:
                    await asyncio.sleep(1)
            except KeyboardInterrupt:
                print("\nShutting down...")
                await gateway.stop()

        asyncio.run(run())
        return

    # ── Multi-channel startup ──────────────────────────────────────
    # Determine which channels to start.
    # Priority: CLI flags > ENABLED_CHANNELS env var > config file > auto-detect
    enabled_channels = os.environ.get("ENABLED_CHANNELS", "").split(",")
    enabled_channels = [c.strip() for c in enabled_channels if c.strip()]

    # CLI flags override everything
    if args.telegram:
        if "telegram" not in enabled_channels:
            enabled_channels.insert(0, "telegram")
    if args.discord:
        if "discord" not in enabled_channels:
            enabled_channels.append("discord")
    if args.matrix:
        if "matrix" not in enabled_channels:
            enabled_channels.append("matrix")
    if args.github:
        if "github" not in enabled_channels:
            enabled_channels.append("github")
    if args.slack:
        if "slack" not in enabled_channels:
            enabled_channels.append("slack")

    # Only fall back to token auto-detection if ENABLED_CHANNELS was never set
    # (i.e. pre-v1.1.3 installs that don't have the var yet)
    if not enabled_channels and not os.environ.get("ENABLED_CHANNELS"):
        if config.channels.telegram_enabled or os.environ.get("TELEGRAM_BOT_TOKEN"):
            enabled_channels.append("telegram")
        if getattr(config.channels, "discord_enabled", False) or os.environ.get(
            "DISCORD_BOT_TOKEN"
        ):
            enabled_channels.append("discord")
        if getattr(config.channels, "matrix_enabled", False) or os.environ.get("MATRIX_HOMESERVER"):
            enabled_channels.append("matrix")
        if getattr(config.channels, "signal_enabled", False) or os.environ.get("SIGNAL_PHONE"):
            enabled_channels.append("signal")
        if getattr(config.channels, "whatsapp_enabled", False) or os.environ.get(
            "WHATSAPP_BRIDGE_PORT"
        ):
            enabled_channels.append("whatsapp")
        if getattr(config.channels, "imessage_enabled", False):
            enabled_channels.append("imessage")
        if getattr(config.channels, "teams_enabled", False) or os.environ.get("TEAMS_APP_ID"):
            enabled_channels.append("teams")
        if getattr(config.channels, "github_enabled", False) or os.environ.get("GITHUB_TOKEN"):
            enabled_channels.append("github")
        if getattr(config.channels, "slack_enabled", False) or os.environ.get("SLACK_BOT_TOKEN"):
            enabled_channels.append("slack")

    # If still nothing, default to CLI
    if not enabled_channels:
        enabled_channels = ["cli"]

    logger.info(f"Enabled channels: {', '.join(enabled_channels)}")

    # Start background channels in threads, primary channel on main thread
    import threading

    background_threads = []
    primary = enabled_channels[0]

    for ch in enabled_channels[1:]:
        if ch == "telegram" and (
            config.channels.telegram_token or os.environ.get("TELEGRAM_BOT_TOKEN")
        ):
            from familiar.channels.telegram import TelegramChannel

            tg = TelegramChannel(agent)
            t = threading.Thread(target=tg.run, name="channel-telegram", daemon=True)
            t.start()
            background_threads.append(t)
            logger.info("Background channel started: Telegram")

        elif ch == "discord" and os.environ.get("DISCORD_BOT_TOKEN"):
            from familiar.channels.discord import DiscordChannel

            dc = DiscordChannel(agent)
            t = threading.Thread(target=dc.run, name="channel-discord", daemon=True)
            t.start()
            background_threads.append(t)
            logger.info("Background channel started: Discord")

        elif ch == "matrix" and os.environ.get("MATRIX_HOMESERVER"):
            from familiar.channels.matrix import MatrixChannel

            mx = MatrixChannel(agent)
            t = threading.Thread(target=mx.run, name="channel-matrix", daemon=True)
            t.start()
            background_threads.append(t)
            logger.info("Background channel started: Matrix")

        elif ch == "signal" and os.environ.get("SIGNAL_PHONE"):
            from familiar.channels.signal import SignalChannel

            phone = os.environ["SIGNAL_PHONE"]
            allowed = os.environ.get("SIGNAL_ALLOWED_CONTACTS", "").split(",")
            allowed = [a.strip() for a in allowed if a.strip()]
            sg = SignalChannel(agent, phone=phone, allowed_contacts=allowed)
            t = threading.Thread(target=sg.run, name="channel-signal", daemon=True)
            t.start()
            background_threads.append(t)
            logger.info("Background channel started: Signal")

        elif ch == "whatsapp":
            from familiar.channels.whatsapp import WhatsAppChannel

            wa = WhatsAppChannel(agent)
            t = threading.Thread(target=wa.run, name="channel-whatsapp", daemon=True)
            t.start()
            background_threads.append(t)
            logger.info("Background channel started: WhatsApp")

        elif ch == "github" and os.environ.get("GITHUB_TOKEN"):
            from familiar.channels.github import GitHubChannel

            gh = GitHubChannel(agent)
            t = threading.Thread(target=gh.run, name="channel-github", daemon=True)
            t.start()
            background_threads.append(t)
            logger.info("Background channel started: GitHub")

        elif ch == "slack" and os.environ.get("SLACK_BOT_TOKEN"):
            from familiar.channels.slack import SlackChannel

            sl = SlackChannel(agent)
            t = threading.Thread(target=sl.run, name="channel-slack", daemon=True)
            t.start()
            background_threads.append(t)
            logger.info("Background channel started: Slack")

        elif ch == "cli":
            # CLI can't run in background (it reads stdin)
            logger.info("CLI channel available after primary channel exits")

    # Start primary channel on main thread (blocking)
    if primary == "telegram":
        if not config.channels.telegram_token and not os.environ.get("TELEGRAM_BOT_TOKEN"):
            print("Error: Telegram token not configured")
            print("Set TELEGRAM_BOT_TOKEN environment variable")
            print("Get one from @BotFather on Telegram")
            sys.exit(1)

        from familiar.channels.telegram import TelegramChannel

        telegram = TelegramChannel(agent)
        telegram.run()

    elif primary == "discord":
        if not os.environ.get("DISCORD_BOT_TOKEN"):
            print("Error: Discord token not configured")
            print("Set DISCORD_BOT_TOKEN environment variable")
            print("Get one from https://discord.com/developers/applications")
            sys.exit(1)

        from familiar.channels.discord import DiscordChannel

        discord_channel = DiscordChannel(agent)
        discord_channel.run()

    elif primary == "matrix":
        if not os.environ.get("MATRIX_HOMESERVER"):
            print("Error: Matrix homeserver not configured")
            print("Set MATRIX_HOMESERVER, MATRIX_USER, and MATRIX_PASSWORD or MATRIX_ACCESS_TOKEN")
            sys.exit(1)

        from familiar.channels.matrix import MatrixChannel

        matrix_channel = MatrixChannel(agent)
        matrix_channel.run()

    elif primary == "signal":
        from familiar.channels.signal import SignalChannel

        phone = os.environ.get("SIGNAL_PHONE", "")
        if not phone:
            print("Error: Signal phone not configured")
            print("Set SIGNAL_PHONE environment variable")
            sys.exit(1)
        signal_channel = SignalChannel(agent, phone=phone, allowed_contacts=[])
        signal_channel.run()

    elif primary == "whatsapp":
        from familiar.channels.whatsapp import WhatsAppChannel

        wa = WhatsAppChannel(agent)
        wa.run()

    elif primary == "github":
        if not os.environ.get("GITHUB_TOKEN"):
            print("Error: GitHub token not configured")
            print("Set GITHUB_TOKEN environment variable")
            print("Get one from https://github.com/settings/tokens")
            sys.exit(1)

        from familiar.channels.github import GitHubChannel

        github_channel = GitHubChannel(agent)
        github_channel.run()

    elif primary == "slack":
        if not os.environ.get("SLACK_BOT_TOKEN"):
            print("Error: Slack bot token not configured")
            print("Set SLACK_BOT_TOKEN and SLACK_APP_TOKEN environment variables")
            sys.exit(1)

        from familiar.channels.slack import SlackChannel

        slack_channel = SlackChannel(agent)
        slack_channel.run()

    else:
        # CLI mode
        from familiar.channels.cli import CLIChannel

        cli = CLIChannel(agent)
        cli.run()


if __name__ == "__main__":
    main()
