//! Display style configuration for tools.
//!
//! User-facing shape under `[style]`:
//!
//! ```toml
//! [tools.my_tool.style]
//! hidden = false           # applies to both request and response
//! parameters = "json"      # how arguments are rendered on the call
//! inline_results = "full"  # how successful results are rendered
//! results_file_link = "osc8"
//!
//! [tools.my_tool.style.error]
//! inline_results = "full"  # overrides on top of `style` for failed results
//! ```
//!
//! [`ErrorStyleConfig`] (`error`) is a per-field overlay applied when the tool
//! result is `Err`.
//! Unset overlay fields take their value from the matching top-level field.
//! The overlay is typed separately so the error path can grow its own fields
//! (severity styling, alternate sinks, etc.) independently of the success path.

use std::{fmt, num::ParseIntError, str::FromStr};

use schematic::{Config, ConfigEnum, Schematic};
use serde::{Deserialize, Serialize};

use crate::{
    BoxedError,
    assignment::{AssignKeyValue, AssignResult, KvAssignment, missing_key},
    conversation::tool::CommandConfigOrString,
    delta::{PartialConfigDelta, delta_opt},
    fill::FillDefaults,
    partial::{ToPartial, partial_opt, partial_opts},
};

/// Display style configuration.
///
/// See the [module-level docs] for the user-facing TOML shape.
///
/// [module-level docs]: self
#[derive(Debug, Clone, PartialEq, Config)]
#[config(rename_all = "snake_case")]
pub struct DisplayStyleConfig {
    /// Whether to hide this tool's calls from terminal output.
    ///
    /// When `true`, the tool executes normally and events are recorded, but no
    /// call header, arguments, or results are rendered to the terminal.
    #[setting(default = false)]
    pub hidden: bool,

    /// How to display the results of the tool call.
    ///
    /// - `full`: Show the full tool call results inline.
    /// - `off`: Never show the tool call results inline.
    /// - `<number>`: Show the first N lines of the tool call results inline.
    #[setting(default)]
    pub inline_results: InlineResults,

    /// How to display the link to the file containing the tool call results.
    ///
    /// - `full`: Show the full file path.
    /// - `osc8`: Show a clickable link (OSC8 escape sequence).
    /// - `off`: Do not show the file path.
    #[setting(default)]
    pub results_file_link: LinkStyle,

    /// How to display the tool call parameters.
    ///
    /// - `json`: Show as JSON.
    /// - `function_call`: Show as a function call (e.g.
    ///   `tool_name(arg=val)`).
    /// - `off`: Do not show parameters.
    /// - `<command>`: Use a custom command to format the parameters.
    #[setting(default)]
    pub parameters: ParametersStyle,

    /// Per-field overrides applied when the tool result is `Err`.
    /// Unset overlay fields take their value from the matching top-level field.
    ///
    /// Sub-fields appear under `[style.error]` in TOML.
    #[setting(nested)]
    pub error: ErrorStyleConfig,
}

impl DisplayStyleConfig {
    /// Return the effective [`InlineResults`] for a tool response.
    ///
    /// For error responses, the `style.error.inline_results` overlay is applied
    /// on top of `style.inline_results`.
    /// Successful responses read `style.inline_results` directly.
    #[must_use]
    pub fn inline_results(&self, is_error: bool) -> &InlineResults {
        if is_error {
            self.error
                .inline_results
                .as_ref()
                .unwrap_or(&self.inline_results)
        } else {
            &self.inline_results
        }
    }

    /// Return the effective [`LinkStyle`] for the results-file link of a tool
    /// response.
    ///
    /// For error responses, the `style.error.results_file_link` overlay is
    /// applied on top of `style.results_file_link`.
    /// Successful responses read `style.results_file_link` directly.
    #[must_use]
    pub fn results_file_link(&self, is_error: bool) -> &LinkStyle {
        if is_error {
            self.error
                .results_file_link
                .as_ref()
                .unwrap_or(&self.results_file_link)
        } else {
            &self.results_file_link
        }
    }
}

impl AssignKeyValue for PartialDisplayStyleConfig {
    fn assign(&mut self, mut kv: KvAssignment) -> AssignResult {
        match kv.key_string().as_str() {
            "" => kv.try_merge_object(self)?,
            "hidden" => self.hidden = kv.try_some_bool()?,
            "inline_results" => self.inline_results = kv.try_some_from_str()?,
            "results_file_link" => self.results_file_link = kv.try_some_from_str()?,
            "parameters" => self.parameters = kv.try_some_from_str()?,
            _ if kv.p("error") => self.error.assign(kv)?,
            _ => return missing_key(&kv),
        }

        Ok(())
    }
}

impl PartialConfigDelta for PartialDisplayStyleConfig {
    fn delta(&self, next: Self) -> Self {
        Self {
            hidden: delta_opt(self.hidden.as_ref(), next.hidden),
            inline_results: delta_opt(self.inline_results.as_ref(), next.inline_results),
            results_file_link: delta_opt(self.results_file_link.as_ref(), next.results_file_link),
            parameters: delta_opt(self.parameters.as_ref(), next.parameters),
            error: self.error.delta(next.error),
        }
    }
}

impl FillDefaults for PartialDisplayStyleConfig {
    fn fill_from(self, defaults: Self) -> Self {
        Self {
            hidden: self.hidden.or(defaults.hidden),
            inline_results: self.inline_results.or(defaults.inline_results),
            results_file_link: self.results_file_link.or(defaults.results_file_link),
            parameters: self.parameters.or(defaults.parameters),
            error: self.error.fill_from(defaults.error),
        }
    }
}

impl ToPartial for DisplayStyleConfig {
    fn to_partial(&self) -> Self::Partial {
        let defaults = Self::Partial::default();

        Self::Partial {
            hidden: partial_opt(&self.hidden, defaults.hidden),
            inline_results: partial_opt(&self.inline_results, defaults.inline_results),
            results_file_link: partial_opt(&self.results_file_link, defaults.results_file_link),
            parameters: partial_opt(&self.parameters, defaults.parameters),
            error: self.error.to_partial(),
        }
    }
}

/// Per-field overrides for rendering failed tool call responses.
///
/// Each field is optional.
/// Callers resolve an unset field by reading the matching field from the parent
/// [`DisplayStyleConfig`].
#[derive(Debug, Clone, PartialEq, Default, Config)]
#[config(rename_all = "snake_case")]
pub struct ErrorStyleConfig {
    /// Override for [`DisplayStyleConfig::inline_results`].
    pub inline_results: Option<InlineResults>,

    /// Override for [`DisplayStyleConfig::results_file_link`].
    pub results_file_link: Option<LinkStyle>,
}

impl AssignKeyValue for PartialErrorStyleConfig {
    fn assign(&mut self, kv: KvAssignment) -> AssignResult {
        match kv.key_string().as_str() {
            "" => kv.try_merge_object(self)?,
            "inline_results" => self.inline_results = kv.try_some_from_str()?,
            "results_file_link" => self.results_file_link = kv.try_some_from_str()?,
            _ => return missing_key(&kv),
        }

        Ok(())
    }
}

impl PartialConfigDelta for PartialErrorStyleConfig {
    fn delta(&self, next: Self) -> Self {
        Self {
            inline_results: delta_opt(self.inline_results.as_ref(), next.inline_results),
            results_file_link: delta_opt(self.results_file_link.as_ref(), next.results_file_link),
        }
    }
}

impl FillDefaults for PartialErrorStyleConfig {
    fn fill_from(self, defaults: Self) -> Self {
        Self {
            inline_results: self.inline_results.or(defaults.inline_results),
            results_file_link: self.results_file_link.or(defaults.results_file_link),
        }
    }
}

impl ToPartial for ErrorStyleConfig {
    fn to_partial(&self) -> Self::Partial {
        let defaults = Self::Partial::default();

        Self::Partial {
            inline_results: partial_opts(self.inline_results.as_ref(), defaults.inline_results),
            results_file_link: partial_opts(
                self.results_file_link.as_ref(),
                defaults.results_file_link,
            ),
        }
    }
}

/// Whether and how to show the tool call results inline in the terminal.
///
/// Even if disabled or truncated, a link will be added to a file containing the
/// full tool call results.
/// Additionally, the full tool call results will be sent back to the assistant,
/// regardless of this setting.
#[derive(Debug, Clone, PartialEq, Serialize, ConfigEnum)]
#[serde(rename_all = "snake_case")]
pub enum InlineResults {
    /// Never show the tool call results inline.
    Off,

    /// Show the full tool call results inline.
    Full,

    /// Show the first N lines of the tool call results inline.
    #[variant(fallback)]
    Truncate(TruncateLines),
}

impl InlineResults {
    /// Returns [`true`] if the results should be truncated.
    #[must_use]
    pub const fn is_truncated(&self) -> bool {
        matches!(self, Self::Truncate(_))
    }
}

impl Default for InlineResults {
    fn default() -> Self {
        Self::Truncate(TruncateLines::default())
    }
}

impl<'de> Deserialize<'de> for InlineResults {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct InlineResultsVisitor;

        impl<'de> serde::de::Visitor<'de> for InlineResultsVisitor {
            type Value = InlineResults;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str(
                    "a boolean, a string (\"off\", \"full\"), or a number for truncation",
                )
            }

            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                if v {
                    Ok(InlineResults::Full)
                } else {
                    Ok(InlineResults::Off)
                }
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match v {
                    "off" => Ok(InlineResults::Off),
                    "full" => Ok(InlineResults::Full),
                    s => s
                        .parse::<usize>()
                        .map(|lines| InlineResults::Truncate(TruncateLines { lines }))
                        .map_err(|_| {
                            serde::de::Error::unknown_variant(v, &["off", "full", "a number"])
                        }),
                }
            }

            fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                usize::try_from(v)
                    .map(|lines| InlineResults::Truncate(TruncateLines { lines }))
                    .map_err(|_| {
                        serde::de::Error::invalid_value(
                            serde::de::Unexpected::Unsigned(v),
                            &"a number",
                        )
                    })
            }

            fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
            where
                A: serde::de::MapAccess<'de>,
            {
                // Reuse the derived deserializer for the complex case (Truncate object)
                #[derive(Deserialize)]
                #[serde(rename_all = "snake_case")]
                enum Helper {
                    Off,
                    Full,
                    Truncate(TruncateLines),
                }

                let helper =
                    Helper::deserialize(serde::de::value::MapAccessDeserializer::new(map))?;
                match helper {
                    Helper::Off => Ok(InlineResults::Off),
                    Helper::Full => Ok(InlineResults::Full),
                    Helper::Truncate(t) => Ok(InlineResults::Truncate(t)),
                }
            }
        }

        deserializer.deserialize_any(InlineResultsVisitor)
    }
}

/// Truncate the tool call results to the first N lines.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TruncateLines {
    /// The number of lines to show.
    pub lines: usize,
}

impl Default for TruncateLines {
    fn default() -> Self {
        Self { lines: 10 }
    }
}

impl TryFrom<&str> for TruncateLines {
    type Error = ParseIntError;

    fn try_from(s: &str) -> Result<Self, Self::Error> {
        s.parse().map(|lines| Self { lines })
    }
}

impl fmt::Display for TruncateLines {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.lines)
    }
}

/// How to display the link to the file containing the tool call results.
#[derive(Debug, Clone, PartialEq, Default, Serialize, ConfigEnum)]
#[serde(rename_all = "lowercase")]
pub enum LinkStyle {
    /// Full (raw) link.
    #[default]
    Full,

    /// Clickable link using the `osc8` escape sequence.
    Osc8,

    /// No link.
    Off,
}

impl<'de> Deserialize<'de> for LinkStyle {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct LinkStyleVisitor;

        impl serde::de::Visitor<'_> for LinkStyleVisitor {
            type Value = LinkStyle;

            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
                formatter.write_str("a boolean or a string (\"off\", \"full\", \"osc8\")")
            }

            fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                if v {
                    Ok(LinkStyle::Full)
                } else {
                    Ok(LinkStyle::Off)
                }
            }

            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                match v {
                    "off" => Ok(LinkStyle::Off),
                    "full" => Ok(LinkStyle::Full),
                    "osc8" => Ok(LinkStyle::Osc8),
                    _ => Err(serde::de::Error::unknown_variant(v, &[
                        "off", "full", "osc8",
                    ])),
                }
            }
        }

        deserializer.deserialize_any(LinkStyleVisitor)
    }
}

/// Define the name to serialize and deserialize for a unit variant.
mod strings {
    use crate::named_unit_variant;

    named_unit_variant!(json);
    named_unit_variant!(function_call);
    named_unit_variant!(off);
}

/// How to display the link to the file containing the tool call results.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, Schematic)]
#[serde(untagged)]
pub enum ParametersStyle {
    /// A JSON representation of the tool call parameters.
    #[default]
    #[serde(with = "strings::json")]
    #[schema(literal = "json")]
    Json,

    /// Function call syntax.
    ///
    /// e.g. `tool_name(arg1 = "value", arg2 = 123)`.
    #[serde(with = "strings::function_call")]
    #[schema(literal = "function_call")]
    FunctionCall,

    /// No parameters are displayed.
    #[serde(with = "strings::off")]
    #[schema(literal = "off")]
    Off,

    /// A custom command to format how the tool call parameters are displayed in
    /// the terminal.
    ///
    /// The command is provided with the json representation of the parameters,
    /// and is expected to return a string that is shown verbatim in the
    /// terminal.
    Custom(CommandConfigOrString),
}

impl ParametersStyle {
    /// Return the custom command if this is a custom style.
    #[must_use]
    pub fn into_custom(self) -> Option<CommandConfigOrString> {
        match self {
            Self::Custom(c) => Some(c),
            _ => None,
        }
    }
}

impl FromStr for ParametersStyle {
    type Err = BoxedError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "off" => Self::Off,
            "json" => Self::Json,
            "function_call" => Self::FunctionCall,
            _ => Self::Custom(CommandConfigOrString::String(s.to_owned())),
        })
    }
}

#[cfg(test)]
#[path = "style_tests.rs"]
mod tests;
