use crate::diff::{FieldChangeKind, VersionDiff};
use crate::layout::RepoLayout;
use crate::parser::ApiSpec;
use crate::plan::FileEdit;

pub fn format_diff_body(diff: &VersionDiff) -> String {
    let mut out = String::new();

    // ── Endpoints ────────────────────────────────────────────────────────

    if !diff.endpoints.added.is_empty() {
        out.push_str("### Added endpoints\n\n");
        out.push_str("| Method | Path | Description | Tag |\n");
        out.push_str("|--------|------|-------------|-----|\n");
        for ep in &diff.endpoints.added {
            let doc = ep.doc.as_deref().unwrap_or("");
            out.push_str(&format!(
                "| `{}` | `{}` | {} | {} |\n",
                ep.method.as_str(),
                ep.path,
                doc,
                ep.tag,
            ));
        }
        out.push('\n');
    }

    if !diff.endpoints.removed.is_empty() {
        out.push_str("### Removed endpoints\n\n");
        out.push_str("| Method | Path | Tag |\n");
        out.push_str("|--------|------|-----|\n");
        for ep in &diff.endpoints.removed {
            out.push_str(&format!(
                "| `{}` | `{}` | {} |\n",
                ep.method.as_str(),
                ep.path,
                ep.tag,
            ));
        }
        out.push('\n');
    }

    let meaningful_changes: Vec<&crate::diff::EndpointChanges> = diff
        .endpoints
        .changed
        .iter()
        .filter(|ec| {
            !ec.added_params.is_empty()
                || !ec.removed_params.is_empty()
                || !ec.changed_params.is_empty()
                || !ec.added_path_params.is_empty()
                || !ec.removed_path_params.is_empty()
                || !ec.contract_changes.is_empty()
        })
        .collect();

    if !meaningful_changes.is_empty() {
        out.push_str("### Changed endpoints\n\n");
        out.push_str("| Method | Path | Change |\n");
        out.push_str("|--------|------|--------|\n");
        for ec in meaningful_changes {
            let change = format_endpoint_change(ec);
            out.push_str(&format!(
                "| `{}` | `{}` | {} |\n",
                ec.method.as_str(),
                ec.path,
                change,
            ));
        }
        out.push('\n');
    }

    // ── Types ────────────────────────────────────────────────────────────

    if !diff.types.added.is_empty() {
        out.push_str("### Added types\n\n");
        out.push_str("| Type | Tag |\n");
        out.push_str("|------|-----|\n");
        for t in &diff.types.added {
            let tags = if t.tags.is_empty() {
                "\u{2014}".to_string()
            } else {
                t.tags.join(", ")
            };
            out.push_str(&format!("| `{}` | {} |\n", t.name, tags));
        }
        out.push('\n');
    }

    if !diff.types.removed.is_empty() {
        out.push_str("### Removed types\n\n");
        out.push_str("| Type | Tag |\n");
        out.push_str("|------|-----|\n");
        for t in &diff.types.removed {
            let tags = if t.tags.is_empty() {
                "\u{2014}".to_string()
            } else {
                t.tags.join(", ")
            };
            out.push_str(&format!("| `{}` | {} |\n", t.name, tags));
        }
        out.push('\n');
    }

    let meaningful_types: Vec<&crate::diff::TypeChanges> = diff
        .types
        .changed
        .iter()
        .filter(|tc| {
            !tc.added_fields.is_empty()
                || !tc.removed_fields.is_empty()
                || !tc.changed_fields.is_empty()
                || !tc.added_variants.is_empty()
                || !tc.removed_variants.is_empty()
        })
        .collect();

    if !meaningful_types.is_empty() {
        out.push_str("### Changed types\n\n");
        out.push_str("| Type | Change |\n");
        out.push_str("|------|--------|\n");
        for tc in meaningful_types {
            let change = format_type_change(tc);
            out.push_str(&format!("| `{}` | {} |\n", tc.name, change));
        }
        out.push('\n');
    }

    if out.is_empty() {
        out.push_str("_No API changes._\n\n");
    }

    out
}

fn format_endpoint_change(ec: &crate::diff::EndpointChanges) -> String {
    let mut parts = Vec::new();
    if !ec.added_params.is_empty() {
        let descs: Vec<String> = ec
            .added_params
            .iter()
            .map(|p| {
                if p.required {
                    format!("`{}` (required)", p.name)
                } else {
                    format!("`{}`", p.name)
                }
            })
            .collect();
        parts.push(format!("added params: {}", descs.join(", ")));
    }
    if !ec.removed_params.is_empty() {
        parts.push(format!(
            "removed params: `{}`",
            ec.removed_params.join("`, `")
        ));
    }
    for pc in &ec.changed_params {
        if let Some((from_t, to_t)) = &pc.type_changed {
            parts.push(format!(
                "param `{}`: type changed `{}` \u{2192} `{}`",
                pc.name, from_t, to_t
            ));
        }
        if !pc.added_enum_values.is_empty() {
            parts.push(format!(
                "param `{}`: +enum values (`{}`)",
                pc.name,
                pc.added_enum_values.join("`, `")
            ));
        }
        if !pc.removed_enum_values.is_empty() {
            parts.push(format!(
                "param `{}`: -enum values (`{}`)",
                pc.name,
                pc.removed_enum_values.join("`, `")
            ));
        }
    }
    if !ec.added_path_params.is_empty() {
        parts.push(format!(
            "added path params: `{}`",
            ec.added_path_params.join("`, `")
        ));
    }
    if !ec.removed_path_params.is_empty() {
        parts.push(format!(
            "removed path params: `{}`",
            ec.removed_path_params.join("`, `")
        ));
    }
    for cc in &ec.contract_changes {
        let aspect = match &cc.aspect {
            crate::diff::ContractAspect::RequestType => "request type",
            crate::diff::ContractAspect::ResponseType => "response type",
            crate::diff::ContractAspect::BodyKind => "body kind",
        };
        let from = cc.from.as_deref().unwrap_or("none");
        let to = cc.to.as_deref().unwrap_or("none");
        parts.push(format!("{aspect} changed: `{from}` \u{2192} `{to}`"));
    }
    parts.join("; ")
}

fn format_type_change(tc: &crate::diff::TypeChanges) -> String {
    let mut parts = Vec::new();
    if !tc.added_fields.is_empty() {
        parts.push(format!("added fields: `{}`", tc.added_fields.join("`, `")));
    }
    if !tc.removed_fields.is_empty() {
        parts.push(format!(
            "removed fields: `{}`",
            tc.removed_fields.join("`, `")
        ));
    }
    for fc in &tc.changed_fields {
        let desc = match &fc.kind {
            FieldChangeKind::BecameOptional => "became optional".to_string(),
            FieldChangeKind::BecameRequired => "became required".to_string(),
            FieldChangeKind::TypeChanged { from, to } => {
                format!("type changed: `{from}` \u{2192} `{to}`")
            }
            FieldChangeKind::InlineEnumChanged { added, removed } => {
                let mut parts = Vec::new();
                if !added.is_empty() {
                    parts.push(format!("+[`{}`]", added.join("`, `")));
                }
                if !removed.is_empty() {
                    parts.push(format!("-[`{}`]", removed.join("`, `")));
                }
                format!("enum variants changed: {}", parts.join(" "))
            }
            FieldChangeKind::DeprecationChanged {
                now_deprecated: true,
            } => "newly deprecated".to_string(),
            FieldChangeKind::DeprecationChanged {
                now_deprecated: false,
            } => "deprecation removed".to_string(),
            FieldChangeKind::NarrowedToEnum { variants } => {
                format!("narrowed to enum: [`{}`]", variants.join("`, `"))
            }
        };
        parts.push(format!("`{}` {}", fc.name, desc));
    }
    if !tc.added_variants.is_empty() {
        parts.push(format!(
            "added variants: `{}`",
            tc.added_variants.join("`, `")
        ));
    }
    if !tc.removed_variants.is_empty() {
        parts.push(format!(
            "removed variants: `{}`",
            tc.removed_variants.join("`, `")
        ));
    }
    parts.join("; ")
}

/// Generates the full body of NIFI_API_CHANGES.md (content between the markers).
/// `all_specs` must be sorted semver-ascending (oldest first).
pub fn generate_api_changes_content(all_specs: &[(String, ApiSpec)]) -> String {
    if all_specs.len() < 2 {
        return String::new();
    }

    let (baseline_version, baseline_spec) = &all_specs[0];
    let mut out = String::new();

    // Iterate newest to oldest, skipping baseline
    for i in (1..all_specs.len()).rev() {
        let (version, spec) = &all_specs[i];
        let (prev_version, prev_spec) = &all_specs[i - 1];

        out.push_str(&format!("## {version}\n\n"));

        // Consecutive diff
        let consec = crate::compute_diff(prev_spec, spec, prev_version, version);
        out.push_str(&format!(
            "<details><summary>vs {prev_version} (consecutive)</summary>\n\n"
        ));
        out.push_str(&format_diff_body(&consec));
        out.push_str("</details>\n\n");

        // Cumulative diff — only when there is more than one step from baseline
        if i > 1 {
            let cumul = crate::compute_diff(baseline_spec, spec, baseline_version, version);
            out.push_str(&format!(
                "<details><summary>vs {baseline_version} (cumulative)</summary>\n\n"
            ));
            out.push_str(&format_diff_body(&cumul));
            out.push_str("</details>\n\n");
        }
    }

    out.trim_end().to_string()
}

const API_CHANGES_START: &str = "<!-- NIFI_API_CHANGES_START -->";
const API_CHANGES_END: &str = "<!-- NIFI_API_CHANGES_END -->";

pub fn emit_api_changes(layout: &RepoLayout, all_specs: &[(String, ApiSpec)]) -> Vec<FileEdit> {
    let content = generate_api_changes_content(all_specs);
    let template = format!(
        "# NiFi API Changes\n\n<!-- markdownlint-disable MD024 MD033 -->\n\nAuto-generated by `cargo run -p nifi-openapi-gen`. Do not edit manually.\n\n{API_CHANGES_START}\n{API_CHANGES_END}\n"
    );
    vec![FileEdit::CreateOrReplaceBlock {
        path: layout.api_changes_md.clone(),
        start_marker: API_CHANGES_START.into(),
        end_marker: API_CHANGES_END.into(),
        content,
        template,
    }]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn emit_api_changes_returns_create_or_replace_block() {
        use crate::layout::RepoLayout;
        use crate::plan::FileEdit;
        use std::path::Path;

        let layout = RepoLayout::from_workspace_root(Path::new("/fake"));
        let edits = emit_api_changes(&layout, &[]);
        assert_eq!(edits.len(), 1);
        assert!(
            matches!(&edits[0], FileEdit::CreateOrReplaceBlock { path, start_marker, template, .. }
                if *path == Path::new("/fake/NIFI_API_CHANGES.md")
                && start_marker.contains("NIFI_API_CHANGES_START")
                && template.contains("# NiFi API Changes")
            )
        );
    }

    #[test]
    fn test_generate_api_changes_content_shape() {
        use std::path::Path;
        let codegen_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
        let specs_dir = codegen_dir.join("specs");
        let versions = crate::util::discover_spec_versions(&specs_dir);
        if versions.len() < 2 {
            return;
        }
        let all_specs: Vec<(String, ApiSpec)> = versions
            .iter()
            .map(|v| {
                let path = specs_dir.join(v).join("nifi-api.json");
                let spec = crate::load(path.to_str().unwrap());
                (v.clone(), spec)
            })
            .collect();

        let content = generate_api_changes_content(&all_specs);

        // Newest version heading present
        let newest = versions.last().unwrap();
        assert!(
            content.contains(&format!("## {newest}")),
            "missing heading for {newest}"
        );

        // Baseline version has no heading
        let oldest = versions.first().unwrap();
        assert!(
            !content.contains(&format!("## {oldest}")),
            "baseline should have no heading"
        );

        // Contains collapsible details blocks
        assert!(content.contains("<details>"));
        assert!(content.contains("</details>"));
        assert!(content.contains("(consecutive)"));

        // Newest version with 3+ versions also has cumulative block
        if versions.len() >= 3 {
            assert!(content.contains("(cumulative)"));
        }
    }
}
