//! TUI export functionality.
//!
//! Provides export capabilities for diff and view modes using the reports module.

use crate::diff::DiffResult;
use crate::model::NormalizedSbom;
use crate::reports::{ReportConfig, ReportFormat, ReportType, create_reporter};
use crate::tui::ViewTab;
use crate::tui::app::TabKind;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};

/// Export format selection for TUI
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExportFormat {
    Json,
    Markdown,
    Html,
    Sarif,
    Csv,
}

impl ExportFormat {
    /// Get file extension for this format
    pub(crate) const fn extension(self) -> &'static str {
        match self {
            Self::Json => "json",
            Self::Markdown => "md",
            Self::Html => "html",
            Self::Sarif => "sarif.json",
            Self::Csv => "csv",
        }
    }

    /// Convert to report format
    const fn to_report_format(self) -> ReportFormat {
        match self {
            Self::Json => ReportFormat::Json,
            Self::Markdown => ReportFormat::Markdown,
            Self::Html => ReportFormat::Html,
            Self::Sarif => ReportFormat::Sarif,
            Self::Csv => ReportFormat::Csv,
        }
    }
}

/// Result of an export operation
#[derive(Debug)]
pub struct ExportResult {
    pub path: PathBuf,
    pub success: bool,
    pub message: String,
}

/// Map a diff-mode tab to its corresponding `ReportType`.
///
/// Tabs that represent the full report (Summary, Quality, Compliance, etc.)
/// map to `ReportType::All`.
#[must_use]
pub const fn tab_to_report_type(tab: TabKind) -> ReportType {
    match tab {
        TabKind::Components => ReportType::Components,
        TabKind::Dependencies => ReportType::Dependencies,
        TabKind::Licenses => ReportType::Licenses,
        TabKind::Vulnerabilities => ReportType::Vulnerabilities,
        TabKind::Summary
        | TabKind::Overview
        | TabKind::Tree
        | TabKind::Quality
        | TabKind::Compliance
        | TabKind::SideBySide
        | TabKind::GraphChanges
        | TabKind::Source => ReportType::All,
    }
}

/// Map a view-mode tab to its corresponding `ReportType`.
#[must_use]
pub const fn view_tab_to_report_type(tab: ViewTab) -> ReportType {
    match tab {
        ViewTab::Tree => ReportType::Components,
        ViewTab::Vulnerabilities => ReportType::Vulnerabilities,
        ViewTab::Licenses => ReportType::Licenses,
        ViewTab::Dependencies => ReportType::Dependencies,
        ViewTab::Overview | ViewTab::Quality | ViewTab::Compliance | ViewTab::Source => {
            ReportType::All
        }
    }
}

/// Get the export scope label for a diff-mode tab.
#[must_use]
pub const fn tab_export_scope(tab: TabKind) -> &'static str {
    match tab {
        TabKind::Components => "Components",
        TabKind::Dependencies => "Dependencies",
        TabKind::Licenses => "Licenses",
        TabKind::Vulnerabilities => "Vulnerabilities",
        _ => "Report",
    }
}

/// Get the export scope label for a view-mode tab.
#[must_use]
pub const fn view_tab_export_scope(tab: ViewTab) -> &'static str {
    match tab {
        ViewTab::Tree => "Components",
        ViewTab::Vulnerabilities => "Vulnerabilities",
        ViewTab::Licenses => "Licenses",
        ViewTab::Dependencies => "Dependencies",
        _ => "Report",
    }
}

/// Expand a filename template with placeholders.
///
/// Supported placeholders:
/// - `{date}` → YYYY-MM-DD
/// - `{time}` → HHMMSS
/// - `{format}` → json, md, html, etc.
/// - `{command}` → diff, view, compliance, matrix
fn expand_template(template: &str, command: &str, format: &ExportFormat) -> String {
    let now = chrono::Local::now();
    template
        .replace("{date}", &now.format("%Y-%m-%d").to_string())
        .replace("{time}", &now.format("%H%M%S").to_string())
        .replace("{format}", format.extension())
        .replace("{command}", command)
}

/// Build an export filename from an optional template, falling back to timestamp.
fn build_export_filename(
    template: Option<&str>,
    command: &str,
    format: &ExportFormat,
    output_dir: Option<&str>,
) -> PathBuf {
    let filename = if let Some(tmpl) = template {
        let expanded = expand_template(tmpl, command, format);
        // Append extension if the template doesn't already have one
        if std::path::Path::new(&expanded).extension().is_some() {
            expanded
        } else {
            format!("{expanded}.{}", format.extension())
        }
    } else {
        let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
        format!("sbom_{command}_{timestamp}.{}", format.extension())
    };
    output_dir.map_or_else(
        || PathBuf::from(&filename),
        |dir| PathBuf::from(dir).join(&filename),
    )
}

/// Export diff results to a file
pub fn export_diff(
    format: ExportFormat,
    result: &DiffResult,
    old_sbom: &NormalizedSbom,
    new_sbom: &NormalizedSbom,
    output_dir: Option<&str>,
    config: &ReportConfig,
    template: Option<&str>,
) -> ExportResult {
    let path = build_export_filename(template, "diff", &format, output_dir);
    export_with_reporter(
        format.to_report_format(),
        result,
        old_sbom,
        new_sbom,
        &path,
        config,
    )
}

/// Export single SBOM to a file (view mode)
pub fn export_view(
    format: ExportFormat,
    sbom: &NormalizedSbom,
    output_dir: Option<&str>,
    config: &ReportConfig,
    template: Option<&str>,
) -> ExportResult {
    let path = build_export_filename(template, "view", &format, output_dir);
    export_view_with_reporter(format.to_report_format(), sbom, &path, config)
}

fn export_with_reporter(
    report_format: ReportFormat,
    result: &DiffResult,
    old_sbom: &NormalizedSbom,
    new_sbom: &NormalizedSbom,
    path: &Path,
    config: &ReportConfig,
) -> ExportResult {
    let reporter = create_reporter(report_format);

    match reporter.generate_diff_report(result, old_sbom, new_sbom, config) {
        Ok(content) => match write_to_file(path, &content) {
            Ok(actual_path) => ExportResult {
                message: format!("Exported to {}", display_path(&actual_path)),
                path: actual_path,
                success: true,
            },
            Err(e) => ExportResult {
                path: path.to_path_buf(),
                success: false,
                message: format!("Failed to write file: {e}"),
            },
        },
        Err(e) => ExportResult {
            path: path.to_path_buf(),
            success: false,
            message: format!("Failed to generate report: {e}"),
        },
    }
}

fn export_view_with_reporter(
    report_format: ReportFormat,
    sbom: &NormalizedSbom,
    path: &Path,
    config: &ReportConfig,
) -> ExportResult {
    let reporter = create_reporter(report_format);

    match reporter.generate_view_report(sbom, config) {
        Ok(content) => match write_to_file(path, &content) {
            Ok(actual_path) => ExportResult {
                message: format!("Exported to {}", display_path(&actual_path)),
                path: actual_path,
                success: true,
            },
            Err(e) => ExportResult {
                path: path.to_path_buf(),
                success: false,
                message: format!("Failed to write file: {e}"),
            },
        },
        Err(e) => ExportResult {
            path: path.to_path_buf(),
            success: false,
            message: format!("Failed to generate report: {e}"),
        },
    }
}

/// Export compliance results to a file.
///
/// All five export formats are supported: JSON, SARIF, Markdown, HTML, CSV.
pub fn export_compliance(
    format: ExportFormat,
    results: &[crate::quality::ComplianceResult],
    selected_standard: usize,
    output_dir: Option<&str>,
    template: Option<&str>,
) -> ExportResult {
    let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
    let result = results.get(selected_standard);

    let (ext, content) = match format {
        ExportFormat::Json => {
            let json = compliance_to_json(results, selected_standard);
            ("json", json)
        }
        ExportFormat::Sarif => {
            let sarif = compliance_to_sarif(result);
            ("sarif.json", sarif)
        }
        ExportFormat::Markdown => {
            let md = compliance_to_markdown(results, selected_standard);
            ("md", md)
        }
        ExportFormat::Html => {
            let html = compliance_to_html(results, selected_standard);
            ("html", html)
        }
        ExportFormat::Csv => {
            let csv = compliance_to_csv(results, selected_standard);
            ("csv", csv)
        }
    };

    let path = if let Some(tmpl) = template {
        let expanded = expand_template(tmpl, "compliance", &format);
        let expanded = if std::path::Path::new(&expanded).extension().is_some() {
            expanded
        } else {
            format!("{expanded}.{ext}")
        };
        output_dir.map_or_else(
            || PathBuf::from(&expanded),
            |dir| PathBuf::from(dir).join(&expanded),
        )
    } else {
        let level_name = result.map_or_else(
            || "all".to_string(),
            |r| r.level.name().to_lowercase().replace(' ', "_"),
        );
        let filename = format!("compliance_{level_name}_{timestamp}.{ext}");
        output_dir.map_or_else(
            || PathBuf::from(&filename),
            |dir| PathBuf::from(dir).join(&filename),
        )
    };

    match write_to_file(&path, &content) {
        Ok(actual_path) => ExportResult {
            message: format!("Compliance exported to {}", display_path(&actual_path)),
            path: actual_path,
            success: true,
        },
        Err(e) => ExportResult {
            path,
            success: false,
            message: format!("Failed to write: {e}"),
        },
    }
}

fn compliance_to_json(results: &[crate::quality::ComplianceResult], selected: usize) -> String {
    use serde_json::{Value, json};

    let to_value = |r: &crate::quality::ComplianceResult| -> Value {
        let violations: Vec<Value> = r
            .violations
            .iter()
            .map(|v| {
                json!({
                    "severity": format!("{:?}", v.severity),
                    "category": v.category.name(),
                    "message": v.message,
                    "element": v.element,
                    "requirement": v.requirement,
                    "remediation": v.remediation_guidance(),
                })
            })
            .collect();

        json!({
            "standard": r.level.name(),
            "is_compliant": r.is_compliant,
            "error_count": r.error_count,
            "warning_count": r.warning_count,
            "info_count": r.info_count,
            "violations": violations,
        })
    };

    let output = results.get(selected).map_or_else(
        || {
            let all: Vec<Value> = results.iter().map(to_value).collect();
            json!({ "standards": all })
        },
        to_value,
    );

    serde_json::to_string_pretty(&output).unwrap_or_default()
}

/// Delegate to the proper SARIF generator in `reports::sarif`.
fn compliance_to_sarif(result: Option<&crate::quality::ComplianceResult>) -> String {
    let Some(result) = result else {
        return r#"{"error": "no compliance result selected"}"#.to_string();
    };

    crate::reports::generate_compliance_sarif(result)
        .unwrap_or_else(|e| format!(r#"{{"error": "{e}"}}"#))
}

fn compliance_to_markdown(results: &[crate::quality::ComplianceResult], selected: usize) -> String {
    let mut md = String::new();

    let items: Vec<&crate::quality::ComplianceResult> = results
        .get(selected)
        .map_or_else(|| results.iter().collect(), |r| vec![r]);

    for r in items {
        let status = if r.is_compliant {
            "COMPLIANT"
        } else {
            "NON-COMPLIANT"
        };
        md.push_str(&format!("# {} - {status}\n\n", r.level.name()));
        md.push_str(&format!(
            "Errors: {} | Warnings: {} | Info: {}\n\n",
            r.error_count, r.warning_count, r.info_count
        ));

        if r.violations.is_empty() {
            md.push_str("No violations found.\n\n");
            continue;
        }

        md.push_str("| Severity | Category | Requirement | Element | Remediation |\n");
        md.push_str("|----------|----------|-------------|---------|-------------|\n");
        for v in &r.violations {
            let element = v.element.as_deref().unwrap_or("-");
            md.push_str(&format!(
                "| {:?} | {} | {} | {} | {} |\n",
                v.severity,
                v.category.name(),
                v.requirement,
                element,
                v.remediation_guidance(),
            ));
        }
        md.push('\n');
    }

    md
}

fn compliance_to_csv(results: &[crate::quality::ComplianceResult], selected: usize) -> String {
    let mut lines =
        vec!["Standard,Severity,Category,Requirement,Element,Message,Remediation".to_string()];

    let items: Vec<&crate::quality::ComplianceResult> = results
        .get(selected)
        .map_or_else(|| results.iter().collect(), |r| vec![r]);

    for r in &items {
        let std_name = r.level.name();
        for v in &r.violations {
            let element = v.element.as_deref().unwrap_or("");
            lines.push(format!(
                "\"{}\",\"{:?}\",\"{}\",\"{}\",\"{}\",\"{}\",\"{}\"",
                std_name,
                v.severity,
                csv_escape(v.category.name()),
                csv_escape(&v.requirement),
                csv_escape(element),
                csv_escape(&v.message),
                csv_escape(v.remediation_guidance()),
            ));
        }
    }

    lines.join("\n")
}

fn compliance_to_html(results: &[crate::quality::ComplianceResult], selected: usize) -> String {
    use crate::reports::escape::escape_html;

    let items: Vec<&crate::quality::ComplianceResult> = results
        .get(selected)
        .map_or_else(|| results.iter().collect(), |r| vec![r]);

    let mut html = String::from(
        r#"<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>SBOM Compliance Report</title>
<style>
body { font-family: system-ui, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 2rem; }
h1, h2 { color: #89b4fa; }
table { border-collapse: collapse; margin: 1rem 0; width: 100%; }
th, td { padding: 8px 12px; border: 1px solid #45475a; text-align: left; }
th { background: #313244; color: #89b4fa; font-weight: 600; }
.error { color: #f38ba8; }
.warning { color: #f9e2af; }
.info { color: #89b4fa; }
.pass { color: #a6e3a1; font-weight: bold; }
.fail { color: #f38ba8; font-weight: bold; }
.summary { margin: 1rem 0; color: #a6adc8; }
</style></head><body>
<h1>SBOM Compliance Report</h1>
<p class="summary">Generated by sbom-tools</p>
"#,
    );

    for r in &items {
        let status_class = if r.is_compliant { "pass" } else { "fail" };
        let status_label = if r.is_compliant {
            "COMPLIANT"
        } else {
            "NON-COMPLIANT"
        };
        html.push_str(&format!(
            "<h2>{} - <span class=\"{status_class}\">{status_label}</span></h2>\n",
            escape_html(r.level.name()),
        ));
        html.push_str(&format!(
            "<p>Errors: {} | Warnings: {} | Info: {}</p>\n",
            r.error_count, r.warning_count, r.info_count
        ));

        if r.violations.is_empty() {
            html.push_str("<p>No violations found.</p>\n");
            continue;
        }

        html.push_str("<table><tr><th>Severity</th><th>Category</th><th>Requirement</th><th>Element</th><th>Message</th><th>Remediation</th></tr>\n");
        for v in &r.violations {
            let sev_class = match v.severity {
                crate::quality::ViolationSeverity::Error => "error",
                crate::quality::ViolationSeverity::Warning => "warning",
                crate::quality::ViolationSeverity::Info => "info",
            };
            let element = v.element.as_deref().unwrap_or("-");
            html.push_str(&format!(
                "<tr><td class=\"{sev_class}\">{:?}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n",
                v.severity,
                escape_html(v.category.name()),
                escape_html(&v.requirement),
                escape_html(element),
                escape_html(&v.message),
                escape_html(v.remediation_guidance()),
            ));
        }
        html.push_str("</table>\n");
    }

    html.push_str("</body></html>");
    html
}

/// Export raw source content to a file.
///
/// Detects XML vs JSON from content prefix and writes to `sbom-source-{label}.{ext}`.
pub fn export_source_content(content: &str, label: &str) -> ExportResult {
    let ext = if content.trim_start().starts_with('<') {
        "xml"
    } else {
        "json"
    };
    let filename = format!("sbom-source-{label}.{ext}");
    let path = PathBuf::from(&filename);

    match write_to_file(&path, content) {
        Ok(actual_path) => ExportResult {
            message: format!("Source exported to {}", display_path(&actual_path)),
            path: actual_path,
            success: true,
        },
        Err(e) => ExportResult {
            path,
            success: false,
            message: format!("Failed to write: {e}"),
        },
    }
}

/// Escape a value for CSV: double any embedded quotes.
fn csv_escape(s: &str) -> String {
    s.replace('"', "\"\"")
}

/// Export matrix results to a file (JSON, CSV, or HTML)
pub fn export_matrix(
    format: ExportFormat,
    result: &crate::diff::MatrixResult,
    template: Option<&str>,
) -> ExportResult {
    let path = build_export_filename(template, "matrix", &format, None);

    let content = match format {
        ExportFormat::Json => matrix_to_json(result),
        ExportFormat::Csv => matrix_to_csv(result),
        ExportFormat::Html => matrix_to_html(result),
        _ => {
            return ExportResult {
                path,
                success: false,
                message: "Matrix export supports JSON, CSV, and HTML".to_string(),
            };
        }
    };

    match write_to_file(&path, &content) {
        Ok(actual_path) => ExportResult {
            message: format!("Matrix exported to {}", display_path(&actual_path)),
            path: actual_path,
            success: true,
        },
        Err(e) => ExportResult {
            path,
            success: false,
            message: format!("Failed to write: {e}"),
        },
    }
}

fn matrix_to_json(result: &crate::diff::MatrixResult) -> String {
    serde_json::to_string_pretty(result).unwrap_or_default()
}

fn matrix_to_csv(result: &crate::diff::MatrixResult) -> String {
    let n = result.sboms.len();
    let mut lines = Vec::with_capacity(n + 1);

    // Header row: empty cell + SBOM names
    let mut header = String::from("\"\"");
    for sbom in &result.sboms {
        header.push_str(&format!(",\"{}\"", sbom.name.replace('"', "\"\"")));
    }
    lines.push(header);

    // Data rows: SBOM name + similarity scores
    for i in 0..n {
        let mut row = format!("\"{}\"", result.sboms[i].name.replace('"', "\"\""));
        for j in 0..n {
            if i == j {
                row.push_str(",1.000");
            } else {
                let score = result.get_similarity(i, j);
                row.push_str(&format!(",{score:.3}"));
            }
        }
        lines.push(row);
    }

    lines.join("\n")
}

fn matrix_to_html(result: &crate::diff::MatrixResult) -> String {
    use crate::reports::escape::escape_html;

    let n = result.sboms.len();
    let mut html = String::from(
        r#"<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>SBOM Similarity Matrix</title>
<style>
body { font-family: system-ui, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 2rem; }
h1 { color: #89b4fa; }
table { border-collapse: collapse; margin: 1rem 0; }
th, td { padding: 8px 12px; border: 1px solid #45475a; text-align: center; }
th { background: #313244; color: #89b4fa; font-weight: 600; }
.high { background: #a6e3a1; color: #1e1e2e; }
.medium { background: #f9e2af; color: #1e1e2e; }
.low { background: #f38ba8; color: #1e1e2e; }
.self { background: #585b70; color: #a6adc8; }
.info { margin: 1rem 0; color: #a6adc8; }
</style></head><body>
<h1>SBOM Similarity Matrix</h1>
<p class="info">Generated by sbom-tools</p>
<table><tr><th></th>"#,
    );

    // Header row
    for sbom in &result.sboms {
        html.push_str(&format!("<th>{}</th>", escape_html(&sbom.name)));
    }
    html.push_str("</tr>");

    // Data rows
    for i in 0..n {
        html.push_str(&format!(
            "<tr><th>{}</th>",
            escape_html(&result.sboms[i].name)
        ));
        for j in 0..n {
            if i == j {
                html.push_str("<td class=\"self\">-</td>");
            } else {
                let score = result.get_similarity(i, j);
                let class = if score >= 0.8 {
                    "high"
                } else if score >= 0.5 {
                    "medium"
                } else {
                    "low"
                };
                html.push_str(&format!("<td class=\"{class}\">{score:.1}%</td>"));
            }
        }
        html.push_str("</tr>");
    }

    html.push_str("</table>");

    // Clustering info
    if let Some(ref clustering) = result.clustering {
        html.push_str("<h2>Clusters</h2><ul>");
        for (idx, cluster) in clustering.clusters.iter().enumerate() {
            let names: Vec<&str> = cluster
                .members
                .iter()
                .filter_map(|&i| result.sboms.get(i).map(|s| s.name.as_str()))
                .collect();
            html.push_str(&format!(
                "<li>Cluster {} (avg similarity: {:.1}%): {}</li>",
                idx + 1,
                cluster.internal_similarity * 100.0,
                names.join(", ")
            ));
        }
        html.push_str("</ul>");
    }

    html.push_str("</body></html>");
    html
}

/// Write content to a file, auto-renaming if the path already exists.
///
/// Returns the actual path written to (which may differ from `path` if
/// the original was renamed to avoid overwriting).
fn write_to_file(path: &Path, content: &str) -> std::io::Result<PathBuf> {
    let actual_path = find_available_path(path);
    let mut file = File::create(&actual_path)?;
    file.write_all(content.as_bytes())?;
    Ok(actual_path)
}

/// Find an available path by appending `_2`, `_3`, etc. if the path already exists.
fn find_available_path(path: &Path) -> PathBuf {
    if !path.exists() {
        return path.to_path_buf();
    }

    let stem = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or("export");
    let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
    let parent = path.parent();

    // Handle double extensions like .sarif.json
    let (base_stem, full_ext) = if stem.ends_with(".sarif") && extension == "json" {
        (stem.strip_suffix(".sarif").unwrap_or(stem), "sarif.json")
    } else {
        (stem, extension)
    };

    for i in 2..=99 {
        let new_name = if full_ext.is_empty() {
            format!("{base_stem}_{i}")
        } else {
            format!("{base_stem}_{i}.{full_ext}")
        };
        let new_path = parent.map_or_else(|| PathBuf::from(&new_name), |p| p.join(&new_name));
        if !new_path.exists() {
            return new_path;
        }
    }

    // Fallback: just return the original path (will overwrite)
    path.to_path_buf()
}

/// Resolve a path to an absolute path for display in status messages.
fn display_path(path: &PathBuf) -> String {
    if path.is_absolute() {
        path.display().to_string()
    } else {
        std::env::current_dir()
            .map(|cwd| cwd.join(path))
            .unwrap_or_else(|_| path.to_path_buf())
            .display()
            .to_string()
    }
}
