package com.correx.core.config

import kotlinx.serialization.Serializable

@Serializable
data class CorrexConfig(
    val server: ServerConfig = ServerConfig(),
    val tui: TuiConfig = TuiConfig(),
    val cli: CliConfig = CliConfig(),
    val tools: ToolsConfig = ToolsConfig(),
    val providers: List<ProviderConfig> = emptyList(),
    val talkie: TalkieConfig = TalkieConfig(),
    val models: List<ModelConfig> = emptyList(),
    val modelsSettings: ModelsSettings = ModelsSettings(),
    val artifacts: List<ArtifactKindConfig> = emptyList(),
    val project: ProjectConfig = ProjectConfig(),
    val personalization: PersonalizationConfig = PersonalizationConfig(),
    val orchestration: OrchestrationKnobs = OrchestrationKnobs(),
    val sampling: SamplingConfig = SamplingConfig(),
    val health: HealthConfig = HealthConfig(),
    val git: GitConfig = GitConfig(),
    val mcp: List<McpServerConfig> = emptyList(),
)

/**
 * An MCP server to mount at startup. Its `tools/list` becomes Correx tools (`mcp__<id>__<name>`),
 * gated at [tier] (default T2 = approval) since an external tool's side effects are opaque.
 */
@Serializable
data class McpServerConfig(
    val id: String,
    val command: List<String>,
    val env: Map<String, String> = emptyMap(),
    val tier: String = "T2",
)

/**
 * Optional transport for a server-owned checkout. Each run executes on a `run/<sessionId>` branch
 * based on [baseBranch] and pushes that branch on terminal completion; the working directory stays
 * a real local path, never a remote URL or mounted client filesystem.
 */
@Serializable
data class GitConfig(
    val enabled: Boolean = false,
    val remote: String = "origin",
    val baseBranch: String = "main",
    /** Optional Git author value, for example `Correx <correx@example.invalid>`. */
    val author: String = "",
)

/**
 * Sampling knobs sent to the llama-server on every *stage* inference request (the main agentic
 * loop). [temperature] and [topP] default to the former hardcoded stage values. [topK], [minP] and
 * [repeatPenalty] are null by default, meaning the request omits them and the model keeps its own
 * default — set them to tighten a rambling local model. maxTokens is not here: it is pinned per-stage
 * to the token budget. Talkie chat/narration have their own [GenerationSettings]/[NarrationSettings].
 */
@Serializable
data class SamplingConfig(
    val temperature: Double = 0.7,
    val topP: Double = 1.0,
    val topK: Int? = null,
    val minP: Double? = null,
    val repeatPenalty: Double? = null,
)

/**
 * Continuous health watch (observability-spec §4). When [enabled], a background monitor polls
 * the on-disk footprint every [intervalMs] and records a degraded/restored event on the edge.
 * [diskWarnBytes] is the absolute event-log + CAS size that trips a warning; [diskWarnGrowthBytesPerMin]
 * is the growth rate that warns log compaction is becoming non-theoretical. Defaults suit a homelab.
 *
 * [llamaLivenessTimeoutMs] bounds the GET /health ping to the llama-server; a connection error or
 * timeout within this window is reported as DEGRADED (liveness). [llamaTpsWarnBelow] is the
 * tokens/sec floor below which the probe reports degradation (KV-cache pressure, thermal throttle);
 * 0.0 disables the degradation dimension (no baseline to compare against on a fresh install).
 *
 * [eventStoreLatencyWarnMs] is the read-latency threshold above which the event-store probe reports
 * DEGRADED. A single cheap read (lastGlobalSequence) is timed each tick; a slow or throwing read
 * signals contention or unavailability. 500 ms suits typical SQLite on a homelab SSD.
 */
@Serializable
data class HealthConfig(
    val enabled: Boolean = true,
    val intervalMs: Long = 30_000,
    val diskWarnBytes: Long = 5_000_000_000,
    val diskWarnGrowthBytesPerMin: Long = 50_000_000,
    val llamaLivenessTimeoutMs: Long = 5_000,
    val llamaTpsWarnBelow: Double = 0.0,
    val eventStoreLatencyWarnMs: Long = 500,
)

/**
 * Operator-tunable orchestration knobs. [stageTimeoutMs] bounds a single stage's inference
 * turn; local models on large turns can need minutes. [journalCompactionTokenThreshold] is the
 * decision-journal size at which compaction kicks in. Mirrors the kernel OrchestrationConfig
 * defaults so the config file and the runtime agree when the section is absent.
 */
@Serializable
data class OrchestrationKnobs(
    val stageTimeoutMs: Long = 180_000,
    val journalCompactionTokenThreshold: Int = 2_000,
    /**
     * Boot-time abandoned-session pickup only resumes sessions whose last event is at most
     * this old; anything staler stays parked (POST /sessions/{id}/resume revives it on demand).
     * 0 disables auto-resume entirely.
     */
    val resumeAbandonedMaxAgeMinutes: Long = 1_440,
    /**
     * Context compression pipeline level (docs/plans/correx-compression-pipeline.md §5), additive
     * 1..9. Default 2 = free format-compress + static cache. Raise toward the 16k wall; ≥3 needs
     * the LLMLingua-2 sidecar at [tokenPrunerUrl] (fails open if absent). Default 4 = format +
     * static cache + token pruning + age-tiering, sized for a 12b on a 16k window.
     */
    val compressionLevel: Int = 4,
    val tokenPrunerUrl: String = "http://127.0.0.1:8199",
    /**
     * Orchestration loop/threshold/budget tuning, read once at server startup. Defaults equal the
     * kernel's former hardcoded constants — an absent section reproduces prior behavior. Reach for
     * these when a local model misbehaves (thrashes a read loop, re-asks clarifications, gets trapped
     * by a stubborn reviewer). Mirrors kernel OrchestrationTuning.
     */
    val maxToolRounds: Int = 30,
    val readLoopNudgeThreshold: Int = 3,
    val rejectionLoopNudgeThreshold: Int = 3,
    val maxFeedbackIssues: Int = 3,
    val repoMapInjectTopK: Int = 30,
    val repoMapFilesPerDir: Int = 8,
    val docsCatalogMax: Int = 20,
    val maxClarificationRounds: Int = 3,
    val reviewBlockMinConfidence: Double = 0.7,
    val reviewBlockRetryCap: Int = 20,
    /** Review→rework cycles before deterministic escalation to the recovery stage. */
    val reviewLoopMaxCycles: Int = 3,
    val defaultMaxRefinement: Int = 3,
    val recoveryRouteBudget: Int = 2,
    val intentRouteBudget: Int = 2,
)

@Serializable
data class PersonalizationConfig(
    val enabled: Boolean = false,
    val learn: Boolean = false,
)

/**
 * Project-scoped, cross-session memory. When [enabled], the decision journal is distilled
 * to durable per-repo memory at session end and retrieved (top-[memoryK]) at session start.
 * The repository key is always the session's recorded bound workspace, never configuration.
 */
@Serializable
data class ProjectConfig(
    val enabled: Boolean = false,
    val memoryK: Int = 5,
    val maxDepth: Int = 4,
    val ignoreGlobs: List<String> = DEFAULT_IGNORES,
    val injectTopK: Int = 30,
) {
    companion object {
        val DEFAULT_IGNORES: List<String> = listOf(
            ".git", "node_modules", "build", "target", ".gradle", "dist", ".idea", ".opencode",
            // Role-prompt templates: loaded by workflow config as LLM system prompts, not project
            // docs — indexing them lets a stage `file_read` another role's prompt as if it were
            // documentation.
            "examples/workflows/prompts",
        )
    }
}

/**
 * A user-declared artifact kind. [schemaPath] points to a JSON file holding the
 * kind's JSON schema (object type, typed properties, required, additionalProperties).
 * A path-to-file (not inline TOML) is used so schemas can nest arbitrarily and be
 * parsed with a real JSON parser. [llmEmitted] kinds are validated against this
 * schema, never trusted.
 */
@Serializable
data class ArtifactKindConfig(
    val id: String,
    val schemaPath: String,
    val llmEmitted: Boolean = false,
)

@Serializable
data class ServerConfig(
    val host: String = "localhost",
    val port: Int = 8080,
)

@Serializable
data class TuiConfig(
    val theme: String = "dark",
    val sessionListLimit: Int = 5,
)

@Serializable
data class CliConfig(
    val defaultOutput: String = "human",
)

@Serializable
data class ToolsConfig(
    val sandboxRoot: String = "",
    val workingDir: String = "",
    val shellAllowedExecutables: List<String> = emptyList(),
    val defaultSystemPromptPath: String = "~/.config/correx/prompts/default_system.md",
    val shellEnabled: Boolean = true,
    val fileReadEnabled: Boolean = true,
    val fileWriteEnabled: Boolean = true,
    val fileEditEnabled: Boolean = true,
    val workspaceRoot: String = "",
    val privilegedLocations: List<String> = DEFAULT_PRIVILEGED_LOCATIONS,
    val interpreterExecutables: List<String> = DEFAULT_INTERPRETERS,
    val networkAllowedHosts: List<String> = emptyList(),
    val networkDeniedHosts: List<String> = emptyList(),
    val allowedWorkspaceRoots: List<String> = emptyList(),
    val research: ResearchConfig = ResearchConfig(),
) {
    companion object {
        val DEFAULT_PRIVILEGED_LOCATIONS: List<String> = listOf(
            "/etc", "/usr", "/bin", "/sbin", "/boot", "/lib", "/lib64",
            "/sys", "/proc", "/dev", "/root",
        )
        val DEFAULT_INTERPRETERS: List<String> = listOf(
            "python", "python2", "python3", "bash", "sh", "zsh", "fish",
            "node", "deno", "ruby", "perl", "php", "Rscript", "lua", "groovy",
        )
    }
}

/**
 * Deep-research workflow (research-workflow-spec). On by default (like shell/file tools):
 * registers the network tools web_search (T1, hits only [searxngUrl]) and web_fetch (T2, which
 * is operator-approved, so nothing leaves the machine unapproved regardless of this flag).
 * [searxngUrl] is the self-hosted SearXNG endpoint; [maxFetchBytes] caps a single fetched page.
 */
@Serializable
data class ResearchConfig(
    val enabled: Boolean = true,
    val searxngUrl: String = "http://localhost:8888",
    val maxResults: Int = 8,
    val maxFetchBytes: Long = 10_000_000,
)

@Serializable
data class ProviderConfig(
    val id: String,
    val type: String,
    val modelId: String,
    val modelPath: String = "",
    val url: String = "http://127.0.0.1:10000",
    val capabilities: Map<String, Double> = emptyMap(),
    // Bearer credentials for remote providers (e.g. NVIDIA NIM). `apiKey` is the literal token;
    // `apiKeyEnv` names an env var to read it from at startup (preferred — keeps the secret out
    // of config files). Both are blank/unused for local providers like llamacpp.
    val apiKey: String = "",
    val apiKeyEnv: String = "",
)

@Serializable
data class TalkieConfig(
    val embedder: EmbedderConfig = EmbedderConfig(),
    val l3: L3Config = L3Config(),
    val conversationKeepLast: Int = 6,
    val retrievalK: Int = 5,
    val tokenBudget: Int = 4096,
    val generation: GenerationSettings = GenerationSettings(),
    val narration: NarrationSettings = NarrationSettings(),
)

/** Sampling/length knobs for router chat + steering replies. */
@Serializable
data class GenerationSettings(
    val temperature: Double = 0.7,
    val topP: Double = 0.9,
    val maxTokens: Int = 512,
)

/** Router narration knobs. Larger maxTokens than chat so reasoning models can
 *  finish "thinking" and still emit the line; maxPerRun caps narrations per run.
 *  1024 proved too tight — reasoning models spent the whole budget thinking and
 *  returned empty content (finishReason=length), so narrations never reached the
 *  TUI. 4096 leaves room to finish reasoning and still produce the line. */
@Serializable
data class NarrationSettings(
    val temperature: Double = 0.7,
    val topP: Double = 0.9,
    val maxTokens: Int = 4096,
    val maxPerRun: Int = 100,
    val modelId: String? = null,
)

@Serializable
data class EmbedderConfig(
    val backend: String = "noop",
    val dimension: Int = 1536,
    val url: String? = null,
    val modelId: String? = null,
)

@Serializable
data class L3Config(
    val backend: String = "in_memory",
    val persistPath: String? = null,
    val pythonExecutable: String = "python3",
    val scriptPath: String? = null,
    val dim: Int = 1536,
    val bitWidth: Int = 4,
)

@Serializable
data class ModelConfig(
    val id: String,
    val modelPath: String,
    /** Default window for implementation stages; leave headroom above their 24K prompt budget. */
    val contextSize: Int = 24_576,
    val params: Map<String, String> = emptyMap(),
    val capabilities: Map<String, Double> = emptyMap(),
)

@Serializable
data class ModelsSettings(
    val defaultModel: String? = null,
    val llamaServerBin: String = "llama-server",
    val host: String = "127.0.0.1",
    val port: Int = 10000,
)
