"""Orchestrator lifecycle: async factory, startup, shutdown, infra management."""

from __future__ import annotations

import asyncio
import logging
import os
import secrets
from pathlib import Path
from typing import TYPE_CHECKING

from sygen_bot.files.allowed_roots import resolve_allowed_roots
from sygen_bot.infra.docker import DockerManager
from sygen_bot.workspace.init import inject_runtime_environment
from sygen_bot.workspace.paths import SygenPaths, resolve_paths
from sygen_bot.workspace.skill_sync import cleanup_sygen_links, sync_bundled_skills, sync_skills

if TYPE_CHECKING:
    from sygen_bot.config import AgentConfig
    from sygen_bot.orchestrator.core import Orchestrator

logger = logging.getLogger(__name__)


def _docker_skill_resync(paths: SygenPaths) -> None:
    """Re-run skill sync with copies so skills resolve inside Docker."""
    sync_bundled_skills(paths, docker_active=True)
    sync_skills(paths, docker_active=True)


async def create_orchestrator(
    config: AgentConfig,
    *,
    agent_name: str = "main",
) -> Orchestrator:
    """Async factory: build an Orchestrator.

    Workspace must already be initialized by the caller (``__main__.load_config``).
    """
    from sygen_bot.orchestrator.core import Orchestrator

    paths = resolve_paths(sygen_home=config.sygen_home)

    # Only set the process-wide env var for the main agent to avoid
    # race conditions in multi-agent mode (sub-agents use per-subprocess env).
    if agent_name == "main":
        os.environ["SYGEN_HOME"] = str(paths.sygen_home)

    docker_container = ""
    docker_mgr: DockerManager | None = None
    if config.docker.enabled:
        docker_mgr = DockerManager(config.docker, paths)
        container = await docker_mgr.setup()
        if container:
            docker_container = container
        else:
            logger.warning("Docker enabled but setup failed; running on host")

    if docker_container:
        await asyncio.to_thread(_docker_skill_resync, paths)

    await asyncio.to_thread(
        inject_runtime_environment,
        paths,
        docker_container=docker_container,
        agent_name=agent_name,
        transport=config.transport,
    )

    orch = Orchestrator(
        config,
        paths,
        docker_container=docker_container,
        agent_name=agent_name,
        interagent_port=config.interagent_port,
    )
    orch._docker = docker_mgr

    from sygen_bot.cli.auth import AuthStatus, check_all_auth

    auth_results = await asyncio.to_thread(check_all_auth)
    orch._providers.apply_auth_results(
        auth_results,
        auth_status_enum=AuthStatus,
        cli_service=orch._cli_service,
    )

    if not orch._providers.available_providers:
        logger.error("No authenticated providers found! CLI calls will fail.")
    else:
        logger.info(
            "Available providers: %s",
            ", ".join(sorted(orch._providers.available_providers)),
        )

    await asyncio.to_thread(orch._providers.init_gemini_state, paths.workspace)

    codex_cache = await orch._observers.init_model_caches(
        on_gemini_refresh=orch._providers.on_gemini_models_refresh
    )
    orch._observers.init_task_observers(
        cron_manager=orch._cron_manager,
        webhook_manager=orch._webhook_manager,
        cli_service=orch._cli_service,
        codex_cache=codex_cache,
    )
    orch._providers._codex_cache_fn = lambda: orch._observers.codex_cache
    await orch._observers.start_all(docker_container=docker_container)

    # MCP server management
    if config.mcp.enabled:
        from sygen_bot.mcp.manager import MCPManager
        from sygen_bot.mcp.tool_router import ToolRouter

        mcp_mgr = MCPManager(config.mcp)
        await mcp_mgr.start()
        orch._mcp_manager = mcp_mgr
        router = ToolRouter(mcp_mgr, paths.workspace)
        orch._tool_router = router
        # Inject MCP config path into CLI service if user hasn't set --mcp-config manually.
        if mcp_mgr.get_all_tools() and "--mcp-config" not in config.cli_parameters.claude:
            from dataclasses import replace as _replace

            cfg_path = router.generate_mcp_config_file()
            orch._cli_service.update_config(
                _replace(orch._cli_service._config, mcp_config_path=str(cfg_path))
            )
        logger.info("MCP manager started with %d tool(s)", len(mcp_mgr.get_all_tools()))

    # Workflow Engine
    if config.workflow.enabled:
        from sygen_bot.workflow.engine import WorkflowEngine
        from sygen_bot.workflow.observer import WorkflowObserver
        from sygen_bot.workflow.registry import WorkflowRegistry

        wf_registry = WorkflowRegistry(
            definitions_dir=paths.workflows_dir,
            runs_path=paths.workflow_runs_path,
        )

        # Notify callback is a no-op stub initially. The real implementation
        # is wired in ``ObserverManager.wire_to_bus()`` once the MessageBus
        # is available (same pattern as cron/background observers).
        async def _noop_notify(
            chat_id: int,
            topic_id: int | None,
            transport: str,
            message: str,
            *,
            run_id: str = "",
            workflow_name: str = "",
        ) -> None:
            logger.warning("Workflow notify called before bus is wired")

        wf_engine = WorkflowEngine(
            wf_registry,
            notify_callback=_noop_notify,
            max_parallel_runs=config.workflow.max_parallel_runs,
            default_step_timeout=config.workflow.default_step_timeout,
        )
        orch._workflow_engine = wf_engine
        orch._observers.workflow = WorkflowObserver(wf_engine, paths)
        logger.info("Workflow engine enabled (%s)", paths.workflows_dir)

    # Advanced RAG pipeline (lazy init — model loading happens on first query)
    if config.rag.enabled:
        from sygen_bot.rag.pipeline import RAGPipeline

        mem_dir = paths.memory_system_dir / "modules"
        vec_dir = paths.memory_system_dir / "vector_db" if config.memory.vector_search else None
        orch._rag_pipeline = RAGPipeline(
            config=config.rag,
            workspace_dir=paths.workspace,
            memory_modules_dir=mem_dir,
            vector_persist_dir=vec_dir,
            embedding_model=config.memory.vector_model,
        )
        logger.info("Advanced RAG pipeline registered (lazy init)")

    # Built-in fileshare server
    if config.fileshare.enabled:
        await start_fileshare_server(orch, config, paths)

    # Direct API server (WebSocket, designed for Tailscale)
    if config.api.enabled:
        await start_api_server(orch, config, paths)

    await orch._observers.start_config_reloader(
        on_hot_reload=orch._on_config_hot_reload,
        on_restart_needed=lambda fields: logger.warning(
            "Config changed but requires restart: %s", ", ".join(fields)
        ),
    )

    return orch


async def start_api_server(
    orch: Orchestrator,
    config: AgentConfig,
    paths: SygenPaths,
) -> None:
    """Initialize and start the direct WebSocket API server."""
    try:
        from sygen_bot.api.server import ApiServer
    except ImportError:
        logger.warning(
            "API server enabled but PyNaCl is not installed. Install with: pip install sygen[api]"
        )
        return

    if not config.api.token:
        from sygen_bot.config import update_config_file_async

        token = secrets.token_urlsafe(32)
        config.api.token = token
        await update_config_file_async(
            paths.config_path,
            api={**config.api.model_dump(), "token": token},
        )
        logger.info("Generated API auth token (persisted to config)")

    default_chat_id = config.api.chat_id or (
        config.allowed_user_ids[0] if config.allowed_user_ids else 1
    )
    server = ApiServer(config.api, default_chat_id=default_chat_id)
    server.set_message_handler(orch.handle_message_streaming)
    server.set_abort_handler(orch.abort)
    server.set_file_context(
        allowed_roots=resolve_allowed_roots(config.file_access, paths.workspace),
        upload_dir=paths.api_files_dir,
        workspace=paths.workspace,
    )
    server.set_provider_info(orch._providers.build_provider_info(orch._observers.codex_cache_obs))
    server.set_active_state_getter(
        lambda: orch._providers.resolve_runtime_target(orch._config.model)
    )

    try:
        await server.start()
    except OSError:
        logger.exception(
            "Failed to start API server on %s:%d",
            config.api.host,
            config.api.port,
        )
        return

    orch._api_stop = server.stop


async def start_fileshare_server(
    orch: Orchestrator,
    config: AgentConfig,
    paths: SygenPaths,
) -> None:
    """Initialize and start the built-in fileshare HTTP server."""
    from sygen_bot.fileshare.server import FileshareServer

    upload_dir = paths.fileshare_uploads_dir
    download_dir = paths.fileshare_downloads_dir
    upload_dir.mkdir(parents=True, exist_ok=True)
    download_dir.mkdir(parents=True, exist_ok=True)

    server = FileshareServer(
        host=config.fileshare.host,
        port=config.fileshare.port,
        upload_dir=upload_dir,
        download_dir=download_dir,
    )

    try:
        await server.start()
    except OSError:
        logger.exception(
            "Failed to start fileshare server on %s:%d",
            config.fileshare.host,
            config.fileshare.port,
        )
        return

    orch._fileshare_stop = server.stop
    orch._fileshare_url = server.base_url

    os.environ["FILESHARE_URL"] = server.base_url
    os.environ["FILESHARE_DOWNLOADS"] = str(download_dir)

    # Run cleanup on startup
    cleanup_days = config.fileshare.auto_cleanup_days
    if cleanup_days > 0:
        removed = await asyncio.to_thread(
            _cleanup_fileshare_dir, upload_dir, cleanup_days
        )
        removed += await asyncio.to_thread(
            _cleanup_fileshare_dir, download_dir, cleanup_days
        )
        if removed:
            logger.info("Fileshare startup cleanup: removed %d old files", removed)


def _cleanup_fileshare_dir(directory: Path, max_age_days: int) -> int:
    """Remove files older than *max_age_days* from *directory*. Return count."""
    import time

    if not directory.is_dir():
        return 0
    cutoff = time.time() - max_age_days * 86400
    removed = 0
    for f in directory.iterdir():
        if f.is_file() and f.stat().st_mtime < cutoff:
            f.unlink(missing_ok=True)
            removed += 1
    return removed


async def ensure_docker(orch: Orchestrator) -> None:
    """Health-check Docker before CLI calls; auto-recover or fall back."""
    if not orch._docker:
        return
    container = await orch._docker.ensure_running()
    if container:
        orch._cli_service.update_docker_container(container)
    elif orch._cli_service._config.docker_container:
        logger.warning("Docker recovery failed, falling back to host execution")
        orch._cli_service.update_docker_container("")


async def shutdown(orch: Orchestrator) -> None:
    """Cleanup on bot shutdown."""
    killed = await orch._process_registry.kill_all_active()
    if killed:
        logger.info("Shutdown terminated %d active CLI process(es)", killed)
    if orch._workflow_engine is not None:
        await orch._workflow_engine.shutdown()
    if orch._fileshare_stop is not None:
        await orch._fileshare_stop()
    if orch._api_stop is not None:
        await orch._api_stop()
    if orch._mcp_manager is not None:
        await orch._mcp_manager.stop()
    await asyncio.to_thread(cleanup_sygen_links, orch._paths)
    await orch._observers.stop_all()
    if orch._docker:
        await orch._docker.teardown()
    logger.info("Orchestrator shutdown")
