package com.correx.apps.server

import com.correx.apps.server.logging.LoggingEventStore
import com.correx.apps.server.registry.FileSystemWorkflowRegistry
import com.correx.apps.server.registry.ProviderRegistry
import com.correx.apps.server.workspace.WorkspaceResolver
import com.correx.core.approvals.domain.DefaultApprovalEngine
import com.correx.core.artifacts.kind.ArtifactKind
import com.correx.core.artifacts.kind.ConfigArtifactKind
import com.correx.core.artifacts.kind.JsonSchema
import com.correx.core.artifactstore.ArtifactStore
import com.correx.apps.server.config.ConfigService
import com.correx.core.config.ConfigHolder
import com.correx.core.config.ConfigLoader
import com.correx.core.config.CorrexConfig
import com.correx.core.config.OperatorProfile
import com.correx.core.config.ProfileLoader
import com.correx.core.config.ProviderConfig
import com.correx.core.context.builder.DefaultContextPackBuilder
import com.correx.core.context.compression.CompressionPolicy
import com.correx.core.inference.EmbeddingRelevanceScorer
import com.correx.infrastructure.compression.HttpTokenPruner
import com.correx.core.context.compression.DefaultContextCompressor
import com.correx.core.context.model.TokenBudget
import com.correx.core.events.EventDispatcher
import com.correx.core.events.stores.EventStore
import com.correx.core.inference.CapabilityScore
import com.correx.core.inference.DefaultInferenceRouter
import com.correx.core.inference.GenerationConfig
import com.correx.core.inference.InferenceProjector
import com.correx.core.inference.InferenceRepository
import com.correx.core.inference.InferenceRouter
import com.correx.core.inference.ModelCapability
import com.correx.core.kernel.orchestration.DefaultOrchestrationReducer
import com.correx.core.kernel.orchestration.DefaultSessionOrchestrator
import com.correx.core.kernel.orchestration.OrchestrationConfig
import com.correx.core.kernel.orchestration.OrchestrationTuning
import com.correx.core.kernel.orchestration.OrchestrationProjector
import com.correx.core.kernel.orchestration.OrchestrationRepository
import com.correx.core.kernel.orchestration.OrchestratorEngines
import com.correx.core.kernel.orchestration.ProcessStaticAnalysisRunner
import com.correx.core.kernel.orchestration.FileSystemContractEvaluator
import com.correx.core.kernel.orchestration.OrchestratorRepositories
import com.correx.core.kernel.orchestration.WorkspaceContext
import com.correx.core.kernel.orchestration.WorkspaceTools
import com.correx.core.kernel.orchestration.WorkspaceToolRegistryProvider
import com.correx.core.kernel.retry.DefaultRetryCoordinator
import com.correx.core.risk.DefaultRiskAssessor
import com.correx.core.talkie.TalkieFacade
import com.correx.core.talkie.l3.L3MetadataRehydrator
import com.correx.core.talkie.model.TalkieConfig
import com.correx.core.sessions.DefaultSessionReducer
import com.correx.core.sessions.DefaultSessionRepository
import com.correx.core.sessions.SessionProjector
import com.correx.core.sessions.projections.replay.DefaultEventReplayer
import com.correx.core.tasks.TaskService
import com.correx.core.tools.registry.ToolRegistry
import com.correx.core.transitions.resolution.DefaultTransitionResolver
import com.correx.core.validation.artifact.ArtifactPayloadValidator
import com.correx.core.validation.pipeline.ValidationPipeline
import com.correx.core.validation.semantic.SemanticValidator
import com.correx.core.validation.semantic.rules.CycleExitRule
import com.correx.core.validation.semantic.rules.MissingToolRule
import com.correx.core.toolintent.ToolCallAssessor
import com.correx.core.toolintent.WorkspacePolicy
import com.correx.core.toolintent.rules.ExecInterpreterRule
import com.correx.core.toolintent.rules.ManifestContainmentRule
import com.correx.core.toolintent.rules.NetworkHostRule
import com.correx.core.toolintent.rules.PathContainmentRule
import com.correx.core.toolintent.rules.ReadBeforeWriteRule
import com.correx.core.toolintent.rules.ReferenceExistsRule
import com.correx.core.toolintent.rules.ShellInFileContentRule
import com.correx.core.toolintent.rules.StaleWriteRule
import com.correx.core.toolintent.rules.WriteScopeRule
import com.correx.apps.server.freestyle.FreestyleDriver
import com.correx.apps.server.inference.SemanticReviewerImpl
import com.correx.apps.server.inference.summarizeWithInference
import com.correx.core.kernel.orchestration.JournalCompactionService
import com.correx.apps.server.personalization.ProfileAdaptationService
import com.correx.core.artifacts.kind.DefaultArtifactKindRegistry
import com.correx.core.events.types.ArtifactId
import com.correx.core.events.types.SessionId
import com.correx.infrastructure.InfrastructureModule
import com.correx.infrastructure.inference.DefaultProviderRegistry
import com.correx.infrastructure.workflow.ExecutionPlanCompiler
import com.correx.infrastructure.workflow.Lsp4jDiagnosticsRunner
import com.correx.infrastructure.workflow.PlanLinter
import com.correx.infrastructure.inference.CapabilityAwareRoutingStrategy
import com.correx.infrastructure.inference.commons.ManagedInferenceRouter
import com.correx.infrastructure.inference.commons.AmdResourceProbe
import com.correx.infrastructure.inference.commons.NvidiaResourceProbe
import com.correx.infrastructure.inference.commons.ResourceProbe
import com.correx.infrastructure.inference.commons.SystemResourceProbe
import com.correx.infrastructure.inference.commons.UnavailableProbe
import com.correx.core.inference.InferenceProvider
import com.correx.infrastructure.tools.DispatchingToolExecutor
import com.correx.infrastructure.tools.task.TaskTools
import com.correx.infrastructure.tools.FileEditConfig
import com.correx.infrastructure.tools.FileReadConfig
import com.correx.infrastructure.tools.FileWriteConfig
import com.correx.infrastructure.tools.ShellConfig
import com.correx.infrastructure.tools.ToolConfig
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

private val log = LoggerFactory.getLogger("com.correx.apps.server.Main")

private val loopbackHosts = setOf("localhost", "127.0.0.1", "::1")

fun main() {
    log.info("=== correx server starting ===")
    val artifactStore = InfrastructureModule.createArtifactStore()
    val eventStore = LoggingEventStore(InfrastructureModule.createEventStore(artifactStore))

    logStoresInfo(eventStore, artifactStore)

    val correxConfig = ConfigLoader.load()
    // Live, swappable config. Hot-tunable fields (orchestration timeout, router/compaction knobs,
    // personalization/project toggles) are read from this holder at use-time; ConfigService swaps
    // it and rebuilds the config-derived services on a TUI edit.
    val configHolder = ConfigHolder(correxConfig)
    val operatorProfile: OperatorProfile? = if (correxConfig.personalization.enabled) {
        ProfileLoader.load()
    } else null
    val eventDispatcher = EventDispatcher(eventStore)

    // Managed path: [[models]] present → correx spawns llama-server at boot
    // Static path:  [[models]] absent  → legacy [[providers]] / env-var behavior
    val firstProvider: InferenceProvider
    val infraRegistry: DefaultProviderRegistry
    val inferenceRouter: InferenceRouter
    // Non-null only on the managed path; backs manual model swap/pin from clients.
    var modelSwapper: ManagedInferenceRouter? = null
    // Live GPU/RAM gauge. The managed pid lets the GPU probe also report the model's process RSS;
    // it stays null on the static path. System RAM (overlaid below) is owner-agnostic, so the gauge
    // renders on both paths regardless of whether correx manages the model process.
    var managedPidSupplier: () -> Long? = { null }
    // Base URL used by the llama-server health probe; derived from whichever path is active.
    var llamaBaseUrl: String = "http://127.0.0.1:10000"

    if (correxConfig.models.isNotEmpty()) {
        val settings = correxConfig.modelsSettings
        val modelManager = InfrastructureModule.createModelManager(settings, eventStore, eventDispatcher)
        val descriptors = correxConfig.models.map { InfrastructureModule.modelConfigToDescriptor(it) }
        val targetId = settings.defaultModel ?: correxConfig.models.first().id
        val targetModelConfig = correxConfig.models.firstOrNull { it.id == targetId }
            ?: correxConfig.models.first()
        log.info("Managed path: loading model '{}' from '{}'", targetModelConfig.id, targetModelConfig.modelPath)
        val descriptor = InfrastructureModule.modelConfigToDescriptor(targetModelConfig)
        val managedProvider = runBlocking { modelManager.load(descriptor) }
        firstProvider = managedProvider
        infraRegistry = InfrastructureModule.createProviderRegistry(listOf(managedProvider))
        // Also register any static providers from [[providers]] (non-managed remotes)
        correxConfig.providers
            .mapNotNull { providerConfig -> buildProviderFromConfig(providerConfig) }
            .forEach { infraRegistry.register(it) }
        // Managed router owns per-stage model selection: it ensures the resolved model is resident
        // before routing (stage.modelId > capability match > default), evicting the prior model.
        val managedRouter = ManagedInferenceRouter(modelManager, descriptors, targetModelConfig.id)
        inferenceRouter = managedRouter
        modelSwapper = managedRouter
        managedPidSupplier = { modelManager.currentPid() }
        llamaBaseUrl = "http://${settings.host}:${settings.port}"
        // Shutdown hook to kill the managed llama-server
        Runtime.getRuntime().addShutdownHook(Thread {
            log.info("Shutdown: unloading managed model '{}'", targetModelConfig.id)
            runBlocking {
                runCatching { modelManager.unload(targetModelConfig.id) }
                    .onFailure { log.warn("Error during managed model unload: {}", it.message) }
            }
        })
    } else {
        val staticProviders = buildProviders(correxConfig)
        require(staticProviders.isNotEmpty()) { "At least one provider must be configured" }
        firstProvider = staticProviders.first()
        infraRegistry = InfrastructureModule.createProviderRegistry(staticProviders)
        inferenceRouter = DefaultInferenceRouter(infraRegistry, CapabilityAwareRoutingStrategy())
        llamaBaseUrl = correxConfig.providers.firstOrNull()?.url
            ?: System.getenv("CORREX_LLAMA_URL")
            ?: "http://127.0.0.1:10000"
    }

    // Build the resource gauge for whichever GPU vendor is present (pid-less GPU/VRAM reads work on
    // both paths), then overlay owner-agnostic system RAM so the gauge always renders (F-001 gauge).
    val gpuProbe: ResourceProbe = when {
        NvidiaResourceProbe.isAvailable() -> NvidiaResourceProbe(pidSupplier = managedPidSupplier)
        AmdResourceProbe.isAvailable() -> AmdResourceProbe(pidSupplier = managedPidSupplier)
        else -> UnavailableProbe
    }
    val resourceProbe: ResourceProbe = SystemResourceProbe(gpuProbe)

    val modelId = firstProvider.id.value
    logModelInfo(modelId, infraRegistry)

    val repositories = buildRepositories(eventStore)
    val approvalEngine = DefaultApprovalEngine()
    val toolsConfig = correxConfig.tools
    val sandboxRoot = System.getenv("CORREX_SANDBOX_ROOT")
        ?.let { Path.of(it) }
        ?: toolsConfig.sandboxRoot.takeIf { it.isNotEmpty() }?.let { Path.of(it) }
        ?: Paths.get(System.getProperty("user.home"), ".config", "correx", "sandbox")
    val explicitWorkingDir = System.getenv("CORREX_WORKING_DIR")
        ?.let { Path.of(it) }
        ?: toolsConfig.workingDir.takeIf { it.isNotEmpty() }?.let { Path.of(it) }
    val explicitWorkspaceRoot = System.getenv("CORREX_WORKSPACE_ROOT")
        ?.let { Path.of(it) }
        ?: toolsConfig.workspaceRoot.takeIf { it.isNotEmpty() }?.let { Path.of(it) }
    val shellAllowedExecutables = toolsConfig.shellAllowedExecutables.toSet()
    val bootWorkspace = resolveBootWorkspace(
        explicitWorkspaceRoot = explicitWorkspaceRoot,
        explicitWorkingDir = explicitWorkingDir,
        processWorkingDir = Path.of(""),
    )
    val workspaceRoot = bootWorkspace.workspaceRoot
    val workingDir = bootWorkspace.workingDir
    if (bootWorkspace.workingDirWasClamped) {
        log.warn(
            "configured working_dir {} is outside workspace_root {}; clamping working_dir to workspace_root",
            explicitWorkingDir,
            workspaceRoot,
        )
    }
    // One shared HTTP client backs both the default and per-workspace registries' research tools
    // (web_search/web_fetch). Built only when research is enabled, so the static path stays offline.
    // Lives for the process lifetime (shared across requests), so it's closed via shutdown hook
    // below rather than `.use { }`.
    val researchHttpClient = if (toolsConfig.research.enabled) {
        io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
    } else {
        null
    }
    val researchToolConfig = com.correx.infrastructure.tools.ResearchToolConfig(
        enabled = toolsConfig.research.enabled,
        searxngUrl = toolsConfig.research.searxngUrl,
        maxResults = toolsConfig.research.maxResults,
        maxFetchBytes = toolsConfig.research.maxFetchBytes,
        httpClient = researchHttpClient,
    )
    // Agents create/update/delete tasks through the tool system (tier-gated like any tool);
    // the service appends to the same event log. Project is derived from the task id prefix.
    // The knowledge retriever is late-bound: the L3 retriever isn't built until later in this
    // composition root, so the holder is captured now and its delegate set once L3 exists (below).
    val taskKnowledgeRetriever = com.correx.apps.server.memory.DeferredTaskKnowledgeRetriever()
    // Inlines linked ADRs/docs into the context bundle. The repo root is known here, so unlike the
    // L3 retriever this needs no late binding.
    val taskDocumentResolver = com.correx.apps.server.tasks.FileTaskDocumentResolver(workspaceRoot)
    // Inline ARTIFACT/SESSION link targets too: both read the event log (+ CAS) that already exists
    // here, so — like the doc resolver — they need no late binding.
    val taskArtifactResolver =
        com.correx.apps.server.tasks.EventStoreTaskArtifactResolver(eventStore, artifactStore)
    val taskSessionResolver =
        com.correx.apps.server.tasks.SessionSummaryTaskSessionResolver(eventStore)
    // Backs POST /tasks/sync-git: reads recent commits so a git hook / CI step can drive task status.
    val gitCommitReader = com.correx.apps.server.tasks.GitCommandCommitReader(workspaceRoot)
    val taskService = TaskService(eventStore)
    val taskTools = TaskTools.forService(
        taskService,
        taskKnowledgeRetriever,
        taskDocumentResolver,
        taskArtifactResolver,
        taskSessionResolver,
        com.correx.apps.server.tasks.EventStoreSessionFactRecorder(eventStore),
        com.correx.apps.server.tasks.EventStoreSessionWrites(eventStore),
    )
    // Retrieves full tool output the kernel spilled to CAS on truncation (ref shown in-context).
    val toolOutputTool = com.correx.infrastructure.tools.ToolOutputTool(artifactStore)
    // Mount configured MCP servers: each server's tools/list becomes Correx tools that ride the normal
    // ToolExecutor path (tier-gated, event-recorded, replay-safe — no special-casing). A server that
    // fails to start is logged and skipped rather than aborting the whole process.
    val mountedMcpServers = correxConfig.mcp.mapNotNull { mcpConfig ->
        runCatching {
            runBlocking {
                com.correx.infrastructure.tools.mcp.McpMounter.mount(
                    serverId = mcpConfig.id,
                    command = mcpConfig.command,
                    env = mcpConfig.env,
                    tier = runCatching { com.correx.core.approvals.Tier.valueOf(mcpConfig.tier) }
                        .getOrDefault(com.correx.core.approvals.Tier.T2),
                )
            }
        }.onSuccess { log.info("Mounted MCP server '{}' ({} tools)", mcpConfig.id, it.tools.size) }
            .onFailure { log.warn("Failed to mount MCP server '{}': {}", mcpConfig.id, it.message) }
            .getOrNull()
    }
    if (mountedMcpServers.isNotEmpty()) {
        Runtime.getRuntime().addShutdownHook(Thread {
            mountedMcpServers.forEach { server ->
                runCatching { server.close() }
                    .onFailure { log.warn("Error closing MCP server '{}': {}", server.serverId, it.message) }
            }
        })
    }
    val mcpTools = mountedMcpServers.flatMap { it.tools }
    // Index the workspace into codebase-memory (#242) off the startup path — a fast index makes its
    // search tools usable by the first session instead of failing "not indexed"; a slow index just
    // means the first stage or two miss it, not a blocked boot.
    if (mcpTools.any { it.name.endsWith("__index_repository") }) {
        Thread {
            runBlocking { bootstrapCodebaseIndex(mcpTools, workspaceRoot, log) }
        }.apply { isDaemon = true; name = "mcp-index-bootstrap" }.start()
    }
    val extraTools = taskTools + toolOutputTool + mcpTools
    val toolRegistry = InfrastructureModule.createToolRegistry(
        buildToolConfig(
            workspaceRoot,
            workingDir,
            shellAllowedExecutables,
            toolsConfig,
            researchToolConfig,
        ),
        extraTools = extraTools,
    )
    val toolExecutor = InfrastructureModule.createToolExecutor(
        registry = toolRegistry,
        eventDispatcher = eventDispatcher,
        workDir = sandboxRoot,
        artifactStore = artifactStore,
    )

    logToolInfo(sandboxRoot, workingDir, shellAllowedExecutables, toolRegistry)

    val privilegedLocations = toolsConfig.privilegedLocations.map { Path.of(it) }
    val workspacePolicy = WorkspacePolicy(workspaceRoot, privilegedLocations)
    val toolCallAssessor = ToolCallAssessor(
        rules = listOf(
            PathContainmentRule(),
            ReadBeforeWriteRule(),
            ReferenceExistsRule(),
            ShellInFileContentRule(),
            WriteScopeRule(),
            StaleWriteRule(),
            ManifestContainmentRule(),
            ExecInterpreterRule(toolsConfig.interpreterExecutables.toSet()),
            NetworkHostRule(
                allowedHosts = toolsConfig.networkAllowedHosts.toSet(),
                deniedHosts = toolsConfig.networkDeniedHosts.toSet(),
            ),
        ),
    )

    val wsToolRegistryProvider = WorkspaceToolRegistryProvider { workspace ->
        val wsRegistry = InfrastructureModule.createToolRegistry(
            buildToolConfigForWorkspace(workspace, shellAllowedExecutables, toolsConfig, researchToolConfig),
            extraTools = extraTools,
        )
        val wsExecutor = DispatchingToolExecutor(wsRegistry)
        WorkspaceTools(registry = wsRegistry, executor = wsExecutor)
    }

    // Constructed before the engines so the context builder can rank freeform turns by relevance
    // to the current query (pipeline §6, query-conditioned selection). Reused by L3 retrieval below.
    val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.talkie.embedder)
    val engines = OrchestratorEngines(
        transitionResolver = DefaultTransitionResolver { condition, ctx -> condition.evaluate(ctx) },
        contextPackBuilder = DefaultContextPackBuilder(
            DefaultContextCompressor(),
            policy = CompressionPolicy(correxConfig.orchestration.compressionLevel),
            tokenPruner = HttpTokenPruner(baseUrl = correxConfig.orchestration.tokenPrunerUrl),
            relevanceScorer = EmbeddingRelevanceScorer(embedder),
        ),
        inferenceRouter = inferenceRouter,
        validationPipeline = ValidationPipeline(
            validators = listOf(
                ArtifactPayloadValidator(),
                SemanticValidator(
                    rules = listOf(
                        CycleExitRule(requirePolicyForCycles = false),
                        MissingToolRule(),
                    ),
                ),
            ),
        ),
        approvalEngine = approvalEngine,
        riskAssessor = DefaultRiskAssessor(),
        promptResolver = { path -> InfrastructureModule.createPromptLoader().load(path) },
        toolRegistry = toolRegistry,
        toolExecutor = toolExecutor,
        toolCallAssessor = toolCallAssessor,
        workspacePolicy = workspacePolicy,
        workspaceToolRegistryProvider = wsToolRegistryProvider,
        staticAnalysisRunner = ProcessStaticAnalysisRunner(),
        lspDiagnosticsRunner = Lsp4jDiagnosticsRunner(),
        contractAssertionEvaluator = FileSystemContractEvaluator(),
        semanticReviewer = SemanticReviewerImpl(inferenceRouter),
    )
    val decisionJournalRepository = InfrastructureModule.createDecisionJournalRepository(eventStore)
    val defaultOrchestrationConfig = OrchestrationConfig(
        sandboxRoot = sandboxRoot,
        defaultSystemPromptPath = toolsConfig.defaultSystemPromptPath,
        // Stage timeout is now an operator-tunable knob ([orchestration] stage_timeout_ms, default
        // 3 min — local models are slow on large turns). ServerModule.orchestrationConfig() reads
        // the live value from the holder per session, so this base value is just the boot default.
        stageTimeoutMs = correxConfig.orchestration.stageTimeoutMs,
        journalCompactionTokenThreshold = correxConfig.orchestration.journalCompactionTokenThreshold,
    )
    val journalCompactionService = JournalCompactionService(
        artifactStore = artifactStore,
        summarize = { prompt -> summarizeWithInference(prompt, SessionId("system"), inferenceRouter) },
        // Supplier reads the live threshold each compaction check (no orchestrator rebuild needed).
        tokenThreshold = { configHolder.get().orchestration.journalCompactionTokenThreshold },
    )
    val configArtifactKindsEarly = loadConfigArtifactKinds(correxConfig)
    val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg ->
        configArtifactKindsEarly.forEach { reg.register(it) }
    }
    val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.talkie.l3)
    // Rebuild the L3 metadata map from the ChatTurnEvent log before serving queries:
    // persisted vectors survive restart but the metadata map does not (no-op for
    // non-rehydratable backends like in_memory).
    runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) }
    val repoKnowledgeRetriever: com.correx.apps.server.memory.L3RepoKnowledgeRetriever? =
        if (correxConfig.project.enabled) {
            com.correx.apps.server.memory.L3RepoKnowledgeRetriever(
                embedder = embedder,
                l3MemoryStore = l3MemoryStore,
                repoRoot = workspaceRoot.toString(),
                eventStore = eventStore,
            )
        } else {
            null
        }
    // L3 retriever now exists — bind it into the task context bundle's knowledge port so the
    // task_context tool and GET /tasks/{id}/context return semantically-relevant snippets.
    taskKnowledgeRetriever.delegate =
        repoKnowledgeRetriever?.let { com.correx.apps.server.memory.RepoKnowledgeTaskRetriever(it) }
    // Plan-compile gate: wrap the ExecutionPlanCompiler as a post-stage check so a compile-invalid
    // architect plan is handed back through the retry-feedback loop instead of parking the session in
    // ACTIVE (post-planning compile dead-end). Returns null on success, the compiler message on failure.
    // Operator sampling defaults ([sampling] config) sent on every stage inference request. maxTokens=0
    // is a placeholder; the loader/compiler pin it per-stage to the token budget via copy().
    val stageSamplingDefaults = with(correxConfig.sampling) {
        GenerationConfig(
            temperature = temperature,
            topP = topP,
            maxTokens = 0,
            topK = topK,
            minP = minP,
            repeatPenalty = repeatPenalty,
        )
    }
    val planCompiler = ExecutionPlanCompiler(
        artifactKindRegistry,
        toolRegistry.all().map { it.name }.toSet(),
        injectRecovery = true,
        samplingDefaults = stageSamplingDefaults,
    )
    // Startup-load orchestration tuning from the [orchestration] config section into the kernel's
    // OrchestrationTuning. ponytail: read once at boot — a config edit needs a restart to take effect
    // (unlike stage_timeout_ms, which ServerModule re-reads per session). Add a supplier if these ever
    // need hot-reload.
    val orchestrationTuning = with(correxConfig.orchestration) {
        OrchestrationTuning(
            maxToolRounds = maxToolRounds,
            readLoopNudgeThreshold = readLoopNudgeThreshold,
            rejectionLoopNudgeThreshold = rejectionLoopNudgeThreshold,
            maxFeedbackIssues = maxFeedbackIssues,
            repoMapInjectTopK = repoMapInjectTopK,
            repoMapFilesPerDir = repoMapFilesPerDir,
            docsCatalogMax = docsCatalogMax,
            maxClarificationRounds = maxClarificationRounds,
            reviewBlockMinConfidence = reviewBlockMinConfidence,
            reviewBlockRetryCap = reviewBlockRetryCap,
            reviewLoopMaxCycles = reviewLoopMaxCycles,
            defaultMaxRefinement = defaultMaxRefinement,
            recoveryRouteBudget = recoveryRouteBudget,
            intentRouteBudget = intentRouteBudget,
        )
    }
    val orchestrator = DefaultSessionOrchestrator(
        repositories = repositories,
        engines = engines.copy(
            planCompilationCheck = { json, candidateId ->
                // Compile first; a compile failure returns its message. If it compiles, also run the
                // deterministic lint here so a hard lint failure (unproduced need, trap state) feeds
                // back to the architect via the same retry loop — rather than the post-planning
                // FreestyleDriver lint rejecting it after the session already reached COMPLETED.
                val compiled = runCatching { planCompiler.compile(json, candidateId) }
                compiled.exceptionOrNull()?.message
                    ?: PlanLinter.lint(compiled.getOrThrow()).takeIf { !it.passed }?.let { lint ->
                        "plan lint failed: " + lint.hardFailures.joinToString("; ") {
                            "${it.code}${it.stageId?.let { s -> " @$s" } ?: ""}: ${it.detail}"
                        }
                    }
            },
        ),
        retryCoordinator = DefaultRetryCoordinator(eventStore),
        artifactStore = artifactStore,
        tokenizer = firstProvider.tokenizer,
        decisionJournalRepository = decisionJournalRepository,
        compactionService = journalCompactionService,
        artifactKindRegistry = artifactKindRegistry,
        repoKnowledgeRetriever = repoKnowledgeRetriever,
        readyTaskCounter = com.correx.apps.server.tasks.ProjectReadyTaskCounter(taskService),
        taskClaimCoordinator = com.correx.apps.server.tasks.DefaultTaskClaimCoordinator(
            taskService,
            com.correx.core.tasks.TaskContextAssembler(
                taskService,
                taskKnowledgeRetriever,
                taskDocumentResolver,
                taskArtifactResolver,
                taskSessionResolver,
            ),
        ),
        tuning = orchestrationTuning,
    )
    val workflowRegistry = FileSystemWorkflowRegistry(
        InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly, stageSamplingDefaults),
    )
    // Builds the router facade from a config snapshot, mapping the [router] block onto the domain
    // TalkieConfig. Reused by ConfigService's rebuild hook so router knob edits apply live (the
    // embedder and L3 store are construction-time and reused; the facade is what gets swapped).
    fun buildTalkieFacade(cfg: CorrexConfig): TalkieFacade {
        val rc = cfg.talkie
        val domainTalkieConfig = TalkieConfig(
            conversationKeepLast = rc.conversationKeepLast,
            retrievalK = rc.retrievalK,
            tokenBudget = TokenBudget(limit = rc.tokenBudget),
            generationConfig = GenerationConfig(
                temperature = rc.generation.temperature,
                topP = rc.generation.topP,
                maxTokens = rc.generation.maxTokens,
            ),
            narrationGenerationConfig = GenerationConfig(
                temperature = rc.narration.temperature,
                topP = rc.narration.topP,
                maxTokens = rc.narration.maxTokens,
            ),
            narrationModelId = rc.narration.modelId,
        )
        return InfrastructureModule.createTalkieFacade(
            eventStore = eventStore,
            inferenceRouter = inferenceRouter,
            config = domainTalkieConfig,
            tokenizer = firstProvider.tokenizer,
            embedder = embedder,
            l3MemoryStore = l3MemoryStore,
            workflowSummaryProvider = {
                workflowRegistry.listAll().map { s ->
                    com.correx.core.talkie.model.WorkflowSummary(s.workflowId, s.description, s.stageIds)
                }
            },
            sessionProfileProvider = { sid ->
                runCatching { repositories.sessionRepository.getSession(sid).state.boundProjectProfile }
                    .getOrNull()
                    ?.let { renderProjectProfileText(it) }
            },
        )
    }
    val routerFacade = buildTalkieFacade(correxConfig)
    val sessionUndoService = com.correx.apps.server.undo.SessionUndoService(
        eventStore = eventStore,
        artifactStore = artifactStore,
        bootRoots = setOf(workspaceRoot, workingDir),
    )
    val bootWorkspaceContext = WorkspaceContext(
        workspaceRoot = workspaceRoot,
        workingDir = workingDir,
        allowedPaths = setOf(workspaceRoot, workingDir),
        privilegedLocations = privilegedLocations,
    )
    val allowedWorkspaceRoots = toolsConfig.allowedWorkspaceRoots.map { Path.of(it) }
    val workspaceResolver = WorkspaceResolver(
        bootDefault = bootWorkspaceContext,
        privilegedLocations = privilegedLocations,
        allowedWorkspaceRoots = allowedWorkspaceRoots,
    )
    // Display-only architect contradiction surfacing (BACKLOG §B-§4): now live via the
    // ServerModule.start() subscription on the architect's `design` ArtifactCreatedEvent (see
    // ServerModule.handleArchitectArtifact). Gated on project.enabled because the checker queries the
    // same L3 "project:<repoRoot>" namespace that ProjectMemoryService.persist populates.
    val architectContradictionChecker: com.correx.apps.server.memory.ArchitectContradictionChecker? =
        if (correxConfig.project.enabled) {
            com.correx.apps.server.memory.ArchitectContradictionChecker(embedder, l3MemoryStore)
        } else {
            null
        }
    // Heuristic concept compiler (design 2026-07-12-acr-concept-compiler.md): promotes recurring
    // validated failure→fix patterns into L3 as retrieval-on-demand concepts. Fires only on ≥N
    // cross-session validated fixes, so it's harmless to run unconditionally.
    val conceptCompilerService =
        com.correx.apps.server.concept.ConceptCompilerService(eventStore, embedder, l3MemoryStore)
    // Built from a config snapshot and reused by ConfigService's rebuild hook so toggling
    // project.enabled / personalization.* applies live to the next session.
    fun buildProjectMemory(cfg: CorrexConfig): com.correx.apps.server.memory.ProjectMemoryService? =
        if (cfg.project.enabled) {
            com.correx.apps.server.memory.ProjectMemoryService(
                config = cfg.project,
                embedder = embedder,
                l3MemoryStore = l3MemoryStore,
                journalRepository = decisionJournalRepository,
                eventStore = eventStore,
                indexer = com.correx.apps.server.memory.RepoMapIndexer(
                    maxDepth = cfg.project.maxDepth,
                    ignoreGlobs = cfg.project.ignoreGlobs,
                ),
            )
        } else {
            null
        }
    fun buildProfileAdaptation(cfg: CorrexConfig): ProfileAdaptationService? =
        if (cfg.personalization.enabled && cfg.personalization.learn) {
            ProfileAdaptationService(
                decisionJournalRepository = decisionJournalRepository,
                summarize = { prompt -> summarizeWithInference(prompt, SessionId("system"), inferenceRouter) },
            )
        } else {
            null
        }
    val projectMemory = buildProjectMemory(correxConfig)
    val profileAdaptationService: ProfileAdaptationService? = buildProfileAdaptation(correxConfig)

    val freestyleDriver = FreestyleDriver(
        eventStore = eventStore,
        compiler = planCompiler,
        planContent = { sid -> orchestrator.validatedArtifactContent(sid, ArtifactId("execution_plan")) },
        config = defaultOrchestrationConfig,
        runPhase2 = { sid, graph, cfg -> orchestrator.run(sid, graph, cfg) },
        requestPlanApproval = { sid, planJson -> orchestrator.requestPlanApproval(sid, planJson) },
        toolCapabilities = toolRegistry.all().associate { it.name to it.requiredCapabilities },
        reflector = com.correx.apps.server.inference.CapabilityGapReflectorImpl(inferenceRouter),
        // Return-to-architect: re-run the planning workflow from the architect stage so it emits a
        // corrected plan after a grounding rejection. rehydrate before (architect needs the analyst's
        // dod, evicted on the planning graph's completion) and after (the fresh execution_plan is
        // evicted again when this re-run completes — lockAndRun's planContent must read it back).
        rerunArchitect = { sid ->
            orchestrator.rehydrate(sid)
            val planningGraph = workflowRegistry.find("freestyle_planning")
                ?: error("freestyle_planning workflow not registered")
            val result = orchestrator.runFrom(
                sid, planningGraph, defaultOrchestrationConfig, com.correx.core.events.types.StageId("architect"),
            )
            orchestrator.rehydrate(sid)
            result
        },
    )
    // observability-spec §4: continuous health watch. Seed the monitor's last-status from the
    // recorded system-session events so a restart doesn't re-emit a degraded already in the log.
    // healthProbeHttpClient lives for the process lifetime; closed via shutdown hook below.
    var healthProbeHttpClient: io.ktor.client.HttpClient? = null
    val healthMonitor = correxConfig.health.let { hc ->
        if (!hc.enabled) {
            null
        } else {
            val dataDir = Paths.get(System.getProperty("user.home"), ".config", "correx")
            val seeded = DefaultEventReplayer(eventStore, com.correx.apps.server.health.HealthProjection())
                .rebuild(com.correx.apps.server.health.SYSTEM_SESSION)
                .subjects.mapValues { it.value.status }
            healthProbeHttpClient = io.ktor.client.HttpClient(io.ktor.client.engine.cio.CIO)
            com.correx.apps.server.health.HealthMonitor(
                eventStore = eventStore,
                probes = listOfNotNull(
                    com.correx.apps.server.health.DiskWatermarkProbe(
                        monitoredPaths = listOf(dataDir),
                        warnBytes = hc.diskWarnBytes,
                        warnGrowthBytesPerMin = hc.diskWarnGrowthBytesPerMin,
                    ),
                    com.correx.apps.server.health.LlamaServerHealthProbe(
                        llamaBaseUrl = llamaBaseUrl,
                        httpClient = healthProbeHttpClient!!,
                        eventStore = eventStore,
                        livenessTimeoutMs = hc.llamaLivenessTimeoutMs,
                        tpsWarnBelow = hc.llamaTpsWarnBelow,
                    ),
                    com.correx.apps.server.health.EventStoreHealthProbe(
                        eventStore = eventStore,
                        latencyWarnMs = hc.eventStoreLatencyWarnMs,
                    ),
                ),
                intervalMs = hc.intervalMs,
                initialStatuses = seeded,
            )
        }
    }
    Runtime.getRuntime().addShutdownHook(
        Thread {
            researchHttpClient?.close()
            healthProbeHttpClient?.close()
        },
    )
    val module = ServerModule(
        orchestrator = orchestrator,
        eventStore = eventStore,
        artifactStore = artifactStore,
        sessionRepository = repositories.sessionRepository,
        workflowRegistry = workflowRegistry,
        providerRegistry = infraRegistry.asServerRegistry(),
        defaultOrchestrationConfig = defaultOrchestrationConfig,
        routerFacade = routerFacade,
        orchestrationRepository = repositories.orchestrationRepository,
        approvalRepository = repositories.approvalRepository,
        toolRegistry = toolRegistry,
        sessionUndoService = sessionUndoService,
        modelSwapper = modelSwapper,
        resourceProbe = resourceProbe,
        workspaceResolver = workspaceResolver,
        narrationMaxPerRun = correxConfig.talkie.narration.maxPerRun,
        projectMemory = projectMemory,
        architectContradictionChecker = architectContradictionChecker,
        conceptCompilerService = conceptCompilerService,
        configHolder = configHolder,
        freestyleDriver = freestyleDriver,
        operatorProfile = operatorProfile,
        profileAdaptationService = profileAdaptationService,
        healthMonitor = healthMonitor,
        taskKnowledgeRetriever = taskKnowledgeRetriever,
        taskDocumentResolver = taskDocumentResolver,
        taskArtifactResolver = taskArtifactResolver,
        taskSessionResolver = taskSessionResolver,
        gitCommitReader = gitCommitReader,
        gitRunBranchTransport = if (correxConfig.git.enabled) {
            com.correx.apps.server.git.GitRunBranchTransport(eventStore, correxConfig.git)
        } else {
            null
        },
    )
    // Wire live config editing: persist to TOML, swap the holder, and rebuild config-derived
    // services. Built after the module so the rebuild hook can swap them in place.
    module.configService = ConfigService(configHolder, ConfigLoader.configPath()) { cfg ->
        module.applyRebuiltServices(
            newTalkieFacade = buildTalkieFacade(cfg),
            newProjectMemory = buildProjectMemory(cfg),
            newProfileAdaptationService = buildProfileAdaptation(cfg),
        )
    }
    module.start()
    log.info("==============================")

    val serverConfig = correxConfig.server
    log.info("  host:port    : {}:{}", serverConfig.host, serverConfig.port)
    if (serverConfig.host !in loopbackHosts) {
        log.warn(
            "Server host '{}' is not loopback — the unauthenticated HTTP/WS surface is exposed to the network",
            serverConfig.host,
        )
    }

    embeddedServer(Netty, host = serverConfig.host, port = serverConfig.port) {
        configureServer(module)
    }.start(wait = true)
}

fun renderProjectProfileText(profile: com.correx.core.sessions.BoundProjectProfile?): String? {
    if (profile == null) return null
    return buildString {
        append("## Project profile\n")
        if (profile.about.isNotBlank()) append(profile.about).append("\n")
        if (profile.conventions.isNotEmpty()) {
            append("### Conventions\n")
            profile.conventions.forEach { append("- ").append(it).append("\n") }
        }
        if (profile.commands.isNotEmpty()) {
            append("### Commands\n")
            profile.commands.forEach { (name, cmd) -> append(name).append(": ").append(cmd).append("\n") }
        }
    }.trimEnd().takeIf { it.isNotBlank() }
}

private fun logModelInfo(
    modelId: String,
    infraRegistry: DefaultProviderRegistry,
) {
    log.info("==========MODEL INFO==========")
    log.info("  model id     : {}", modelId)
    log.info("  providers    : {} registered", infraRegistry.listAll().size)
}

private fun logToolInfo(
    sandboxRoot: Path?,
    workingDir: Path?,
    shellAllowedExecutables: Set<String>,
    toolRegistry: ToolRegistry,
) {
    log.info("==========TOOLS INFO==========")
    log.info("  sandbox root : {}", sandboxRoot)
    log.info("  working dir  : {}", workingDir)
    if (shellAllowedExecutables.isEmpty()) {
        log.warn("  shell tool   : no executable allowlist configured — all executables allowed")
    }
    log.info("  tools        : {}", toolRegistry.all().map { it.name })
}

private fun logStoresInfo(
    eventStore: LoggingEventStore,
    artifactStore: ArtifactStore,
) {
    log.info("==========STORE INFO==========")
    log.info("  event store  : {}", eventStore::class.simpleName)
    log.info("  artifact store: {}", artifactStore::class.simpleName)
}

// Build user-declared artifact kinds from config. Each [[artifacts]] entry points to a JSON
// schema file; we parse it into a JsonSchema and wrap it in a ConfigArtifactKind. A schema that
// can't be loaded is skipped with a warning — any workflow that references the kind then fails
// loudly at load time ("Unknown artifact kind"), rather than running with an unvalidated kind.
private fun loadConfigArtifactKinds(config: CorrexConfig): List<ArtifactKind> {
    val configDir = ConfigLoader.configPath().parent
    return config.artifacts.mapNotNull { decl ->
        val schemaPath = resolveConfigRelativePath(decl.schemaPath, configDir)
        val schema = runCatching {
            Json.decodeFromString(JsonSchema.serializer(), Files.readString(schemaPath))
        }.getOrElse { e ->
            log.warn("artifact kind '{}' schema '{}' failed to load: {}", decl.id, schemaPath, e.message)
            return@mapNotNull null
        }
        ConfigArtifactKind(id = decl.id, schema = schema, llmEmitted = decl.llmEmitted)
    }
}

private fun resolveConfigRelativePath(raw: String, configDir: Path?): Path {
    val expanded = if (raw.startsWith("~/")) {
        Paths.get(System.getProperty("user.home"), raw.removePrefix("~/"))
    } else {
        Path.of(raw)
    }
    return when {
        expanded.isAbsolute -> expanded
        configDir != null -> configDir.resolve(expanded)
        else -> expanded
    }
}

private fun buildProviders(config: CorrexConfig): List<InferenceProvider> {
    return if (config.providers.isNotEmpty()) {
        log.info("Loading {} provider(s) from config", config.providers.size)
        config.providers.mapNotNull { providerConfig -> buildProviderFromConfig(providerConfig) }
    } else {
        log.info("No providers in config; using env var fallback")
        listOf(
            InfrastructureModule.createLlamaCppProvider(
                modelId = System.getenv("CORREX_MODEL_ID") ?: "default",
                modelPath = System.getenv("CORREX_MODEL_PATH") ?: "",
                baseUrl = System.getenv("CORREX_LLAMA_URL") ?: "http://127.0.0.1:10000",
            ),
        )
    }
}

// Single dispatch from a [[providers]] entry to a concrete InferenceProvider.
private fun buildProviderFromConfig(config: ProviderConfig): InferenceProvider? {
    val capabilities = parseCapabilitiesFromConfig(config.capabilities)
    return when (config.type.lowercase()) {
        "llamacpp" -> InfrastructureModule.createLlamaCppProvider(
            modelId = config.modelId,
            modelPath = config.modelPath,
            baseUrl = config.url,
            capabilities = capabilities,
        )

        "nim", "openai" -> {
            val apiKey = resolveApiKey(config)
            if (apiKey.isBlank()) {
                log.error(
                    "Provider '{}' (type {}) has no API key (set api_key or api_key_env); skipping",
                    config.id, config.type,
                )
                null
            } else {
                InfrastructureModule.createOpenAiCompatProvider(
                    modelId = config.modelId,
                    baseUrl = config.url,
                    apiKey = apiKey,
                    idPrefix = config.type.lowercase(),
                    capabilities = capabilities,
                )
            }
        }

        else -> {
            log.error("Unknown provider type: {}", config.type)
            null
        }
    }
}

// Prefer the literal api_key; otherwise read the named env var. Returns "" when neither resolves.
private fun resolveApiKey(config: ProviderConfig): String = when {
    config.apiKey.isNotBlank() -> config.apiKey
    config.apiKeyEnv.isNotBlank() -> System.getenv(config.apiKeyEnv).orEmpty()
    else -> ""
}

private fun parseCapabilitiesFromConfig(capsMap: Map<String, Double>): Set<CapabilityScore> {
    val result = mutableSetOf<CapabilityScore>()
    capsMap.forEach { (capName, score) ->
        val capability = when (capName.lowercase()) {
            "general" -> ModelCapability.General
            "coding" -> ModelCapability.Coding
            "reasoning" -> ModelCapability.Reasoning
            "summarization" -> ModelCapability.Summarization
            "toolcalling" -> ModelCapability.ToolCalling
            else -> {
                log.warn("Unknown capability name: {}", capName)
                return@forEach
            }
        }
        result.add(CapabilityScore(capability, score))
    }
    return result
}

private fun buildRepositories(
    eventStore: EventStore,
): OrchestratorRepositories = OrchestratorRepositories(
    eventStore = eventStore,
    inferenceRepository = InferenceRepository(DefaultEventReplayer(eventStore, InferenceProjector())),
    orchestrationRepository = OrchestrationRepository(
        DefaultEventReplayer(eventStore, OrchestrationProjector(DefaultOrchestrationReducer())),
    ),
    sessionRepository = DefaultSessionRepository(
        DefaultEventReplayer(eventStore, SessionProjector(DefaultSessionReducer())),
    ),
    artifactRepository = InfrastructureModule.createArtifactRepository(eventStore),
    approvalRepository = InfrastructureModule.createApprovalRepository(eventStore),
)

// File tools write IN PLACE into the workspace (cwd / allow-listed dirs). The old
// sandboxRoot dual-write is retired; reversibility is event-sourced (FileWrittenEvent +
// CAS pre/post images, captured by SandboxedToolExecutor). sandboxRoot survives only as
// the executor's scratch workDir, never as a write destination.
private fun buildToolConfig(
    workspaceRoot: Path,
    workingDir: Path,
    shellAllowedExecutables: Set<String>,
    toolsConfig: com.correx.core.config.ToolsConfig,
    research: com.correx.infrastructure.tools.ResearchToolConfig,
): ToolConfig {
    val allowed = setOf(workspaceRoot)
    return ToolConfig(
        shell = ShellConfig(
            enabled = toolsConfig.shellEnabled,
            allowedExecutables = shellAllowedExecutables,
            workingDir = workingDir,
        ),
        fileRead = FileReadConfig(
            enabled = toolsConfig.fileReadEnabled,
            allowedPaths = allowed,
            workingDir = workingDir,
        ),
        fileWrite = FileWriteConfig(
            allowedPaths = allowed,
            enabled = toolsConfig.fileWriteEnabled,
            workingDir = workingDir,
        ),
        fileEdit = FileEditConfig(
            enabled = toolsConfig.fileEditEnabled,
            allowedPaths = allowed,
            workingDir = workingDir,
        ),
        research = research,
    )
}

private fun buildToolConfigForWorkspace(
    workspace: WorkspaceContext,
    shellAllowedExecutables: Set<String>,
    toolsConfig: com.correx.core.config.ToolsConfig,
    research: com.correx.infrastructure.tools.ResearchToolConfig,
): ToolConfig = ToolConfig(
    research = research,
    shell = ShellConfig(
        enabled = toolsConfig.shellEnabled,
        allowedExecutables = shellAllowedExecutables,
        workingDir = workspace.workingDir,
    ),
    fileRead = FileReadConfig(
        enabled = toolsConfig.fileReadEnabled,
        allowedPaths = workspace.allowedPaths,
        workingDir = workspace.workingDir,
    ),
    fileWrite = FileWriteConfig(
        enabled = toolsConfig.fileWriteEnabled,
        allowedPaths = workspace.allowedPaths,
        workingDir = workspace.workingDir,
    ),
    fileEdit = FileEditConfig(
        enabled = toolsConfig.fileEditEnabled,
        allowedPaths = workspace.allowedPaths,
        workingDir = workspace.workingDir,
    ),
)

private fun DefaultProviderRegistry.asServerRegistry(): ProviderRegistry = object : ProviderRegistry {
    override fun listAll() = this@asServerRegistry.listAll()
    override suspend fun healthCheckAll() = this@asServerRegistry.healthCheckAll()
}
