"""CLI commands — gateway_cmd."""

import asyncio
import os
import platform
import signal
import shutil
import subprocess
import sys
from pathlib import Path

import typer
from rich.console import Console
from rich.table import Table

from flowly import __version__, __logo__

console = Console()

# ============================================================================


def gateway(
    port: int = typer.Option(18790, "--port", "-p", help="Gateway port"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output"),
    persona: str = typer.Option("", "--persona", help="Bot persona (default, jarvis, pirate, samurai, casual, professor, butler, friday)"),
):
    """Start the flowly gateway."""
    from flowly.config.loader import load_config, get_data_dir
    from flowly.bus.queue import MessageBus
    from flowly.providers.litellm_provider import LiteLLMProvider
    from flowly.agent.loop import AgentLoop
    from flowly.channels.manager import ChannelManager
    from flowly.cron.service import CronService
    from flowly.cron.types import CronJob
    from flowly.heartbeat.service import HeartbeatService
    from flowly.gateway.server import GatewayServer

    import logging
    if verbose:
        logging.basicConfig(level=logging.DEBUG)
    else:
        logging.basicConfig(level=logging.WARNING)

    from flowly import __banner__
    console.print(f"[cyan]{__banner__.format(version=__version__)}[/cyan]")
    console.print(f"Starting gateway on port {port}...")

    # Load .env from data dir (contains MOLTBOT_PROXY_JWT_SECRET, API keys, etc.)
    data_dir = get_data_dir()
    env_file = data_dir / ".env"
    if env_file.exists():
        for line in env_file.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            key, _, value = line.partition("=")
            key, value = key.strip(), value.strip()
            if key and key not in os.environ:
                os.environ[key] = value

    config = load_config()

    # Resolve persona: CLI flag overrides config
    active_persona = persona if persona else config.agents.defaults.persona
    if active_persona:
        console.print(f"[dim]Persona: {active_persona}[/dim]")

    # Create components
    bus = MessageBus()

    # Create provider (supports OpenRouter, Anthropic, OpenAI)
    api_key = config.get_api_key()
    api_base = config.get_api_base()

    if not api_key:
        console.print("[red]Error: No API key configured.[/red]")
        console.print("Set one in ~/.flowly/config.json under providers.openrouter.apiKey")
        raise typer.Exit(1)

    provider = LiteLLMProvider(
        api_key=api_key,
        api_base=api_base,
        default_model=config.agents.defaults.model,
        fallback_keys=config.get_fallback_keys(),
        provider_name=config.get_active_provider_name(),
    )

    # Create cron service first (agent needs it)
    cron_store_path = get_data_dir() / "cron" / "jobs.json"
    cron = CronService(cron_store_path)

    # Build compaction config from settings
    from flowly.compaction.types import CompactionConfig, MemoryFlushConfig
    compaction_cfg = config.agents.defaults.compaction
    compaction_config = CompactionConfig(
        mode=compaction_cfg.mode,
        reserve_tokens_floor=compaction_cfg.reserve_tokens_floor,
        max_history_share=compaction_cfg.max_history_share,
        context_window=compaction_cfg.context_window,
        memory_flush=MemoryFlushConfig(
            enabled=compaction_cfg.memory_flush.enabled,
            soft_threshold_tokens=compaction_cfg.memory_flush.soft_threshold_tokens,
            prompt=compaction_cfg.memory_flush.prompt,
            system_prompt=compaction_cfg.memory_flush.system_prompt,
        ),
    )

    # Build exec config
    from flowly.exec.types import ExecConfig
    exec_cfg = config.tools.exec
    exec_config = ExecConfig(
        enabled=exec_cfg.enabled,
        security=exec_cfg.security,
        ask=exec_cfg.ask,
        timeout_seconds=exec_cfg.timeout_seconds,
        max_output_chars=exec_cfg.max_output_chars,
        approval_timeout_seconds=exec_cfg.approval_timeout_seconds,
    )

    # Create agent with cron service
    agent = AgentLoop(
        bus=bus,
        provider=provider,
        workspace=config.workspace_path,
        model=config.agents.defaults.model,
        action_temperature=config.agents.defaults.action_temperature,
        action_tool_retries=config.agents.defaults.action_tool_retries,
        max_iterations=config.agents.defaults.max_tool_iterations,
        brave_api_key=config.tools.web.search.api_key or None,
        cron_service=cron,
        context_messages=config.agents.defaults.context_messages,
        compaction_config=compaction_config,
        exec_config=exec_config,
        trello_config=config.integrations.trello,
        voice_config=config.integrations.voice,
        x_config=config.integrations.x,
        persona=active_persona,
        memory_search_config=config.agents.defaults.memory_search,
        state_dir=get_data_dir(),
        main_config=config,
    )

    # Multi-agent setup (if agents are configured in config.json)
    multi_agents = config.agents.agents
    multi_teams = config.agents.teams

    if multi_agents:
        from flowly.multiagent.router import AgentRouter
        from flowly.multiagent.orchestrator import TeamOrchestrator
        from flowly.multiagent.setup import ensure_agent_directory
        from flowly.agent.tools.delegate import DelegateTool

        ma_router = AgentRouter(multi_agents, multi_teams)
        ma_orchestrator = TeamOrchestrator(ma_router)

        # Setup agent working directories
        agents_workspace = config.workspace_path / "agents"
        for aid, acfg in multi_agents.items():
            agent_dir = agents_workspace / aid
            ensure_agent_directory(agent_dir, aid, multi_agents, multi_teams)

        # Register delegate_to tool on main agent
        delegate_tool = DelegateTool(multi_agents, multi_teams, agents_workspace, bus)
        agent.tools.register(delegate_tool)

        # Wrap _process_message with multi-agent routing
        _original_process = agent._process_message

        async def _routed_process(msg):
            from flowly.bus.events import InboundMessage as _IB, OutboundMessage as _OB

            # Update delegate tool context so background results go to the right chat
            delegate_tool.set_context(msg.channel, msg.chat_id)

            # System messages bypass routing
            if msg.channel == "system":
                return await _original_process(msg)

            # Background delegate result — model should summarize, NOT re-delegate
            if msg.content.startswith("[DELEGATE_RESULT:"):
                # Temporarily remove delegate_to tool to prevent loops
                agent.tools.unregister("delegate_to")
                try:
                    return await _original_process(msg)
                finally:
                    # Restore the tool for future messages
                    agent.tools.register(delegate_tool)

            # Route @mentions
            routing = ma_router.route(msg.content)

            if routing.agent_id == "default" or routing.agent_id not in multi_agents:
                return await _original_process(msg)

            # @mention detected — rewrite message so the main agent uses delegate_to tool
            # This way the model responds naturally AND the task runs in background
            msg.content = (
                f"[SYSTEM: User wants to talk to @{routing.agent_id}. "
                f"Use the delegate_to tool with agent_id=\"{routing.agent_id}\" "
                f"and the following message.]\n\n{routing.message}"
            )
            return await _original_process(msg)

        agent._process_message = _routed_process

        agent_names = [f"@{aid} ({acfg.name})" for aid, acfg in multi_agents.items()]
        console.print(f"[green]✓[/green] Multi-agent: {', '.join(agent_names)}")
        if multi_teams:
            team_names = [f"@{tid} ({tcfg.name})" for tid, tcfg in multi_teams.items()]
            console.print(f"[green]✓[/green] Teams: {', '.join(team_names)}")

    # Set cron job callback (needs agent to be created first)
    async def on_cron_job(job: CronJob) -> str | None:
        """Execute a cron job through the agent."""

        async def _notify_error(error_text: str) -> None:
            """Send an error notification to the user if deliver is configured."""
            channel = job.payload.channel or "telegram"
            to = job.payload.to
            if job.payload.deliver and to:
                from flowly.bus.events import OutboundMessage
                await bus.publish_outbound(OutboundMessage(
                    channel=channel,
                    chat_id=to,
                    content=f"⚠️ Scheduled task '{job.name}' failed:\n{error_text}",
                ))

        def _inject_cron_result_to_session(
            job: CronJob, result: str, *, is_error: bool = False
        ) -> None:
            """Inject cron job result into the user's original session so the
            agent is aware of what happened when the user continues chatting."""
            channel = job.payload.channel or "telegram"
            chat_id = job.payload.to
            if not chat_id:
                return
            session_key = f"{channel}:{chat_id}"
            session = agent.sessions.get_or_create(session_key)
            status = "ERROR" if is_error else "COMPLETED"
            session.add_message(
                "system",
                f"[Cron Job {status}: {job.name}]\n{result}",
            )
            agent.sessions.save(session)

        if job.payload.kind == "tool_call":
            tool_name = job.payload.tool_name
            if not tool_name:
                raise ValueError(f"Cron job '{job.id}' is tool_call but tool_name is missing")

            delivery_channel = job.payload.channel or "telegram"
            delivery_to = job.payload.to

            # Rehydrate tool contexts for direct cron-triggered tool execution.
            if delivery_to:
                for context_tool_name in ("message", "spawn", "cron", "voice_call"):
                    context_tool = agent.tools.get(context_tool_name)
                    if context_tool and hasattr(context_tool, "set_context"):
                        context_tool.set_context(delivery_channel, delivery_to)

            try:
                result = await agent.tools.execute(tool_name, job.payload.tool_args or {})
            except Exception as e:
                err = f"Tool '{tool_name}' raised an exception: {e}"
                logger.error(f"Cron job '{job.name}' tool_call failed: {e}")
                _inject_cron_result_to_session(job, err, is_error=True)
                await _notify_error(err)
                return f"__error__:{err}"

            is_error = bool(result and result.startswith("Error"))

            # Inject result into user's session so agent knows what happened
            _inject_cron_result_to_session(job, result or "✓ Done.", is_error=is_error)

            if job.payload.deliver and delivery_to:
                from flowly.bus.events import OutboundMessage
                await bus.publish_outbound(OutboundMessage(
                    channel=delivery_channel,
                    chat_id=delivery_to,
                    content=result or "✓ Done.",
                ))

            return result

        # Build the prompt - if delivery is requested, tell agent to return plain text
        prompt = job.payload.message

        if job.payload.deliver:
            channel = job.payload.channel or "telegram"
            if job.payload.to:
                # Tell agent to return plain text — we handle delivery automatically
                cron_prompt = (
                    f"[Scheduled Task: {job.name}]\n"
                    f"{job.payload.message}\n\n"
                    f"Return your response as plain text. Do NOT use the message tool — "
                    f"your response will be delivered automatically. "
                    f"If any tool fails or returns an error, include the error details in your response."
                )
                try:
                    response = await agent.process_direct(cron_prompt, session_key=f"cron:{job.id}")
                except Exception as e:
                    err = f"Agent execution failed: {e}"
                    logger.error(f"Cron job '{job.name}' agent_turn failed: {e}")
                    await _notify_error(err)
                    return f"__error__:{err}"

                if response:
                    from flowly.bus.events import OutboundMessage
                    await bus.publish_outbound(OutboundMessage(
                        channel=channel,
                        chat_id=job.payload.to,
                        content=response,
                    ))

                return response
            else:
                # No specific target - ask agent to use message tool
                prompt = (
                    f"[Scheduled Task: {job.name}]\n"
                    f"{job.payload.message}\n\n"
                    f"After completing the task, send the result to the user using the message tool. "
                    f"If any tool fails or returns an error, report it clearly in your message."
                )

        try:
            response = await agent.process_direct(prompt, session_key=f"cron:{job.id}")
        except Exception as e:
            err = f"Agent execution failed: {e}"
            logger.error(f"Cron job '{job.name}' agent_turn failed: {e}")
            await _notify_error(err)
            return f"__error__:{err}"

        return response

    cron.on_job = on_cron_job

    # Create heartbeat service — reads config from agents.defaults.heartbeat
    hb_cfg = config.agents.defaults.heartbeat

    async def on_heartbeat(prompt: str) -> str:
        """Execute heartbeat through the agent with a fresh isolated session each tick."""
        # Clear previous heartbeat history so stale context doesn't bleed into this tick.
        hb_session = agent.sessions.get_or_create("heartbeat:tick")
        hb_session.clear()
        agent.sessions.save(hb_session)
        return await agent.process_direct(prompt, session_key="heartbeat:tick")

    active_hours_dict: dict[str, str] | None = None
    if hb_cfg.active_hours:
        active_hours_dict = {
            "start": hb_cfg.active_hours.start,
            "end": hb_cfg.active_hours.end,
            "timezone": hb_cfg.active_hours.timezone,
        }

    heartbeat = HeartbeatService(
        workspace=config.workspace_path,
        on_heartbeat=on_heartbeat,
        interval_s=hb_cfg.every_minutes * 60,
        enabled=hb_cfg.enabled,
        active_hours=active_hours_dict,
        deliver=hb_cfg.deliver,
    )

    # Create channel manager
    channels = ChannelManager(config, bus)

    # Set up compact/clear callbacks for channels + gateway
    async def on_compact(session_key: str, instructions: str | None = None) -> dict:
        """Handle /compact command from channels or desktop."""
        return await agent.compact_session(session_key, instructions)

    async def on_clear(session_key: str) -> dict:
        """Handle /clear command — clear session history."""
        session = agent.sessions.get_or_create(session_key)
        msg_count = len(session.messages)
        session.clear()
        agent.sessions.save(session)
        return {"success": True, "message": f"Cleared {msg_count} messages"}

    channels.set_compact_callback(on_compact)

    # Legacy bridge fallback (disabled by default; integrated Python plugin is official path)
    legacy_voice_bridge_enabled = bool(config.integrations.voice.legacy_bridge_enabled)

    # Create gateway API callback for legacy voice bridge
    async def on_voice_message(call_sid: str, from_number: str, text: str) -> str:
        """Handle voice message from voice bridge."""
        # Use Telegram session if configured, otherwise use voice-specific session
        telegram_chat_id = config.integrations.voice.telegram_chat_id
        if telegram_chat_id:
            session_key = f"telegram:{telegram_chat_id}"
        else:
            session_key = f"voice:{call_sid}"

        # Format message with clear voice context
        prompt = f"""[ACTIVE PHONE CALL]
Call SID: {call_sid}
Caller: {from_number}

User said: "{text}"

IMPORTANT RULES:
1. This is a phone call — the user can only hear what you say.
2. Only use safe tools if needed (voice_call end/list, screenshot, message, system).
3. If you take a screenshot, it goes to Telegram automatically — tell the user "I sent the screenshot to Telegram".
4. To hang up: voice_call(action="end_call", call_sid="{call_sid}", message="Goodbye!")
5. Keep it short and clear — long sentences are hard to understand on the phone.

Respond to the user now:"""
        response = await agent.process_direct(prompt, session_key=session_key)
        return response or "Sorry, something went wrong. Could you say that again?"

    async def on_cron_run(job_id: str, force: bool) -> bool:
        return await cron.run_job(job_id, force=force)

    async def on_chat_message(
        session_key: str,
        message: str,
        run_id: str,
        stream_callback=None,
        media: list[str] | None = None,
    ) -> str:
        return await agent.process_direct(
            content=message,
            session_key=session_key,
            stream_callback=stream_callback,
            media=media,
        )

    # Artifact store for gateway
    artifact_store = None
    if config.tools.artifact.enabled:
        from flowly.artifacts.store import get_store as get_artifact_store
        artifact_store = get_artifact_store(get_data_dir())

    gateway_server = GatewayServer(
        host=config.gateway.host,
        port=port,
        on_voice_message=on_voice_message if legacy_voice_bridge_enabled else None,
        on_cron_run=on_cron_run,
        on_cron_reload=cron.reload,
        on_chat_message=on_chat_message,
        sessions=agent.sessions,
        subagent_registry=agent._subagent_registry,
        artifact_store=artifact_store,
        on_compact=on_compact,
        on_clear=on_clear,
    )

    # Wire browser_tab tool to gateway server
    agent.set_gateway_server(gateway_server)

    # Wire artifact broadcast callback — push to desktop (gateway) AND relay (web channel)
    if artifact_store:
        artifact_tool = agent.tools.get("artifact")
        if artifact_tool:
            async def _broadcast_artifact(event_name: str, data: dict) -> None:
                # Desktop clients (direct WS)
                await gateway_server._broadcast_artifact_event(event_name, data)
                # Relay (web channel) — so relay can sync to S3 + Firestore
                web = channels.get_channel("web")
                if web and hasattr(web, "_ws") and web._ws:
                    import json as _json
                    try:
                        await web._ws.send(_json.dumps({
                            "type": "event",
                            "event": event_name,
                            "data": data,
                        }))
                    except Exception:
                        pass  # Non-critical — relay sync is best-effort
            artifact_tool.set_on_change(_broadcast_artifact)

    # Wire auto-compaction notification — push to relay (web channel) + desktop (gateway)
    async def _on_auto_compaction(
        session_key: str, tokens_before: int, tokens_after: int, messages_removed: int,
        phase: str = "completed",
    ) -> None:
        data = {
            "phase": phase,
            "tokensBefore": tokens_before,
            "tokensAfter": tokens_after,
            "messagesRemoved": messages_removed,
            "sessionKey": session_key,
        }
        # Relay (web channel)
        web = channels.get_channel("web")
        if web and hasattr(web, "send_compaction_event"):
            await web.send_compaction_event(session_key, tokens_before, tokens_after, messages_removed, phase)
        # Desktop clients (direct WS)
        await gateway_server._broadcast_compaction_event(data)

    agent._on_compaction = _on_auto_compaction

    if channels.enabled_channels:
        console.print(f"[green]✓[/green] Channels enabled: {', '.join(channels.enabled_channels)}")
    else:
        console.print("[yellow]Warning: No channels enabled[/yellow]")

    cron_status = cron.status()
    if cron_status["jobs"] > 0:
        console.print(f"[green]✓[/green] Cron: {cron_status['jobs']} scheduled jobs")

    console.print(f"[green]✓[/green] Heartbeat: every 30m")
    if legacy_voice_bridge_enabled:
        console.print("[yellow]⚠[/yellow] Legacy voice bridge fallback enabled")

    # Initialize voice plugin if enabled
    voice_plugin = None
    if config.integrations.voice.enabled:
        voice_cfg = config.integrations.voice
        if voice_cfg.twilio_account_sid and voice_cfg.twilio_auth_token:
            try:
                from flowly.voice.plugin import VoicePlugin
                voice_plugin = VoicePlugin(config, agent)
                # Connect voice plugin to agent's voice tool
                agent.set_voice_plugin(voice_plugin)
                console.print(f"[green]✓[/green] Voice calls: initializing...")
            except Exception as e:
                console.print(f"[yellow]Warning: Voice plugin failed to initialize: {e}[/yellow]")
        else:
            console.print(f"[yellow]Warning: Voice enabled but Twilio credentials not configured[/yellow]")

    console.print(f"[green]✓[/green] API: http://{config.gateway.host}:{port}")

    async def run():
        shutdown_event = asyncio.Event()

        def signal_handler():
            console.print("\n[yellow]Shutting down...[/yellow]")
            shutdown_event.set()

        if platform.system() == "Windows":
            # Windows asyncio doesn't support loop.add_signal_handler
            signal.signal(signal.SIGINT, lambda s, f: signal_handler())
            signal.signal(signal.SIGTERM, lambda s, f: signal_handler())
        else:
            loop = asyncio.get_running_loop()
            for sig in (signal.SIGINT, signal.SIGTERM):
                loop.add_signal_handler(sig, signal_handler)

        try:
            await gateway_server.start()
            await cron.start()
            await heartbeat.start(run_on_start=True)

            # Start voice plugin if available
            if voice_plugin:
                await voice_plugin.start(host="0.0.0.0", port=8765)
                if voice_plugin._ngrok_tunnel:
                    console.print(f"[green]✓[/green] Voice calls: ngrok tunnel ({voice_plugin._webhook_base_url})")
                else:
                    console.print(f"[green]✓[/green] Voice calls: integrated ({voice_plugin._webhook_base_url})")

            # Connect exec approval manager to channels and gateway
            from flowly.exec.approval_manager import get_approval_manager
            _approval_mgr = get_approval_manager()

            async def _notify_approval(pending) -> None:
                """Notify all channels about a pending exec approval."""
                import time as _time
                timeout_s = max(1, int(pending.expires_at - _time.time()))

                # Telegram: send inline buttons
                tg = channels.get_channel("telegram")
                if tg and hasattr(tg, "send_approval_prompt"):
                    # Determine which chat_id to send to
                    session_key = pending.session_key or ""
                    if session_key.startswith("telegram:"):
                        try:
                            chat_id = int(session_key.split(":", 1)[1])
                            await tg.send_approval_prompt(chat_id, pending.id, pending.request.command, timeout_s)
                        except (ValueError, IndexError):
                            pass

                # Web channel (relay): push to iOS/browser clients
                web = channels.get_channel("web")
                if web and hasattr(web, "send_approval_event"):
                    session_key = pending.session_key or ""
                    if session_key.startswith("web:"):
                        await web.send_approval_event(
                            session_key, pending.id, pending.request.command, pending.expires_at,
                        )

                # Gateway: push event to all desktop clients (local WS)
                await gateway_server.broadcast_approval_request(
                    pending.id, pending.request.command, pending.session_key, pending.expires_at,
                )

            _approval_mgr.add_notify_callback(_notify_approval)

            # Run until shutdown signal
            async def run_until_shutdown():
                await asyncio.gather(
                    agent.run(),
                    channels.start_all(),
                )

            # Create main task
            main_task = asyncio.create_task(run_until_shutdown())

            # Wait for either shutdown signal or task completion
            done, pending = await asyncio.wait(
                [main_task, asyncio.create_task(shutdown_event.wait())],
                return_when=asyncio.FIRST_COMPLETED
            )

            # Cancel pending tasks
            for task in pending:
                task.cancel()
                try:
                    await task
                except asyncio.CancelledError:
                    pass

        finally:
            # Graceful shutdown
            console.print("[dim]Cleaning up...[/dim]")
            if voice_plugin:
                await voice_plugin.stop()
            await gateway_server.stop()
            heartbeat.stop()
            cron.stop()
            agent.stop()
            await channels.stop_all()
            console.print("[green]✓[/green] Shutdown complete")

    asyncio.run(run())




