use std::collections::HashSet;
use std::sync::LazyLock;

use serde::Deserialize;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;
use tracing::warn;

const CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION: u8 = 4;
const CONNECTOR_NAME_TEMPLATE_VAR: &str = "{connector_name}";

static CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES: LazyLock<
    Option<Vec<ConsequentialToolMessageTemplate>>,
> = LazyLock::new(load_consequential_tool_message_templates);

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct RenderedMcpToolApprovalTemplate {
    pub(crate) question: String,
    pub(crate) elicitation_message: String,
    pub(crate) tool_params: Option<Value>,
    pub(crate) tool_params_display: Vec<RenderedMcpToolApprovalParam>,
}

#[derive(Clone, Debug, PartialEq, Serialize)]
pub(crate) struct RenderedMcpToolApprovalParam {
    pub(crate) name: String,
    pub(crate) value: Value,
    pub(crate) display_name: String,
}

#[derive(Debug, Deserialize)]
struct ConsequentialToolMessageTemplatesFile {
    schema_version: u8,
    templates: Vec<ConsequentialToolMessageTemplate>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
struct ConsequentialToolMessageTemplate {
    connector_id: String,
    server_name: String,
    tool_title: String,
    template: String,
    template_params: Vec<ConsequentialToolTemplateParam>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
struct ConsequentialToolTemplateParam {
    name: String,
    label: String,
}

pub(crate) fn render_mcp_tool_approval_template(
    server_name: &str,
    connector_id: Option<&str>,
    connector_name: Option<&str>,
    tool_title: Option<&str>,
    tool_params: Option<&Value>,
) -> Option<RenderedMcpToolApprovalTemplate> {
    let templates = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.as_ref()?;
    render_mcp_tool_approval_template_from_templates(
        templates,
        server_name,
        connector_id,
        connector_name,
        tool_title,
        tool_params,
    )
}

fn load_consequential_tool_message_templates() -> Option<Vec<ConsequentialToolMessageTemplate>> {
    let templates = match serde_json::from_str::<ConsequentialToolMessageTemplatesFile>(
        include_str!("consequential_tool_message_templates.json"),
    ) {
        Ok(templates) => templates,
        Err(err) => {
            warn!(error = %err, "failed to parse consequential tool approval templates");
            return None;
        }
    };

    if templates.schema_version != CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION {
        warn!(
            found_schema_version = templates.schema_version,
            expected_schema_version = CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES_SCHEMA_VERSION,
            "unexpected consequential tool approval templates schema version"
        );
        return None;
    }

    Some(templates.templates)
}

fn render_mcp_tool_approval_template_from_templates(
    templates: &[ConsequentialToolMessageTemplate],
    server_name: &str,
    connector_id: Option<&str>,
    connector_name: Option<&str>,
    tool_title: Option<&str>,
    tool_params: Option<&Value>,
) -> Option<RenderedMcpToolApprovalTemplate> {
    let connector_id = connector_id?;
    let tool_title = tool_title.map(str::trim).filter(|name| !name.is_empty())?;
    let template = templates.iter().find(|template| {
        template.server_name == server_name
            && template.connector_id == connector_id
            && template.tool_title == tool_title
    })?;
    let elicitation_message = render_question_template(&template.template, connector_name)?;
    let (tool_params, tool_params_display) = match tool_params {
        Some(Value::Object(tool_params)) => {
            render_tool_params(tool_params, &template.template_params)?
        }
        Some(_) => return None,
        None => (None, Vec::new()),
    };

    Some(RenderedMcpToolApprovalTemplate {
        question: elicitation_message.clone(),
        elicitation_message,
        tool_params,
        tool_params_display,
    })
}

fn render_question_template(template: &str, connector_name: Option<&str>) -> Option<String> {
    let template = template.trim();
    if template.is_empty() {
        return None;
    }

    if template.contains(CONNECTOR_NAME_TEMPLATE_VAR) {
        let connector_name = connector_name
            .map(str::trim)
            .filter(|name| !name.is_empty())?;
        return Some(template.replace(CONNECTOR_NAME_TEMPLATE_VAR, connector_name));
    }

    Some(template.to_string())
}

fn render_tool_params(
    tool_params: &Map<String, Value>,
    template_params: &[ConsequentialToolTemplateParam],
) -> Option<(Option<Value>, Vec<RenderedMcpToolApprovalParam>)> {
    let mut display_params = Vec::new();
    let mut display_names = HashSet::new();
    let mut handled_names = HashSet::new();

    for template_param in template_params {
        let label = template_param.label.trim();
        if label.is_empty() {
            return None;
        }
        let Some(value) = tool_params.get(&template_param.name) else {
            continue;
        };
        if !display_names.insert(label.to_string()) {
            return None;
        }
        display_params.push(RenderedMcpToolApprovalParam {
            name: template_param.name.clone(),
            value: value.clone(),
            display_name: label.to_string(),
        });
        handled_names.insert(template_param.name.as_str());
    }

    let mut remaining_params = tool_params
        .iter()
        .filter(|(name, _)| !handled_names.contains(name.as_str()))
        .collect::<Vec<_>>();
    remaining_params.sort_by_key(|(name, _)| *name);

    for (name, value) in remaining_params {
        if handled_names.contains(name.as_str()) {
            continue;
        }
        if !display_names.insert(name.clone()) {
            return None;
        }
        display_params.push(RenderedMcpToolApprovalParam {
            name: name.clone(),
            value: value.clone(),
            display_name: name.clone(),
        });
    }

    Some((Some(Value::Object(tool_params.clone())), display_params))
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;
    use serde_json::json;

    use super::*;

    #[test]
    fn renders_exact_match_with_readable_param_labels() {
        let templates = vec![ConsequentialToolMessageTemplate {
            connector_id: "calendar".to_string(),
            server_name: "codex_apps".to_string(),
            tool_title: "create_event".to_string(),
            template: "Allow {connector_name} to create an event?".to_string(),
            template_params: vec![
                ConsequentialToolTemplateParam {
                    name: "calendar_id".to_string(),
                    label: "Calendar".to_string(),
                },
                ConsequentialToolTemplateParam {
                    name: "title".to_string(),
                    label: "Title".to_string(),
                },
            ],
        }];

        let rendered = render_mcp_tool_approval_template_from_templates(
            &templates,
            "codex_apps",
            Some("calendar"),
            Some("Calendar"),
            Some("create_event"),
            Some(&json!({
                "title": "Roadmap review",
                "calendar_id": "primary",
                "timezone": "UTC",
            })),
        );

        assert_eq!(
            rendered,
            Some(RenderedMcpToolApprovalTemplate {
                question: "Allow Calendar to create an event?".to_string(),
                elicitation_message: "Allow Calendar to create an event?".to_string(),
                tool_params: Some(json!({
                    "title": "Roadmap review",
                    "calendar_id": "primary",
                    "timezone": "UTC",
                })),
                tool_params_display: vec![
                    RenderedMcpToolApprovalParam {
                        name: "calendar_id".to_string(),
                        value: json!("primary"),
                        display_name: "Calendar".to_string(),
                    },
                    RenderedMcpToolApprovalParam {
                        name: "title".to_string(),
                        value: json!("Roadmap review"),
                        display_name: "Title".to_string(),
                    },
                    RenderedMcpToolApprovalParam {
                        name: "timezone".to_string(),
                        value: json!("UTC"),
                        display_name: "timezone".to_string(),
                    },
                ],
            })
        );
    }

    #[test]
    fn returns_none_when_no_exact_match_exists() {
        let templates = vec![ConsequentialToolMessageTemplate {
            connector_id: "calendar".to_string(),
            server_name: "codex_apps".to_string(),
            tool_title: "create_event".to_string(),
            template: "Allow {connector_name} to create an event?".to_string(),
            template_params: Vec::new(),
        }];

        assert_eq!(
            render_mcp_tool_approval_template_from_templates(
                &templates,
                "codex_apps",
                Some("calendar"),
                Some("Calendar"),
                Some("delete_event"),
                Some(&json!({})),
            ),
            None
        );
    }

    #[test]
    fn returns_none_when_relabeling_would_collide() {
        let templates = vec![ConsequentialToolMessageTemplate {
            connector_id: "calendar".to_string(),
            server_name: "codex_apps".to_string(),
            tool_title: "create_event".to_string(),
            template: "Allow {connector_name} to create an event?".to_string(),
            template_params: vec![ConsequentialToolTemplateParam {
                name: "calendar_id".to_string(),
                label: "timezone".to_string(),
            }],
        }];

        assert_eq!(
            render_mcp_tool_approval_template_from_templates(
                &templates,
                "codex_apps",
                Some("calendar"),
                Some("Calendar"),
                Some("create_event"),
                Some(&json!({
                    "calendar_id": "primary",
                    "timezone": "UTC",
                })),
            ),
            None
        );
    }

    #[test]
    fn bundled_templates_load() {
        assert_eq!(CONSEQUENTIAL_TOOL_MESSAGE_TEMPLATES.is_some(), true);
    }

    #[test]
    fn renders_literal_template_without_connector_substitution() {
        let templates = vec![ConsequentialToolMessageTemplate {
            connector_id: "github".to_string(),
            server_name: "codex_apps".to_string(),
            tool_title: "add_comment".to_string(),
            template: "Allow GitHub to add a comment to a pull request?".to_string(),
            template_params: Vec::new(),
        }];

        let rendered = render_mcp_tool_approval_template_from_templates(
            &templates,
            "codex_apps",
            Some("github"),
            /*connector_name*/ None,
            Some("add_comment"),
            Some(&json!({})),
        );

        assert_eq!(
            rendered,
            Some(RenderedMcpToolApprovalTemplate {
                question: "Allow GitHub to add a comment to a pull request?".to_string(),
                elicitation_message: "Allow GitHub to add a comment to a pull request?".to_string(),
                tool_params: Some(json!({})),
                tool_params_display: Vec::new(),
            })
        );
    }

    #[test]
    fn returns_none_when_connector_placeholder_has_no_value() {
        let templates = vec![ConsequentialToolMessageTemplate {
            connector_id: "calendar".to_string(),
            server_name: "codex_apps".to_string(),
            tool_title: "create_event".to_string(),
            template: "Allow {connector_name} to create an event?".to_string(),
            template_params: Vec::new(),
        }];

        assert_eq!(
            render_mcp_tool_approval_template_from_templates(
                &templates,
                "codex_apps",
                Some("calendar"),
                /*connector_name*/ None,
                Some("create_event"),
                Some(&json!({})),
            ),
            None
        );
    }
}
