use chrono::NaiveDate;
use jp_config::model::{
    id::ModelIdConfig,
    parameters::{CustomReasoningConfig, ReasoningConfig, ReasoningEffort},
};
use tracing::warn;

/// Details about a model for a given provider, as specified by the provider.
#[derive(Debug, Clone, PartialEq)]
pub struct ModelDetails {
    /// The id of the model.
    pub id: ModelIdConfig,

    /// The display name of the model, if known.
    pub display_name: Option<String>,

    /// The context window size in tokens, if known.
    pub context_window: Option<u32>,

    /// The maximum output tokens, if known.
    pub max_output_tokens: Option<u32>,

    /// Whether the model supports reasoning, if unknown, this value is left to
    /// `None`.
    pub reasoning: Option<ReasoningDetails>,

    /// The knowledge cutoff date, if known.
    pub knowledge_cutoff: Option<NaiveDate>,

    /// Deprecation status of the model, if known.
    pub deprecated: Option<ModelDeprecation>,

    /// Whether the model supports structured output (JSON schema responses).
    ///
    /// `None` means unknown (e.g. custom deployments).
    /// Providers should set this to `Some(true)` or `Some(false)` for known
    /// models.
    pub structured_output: Option<bool>,

    /// Provider-specific features.
    pub features: Vec<&'static str>,
}

impl ModelDetails {
    #[must_use]
    pub fn empty(id: ModelIdConfig) -> Self {
        Self {
            id,
            display_name: None,
            context_window: None,
            max_output_tokens: None,
            reasoning: None,
            knowledge_cutoff: None,
            deprecated: None,
            structured_output: None,
            features: vec![],
        }
    }

    /// Returns `true` if the model is known to support structured output.
    #[must_use]
    pub fn supports_structured_output(&self) -> bool {
        self.structured_output.unwrap_or(false)
    }

    /// Returns `true` if the model supports assistant prefill.
    #[must_use]
    pub fn supports_prefill(&self) -> bool {
        self.features.contains(&"prefill")
    }

    #[must_use]
    pub fn name(&self) -> &str {
        self.display_name.as_deref().unwrap_or(&self.id.name)
    }

    #[must_use]
    pub fn custom_reasoning_config(
        &self,
        config: Option<ReasoningConfig>,
    ) -> Option<CustomReasoningConfig> {
        match self.reasoning {
            // Unknown support
            None => match config {
                // Unconfigured or off, so disabled.
                None | Some(ReasoningConfig::Off) => None,

                // Auto configured, so use medium effort.
                Some(ReasoningConfig::Auto) => Some(CustomReasoningConfig {
                    effort: ReasoningEffort::Medium,
                    exclude: false,
                }),

                // Custom configuration, so use it.
                Some(ReasoningConfig::Custom(custom)) => Some(custom),
            },

            // Unsupported
            Some(ReasoningDetails::Unsupported) => match config {
                // Unconfigured, auto or off, so disabled.
                None | Some(ReasoningConfig::Auto | ReasoningConfig::Off) => None,

                // Custom configuration, invalid, so warn + disabled.
                Some(ReasoningConfig::Custom(config)) => {
                    warn!(
                        id = %self.id,
                        ?config,
                        "Model does not support reasoning, but the configuration explicitly \
                        enabled it. Reasoning will be disabled to avoid failed requests."
                    );

                    None
                }
            },

            // Budgetted
            Some(ReasoningDetails::Budgetted { .. }) => match config {
                // Off, so disabled.
                Some(ReasoningConfig::Off) => None,

                // Unconfigured, or auto, so medium effort.
                None | Some(ReasoningConfig::Auto) => Some(CustomReasoningConfig {
                    effort: ReasoningEffort::Medium,
                    exclude: false,
                }),

                // Custom configuration, so use it.
                Some(ReasoningConfig::Custom(custom)) => Some(custom),
            },

            // Leveled
            Some(ReasoningDetails::Leveled {
                none: _,
                xlow: _,
                low,
                medium,
                high,
                xhigh,
            }) => match config {
                // Off, so disabled.
                Some(ReasoningConfig::Off) => None,

                // Auto configured, so use medium effort if the model supports
                // it, otherwise high or low.
                None | Some(ReasoningConfig::Auto) => Some(CustomReasoningConfig {
                    effort: if medium {
                        ReasoningEffort::Medium
                    } else if high {
                        ReasoningEffort::High
                    } else if xhigh {
                        ReasoningEffort::XHigh
                    } else if low {
                        ReasoningEffort::Low
                    } else {
                        ReasoningEffort::Xlow
                    },
                    exclude: false,
                }),

                // Custom configuration, so use it.
                Some(ReasoningConfig::Custom(custom)) => Some(custom),
            },

            // Adaptive
            Some(ReasoningDetails::Adaptive { .. }) => match config {
                // Off, so disabled.
                Some(ReasoningConfig::Off) => None,

                // Unconfigured or auto, so use high effort (the API default).
                None | Some(ReasoningConfig::Auto) => Some(CustomReasoningConfig {
                    effort: ReasoningEffort::High,
                    exclude: false,
                }),

                // Custom configuration, so use it.
                Some(ReasoningConfig::Custom(custom)) => Some(custom),
            },
        }
    }
}

/// The deprecation status of a model.
#[derive(Debug, Clone, PartialEq, Default)]
pub enum ModelDeprecation {
    /// The model is active and available for use.
    #[default]
    Active,

    /// The model is deprecated and will be removed at some point in the future.
    Deprecated {
        /// Any details about the deprecation.
        ///
        /// This could include a link to the deprecation notice, a reason for
        /// deprecation, or recommended replacements.
        note: String,

        /// The date on which the model will be retired, if known.
        retire_at: Option<NaiveDate>,
    },
}

impl ModelDeprecation {
    pub fn deprecated(note: &impl ToString, retire_at: Option<NaiveDate>) -> Self {
        Self::Deprecated {
            note: note.to_string(),
            retire_at,
        }
    }
}

/// Details about the reasoning capabilities of a model.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ReasoningDetails {
    /// Reasoning is not supported.
    Unsupported,

    /// Budgetted reasoning support.
    ///
    /// Most models allow specifying the minimum and maximum number of tokens
    /// that the model can use to "reason".
    Budgetted {
        /// The minimum number of reasoning tokens required to generate a
        /// response.
        /// Usually zero, but can be non-zero for certain models.
        min_tokens: u32,

        /// The maximum number of reasoning tokens that can be generated.
        max_tokens: Option<u32>,
    },

    /// Level-based reasoning support.
    ///
    /// Some models, such as Google's Gemini 3, do not support token-based
    /// reasoning configuration, but instead offer specific "efforts" of
    /// reasoning, such as low/medium/high effort.
    Leveled {
        /// Whether the model supports completely disabling reasoning.
        none: bool,

        /// Whether the model supports extremely low effort reasoning.
        xlow: bool,

        /// Whether the model supports low effort reasoning.
        low: bool,

        /// Whether the model supports medium effort reasoning.
        medium: bool,

        /// Whether the model supports high effort reasoning.
        high: bool,

        /// Whether the model supports extremely high effort reasoning.
        xhigh: bool,
    },

    /// Adaptive reasoning support.
    ///
    /// The model dynamically decides when and how much to think based on task
    /// complexity.
    /// Uses effort levels (low/medium/high/xhigh/max) instead of token budgets.
    ///
    /// Currently only supported by Claude Opus 4.6+.
    Adaptive {
        /// Whether the model supports `xhigh` (extra high) effort level.
        xhigh: bool,

        /// Whether the model supports `max` effort level.
        max: bool,
    },
}

impl ReasoningDetails {
    #[must_use]
    pub fn budgetted(min_tokens: u32, max_tokens: Option<u32>) -> Self {
        Self::Budgetted {
            min_tokens,
            max_tokens,
        }
    }

    #[must_use]
    #[expect(clippy::fn_params_excessive_bools)]
    pub fn leveled(
        none: bool,
        xlow: bool,
        low: bool,
        medium: bool,
        high: bool,
        xhigh: bool,
    ) -> Self {
        Self::Leveled {
            none,
            xlow,
            low,
            medium,
            high,
            xhigh,
        }
    }

    #[must_use]
    pub fn adaptive(xhigh: bool, max: bool) -> Self {
        Self::Adaptive { xhigh, max }
    }

    #[must_use]
    pub fn unsupported() -> Self {
        Self::Unsupported
    }

    #[must_use]
    pub fn min_tokens(&self) -> u32 {
        match self {
            Self::Budgetted { min_tokens, .. } => *min_tokens,
            Self::Leveled { .. } | Self::Adaptive { .. } | Self::Unsupported => 0,
        }
    }

    #[must_use]
    pub fn max_tokens(&self) -> Option<u32> {
        match self {
            Self::Budgetted { max_tokens, .. } => *max_tokens,
            Self::Leveled { .. } | Self::Adaptive { .. } | Self::Unsupported => None,
        }
    }

    /// Returns the lowest reasoning effort level supported by this model, if
    /// known.
    ///
    /// `Leveled` models return their lowest supported level.
    /// Other variants return `Option::None` — callers should decide how to
    /// handle "disable reasoning" for their provider (e.g. token budget 0,
    /// effort `minimal`, `thinking: disabled`, etc.).
    #[must_use]
    pub fn lowest_effort(&self) -> Option<ReasoningEffort> {
        match self {
            Self::Leveled {
                none,
                xlow,
                low,
                medium,
                high,
                xhigh,
            } => {
                if *none {
                    Some(ReasoningEffort::None)
                } else if *xlow {
                    Some(ReasoningEffort::Xlow)
                } else if *low {
                    Some(ReasoningEffort::Low)
                } else if *medium {
                    Some(ReasoningEffort::Medium)
                } else if *high {
                    Some(ReasoningEffort::High)
                } else if *xhigh {
                    Some(ReasoningEffort::XHigh)
                } else {
                    None
                }
            }
            _ => None,
        }
    }

    #[must_use]
    pub fn is_unsupported(&self) -> bool {
        matches!(self, Self::Unsupported)
    }

    #[must_use]
    pub fn is_budgetted(&self) -> bool {
        matches!(self, Self::Budgetted { .. })
    }

    #[must_use]
    pub fn is_leveled(&self) -> bool {
        matches!(self, Self::Leveled { .. })
    }

    #[must_use]
    pub fn is_adaptive(&self) -> bool {
        matches!(self, Self::Adaptive { .. })
    }
}
