"""Daemon lifespan context manager and subsystem init helpers.

Extracted from ``server.py`` -- this is the core startup/shutdown
orchestrator. Init order is load-bearing:
    embedding -> vector store -> activity -> agents
"""

import asyncio
import atexit
import logging
import sqlite3
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from typing import TYPE_CHECKING

from fastapi import FastAPI

from open_agent_kit.config.paths import OAK_DIR
from open_agent_kit.features.team.constants import (
    CI_ACTIVITIES_DB_FILENAME,
    CI_CHROMA_DIR,
    CI_CLOUD_RELAY_ERROR_CONNECT_FAILED,
    CI_CLOUD_RELAY_LOG_AUTO_CONNECT,
    CI_CLOUD_RELAY_LOG_AUTO_CONNECT_FAILED,
    CI_CLOUD_RELAY_LOG_CONNECTED,
    CI_DATA_DIR,
    CI_LOG_FILE,
    CI_PID_FILE,
    CI_TOKEN_FILE,
    SHUTDOWN_TASK_TIMEOUT_SECONDS,
)
from open_agent_kit.features.team.daemon.state import (
    get_data_collection_policy,
    get_state,
)
from open_agent_kit.features.team.embeddings import EmbeddingProviderChain

if TYPE_CHECKING:
    from open_agent_kit.features.team.config import CIConfig
    from open_agent_kit.features.team.daemon.state import DaemonState
    from open_agent_kit.features.team.embeddings.base import EmbeddingProvider

logger = logging.getLogger(__name__)


async def _init_cloud_relay(state: "DaemonState", project_root: Path) -> None:
    """Auto-connect cloud relay if configured.

    Supports two modes:
    - Publisher: cloud_relay.auto_connect=True (set after successful deploy).
    - Consumer: team.auto_sync=True with relay_worker_url + api_key configured.

    Non-critical: failures are logged but do not prevent startup.
    """
    ci_config = state.ci_config
    if not ci_config:
        return

    relay_config = ci_config.cloud_relay
    team_config = ci_config.team

    # Only auto-connect when at least one path opted in
    if not (relay_config.auto_connect or team_config.auto_sync):
        return

    worker_url, token = ci_config.resolve_relay_credentials()
    if not worker_url or not token:
        return

    from open_agent_kit.features.team.daemon.manager import (
        get_project_port,
    )

    logger.info(CI_CLOUD_RELAY_LOG_AUTO_CONNECT)
    try:
        from open_agent_kit.features.team.activity.store.backup import (
            get_machine_identifier,
        )
        from open_agent_kit.features.team.cloud_relay.client import (
            CloudRelayClient,
        )

        ci_data_dir = project_root / OAK_DIR / CI_DATA_DIR
        port = get_project_port(project_root, ci_data_dir)
        machine_id = get_machine_identifier(project_root)

        client = CloudRelayClient(
            tool_timeout_seconds=relay_config.tool_timeout_seconds,
            reconnect_max_seconds=relay_config.reconnect_max_seconds,
        )
        relay_status = await client.connect(worker_url, token, port, machine_id=machine_id)
        state.cloud_relay_client = client

        if relay_status.connected:
            logger.info(CI_CLOUD_RELAY_LOG_CONNECTED.format(worker_url=worker_url))
            state.cache_relay_credentials(worker_url, token, port, machine_id)
        else:
            error_detail = relay_status.error or CI_CLOUD_RELAY_ERROR_CONNECT_FAILED.format(
                error="unknown"
            )
            logger.warning(CI_CLOUD_RELAY_LOG_AUTO_CONNECT_FAILED.format(error=error_detail))
    except (OSError, ValueError, RuntimeError, ConnectionError) as e:
        logger.warning(CI_CLOUD_RELAY_LOG_AUTO_CONNECT_FAILED.format(error=e))


def _migrate_env_to_user_config(project_root: Path) -> None:
    """One-time migration: move persisted .env values to user override config.

    Moves OAK_SWARM_TOKEN, OAK_SWARM_AGENT_TOKEN, and OAK_CI_BACKUP_DIR from
    the project-root .env file into .oak/config.{machine_id}.yaml.  Idempotent:
    skips values already present in user config.
    """
    from open_agent_kit.features.swarm.constants import (
        SWARM_ENV_VAR_AGENT_TOKEN,
        SWARM_ENV_VAR_TOKEN,
        SWARM_USER_CONFIG_KEY_AGENT_TOKEN,
        SWARM_USER_CONFIG_KEY_TOKEN,
        SWARM_USER_CONFIG_SECTION,
    )
    from open_agent_kit.features.team.config.user_store import (
        read_user_value,
        write_user_value,
    )
    from open_agent_kit.features.team.constants.backup import (
        BACKUP_USER_CONFIG_KEY_DIR,
        BACKUP_USER_CONFIG_SECTION,
        OAK_CI_BACKUP_DIR_ENV,
    )
    from open_agent_kit.utils.env_utils import read_env_value, remove_env_key

    migrations: list[tuple[str, str, str, str]] = [
        (
            SWARM_ENV_VAR_TOKEN,
            SWARM_USER_CONFIG_SECTION,
            SWARM_USER_CONFIG_KEY_TOKEN,
            "swarm_token",
        ),
        (
            SWARM_ENV_VAR_AGENT_TOKEN,
            SWARM_USER_CONFIG_SECTION,
            SWARM_USER_CONFIG_KEY_AGENT_TOKEN,
            "swarm agent_token",
        ),
        (
            OAK_CI_BACKUP_DIR_ENV,
            BACKUP_USER_CONFIG_SECTION,
            BACKUP_USER_CONFIG_KEY_DIR,
            "backup_dir",
        ),
    ]

    for env_var, section, key, label in migrations:
        env_value = read_env_value(project_root, env_var)
        if not env_value:
            continue
        # Skip if already migrated
        if read_user_value(project_root, section, key):
            # Still remove from .env to avoid confusion
            remove_env_key(project_root, env_var)
            continue
        write_user_value(project_root, section, key, env_value)
        remove_env_key(project_root, env_var)
        logger.info("Migrated %s from .env to user config", label)


async def _ensure_swarm_tokens(state: "DaemonState", project_root: Path) -> None:
    """Auto-fetch swarm_token and agent_token if missing.

    On a fresh clone, the node has swarm_url in config.yaml (git-tracked) but
    no tokens in user config.  The swarm_token is fetched from the relay worker
    (which already has it from the team join), and the agent_token is then
    fetched from the swarm worker using the swarm_token.
    """
    import yaml

    from open_agent_kit.features.swarm.constants import (
        CI_CONFIG_KEY_SWARM,
        CI_CONFIG_SWARM_KEY_URL,
        SWARM_USER_CONFIG_KEY_AGENT_TOKEN,
        SWARM_USER_CONFIG_KEY_TOKEN,
        SWARM_USER_CONFIG_SECTION,
    )
    from open_agent_kit.features.team.config.user_store import (
        read_user_value,
        write_user_value,
    )

    config_path = project_root / OAK_DIR / "config.yaml"
    if not config_path.exists():
        return
    try:
        with open(config_path, encoding="utf-8") as f:
            file_data = yaml.safe_load(f) or {}
    except Exception:
        return

    swarm = file_data.get(CI_CONFIG_KEY_SWARM, {})
    if not isinstance(swarm, dict):
        return

    swarm_url = swarm.get(CI_CONFIG_SWARM_KEY_URL)
    if not swarm_url:
        return

    # Step 1: Ensure swarm_token — fetch from relay if missing
    swarm_token = read_user_value(
        project_root, SWARM_USER_CONFIG_SECTION, SWARM_USER_CONFIG_KEY_TOKEN
    )
    if not swarm_token and state.cloud_relay_client:
        swarm_token = await _fetch_swarm_token_from_relay(state)
        if swarm_token:
            write_user_value(
                project_root, SWARM_USER_CONFIG_SECTION, SWARM_USER_CONFIG_KEY_TOKEN, swarm_token
            )
            logger.info("Auto-fetched swarm_token from relay on startup")

    if not swarm_token:
        return

    # Step 2: Ensure agent_token — fetch from swarm worker if missing
    if not read_user_value(
        project_root, SWARM_USER_CONFIG_SECTION, SWARM_USER_CONFIG_KEY_AGENT_TOKEN
    ):
        from open_agent_kit.features.team.daemon.routes.swarm_config import _fetch_agent_token

        agent_token = await _fetch_agent_token(swarm_url, swarm_token)
        if agent_token:
            write_user_value(
                project_root,
                SWARM_USER_CONFIG_SECTION,
                SWARM_USER_CONFIG_KEY_AGENT_TOKEN,
                agent_token,
            )
            logger.info("Auto-fetched swarm agent_token on startup")


async def _fetch_swarm_token_from_relay(state: "DaemonState") -> str | None:
    """Fetch swarm_token from the relay worker's GET /api/swarm/config."""
    import httpx

    client = state.cloud_relay_client
    if client is None:
        return None

    ci_config = state.ci_config
    if ci_config is None:
        return None

    relay_url, relay_token = ci_config.resolve_relay_credentials()
    if not relay_url or not relay_token:
        return None

    url = relay_url.rstrip("/") + "/api/swarm/config"
    try:
        async with httpx.AsyncClient(timeout=10) as http:
            resp = await http.get(
                url,
                headers={"Authorization": f"Bearer {relay_token}"},
            )
        if resp.is_success:
            data = resp.json()
            token = data.get("swarm_token")
            if token:
                return str(token)
        logger.debug("Relay returned %s for swarm config", resp.status_code)
    except httpx.HTTPError as exc:
        logger.debug("Failed to fetch swarm config from relay: %s", exc)
    return None


def _init_embedding(state: "DaemonState", project_root: Path) -> bool:
    """Create and verify the embedding provider.

    Returns True if the provider is available for immediate use.
    """
    from open_agent_kit.features.team.embeddings.provider_chain import (
        create_provider_from_config,
    )

    ci_config = state.ci_config
    if ci_config is None:
        state.embedding_chain = None
        return False

    try:
        primary_provider = create_provider_from_config(ci_config.embedding)
    except (OSError, ValueError, RuntimeError) as e:
        logger.warning(f"Failed to create embedding provider: {e}")
        logger.info("Configure your provider in the Settings tab to start indexing.")
        state.embedding_chain = None
        return False

    if not primary_provider.is_available:
        logger.warning(
            f"Embedding provider {primary_provider.name} not available. "
            "Make sure Ollama is running or configure an OpenAI-compatible provider."
        )
        logger.info("Configure your provider in the Settings tab to start indexing.")
        state.embedding_chain = EmbeddingProviderChain(providers=[primary_provider])
        return False

    state.embedding_chain = EmbeddingProviderChain(providers=[primary_provider])

    # Verify dimensions on startup - detect actual model output dimensions
    _verify_embedding_dimensions(primary_provider, ci_config, project_root)

    logger.info(
        f"Created embedding provider: {primary_provider.name} "
        f"(dims={ci_config.embedding.get_dimensions()}, "
        f"max_chunk={ci_config.embedding.get_max_chunk_chars()})"
    )
    return True


def _verify_embedding_dimensions(
    primary_provider: "EmbeddingProvider", ci_config: "CIConfig", project_root: Path
) -> None:
    """Detect actual model dimensions and update config if needed."""
    try:
        test_result = primary_provider.embed(["dimension test"])
        if not (test_result.embeddings and len(test_result.embeddings) > 0):
            return

        detected_dims = len(test_result.embeddings[0])
        config_dims = ci_config.embedding.dimensions

        if config_dims is None:
            from open_agent_kit.features.team.config import save_ci_config

            ci_config.embedding.dimensions = detected_dims
            save_ci_config(project_root, ci_config)
            logger.info(f"Auto-detected and saved embedding dimensions: {detected_dims}")
        elif config_dims != detected_dims:
            from open_agent_kit.features.team.config import save_ci_config

            logger.warning(
                f"Config dimensions ({config_dims}) don't match actual model "
                f"output ({detected_dims}). This model doesn't support dimension "
                f"truncation - updating config to {detected_dims}."
            )
            ci_config.embedding.dimensions = detected_dims
            save_ci_config(project_root, ci_config)
    except (OSError, RuntimeError, ValueError) as e:
        logger.warning(f"Could not verify dimensions: {e}")


def _init_vector_store_and_indexer(
    state: "DaemonState", project_root: Path, provider_available: bool
) -> None:
    """Initialize vector store and code indexer.

    Requires ``state.embedding_chain`` to be set. If the embedding chain is
    ``None`` the vector store and indexer are left as ``None``.
    """
    from open_agent_kit.features.team.daemon.background import (
        _background_index,
    )

    if state.embedding_chain is None:
        logger.warning("Skipping vector store initialization - no embedding provider")
        state.vector_store = None
        state.indexer = None
        return

    ci_config = state.ci_config
    if ci_config is None:
        return

    ci_data_dir = project_root / OAK_DIR / CI_DATA_DIR / CI_CHROMA_DIR

    from open_agent_kit.features.team.memory.store import VectorStore

    state.vector_store = VectorStore(
        persist_directory=ci_data_dir,
        embedding_provider=state.embedding_chain,
    )
    logger.info(f"Vector store initialized at {ci_data_dir}")

    # Initialize indexer with configured chunk size
    from open_agent_kit.features.team.indexing.chunker import ChunkerConfig
    from open_agent_kit.features.team.indexing.indexer import (
        CodebaseIndexer,
        IndexerConfig,
    )

    chunker_config = ChunkerConfig(
        max_chunk_chars=ci_config.embedding.get_max_chunk_chars(),
    )

    # Get combined exclusion patterns from config (defaults + user patterns)
    combined_patterns = ci_config.get_combined_exclude_patterns()
    user_patterns = ci_config.get_user_exclude_patterns()
    if user_patterns:
        logger.debug(f"User exclude patterns: {user_patterns}")

    indexer_config = IndexerConfig(ignore_patterns=combined_patterns)

    state.indexer = CodebaseIndexer(
        project_root=project_root,
        vector_store=state.vector_store,
        config=indexer_config,
        chunker_config=chunker_config,
    )

    # Start background indexing only if provider is available
    if provider_available:
        task = asyncio.create_task(_background_index(), name="background_index")
        state.background_tasks.append(task)
    else:
        logger.info(
            "Skipping auto-index - provider not available. Save settings to start indexing."
        )


async def _init_activity(state: "DaemonState", project_root: Path) -> None:
    """Initialize the activity store and processor.

    Requires ``state.vector_store`` to be set for full processor init.
    """
    from open_agent_kit.features.team.daemon.lifecycle.sync_check import (
        check_and_rebuild_chromadb,
    )

    ci_config = state.ci_config
    if ci_config is None:
        return

    from open_agent_kit.features.team.activity import (
        ActivityProcessor,
        ActivityStore,
    )
    from open_agent_kit.features.team.activity.store.backup import (
        get_machine_identifier,
    )

    activity_db_path = project_root / OAK_DIR / CI_DATA_DIR / CI_ACTIVITIES_DB_FILENAME
    state.machine_id = get_machine_identifier(project_root)
    state.activity_store = ActivityStore(activity_db_path, machine_id=state.machine_id)
    logger.info(f"Activity store initialized at {activity_db_path}")

    # Create processor with config accessor so summarizer/context_budget/
    # session_quality read live config (no stale snapshots after UI changes).
    config_accessor = lambda: state.ci_config  # noqa: E731

    if state.vector_store:
        state.activity_processor = ActivityProcessor(
            activity_store=state.activity_store,
            vector_store=state.vector_store,
            project_root=str(project_root),
            config_accessor=config_accessor,
        )

        # Check for SQLite/ChromaDB mismatch on startup
        await check_and_rebuild_chromadb(state)

        # Schedule background processing using config interval
        bg_interval = ci_config.agents.background_processing_interval_seconds
        state.activity_processor.schedule_background_processing(
            interval_seconds=bg_interval,
            state_accessor=lambda: state,
        )
        logger.info(
            f"Activity processor initialized with background scheduling (interval={bg_interval}s)"
        )


def _init_agents(state: "DaemonState", project_root: Path) -> None:
    """Initialize the agent subsystem (registry, executor, scheduler).

    Non-critical: failures are logged but do not prevent startup.
    """
    ci_config = state.ci_config
    if ci_config is None:
        return

    if not ci_config.agents.enabled:
        logger.info("Agent subsystem disabled in config")
        return

    from open_agent_kit.features.agent_runtime import (
        AgentExecutor,
        AgentRegistry,
    )

    state.agent_registry = AgentRegistry(project_root=project_root)
    agent_count = state.agent_registry.load_all()
    logger.info(f"Agent registry loaded {agent_count} agents")

    config_accessor = lambda: state.ci_config  # noqa: E731
    state.agent_executor = AgentExecutor(
        project_root=project_root,
        agent_config=ci_config.agents,
        retrieval_engine=state.retrieval_engine,
        activity_store=state.activity_store,
        vector_store=state.vector_store,
        config_accessor=config_accessor,
    )
    logger.info(f"Agent executor initialized (cache_size={ci_config.agents.executor_cache_size})")

    # Initialize scheduler if activity_store is available
    if state.activity_store:
        from open_agent_kit.features.agent_runtime.scheduler import (
            AgentScheduler,
        )

        state.agent_scheduler = AgentScheduler(
            activity_store=state.activity_store,
            agent_registry=state.agent_registry,
            agent_executor=state.agent_executor,
            agent_config=ci_config.agents,
            config_accessor=config_accessor,
        )
        # Sync schedules from YAML definitions to database
        sync_result = state.agent_scheduler.sync_schedules()
        logger.info(
            f"Agent scheduler initialized: {sync_result['total']} schedules "
            f"({sync_result['created']} created, {sync_result['updated']} updated)"
        )
        # Start the background scheduling loop (uses config interval)
        state.agent_scheduler.start()


def _init_team_sync(state: "DaemonState") -> None:
    """Initialize team outbox sync if configured.

    Enables outbox writes in the activity store and starts the background
    obs flush worker that pushes observations via the cloud relay.
    Non-critical: failures are logged but do not prevent startup.
    """
    ci_config = state.ci_config
    if not ci_config or not ci_config.team.auto_sync:
        return
    if not state.activity_store:
        logger.debug("Team sync skipped: no activity store")
        return

    # Enable outbox writes in the store (atomic with data writes)
    state.activity_store.team_outbox_enabled = True

    # Wire policy accessor so outbox hooks can check data collection policy
    state.activity_store._team_policy_accessor = get_data_collection_policy

    # Also wire policy accessor into the relay client so capabilities are dynamic
    if state.cloud_relay_client is not None:
        state.cloud_relay_client.set_policy_accessor(get_data_collection_policy)

    from open_agent_kit.features.team.relay.identity import (
        get_project_identity,
    )
    from open_agent_kit.features.team.relay.outbox.worker import (
        ObsFlushWorker,
    )

    project_id = (
        get_project_identity(state.project_root).full_id if state.project_root else "unknown"
    )

    # Start obs flush worker (flushes outbox via cloud relay)
    worker = ObsFlushWorker(
        store=state.activity_store,
        config=ci_config.team,
        project_id=project_id,
    )
    # Relay client will be set when the cloud relay connects
    if state.cloud_relay_client is not None:
        worker.set_relay_client(state.cloud_relay_client)

        # Wire obs applier so incoming peer observations are applied locally
        from open_agent_kit.features.team.relay.sync.obs_applier import (
            RemoteObsApplier,
        )

        applier = RemoteObsApplier(state.activity_store)
        state.cloud_relay_client.set_obs_applier(applier)

    worker.start()
    state.team_sync_worker = worker
    logger.info("Obs flush worker started")


def _cleanup_pid_and_token(project_root: Path) -> None:
    """Remove PID and token files so the port is not considered occupied.

    Called during graceful shutdown *and* registered as an ``atexit`` handler
    so that even an unclean exit (SIGTERM with no lifespan teardown, uncaught
    exception, etc.) leaves the filesystem in a clean state.
    """
    ci_data_dir = project_root / OAK_DIR / CI_DATA_DIR
    for filename in (CI_PID_FILE, CI_TOKEN_FILE):
        path = ci_data_dir / filename
        try:
            path.unlink(missing_ok=True)
        except OSError:
            pass


async def _shutdown(state: "DaemonState") -> None:
    """Graceful shutdown sequence for all subsystems."""
    logger.info("Initiating graceful shutdown...")

    # 1. Cancel background tasks and wait for them with timeout
    for task in state.background_tasks:
        if not task.done():
            task.cancel()
            try:
                await asyncio.wait_for(task, timeout=SHUTDOWN_TASK_TIMEOUT_SECONDS)
            except asyncio.CancelledError:
                pass
            except TimeoutError:
                logger.warning(
                    f"Task {task.get_name()} did not cancel within {SHUTDOWN_TASK_TIMEOUT_SECONDS}s"
                )
            except (RuntimeError, OSError) as e:
                logger.warning(f"Error cancelling task {task.get_name()}: {e}")
    state.background_tasks.clear()

    # 2. Activity processor uses daemon timers that auto-terminate on shutdown
    # No explicit stop needed - daemon threads exit with the process
    if state.activity_processor:
        logger.info("Activity processor will terminate with daemon shutdown")

    # 3. Close any active interactive sessions
    if state.interactive_session_manager:
        state.interactive_session_manager = None

    # 4. Stop agent scheduler
    if state.agent_scheduler:
        logger.info("Stopping agent scheduler...")
        try:
            state.agent_scheduler.stop()
        except (RuntimeError, OSError) as e:
            logger.warning(f"Error stopping agent scheduler: {e}")
        finally:
            state.agent_scheduler = None

    # 5. Stop team sync worker
    if state.team_sync_worker:
        logger.info("Stopping team sync worker...")
        try:
            state.team_sync_worker.stop()
        except (RuntimeError, OSError) as e:
            logger.warning(f"Error stopping team sync worker: {e}")
        finally:
            state.team_sync_worker = None

    # 6. Disconnect cloud relay if connected
    if state.cloud_relay_client:
        logger.info("Disconnecting cloud relay...")
        try:
            await state.cloud_relay_client.disconnect()
        except (RuntimeError, OSError) as e:
            logger.warning(f"Error disconnecting cloud relay: {e}")
        finally:
            state.cloud_relay_client = None

    # 7. Stop file watcher and wait for thread cleanup
    if state.file_watcher:
        logger.info("Stopping file watcher...")
        try:
            state.file_watcher.stop()
            # Give watcher thread time to exit cleanly
            await asyncio.sleep(0.5)
        except (RuntimeError, OSError, AttributeError) as e:
            logger.warning(f"Error stopping file watcher: {e}")
        finally:
            state.file_watcher = None

    # Clean up PID and token files so the port is not considered occupied
    if state.project_root:
        _cleanup_pid_and_token(state.project_root)

    logger.info("Team daemon shutdown complete")


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
    """Manage daemon lifecycle.

    Initialization order matters: embedding -> vector store -> activity -> agents.
    Each helper is self-contained and logs its own errors so failures in one
    subsystem do not block the rest of startup.
    """
    from open_agent_kit.features.team.config import load_ci_config
    from open_agent_kit.features.team.daemon.lifecycle.logging_setup import (
        configure_logging,
    )
    from open_agent_kit.features.team.daemon.lifecycle.maintenance import (
        run_governance_prune,
    )
    from open_agent_kit.features.team.daemon.lifecycle.version_check import (
        check_upgrade_needed,
        check_version,
        periodic_version_check,
    )

    state = get_state()

    # Get project root from state (set by create_app)
    project_root = state.project_root or Path.cwd()
    state.initialize(project_root)

    # Register atexit handler so PID/token files are cleaned up even on
    # unclean exits (uncaught exceptions, interpreter shutdown).
    atexit.register(_cleanup_pid_and_token, project_root)

    # Python's default SIGTERM handler exits without running atexit handlers.
    # Install a handler that triggers a clean interpreter shutdown so atexit
    # (and the lifespan teardown, if uvicorn propagates) actually runs.
    import signal

    def _sigterm_handler(signum: int, frame: object) -> None:
        raise SystemExit(0)

    signal.signal(signal.SIGTERM, _sigterm_handler)

    # Load configuration
    ci_config = load_ci_config(project_root)
    state.ci_config = ci_config
    state.config = ci_config.to_dict()

    # Configure logging
    effective_log_level = ci_config.get_effective_log_level()
    log_file = project_root / OAK_DIR / CI_DATA_DIR / CI_LOG_FILE
    configure_logging(effective_log_level, log_file=log_file, log_rotation=ci_config.log_rotation)
    state.log_level = effective_log_level

    logger.info(f"Team daemon starting up (log_level={effective_log_level})")
    if effective_log_level == "DEBUG":
        logger.debug("Debug logging enabled - verbose output active")

    # Initialize secrets redaction patterns (before any activity storage)
    from open_agent_kit.features.team.utils.redact import (
        initialize as initialize_redaction,
    )

    ci_data_dir = project_root / OAK_DIR / CI_DATA_DIR
    initialize_redaction(ci_data_dir)

    # --- Subsystem init (order matters: embedding -> vector store -> activity -> agents) ---

    # One-time migration: move legacy git-tracked scaffold to .oak/ci/cloud-relay
    from open_agent_kit.features.team.cloud_relay.scaffold import (
        migrate_scaffold_dir,
    )

    migrate_scaffold_dir(project_root)

    # One-time migration: move .env secrets to user override config
    _migrate_env_to_user_config(project_root)

    await _init_cloud_relay(state, project_root)
    await _ensure_swarm_tokens(state, project_root)

    provider_available = _init_embedding(state, project_root)

    try:
        _init_vector_store_and_indexer(state, project_root, provider_available)

        try:
            await _init_activity(state, project_root)
        except (OSError, ValueError, RuntimeError, sqlite3.Error) as e:
            logger.warning(f"Failed to initialize activity store: {e}")
            state.activity_store = None
            state.activity_processor = None

        try:
            _init_agents(state, project_root)
        except ImportError as e:
            logger.warning(f"Agent subsystem unavailable (SDK not installed): {e}")
        except (OSError, ValueError, RuntimeError) as e:
            logger.warning(f"Failed to initialize agent subsystem: {e}")
            state.agent_registry = None
            state.agent_executor = None
            state.agent_scheduler = None

        # Initialize interactive session manager for ACP
        try:
            from open_agent_kit.features.agent_runtime.interactive import (
                InteractiveSessionManager,
            )

            if state.activity_store is None:
                logger.warning("Interactive session manager unavailable (no activity store)")
                return
            state.interactive_session_manager = InteractiveSessionManager(
                project_root=project_root,
                activity_store=state.activity_store,
                retrieval_engine=state.retrieval_engine,
                vector_store=state.vector_store,
                agent_registry=state.agent_registry,
                activity_processor=state.activity_processor,
            )
            logger.info("Interactive session manager initialized for ACP")
        except ImportError as e:
            logger.warning(f"Interactive session manager unavailable (SDK not installed): {e}")
        except (OSError, ValueError, RuntimeError) as e:
            logger.warning(f"Failed to initialize interactive session manager: {e}")

    except (OSError, ValueError, RuntimeError, sqlite3.Error) as e:
        logger.warning(f"Failed to initialize: {e}")
        state.vector_store = None
        state.indexer = None

    # Team sync: start outbox sync worker
    try:
        _init_team_sync(state)
    except (OSError, ValueError, RuntimeError, sqlite3.Error) as e:
        logger.warning(f"Failed to initialize team sync: {e}")

    # Run one immediate version + upgrade check, then launch periodic loop
    check_version(state)
    check_upgrade_needed(state)
    version_check_task = asyncio.create_task(periodic_version_check(), name="version_check")
    state.background_tasks.append(version_check_task)

    # Run one immediate governance audit prune (ongoing pruning is power-aware via ActivityProcessor)
    run_governance_prune(state)

    yield

    await _shutdown(state)
