//! Loads curator configuration from the server TOML.
//!
//! Extracted from `main.rs` to be testable via integration tests.
//! Exposes [`build_curator_pipeline`], which instantiates the appropriate backend
//! based on the `[curator]` section of the configuration file.
//!
//! ## Backward compatibility
//!
//! - TOML file absent → offline heuristic mode (not a fatal error).
//! - `[curator]` section absent → heuristic mode (TOML default).
//! - `backend = "openai_compat"` + valid `[curator.llm]` → LLM mode + CircuitBreaker.
//! - `backend = "openai_compat"` BUT `[curator.llm]` absent → warn + heuristic fallback.
//!
//! ## Planned evolution
//!
//! The `ollama_compat`, `anthropic_compat`, and `gemini_compat` backends are
//! delegated to `gradatum-curator::CuratorPipeline::from_config()` — their
//! implementation is transparent from this module.

use figment::{
    Figment,
    providers::{Format, Toml},
};
use gradatum_curator::{CuratorLlmConfig, CuratorPipeline, CuratorPipelineConfig};
use serde::{Deserialize, Serialize};
use tracing::{error, info, warn};

// ── Deserialization structs for the [curator] TOML section ───────────────────
//
// These local structs avoid depending on `gradatum-server` (cyclic dependency).
// Manually kept in sync with `gradatum_server::config::CuratorConfig` + `LlmConfig`.
// Same local-DTO pattern as dispatch.rs.

/// Curator config read from the `[curator]` section of the server TOML.
///
/// Duplicated from `gradatum_server::config::CuratorConfig` to avoid the
/// `gradatum-worker → gradatum-server` dependency.
///
/// ## Gating fields
///
/// Fields such as `llm_review_enabled` and `confidence_threshold` are propagated to
/// [`CuratorPipelineConfig`] via [`From<&WorkerCuratorConfig>`].
/// Without them, `llm_review_enabled` stays `false` (default) and the LLM is never
/// called regardless of the TOML config.
///
/// ## Manual synchronisation
///
/// Keep in sync with `gradatum_core::config::CuratorConfig` (9 gating fields).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerCuratorConfig {
    /// `"heuristic"` (default) | `"openai_compat"` | `"ollama_compat"` | ...
    #[serde(default = "default_curator_backend")]
    pub backend: String,
    /// Optional LLM config. Absent = pure heuristic.
    #[serde(default)]
    pub llm: Option<WorkerLlmConfig>,

    // ── Gating fields (sync with gradatum_core::config::CuratorConfig) ────────
    /// Heuristic direct-admission threshold (0.0–1.0). `None` → 0.8.
    #[serde(default)]
    pub heuristic_admit_threshold: Option<f32>,
    /// Default status assigned by the heuristic (kebab-case). `None` → "pending-review".
    #[serde(default)]
    pub heuristic_default_status: Option<String>,
    /// Enables LLM review for notes below `confidence_threshold`. `None` → false.
    #[serde(default)]
    pub llm_review_enabled: Option<bool>,
    /// Confidence threshold below which LLM review is triggered. `None` → 0.7.
    #[serde(default)]
    pub confidence_threshold: Option<f32>,
    /// URL of the LLM review endpoint (OpenAI Chat API compatible).
    #[serde(default)]
    pub llm_review_endpoint: Option<String>,
    /// LLM model used for review.
    #[serde(default)]
    pub llm_review_model: Option<String>,
    /// Timeout in milliseconds for LLM review calls. `None` → backend default.
    #[serde(default)]
    pub llm_review_timeout_ms: Option<u32>,
    /// Maximum number of tokens generated by the review LLM.
    #[serde(default)]
    pub llm_review_max_tokens: Option<u32>,
    /// Behaviour on LLM failure. `None` → "pending-review-fallback".
    /// Values: `"pending-review-fallback"` | `"reject"` | `"admit-pending-review"`.
    #[serde(default)]
    pub llm_review_fallback: Option<String>,
}

fn default_curator_backend() -> String {
    "heuristic".to_string()
}

impl Default for WorkerCuratorConfig {
    fn default() -> Self {
        Self {
            backend: default_curator_backend(),
            llm: None,
            heuristic_admit_threshold: None,
            heuristic_default_status: None,
            llm_review_enabled: None,
            confidence_threshold: None,
            llm_review_endpoint: None,
            llm_review_model: None,
            llm_review_timeout_ms: None,
            llm_review_max_tokens: None,
            llm_review_fallback: None,
        }
    }
}

/// LLM config read from `[curator.llm]` of the server TOML.
///
/// Duplicated from `gradatum_server::config::LlmConfig`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerLlmConfig {
    /// `"openai_compat"` | `"ollama_compat"` | `"anthropic_compat"` | `"gemini_compat"`.
    pub backend: String,
    /// Base URL of the endpoint (without path).
    pub base_url: String,
    /// Model name (e.g. `"my-model"`, `"Qwen3-4B-Instruct-2507"`).
    pub model: String,
    /// Name of the env variable carrying the bearer token. Absent = no auth.
    #[serde(default)]
    pub api_key_env: Option<String>,
    /// Per-request timeout in milliseconds. Default: 5 000 ms.
    #[serde(default = "default_timeout_ms")]
    pub timeout_ms: u64,
    /// Maximum number of tokens generated by the LLM. `None` → backend default (1 024).
    ///
    /// Redundant with `WorkerCuratorConfig.llm_review_max_tokens` but kept here for
    /// TOML readability: one can write `[curator.llm] max_tokens = 2048`.
    /// Resolution priority: `llm_review_max_tokens` (top-level `[curator]` field) is
    /// propagated directly into `CuratorPipelineConfig`; this `[curator.llm] max_tokens`
    /// field is ignored by the pipeline (not propagated in `From<&WorkerCuratorConfig>`).
    #[serde(default)]
    pub max_tokens: Option<u32>,
}

fn default_timeout_ms() -> u64 {
    5000
}

impl From<&WorkerCuratorConfig> for CuratorPipelineConfig {
    /// Converts the worker TOML config into the curator pipeline config.
    ///
    /// All gating fields are propagated explicitly to prevent `llm_review_enabled`
    /// from staying `false` (its default) despite `llm_review_enabled = true` in the TOML.
    fn from(w: &WorkerCuratorConfig) -> Self {
        CuratorPipelineConfig {
            backend: w.backend.clone(),
            llm: w.llm.as_ref().map(|l| CuratorLlmConfig {
                backend: l.backend.clone(),
                base_url: l.base_url.clone(),
                model: l.model.clone(),
                api_key_env: l.api_key_env.clone(),
                timeout_ms: l.timeout_ms,
            }),
            // ── Gating fields — direct propagation from TOML ─────────────────
            heuristic_admit_threshold: w.heuristic_admit_threshold,
            heuristic_default_status: w.heuristic_default_status.clone(),
            llm_review_enabled: w.llm_review_enabled,
            confidence_threshold: w.confidence_threshold,
            llm_review_endpoint: w.llm_review_endpoint.clone(),
            llm_review_model: w.llm_review_model.clone(),
            llm_review_timeout_ms: w.llm_review_timeout_ms,
            llm_review_max_tokens: w.llm_review_max_tokens,
            llm_review_fallback: w.llm_review_fallback.clone(),
        }
    }
}

/// Returns `true` if the figment error indicates an absent section (not a TOML parse error).
///
/// `figment::Error` is a linked list of errors (`prev: Option<Box<Error>>`).
/// `IntoIterator` is implemented but consumes the value — clone to iterate.
/// A section is considered "absent" when all errors are `MissingField`.
fn is_missing_field_error(e: &figment::Error) -> bool {
    // figment::Error is Clone — clone to iterate without consuming the reference.
    e.clone().into_iter().all(|inner| inner.missing())
}

/// Loads the `[curator]` section from the config file and builds the pipeline.
///
/// ## Behaviour
///
/// - File absent → log warn + return offline heuristic (not a fatal error).
/// - `[curator]` section absent in the file → heuristic mode (TOML default).
/// - `backend = "openai_compat"` + valid `[curator.llm]` → LLM mode with CircuitBreaker.
/// - `backend = "openai_compat"` BUT `[curator.llm]` absent → log warn + heuristic fallback.
///
/// ## Side effects
///
/// In LLM mode: reads the `api_key_env` environment variable if configured.
pub fn build_curator_pipeline(config_path: &std::path::Path) -> CuratorPipeline {
    if !config_path.exists() {
        warn!(
            config = %config_path.display(),
            "Fichier config absent — Curator initialisé en mode heuristic offline"
        );
        return CuratorPipeline::new();
    }

    // Load only the [curator] section from the TOML via figment.
    //
    // Strategy:
    //   1. extract_inner("curator") from the TOML (no .nested() — not applicable here).
    //   2. If the section is absent (MissingField) → WorkerCuratorConfig defaults.
    //   3. If the section is malformed → log error + heuristic fallback.
    //
    // Note: .nested() is for TOML with profiles (e.g. [default], [production]).
    // server.toml is a flat TOML with tables ([curator], [curator.llm]) —
    // do NOT use .nested(), otherwise figment treats [curator] as a profile and
    // extract_inner returns MissingField.
    //
    // Why not use Serialized::defaults() + extract_inner("curator"):
    // Serialized injects defaults at the figment root (keys "backend", "llm"),
    // while extract_inner("curator") looks under the "curator.*" namespace.
    // The two do not compose — defaults would not be visible to extract_inner.
    let curator_cfg: WorkerCuratorConfig = {
        let fig = Figment::new().merge(Toml::file(config_path));
        match fig.extract_inner::<WorkerCuratorConfig>("curator") {
            Ok(cfg) => cfg,
            Err(e) if is_missing_field_error(&e) => {
                // [curator] section absent from TOML → default heuristic mode.
                info!(
                    config = %config_path.display(),
                    "Section [curator] absente — mode heuristic offline par défaut"
                );
                WorkerCuratorConfig::default()
            }
            Err(e) => {
                error!(
                    config = %config_path.display(),
                    error = %e,
                    "Échec parse config curator — fallback heuristic offline"
                );
                return CuratorPipeline::new();
            }
        }
    };

    let pipeline_cfg = CuratorPipelineConfig::from(&curator_cfg);

    // Validation: LLM backend configured but llm section absent → log warn + fallback.
    if curator_cfg.backend != "heuristic" && curator_cfg.llm.is_none() {
        warn!(
            backend = %curator_cfg.backend,
            "backend curator LLM configuré mais [curator.llm] absent — fallback heuristic offline"
        );
        return CuratorPipeline::new();
    }

    let pipeline = CuratorPipeline::from_config(&pipeline_cfg);

    // Observability log at boot — gating fields visible for diagnostics.
    // Confirms that llm_review_enabled=true is correctly loaded from server.toml.
    info!(
        backend = %curator_cfg.backend,
        llm_review_enabled = curator_cfg.llm_review_enabled.unwrap_or(false),
        confidence_threshold = curator_cfg.confidence_threshold.unwrap_or(0.7),
        heuristic_admit_threshold = ?curator_cfg.heuristic_admit_threshold,
        llm_review_fallback = curator_cfg.llm_review_fallback.as_deref().unwrap_or("pending-review-fallback"),
        base_url = ?curator_cfg.llm.as_ref().map(|l| &l.base_url),
        model = ?curator_cfg.llm.as_ref().map(|l| &l.model),
        timeout_ms = ?curator_cfg.llm.as_ref().map(|l| l.timeout_ms),
        "Curator config loaded"
    );

    pipeline
}
