"""
Starlette-based web server for the Suzent AI agent application.

This server provides a REST API with the following endpoints:
- /chat: Stream agent responses via SSE
- /chat/stop: Stop active streaming sessions
- /config: Get application configuration
- /plans: List plan versions for a chat
- /plan: Get current plan and history
- /chats: List, create, update, and delete chats

The application uses modular routing with separated concerns for maintainability.
"""

import asyncio
import os
import sys
from contextlib import asynccontextmanager
from dotenv import load_dotenv
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from starlette.routing import Route, WebSocketRoute

from suzent.logger import get_logger, setup_logging
from suzent.routes.chat_routes import (
    approve_tool,
    chat,
    chat_send,
    create_chat,
    deactivate_tool,
    delete_chat,
    get_chat,
    get_chats,
    get_permission_mode,
    live_stream,
    mark_chat_read,
    retry_chat,
    steer_chat,
    steer_chat_send,
    set_permission_mode,
    stop_chat,
    update_chat,
)
from suzent.routes.compact_routes import compact_chat
from suzent.routes.chatgpt_routes import (
    get_chatgpt_status,
    logout_chatgpt,
    start_chatgpt_login,
)
from suzent.routes.commands_routes import get_commands
from suzent.routes.project_routes import (
    create_project,
    delete_project,
    list_projects,
    move_all_chats,
    move_chat_to_project,
    update_project,
)
from suzent.routes.permission_routes import (
    create_permission_rule,
    delete_permission_rule,
    get_chat_permission_state,
    get_permissions,
)
from suzent.routes.config_routes import (
    get_api_keys_status,
    get_config,
    get_embedding_models,
    save_api_keys,
    save_preferences,
    save_global_sandbox_config,
    verify_provider,
    get_social_config,
    save_social_config,
    get_global_cost,
    get_chat_cost,
    get_daily_cost,
    get_hourly_cost,
    get_activity_grid,
    get_models_cost,
    get_activity_cost,
    get_role_models,
    save_role_models,
    get_role_suggestions,
    save_custom_provider,
    delete_custom_provider,
    sync_capabilities,
)
from suzent.routes.sync_routes import (
    create_sync_profile,
    disable_secret_sync,
    enable_secret_sync,
    get_github_auth_status,
    get_sync_ahead_behind,
    get_sync_profiles,
    get_sync_quickstart_info,
    get_sync_status,
    lock_shibboleth,
    logout_github_auth,
    poll_github_auth,
    pull_sync,
    push_sync,
    quickstart_sync,
    generate_mnemonic,
    register_device_mnemonic,
    resolve_conflicts_agent,
    rotate_mnemonic,
    run_auto_sync,
    save_auto_config,
    start_github_auth,
    stop_conflict_resolution,
    unlock_mnemonic,
    unlock_shibboleth,
    validate_sync_profile,
)
from suzent.routes.goal_task_routes import (
    get_project_goal,
    update_project_goal,
    get_project_tasks,
    get_project_kanban,
    create_project_task,
    update_project_task,
    delete_project_task,
)
from suzent.routes.mcp_routes import (
    list_mcp_servers,
    add_mcp_server,
    update_mcp_server,
    remove_mcp_server,
    set_mcp_server_enabled,
    test_mcp_server,
)
from suzent.routes.memory_routes import (
    get_core_memory,
    update_core_memory_block,
    search_archival_memory,
    delete_archival_memory,
    get_memory_stats,
    consolidate_memory,
    lint_memory,
    get_dream_status,
)
from suzent.routes.sandbox_routes import (
    list_sandbox_files,
    search_file_mentions,
    read_sandbox_file,
    write_sandbox_file,
    delete_sandbox_file,
    serve_sandbox_file,
    serve_sandbox_file_wildcard,
    upload_files,
)
from suzent.routes.skill_routes import get_skills, reload_skills, toggle_skill
from suzent.routes.system_routes import list_host_files, open_in_explorer
from suzent.routes.session_routes import (
    get_session_transcript,
    get_session_state,
    get_memory_daily_log,
    list_memory_daily_logs,
    get_memory_file,
    reindex_memories,
)
from suzent.routes.browser_routes import browser_websocket_endpoint
from suzent.routes.node_routes import (
    node_websocket_endpoint,
    list_nodes,
    describe_node,
    invoke_node_command,
)
from suzent.routes.cron_routes import (
    list_cron_jobs,
    create_cron_job,
    update_cron_job,
    delete_cron_job,
    trigger_cron_job,
    get_cron_status,
    get_cron_notifications,
    get_cron_job_runs,
    install_cron_presets,
)
from suzent.routes.heartbeat_routes import (
    get_heartbeat_status,
    enable_heartbeat,
    disable_heartbeat,
    trigger_heartbeat,
    get_heartbeat_md,
    save_heartbeat_md,
    set_heartbeat_interval,
    get_heartbeat_global_config,
    save_heartbeat_global_config,
)
from suzent.routes.a2ui_routes import a2ui_action, a2ui_answer
from suzent.routes.subagent_routes import (
    list_active_subagents,
    list_subagents,
    get_subagent,
    stop_subagent_route,
    clear_stuck_subagents_route,
    stream_subagents,
)
from suzent.routes.event_bus_routes import event_bus_stream
from suzent.channels.manager import ChannelManager
from suzent.nodes.manager import NodeManager

from suzent.core.social_brain import SocialBrain
from suzent.core.scheduler import SchedulerBrain
from suzent.core.heartbeat import HeartbeatRunner
from suzent.sync.automation import SyncAutomationRunner
from suzent.sync.service import GitHubSyncService
from suzent.config import PROJECT_DIR as _project_dir

load_dotenv(_project_dir / ".env")

# Ensure stdout/stderr use UTF-8 on Windows
try:
    if hasattr(sys.stdout, "reconfigure"):
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    if hasattr(sys.stderr, "reconfigure"):
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass

# Setup logging
if "--debug" in sys.argv:
    os.environ["LOG_LEVEL"] = "DEBUG"

log_level = os.getenv("LOG_LEVEL", "INFO")
log_file = os.getenv("LOG_FILE")
setup_logging(level=log_level, log_file=log_file)

logger = get_logger(__name__)

social_brain: SocialBrain = None
channel_manager: ChannelManager = None
node_manager: NodeManager = None
scheduler_brain: SchedulerBrain = None
heartbeat_runner: HeartbeatRunner = None
sync_automation_runner: SyncAutomationRunner = None


_social_reload_lock = asyncio.Lock()


def _build_social_from_config(
    social_config: dict,
) -> tuple["ChannelManager", "SocialBrain"]:
    cm = ChannelManager()
    cm.load_drivers_from_config(social_config)

    allowed_users = set(social_config.get("allowed_users", []))
    env_allowed = os.environ.get("ALLOWED_SOCIAL_USERS", "")
    if env_allowed:
        allowed_users.update([u.strip() for u in env_allowed.split(",") if u.strip()])

    platform_allowlists = {}
    for platform, settings in social_config.items():
        if isinstance(settings, dict) and "allowed_users" in settings:
            platform_allowlists[platform] = settings.get("allowed_users", [])

    handshake_cfg = social_config.get("handshake", {})
    sb = SocialBrain(
        cm,
        allowed_users=list(allowed_users),
        platform_allowlists=platform_allowlists,
        model=social_config.get("model"),
        memory_enabled=social_config.get("memory_enabled", True),
        tools=social_config.get("tools"),
        mcp_enabled=social_config.get("mcp_enabled"),
        handshake_enabled=handshake_cfg.get("enabled", False),
        handshake_greeting=handshake_cfg.get("greeting"),
    )
    return cm, sb


async def reload_social(starlette_app, social_config: dict) -> None:
    """Stop the current social stack and restart it from the given config."""
    global social_brain, channel_manager

    async with _social_reload_lock:
        if social_brain is not None:
            await _stop(social_brain.stop(), "SocialBrain")
        if channel_manager is not None:
            await _stop(channel_manager.stop_all(), "ChannelManager")

        new_cm, new_sb = _build_social_from_config(social_config)
        social_brain = new_sb
        channel_manager = new_cm
        starlette_app.state.social_brain = new_sb

    try:
        await new_cm.start_all()
        await new_sb.start()
        logger.info("Social stack reloaded successfully.")
    except Exception as e:
        logger.error(f"Error starting reloaded social stack: {e}")


# ---------------------------------------------------------------------------
# Pairing / handshake REST handlers
# ---------------------------------------------------------------------------


async def list_pairings(request: Request) -> JSONResponse:
    """GET /social/pairing — list pending pairing requests."""
    brain: SocialBrain = getattr(request.app.state, "social_brain", None)
    if brain is None:
        return JSONResponse({"pairings": []})
    return JSONResponse({"pairings": brain.list_pairings()})


async def _parse_pairing_request(request: Request):
    """Return (brain, token, None) on success, or (None, None, error_response) on failure."""
    brain: SocialBrain = getattr(request.app.state, "social_brain", None)
    if brain is None:
        return (
            None,
            None,
            JSONResponse({"error": "Social brain not running"}, status_code=503),
        )
    try:
        data = await request.json()
        token = str(data.get("token", "")).strip()
    except Exception:
        return None, None, JSONResponse({"error": "Invalid payload"}, status_code=400)
    if not token:
        return None, None, JSONResponse({"error": "token is required"}, status_code=400)
    return brain, token, None


async def approve_pairing(request: Request) -> JSONResponse:
    """POST /social/pairing/approve  body: {"token": "..."}"""
    brain, token, err = await _parse_pairing_request(request)
    if err is not None:
        return err
    ok = await brain.approve_by_token(token)
    if not ok:
        return JSONResponse({"error": "Invalid or expired token"}, status_code=404)
    return JSONResponse({"success": True})


async def deny_pairing(request: Request) -> JSONResponse:
    """POST /social/pairing/deny  body: {"token": "..."}"""
    brain, token, err = await _parse_pairing_request(request)
    if err is not None:
        return err
    ok = await brain.deny_by_token(token)
    if not ok:
        return JSONResponse({"error": "Invalid or expired token"}, status_code=404)
    return JSONResponse({"success": True})


async def _refresh_provider_models() -> None:
    """Background task: discover available models for all configured providers."""
    await asyncio.sleep(3)  # let server finish starting up

    try:
        from suzent.core.providers import ProviderFactory, PROVIDER_REGISTRY
        from suzent.core.providers.helpers import resolve_api_key
        from suzent.core.model_registry import (
            save_discovered_models,
            get_model_registry,
        )

        refreshed = 0
        for spec in PROVIDER_REGISTRY:
            if not spec.env_keys:
                continue
            if not resolve_api_key(spec.id):
                continue
            try:
                provider = ProviderFactory.get_provider(spec.id, {})
                models = await provider.list_models()
                if models:
                    save_discovered_models(spec.id, [m.id for m in models])
                    refreshed += 1
            except Exception:
                pass  # credentials invalid or network unreachable — skip silently

        if refreshed:
            get_model_registry().reload()
            logger.info(
                "Background model refresh completed for {} provider(s)", refreshed
            )

        # Pull capability metadata from LiteLLM after model discovery
        try:
            from suzent.core.model_registry import sync_from_litellm

            await sync_from_litellm()
            get_model_registry().reload()
        except Exception as exc:
            logger.debug("LiteLLM capability sync skipped: {}", exc)
    except Exception as exc:
        logger.debug("Background model refresh failed: {}", exc)


async def startup():
    """Initialize services on application startup."""
    from suzent.memory.lifecycle import init_memory_system, _memory_rag_hook
    from suzent.core.system_reminder import register_global_hook, register_per_turn_hook
    from suzent.skills.hooks import skills_reminder_hook
    from suzent.tools.plan_hooks import plan_reminder_hook
    from suzent.database import get_database

    logger.info("Application startup - initializing services")

    try:
        from genai_prices import UpdatePrices

        UpdatePrices().start()
        logger.info("genai-prices background updater started")
    except Exception as e:
        logger.warning(f"Failed to start genai-prices updater: {e}")

    register_global_hook(skills_reminder_hook)
    register_global_hook(plan_reminder_hook)
    register_per_turn_hook(_memory_rag_hook)

    from suzent.tools.browsing_tool import BrowserSessionManager
    from suzent.config import CONFIG

    try:
        CONFIG.reload()
    except Exception as e:
        logger.error(f"Failed to reload config: {e}")

    try:
        BrowserSessionManager.get_instance().set_main_loop(asyncio.get_running_loop())
    except Exception as e:
        logger.error(f"Failed to set browser session loop: {e}")

    try:
        from suzent.core.mcp_store import migrate_from_db

        migrated = migrate_from_db()
        if migrated:
            logger.info(
                f"Migrated {migrated} MCP servers from database to mcp_servers.json"
            )
    except Exception as e:
        logger.warning(f"MCP store migration failed: {e}")

    db = get_database()
    try:
        # Load all stored secrets into os.environ via SecretManager
        from suzent.core.secrets import get_secret_manager

        sm = get_secret_manager()
        loaded_count = sm.inject_all_to_env()

        # Also load non-secret config blobs (e.g. _PROVIDER_CONFIG_)
        api_keys = db.get_api_keys() or {}
        for key, value in api_keys.items():
            if key.startswith("_") and value and key not in os.environ:
                os.environ[key] = value

        if loaded_count > 0:
            logger.info(f"Loaded {loaded_count} secrets via {sm.backend_name}")
    except Exception as e:
        logger.error(f"Failed to load secrets on startup: {e}")
        # Fallback: load non-secret config blobs only.
        try:
            api_keys = db.get_api_keys() or {}
            for key, value in api_keys.items():
                if key.startswith("_") and value:
                    os.environ[key] = value
        except Exception:
            pass

    async def init_background_services(cm, sb):
        """Run heavy initialization tasks (memory, channels, social, scheduler) in background.

        Each subsystem is isolated: memory init must not be skipped or blocked by a
        social-messaging failure (and vice versa). cm/sb may be None when social
        config failed to build — that's fine, memory and the rest still start.
        """
        try:
            await init_memory_system()
        except Exception as e:
            logger.error(f"Failed to initialize memory system: {e}")
        try:
            if cm is not None:
                await cm.start_all()
            if sb is not None:
                await sb.start()
            logger.info("Background services started successfully")
        except Exception as e:
            logger.error(f"Failed to start background services: {e}")

        # Start scheduler
        global scheduler_brain
        try:
            scheduler_brain = SchedulerBrain(tick_interval=30.0)
            app.state.scheduler_brain = scheduler_brain
            await scheduler_brain.start()
            logger.info("SchedulerBrain started successfully")
        except Exception as e:
            logger.error(f"Failed to start SchedulerBrain: {e}")

        # Start heartbeat
        global heartbeat_runner
        try:
            heartbeat_runner = HeartbeatRunner(interval_minutes=1)
            app.state.heartbeat_runner = heartbeat_runner
            await heartbeat_runner.start()
        except Exception as e:
            logger.error(f"Failed to start HeartbeatRunner: {e}")

        global sync_automation_runner
        try:
            app.state.github_sync_service = GitHubSyncService()
            sync_automation_runner = SyncAutomationRunner(app.state.github_sync_service)
            app.state.sync_automation_runner = sync_automation_runner
            await sync_automation_runner.start()
        except Exception as e:
            logger.error(f"Failed to start SyncAutomationRunner: {e}")

    global node_manager
    node_manager = NodeManager()
    app.state.node_manager = node_manager

    try:
        from suzent.nodes.local_node import LocalNode

        local_node = LocalNode(display_name="Local PC")
        node_manager.register_node(local_node)
        cap_names = ", ".join(c.name for c in local_node.capabilities)
        logger.info(f"Local node registered: {cap_names}")
    except Exception as e:
        logger.warning(f"Failed to register local node: {e}")

    logger.info("Node system initialized")

    global social_brain, channel_manager
    if social_brain is not None:
        logger.warning("Social brain already initialized, skipping duplicate startup.")
        return

    # Build social objects best-effort. A failure here must NOT prevent the
    # background services (memory, scheduler, heartbeat, sync) from starting —
    # so the launch below is unconditional and outside this try.
    try:
        import json
        from suzent.config import PROJECT_DIR

        config_path = PROJECT_DIR / "config/social.json"
        social_config = {}

        if config_path.exists():
            try:
                with open(config_path, "r") as f:
                    social_config = json.load(f)
                logger.info(f"Loaded social config from {config_path}")
            except Exception as e:
                logger.error(f"Failed to load social config: {e}")

        channel_manager, social_brain = _build_social_from_config(social_config)
        app.state.social_brain = social_brain

    except Exception as e:
        logger.error(f"Failed to initialize Social Messaging: {e}")

    # Launch heavy background init regardless of social outcome. cm/sb may be
    # None — init_background_services tolerates that.
    asyncio.create_task(init_background_services(channel_manager, social_brain))

    # Silently refresh model lists for all configured providers in the background.
    asyncio.create_task(_refresh_provider_models())


def ensure_app_data():
    """Ensure required user data directories exist."""
    from suzent.config import (
        CACHE_DIR,
        DATA_DIR,
        EXTERNAL_SKILLS_DIR,
        OFFICIAL_SKILLS_DIR,
        RUNTIME_DIR,
        SKILLS_ROOT_DIR,
        USER_CONFIG_DIR,
        USER_SKILLS_DIR,
    )

    # Redirect LiteLLM's ChatGPT token storage into Suzent's own config dir.
    chatgpt_token_dir = USER_CONFIG_DIR / "chatgpt"
    os.environ.setdefault("CHATGPT_TOKEN_DIR", str(chatgpt_token_dir))

    print("INFO: Starting App Data Verification...", flush=True)

    for target in [
        DATA_DIR,
        RUNTIME_DIR,
        CACHE_DIR,
        USER_CONFIG_DIR,
        SKILLS_ROOT_DIR,
        OFFICIAL_SKILLS_DIR,
        USER_SKILLS_DIR,
        EXTERNAL_SKILLS_DIR,
    ]:
        if target.exists():
            logger.debug(f"Directory exists: {target}")
        else:
            logger.warning(f"Expected directory missing: {target}")
            # Attempt to create it so the app doesn't crash
            try:
                target.mkdir(parents=True, exist_ok=True)
                logger.info(f"Created empty directory: {target}")
            except Exception as e:
                logger.error(f"Failed to create directory {target}: {e}")

    print("INFO: App Data Verification Complete.", flush=True)


async def _stop(coro, name: str, timeout: float = 5.0):
    try:
        await asyncio.wait_for(coro, timeout=timeout)
    except asyncio.TimeoutError:
        logger.warning(f"{name} did not stop within {timeout}s, continuing shutdown")
    except Exception as e:
        logger.error(f"Error stopping {name}: {e}")


async def shutdown():
    """Cleanup services on application shutdown."""
    from suzent.memory.lifecycle import shutdown_memory_system
    from suzent.a2ui import pending as pending_questions

    logger.info("Application shutdown - cleaning up services")

    global \
        social_brain, \
        channel_manager, \
        node_manager, \
        scheduler_brain, \
        heartbeat_runner, \
        sync_automation_runner

    # Cancel any pending ask_question futures so their tasks can exit cleanly
    pending_questions.cancel_all()

    if heartbeat_runner:
        await _stop(heartbeat_runner.stop(), "HeartbeatRunner")

    if sync_automation_runner:
        await _stop(sync_automation_runner.stop(), "SyncAutomationRunner")

    if scheduler_brain:
        await _stop(scheduler_brain.stop(), "SchedulerBrain")

    if social_brain:
        await _stop(social_brain.stop(), "SocialBrain")

    if channel_manager:
        await _stop(channel_manager.stop_all(), "ChannelManager")

    if node_manager:
        for node in list(node_manager.nodes.values()):
            if hasattr(node, "close"):
                try:
                    await node.close()
                except Exception:
                    pass
            node_manager.unregister_node(node.node_id)
        logger.info("Node system shut down")

    try:
        from suzent.core.task_registry import get_task_registry

        await get_task_registry().shutdown(timeout=3.0)
    except Exception as e:
        logger.error(f"Error shutting down task registry: {e}")

    await shutdown_memory_system()

    try:
        from suzent.tools.browsing_tool import BrowserSessionManager

        await _stop(
            BrowserSessionManager.get_instance().close_session(), "BrowserSession"
        )
    except Exception as e:
        logger.error(f"Error shutting down browser session: {e}")

    # Gracefully shut down litellm's logging worker to avoid "Event loop is closed" noise
    try:
        from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER

        task = GLOBAL_LOGGING_WORKER._worker_task
        if task and not task.done():
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
            except Exception:
                pass
    except Exception:
        pass


@asynccontextmanager
async def lifespan(app):
    await startup()
    yield
    await shutdown()


app = Starlette(
    debug=True,
    lifespan=lifespan,
    routes=[
        Route("/chat", chat, methods=["POST"]),
        Route("/chat/send", chat_send, methods=["POST"]),
        Route("/chat/live", live_stream, methods=["POST"]),
        Route("/chat/stop", stop_chat, methods=["POST"]),
        Route("/chat/steer", steer_chat, methods=["POST"]),
        Route("/chat/steer-send", steer_chat_send, methods=["POST"]),
        Route("/chat/approve-tool", approve_tool, methods=["POST"]),
        Route("/chat/deactivate-tool", deactivate_tool, methods=["POST"]),
        Route("/chat/compact", compact_chat, methods=["POST"]),
        Route("/chat/retry", retry_chat, methods=["POST"]),
        Route("/commands", get_commands, methods=["GET"]),
        Route("/chats", get_chats, methods=["GET"]),
        Route("/chats", create_chat, methods=["POST"]),
        Route("/chats/{chat_id}/mark-read", mark_chat_read, methods=["POST"]),
        Route(
            "/chats/{chat_id}/permission-mode",
            get_permission_mode,
            methods=["GET"],
        ),
        Route(
            "/chats/{chat_id}/permission-mode",
            set_permission_mode,
            methods=["PUT"],
        ),
        Route(
            "/chats/{chat_id}/permission-state",
            get_chat_permission_state,
            methods=["GET"],
        ),
        Route("/chats/{chat_id}/project", move_chat_to_project, methods=["POST"]),
        Route("/chats/{chat_id}", get_chat, methods=["GET"]),
        Route("/chats/{chat_id}", update_chat, methods=["PUT"]),
        Route("/chats/{chat_id}", delete_chat, methods=["DELETE"]),
        Route("/permissions", get_permissions, methods=["GET"]),
        Route("/permissions/rules", create_permission_rule, methods=["POST"]),
        Route(
            "/permissions/rules/{rule_id}",
            delete_permission_rule,
            methods=["DELETE"],
        ),
        Route("/projects", list_projects, methods=["GET"]),
        Route("/projects", create_project, methods=["POST"]),
        Route("/projects/{project_id}", update_project, methods=["PATCH"]),
        Route("/projects/{project_id}", delete_project, methods=["DELETE"]),
        Route("/projects/{project_id}/move-chats", move_all_chats, methods=["POST"]),
        Route("/project/goal", get_project_goal, methods=["GET"]),
        Route("/project/goal/action", update_project_goal, methods=["POST"]),
        Route("/project/tasks", get_project_tasks, methods=["GET"]),
        Route("/project/kanban", get_project_kanban, methods=["GET"]),
        Route("/project/tasks", create_project_task, methods=["POST"]),
        Route("/project/tasks/{task_id:int}", update_project_task, methods=["PATCH"]),
        Route("/project/tasks/{task_id:int}", delete_project_task, methods=["DELETE"]),
        Route("/config", get_config, methods=["GET"]),
        Route("/chatgpt/status", get_chatgpt_status, methods=["GET"]),
        Route("/chatgpt/login", start_chatgpt_login, methods=["POST"]),
        Route("/chatgpt/logout", logout_chatgpt, methods=["POST"]),
        Route("/preferences", save_preferences, methods=["POST"]),
        Route("/config/sandbox-global", save_global_sandbox_config, methods=["POST"]),
        Route("/config/api-keys", get_api_keys_status, methods=["GET"]),
        Route("/config/api-keys", save_api_keys, methods=["POST"]),
        Route(
            "/config/providers/{provider_id}/verify", verify_provider, methods=["POST"]
        ),
        Route("/config/embedding-models", get_embedding_models, methods=["GET"]),
        Route("/config/cost/global", get_global_cost, methods=["GET"]),
        Route("/config/cost/daily", get_daily_cost, methods=["GET"]),
        Route("/config/cost/hourly", get_hourly_cost, methods=["GET"]),
        Route("/config/cost/activity-grid", get_activity_grid, methods=["GET"]),
        Route("/config/cost/models", get_models_cost, methods=["GET"]),
        Route("/config/cost/activity", get_activity_cost, methods=["GET"]),
        Route("/config/cost/chat/{chat_id}", get_chat_cost, methods=["GET"]),
        Route("/config/role-models", get_role_models, methods=["GET"]),
        Route("/config/role-models", save_role_models, methods=["POST"]),
        Route("/config/role-suggestions", get_role_suggestions, methods=["GET"]),
        Route("/config/providers/custom", save_custom_provider, methods=["POST"]),
        Route(
            "/config/providers/custom/{provider_id}",
            delete_custom_provider,
            methods=["DELETE"],
        ),
        Route("/config/capabilities/sync", sync_capabilities, methods=["POST"]),
        Route("/config/social", get_social_config, methods=["GET"]),
        Route("/config/social", save_social_config, methods=["POST"]),
        Route("/sync/status", get_sync_status, methods=["GET"]),
        Route("/sync/quickstart/info", get_sync_quickstart_info, methods=["GET"]),
        Route("/sync/quickstart", quickstart_sync, methods=["POST"]),
        Route("/sync/profiles", get_sync_profiles, methods=["GET"]),
        Route("/sync/profiles", create_sync_profile, methods=["POST"]),
        Route("/sync/validate", validate_sync_profile, methods=["POST"]),
        Route("/sync/ahead-behind", get_sync_ahead_behind, methods=["GET"]),
        Route("/sync/pull", pull_sync, methods=["POST"]),
        Route("/sync/push", push_sync, methods=["POST"]),
        Route("/sync/auto", save_auto_config, methods=["POST"]),
        Route("/sync/auto/run", run_auto_sync, methods=["POST"]),
        Route(
            "/sync/conflicts/resolve-agent", resolve_conflicts_agent, methods=["POST"]
        ),
        Route("/sync/conflicts/stop", stop_conflict_resolution, methods=["POST"]),
        Route("/sync/shibboleth/unlock", unlock_shibboleth, methods=["POST"]),
        Route("/sync/shibboleth/lock", lock_shibboleth, methods=["POST"]),
        Route("/sync/secrets/enable", enable_secret_sync, methods=["POST"]),
        Route("/sync/secrets/disable", disable_secret_sync, methods=["POST"]),
        Route("/sync/secrets/unlock", unlock_mnemonic, methods=["POST"]),
        Route("/sync/secrets/rotate", rotate_mnemonic, methods=["POST"]),
        Route(
            "/sync/secrets/register-device", register_device_mnemonic, methods=["POST"]
        ),
        Route("/sync/secrets/generate-mnemonic", generate_mnemonic, methods=["POST"]),
        Route("/sync/auth/start", start_github_auth, methods=["POST"]),
        Route("/sync/auth/poll", poll_github_auth, methods=["POST"]),
        Route("/sync/auth/status", get_github_auth_status, methods=["GET"]),
        Route("/sync/auth/logout", logout_github_auth, methods=["POST"]),
        Route("/social/pairing", list_pairings, methods=["GET"]),
        Route("/social/pairing/approve", approve_pairing, methods=["POST"]),
        Route("/social/pairing/deny", deny_pairing, methods=["POST"]),
        Route("/mcp_servers", list_mcp_servers, methods=["GET"]),
        Route("/mcp_servers", add_mcp_server, methods=["POST"]),
        Route("/mcp_servers/update", update_mcp_server, methods=["POST"]),
        Route("/mcp_servers/remove", remove_mcp_server, methods=["POST"]),
        Route("/mcp_servers/enabled", set_mcp_server_enabled, methods=["POST"]),
        Route("/mcp_servers/test", test_mcp_server, methods=["POST"]),
        Route("/sandbox/files", list_sandbox_files, methods=["GET"]),
        Route("/sandbox/mentions", search_file_mentions, methods=["GET"]),
        Route("/sandbox/read_file", read_sandbox_file, methods=["GET"]),
        Route("/sandbox/file", write_sandbox_file, methods=["POST", "PUT"]),
        Route("/sandbox/file", delete_sandbox_file, methods=["DELETE"]),
        Route("/sandbox/serve", serve_sandbox_file, methods=["GET"]),
        Route(
            "/sandbox/serve/{chat_id}/{file_path:path}",
            serve_sandbox_file_wildcard,
            methods=["GET"],
        ),
        Route("/sandbox/upload", upload_files, methods=["POST"]),
        Route("/system/files", list_host_files, methods=["GET"]),
        Route("/system/open_explorer", open_in_explorer, methods=["POST"]),
        Route("/memory/core", get_core_memory, methods=["GET"]),
        Route("/memory/core", update_core_memory_block, methods=["PUT"]),
        Route("/memory/archival", search_archival_memory, methods=["GET"]),
        Route(
            "/memory/archival/{memory_id}", delete_archival_memory, methods=["DELETE"]
        ),
        Route("/memory/stats", get_memory_stats, methods=["GET"]),
        Route("/memory/daily", list_memory_daily_logs, methods=["GET"]),
        Route("/memory/daily/{date}", get_memory_daily_log, methods=["GET"]),
        Route("/memory/file", get_memory_file, methods=["GET"]),
        Route("/memory/reindex", reindex_memories, methods=["POST"]),
        Route("/memory/dream/status", get_dream_status, methods=["GET"]),
        Route("/memory/consolidate", consolidate_memory, methods=["POST"]),
        Route("/memory/lint", lint_memory, methods=["POST"]),
        Route(
            "/session/{session_id}/transcript",
            get_session_transcript,
            methods=["GET"],
        ),
        Route(
            "/session/{session_id}/state",
            get_session_state,
            methods=["GET"],
        ),
        Route("/skills", get_skills, methods=["GET"]),
        Route("/skills/reload", reload_skills, methods=["POST"]),
        Route("/skills/{skill_name}/toggle", toggle_skill, methods=["POST"]),
        WebSocketRoute("/ws/browser", browser_websocket_endpoint),
        WebSocketRoute("/ws/node", node_websocket_endpoint),
        Route("/nodes", list_nodes, methods=["GET"]),
        Route("/nodes/{node_id}", describe_node, methods=["GET"]),
        Route("/nodes/{node_id}/invoke", invoke_node_command, methods=["POST"]),
        Route("/cron/jobs", list_cron_jobs, methods=["GET"]),
        Route("/cron/jobs", create_cron_job, methods=["POST"]),
        Route("/cron/jobs/{job_id:int}", update_cron_job, methods=["PUT"]),
        Route("/cron/jobs/{job_id:int}", delete_cron_job, methods=["DELETE"]),
        Route("/cron/jobs/{job_id:int}/trigger", trigger_cron_job, methods=["POST"]),
        Route("/cron/presets/install", install_cron_presets, methods=["POST"]),
        Route("/cron/status", get_cron_status, methods=["GET"]),
        Route("/cron/notifications", get_cron_notifications, methods=["GET"]),
        Route("/cron/jobs/{job_id:int}/runs", get_cron_job_runs, methods=["GET"]),
        Route("/heartbeat/status", get_heartbeat_status, methods=["GET"]),
        Route("/heartbeat/enable", enable_heartbeat, methods=["POST"]),
        Route("/heartbeat/disable", disable_heartbeat, methods=["POST"]),
        Route("/heartbeat/trigger", trigger_heartbeat, methods=["POST"]),
        Route("/heartbeat/md", get_heartbeat_md, methods=["GET"]),
        Route("/heartbeat/md", save_heartbeat_md, methods=["PUT"]),
        Route("/heartbeat/interval", set_heartbeat_interval, methods=["PUT", "POST"]),
        Route("/heartbeat/config", get_heartbeat_global_config, methods=["GET"]),
        Route("/heartbeat/config", save_heartbeat_global_config, methods=["POST"]),
        Route("/canvas/{chat_id}/action", a2ui_action, methods=["POST"]),
        Route("/canvas/{chat_id}/answer", a2ui_answer, methods=["POST"]),
        Route("/events/stream", event_bus_stream, methods=["GET"]),
        Route("/subagents/active", list_active_subagents, methods=["GET"]),
        Route("/subagents/stream", stream_subagents, methods=["GET"]),
        Route("/subagents/clear-stuck", clear_stuck_subagents_route, methods=["POST"]),
        Route("/subagents", list_subagents, methods=["GET"]),
        Route("/subagents/{task_id}", get_subagent, methods=["GET"]),
        Route("/subagents/{task_id}/stop", stop_subagent_route, methods=["POST"]),
    ],
    middleware=[
        Middleware(
            CORSMiddleware,
            allow_origins=["*"],
            allow_methods=["*"],
            allow_headers=["*"],
        )
    ],
)


if __name__ == "__main__":
    import datetime

    import uvicorn

    # File logging in runtime dir (stdout must stay clean for Tauri port detection)
    try:
        from suzent.config import RUNTIME_DIR

        RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
        debug_log = RUNTIME_DIR / "suzent_startup.log"
        from loguru import logger as _debug_logger

        _debug_logger.add(str(debug_log), rotation="10 MB", retention=2, level="DEBUG")
        _debug_logger.info(
            f"--- SERVER PROCESS STARTING AT {datetime.datetime.now()} ---"
        )
    except Exception:
        pass

    ensure_app_data()

    from suzent.config import DEFAULT_PORT

    _port_str = os.getenv("SUZENT_PORT", "").strip()
    port = int(_port_str) if _port_str else DEFAULT_PORT
    host = os.getenv("SUZENT_HOST", "0.0.0.0")

    def write_port_file(effective_port: int) -> None:
        """Write the effective port to a file for CLI discovery."""
        from suzent.config import RUNTIME_DIR

        RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
        port_file = RUNTIME_DIR / "server.port"
        try:
            port_file.write_text(str(effective_port), encoding="utf-8")
        except Exception:
            pass

    def remove_port_file() -> None:
        """Remove the port file on shutdown."""
        from suzent.config import RUNTIME_DIR

        port_file = RUNTIME_DIR / "server.port"
        try:
            if port_file.exists():
                port_file.unlink()
        except Exception:
            pass

    def report_port(effective_port: int) -> None:
        """Report the server port via multiple channels for reliability."""
        port_msg = f"SERVER_PORT:{effective_port}"
        logger.critical(port_msg)
        print(port_msg, flush=True)
        try:
            sys.stdout.buffer.write(f"{port_msg}\n".encode("utf-8"))
            sys.stdout.buffer.flush()
        except Exception:
            pass

        # Write to file for CLI discovery
        write_port_file(effective_port)

    import socket as _socket
    import threading

    # Pre-bind a socket before entering the asyncio event loop.
    # For port=0, this lets the OS choose a port dynamically so we can report it early.
    # For explicit ports (e.g., 8000), this verifies the port is available immediately,
    # and if it fails (e.g., Windows Hyper-V reserved ports), we can forceful exit
    # before uvicorn/tokio start up, avoiding PyO3 panics during graceful shutdown.
    _sock = None
    try:
        _sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
        _sock.setsockopt(_socket.SOL_SOCKET, _socket.SO_REUSEADDR, 1)
        try:
            _sock.bind((host, port))
        except OSError as e:
            raise e
        _sock.listen()
        _sock.set_inheritable(True)
        effective_port = _sock.getsockname()[1]
        report_port(effective_port)
        if port == 0:
            logger.info(f"Dynamic port assigned: {effective_port}")
    except OSError as e:
        logger.critical(f"Failed to bind to {host}:{port}: {e}")
        logger.critical(
            "Please set the SUZENT_PORT environment variable to a different port (e.g. set SUZENT_PORT=8001)"
        )
        if _sock is not None:
            try:
                _sock.close()
            except Exception:
                pass
        # Exit forcefully to prevent PyO3 panics during graceful shutdown if we haven't fully initialized
        os._exit(1)
    except Exception:
        if _sock is not None:
            try:
                _sock.close()
            except Exception:
                pass
        raise

    async def main():
        if port == 0:
            # Start process-lifetime monitors only when launched by Tauri (port=0 mode).
            # These threads must be started inside the async loop so os._exit() doesn't
            # fire before the event loop is running.
            def monitor_stdin():
                try:
                    if not sys.stdin.read(1):
                        os._exit(0)
                except Exception:
                    os._exit(0)

            def monitor_parent(pid):
                import subprocess
                import time

                logger.info(f"Starting parent monitor for PID {pid}")

                while True:
                    try:
                        if sys.platform == "win32":
                            result = subprocess.run(
                                [
                                    "tasklist",
                                    "/FI",
                                    f"PID eq {pid}",
                                    "/FO",
                                    "CSV",
                                    "/NH",
                                ],
                                capture_output=True,
                                text=True,
                                check=False,
                            )
                            exists = str(pid) in result.stdout
                        else:
                            os.kill(pid, 0)
                            exists = True
                    except OSError:
                        exists = False
                    except Exception as e:
                        logger.error(f"Parent monitor failed: {e}")
                        return

                    if not exists:
                        logger.critical(f"Parent process {pid} died. Shutting down.")
                        os._exit(0)
                    time.sleep(1)

            parent_pid = os.getppid()
            threading.Thread(target=monitor_stdin, daemon=True).start()
            threading.Thread(
                target=monitor_parent, args=(parent_pid,), daemon=True
            ).start()

        # Use port=0 in config so uvicorn doesn't try to bind (we pass the socket).
        # _sock is closed in the finally block if uvicorn never takes ownership.
        bind_port = 0 if _sock else effective_port
        config = uvicorn.Config(
            app,
            host=host,
            port=bind_port,
            log_level=log_level.lower(),
            ws="wsproto",
            timeout_graceful_shutdown=5,  # force-close lingering SSE connections after 5s
        )
        server = uvicorn.Server(config)
        sockets = [_sock] if _sock else None
        try:
            await server.serve(sockets=sockets)
        finally:
            if _sock is not None:
                try:
                    _sock.close()
                except OSError:
                    # Expected: uvicorn already closed the socket on normal shutdown
                    pass
                except Exception as e:
                    logger.debug(f"Error closing pre-bound socket: {e}")

            # Clean up port file
            remove_port_file()

    if port == 0:
        logger.info(
            f"Starting Suzent server with dynamic port assignment (port={effective_port})..."
        )
    else:
        logger.info(f"Starting Suzent server on http://{host}:{effective_port}")

    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        # Graceful shutdown timed out or was interrupted a second time — force exit.
        os._exit(0)
