use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::fs;
use std::path::PathBuf;

use chrono::Utc;
use clap::Parser;
use serde::Serialize;
use serde_json::{Map, Value};

type DynError = Box<dyn Error + Send + Sync + 'static>;

#[derive(Debug, Parser)]
#[command(
    name = "openapi_alignment_report",
    about = "Compare two OpenAPI specs and generate plugin-level alignment report"
)]
struct Cli {
    #[arg(long, value_name = "path")]
    reference: PathBuf,

    #[arg(long, value_name = "path")]
    target: PathBuf,

    #[arg(long, value_name = "path")]
    output_json: Option<PathBuf>,

    #[arg(long, value_name = "path")]
    output_md: Option<PathBuf>,

    #[arg(long, default_value_t = 20)]
    list_limit: usize,

    #[arg(long)]
    fail_on_default_missing: bool,
}

#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize)]
struct OperationKey {
    method: String,
    path: String,
}

#[derive(Debug, Clone)]
struct OperationInfo {
    plugin: String,
    request_schema: Option<Value>,
    response_schema: Option<Value>,
}

#[derive(Debug, Serialize)]
struct Report {
    timestamp: String,
    reference: ReportInput,
    target: ReportInput,
    summary: Summary,
    gates: Gates,
    plugins: Vec<PluginReport>,
    top_missing: Vec<String>,
    top_extra: Vec<String>,
    top_schema_mismatch: Vec<String>,
}

#[derive(Debug, Serialize)]
struct ReportInput {
    path: String,
    operations: usize,
}

#[derive(Debug, Serialize)]
struct Summary {
    matched_operations: usize,
    missing_operations: usize,
    extra_operations: usize,
    route_coverage_pct: f64,
    schema_mismatch_operations: usize,
    schema_compatibility_pct: f64,
}

#[derive(Debug, Serialize)]
struct Gates {
    default_plugin_complete: bool,
}

#[derive(Debug, Serialize)]
struct PluginReport {
    plugin: String,
    reference_operations: usize,
    matched_operations: usize,
    missing_operations: usize,
    extra_operations: usize,
    schema_mismatch_operations: usize,
    route_coverage_pct: f64,
    schema_compatibility_pct: f64,
    missing_examples: Vec<String>,
    extra_examples: Vec<String>,
    schema_mismatch_examples: Vec<String>,
}

#[derive(Default)]
struct PluginAccumulator {
    reference_operations: usize,
    matched_operations: usize,
    missing: Vec<OperationKey>,
    extra: Vec<OperationKey>,
    schema_mismatch: Vec<OperationKey>,
}

fn main() -> Result<(), DynError> {
    let cli = Cli::parse();

    let reference_spec = load_openapi(&cli.reference)?;
    let target_spec = load_openapi(&cli.target)?;

    let reference_ops: BTreeMap<OperationKey, OperationInfo> = extract_operations(&reference_spec)?;
    let target_ops: BTreeMap<OperationKey, OperationInfo> = extract_operations(&target_spec)?;

    let mut reference_keys: BTreeSet<OperationKey> = BTreeSet::new();
    let mut target_keys: BTreeSet<OperationKey> = BTreeSet::new();
    for key in reference_ops.keys() {
        reference_keys.insert(key.clone());
    }
    for key in target_ops.keys() {
        target_keys.insert(key.clone());
    }

    let mut missing: Vec<OperationKey> = Vec::new();
    let mut extra: Vec<OperationKey> = Vec::new();
    let mut schema_mismatches: Vec<OperationKey> = Vec::new();

    let mut plugin_acc: BTreeMap<String, PluginAccumulator> = BTreeMap::new();

    for key in &reference_keys {
        let reference_info: &OperationInfo = reference_ops.get(key).ok_or_else(|| {
            format!(
                "internal error: missing reference op {}/{}",
                key.method, key.path
            )
        })?;

        let plugin_name: String = reference_info.plugin.clone();
        let entry: &mut PluginAccumulator = plugin_acc.entry(plugin_name).or_default();
        entry.reference_operations += 1;

        match target_ops.get(key) {
            Some(target_info) => {
                entry.matched_operations += 1;

                if !schema_compatible(reference_info, target_info) {
                    schema_mismatches.push(key.clone());
                    entry.schema_mismatch.push(key.clone());
                }
            }
            None => {
                missing.push(key.clone());
                entry.missing.push(key.clone());
            }
        }
    }

    for key in &target_keys {
        if reference_ops.contains_key(key) {
            continue;
        }

        extra.push(key.clone());

        let target_plugin: String = target_ops
            .get(key)
            .map(|v| v.plugin.clone())
            .unwrap_or_else(|| "Unknown".to_string());
        let entry = plugin_acc.entry(target_plugin).or_default();
        entry.extra.push(key.clone());
    }

    let matched: usize = reference_keys.len().saturating_sub(missing.len());
    let route_coverage_pct: f64 = pct(matched, reference_keys.len());
    let schema_compatibility_pct: f64 =
        pct(matched.saturating_sub(schema_mismatches.len()), matched);

    let mut plugin_reports: Vec<PluginReport> = Vec::new();
    for (plugin, acc) in &plugin_acc {
        let route_plugin_pct: f64 = pct(acc.matched_operations, acc.reference_operations);
        let schema_plugin_pct: f64 = pct(
            acc.matched_operations
                .saturating_sub(acc.schema_mismatch.len()),
            acc.matched_operations,
        );

        plugin_reports.push(PluginReport {
            plugin: plugin.clone(),
            reference_operations: acc.reference_operations,
            matched_operations: acc.matched_operations,
            missing_operations: acc.missing.len(),
            extra_operations: acc.extra.len(),
            schema_mismatch_operations: acc.schema_mismatch.len(),
            route_coverage_pct: route_plugin_pct,
            schema_compatibility_pct: schema_plugin_pct,
            missing_examples: render_keys(&acc.missing, cli.list_limit),
            extra_examples: render_keys(&acc.extra, cli.list_limit),
            schema_mismatch_examples: render_keys(&acc.schema_mismatch, cli.list_limit),
        });
    }

    plugin_reports.sort_by(|a, b| a.plugin.cmp(&b.plugin));

    let default_missing: usize = plugin_reports
        .iter()
        .find(|r| r.plugin == "Default")
        .map(|r| r.missing_operations)
        .unwrap_or(0);

    let report: Report = Report {
        timestamp: Utc::now().to_rfc3339(),
        reference: ReportInput {
            path: cli.reference.to_string_lossy().into_owned(),
            operations: reference_keys.len(),
        },
        target: ReportInput {
            path: cli.target.to_string_lossy().into_owned(),
            operations: target_keys.len(),
        },
        summary: Summary {
            matched_operations: matched,
            missing_operations: missing.len(),
            extra_operations: extra.len(),
            route_coverage_pct,
            schema_mismatch_operations: schema_mismatches.len(),
            schema_compatibility_pct,
        },
        gates: Gates {
            default_plugin_complete: default_missing == 0,
        },
        plugins: plugin_reports,
        top_missing: render_keys(&missing, cli.list_limit),
        top_extra: render_keys(&extra, cli.list_limit),
        top_schema_mismatch: render_keys(&schema_mismatches, cli.list_limit),
    };

    if let Some(path) = &cli.output_json {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(path, serde_json::to_string_pretty(&report)?)?;
        eprintln!("[ok] wrote json report: {}", path.display());
    }

    let markdown: String = render_markdown(&report);
    if let Some(path) = &cli.output_md {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::write(path, markdown.as_bytes())?;
        eprintln!("[ok] wrote markdown report: {}", path.display());
    }

    if cli.output_md.is_none() {
        println!("{markdown}");
    }

    if cli.fail_on_default_missing && !report.gates.default_plugin_complete {
        return Err("Gate failed: Default plugin is not complete".into());
    }

    Ok(())
}

fn load_openapi(path: &PathBuf) -> Result<Value, DynError> {
    let raw: String = fs::read_to_string(path)?;

    if let Ok(v) = serde_json::from_str::<Value>(&raw) {
        return Ok(v);
    }

    let v: Value = serde_yaml::from_str(&raw)?;
    Ok(v)
}

fn extract_operations(spec: &Value) -> Result<BTreeMap<OperationKey, OperationInfo>, DynError> {
    let paths = spec
        .get("paths")
        .and_then(Value::as_object)
        .ok_or("OpenAPI spec missing paths object")?;

    let mut ops: BTreeMap<OperationKey, OperationInfo> = BTreeMap::new();

    for (path, methods) in paths {
        let Some(methods_obj) = methods.as_object() else {
            continue;
        };

        for (method, operation) in methods_obj {
            if !is_http_method(method) {
                continue;
            }
            let Some(op_obj) = operation.as_object() else {
                continue;
            };

            let key: OperationKey = OperationKey {
                method: method.to_uppercase(),
                path: path.clone(),
            };

            let plugin: String = op_obj
                .get("tags")
                .and_then(Value::as_array)
                .and_then(|tags| tags.iter().find_map(Value::as_str))
                .unwrap_or("Default")
                .to_string();

            let request_schema: Option<_> = extract_request_schema(operation);
            let response_schema: Option<_> = extract_response_schema(operation);

            ops.insert(
                key,
                OperationInfo {
                    plugin,
                    request_schema,
                    response_schema,
                },
            );
        }
    }

    Ok(ops)
}

fn is_http_method(method: &str) -> bool {
    matches!(
        method,
        "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "trace"
    )
}

fn extract_request_schema(operation: &Value) -> Option<Value> {
    operation
        .get("requestBody")
        .and_then(|v| v.get("content"))
        .and_then(extract_content_schema)
}

fn extract_response_schema(operation: &Value) -> Option<Value> {
    let responses = operation.get("responses")?.as_object()?;

    let mut response_keys: Vec<&String> = responses.keys().filter(|k| k.starts_with('2')).collect();
    response_keys.sort();

    for code in response_keys {
        let response = responses.get(code)?;
        if let Some(schema) = response.get("content").and_then(extract_content_schema) {
            return Some(schema);
        }
    }

    None
}

fn extract_content_schema(content: &Value) -> Option<Value> {
    let content_obj = content.as_object()?;

    if let Some(schema) = content_obj
        .get("application/json")
        .and_then(|media| media.get("schema"))
    {
        return Some(schema.clone());
    }

    for media in content_obj.values() {
        if let Some(schema) = media.get("schema") {
            return Some(schema.clone());
        }
    }

    None
}

fn schema_compatible(reference: &OperationInfo, target: &OperationInfo) -> bool {
    values_equal(
        reference.request_schema.as_ref(),
        target.request_schema.as_ref(),
    ) && values_equal(
        reference.response_schema.as_ref(),
        target.response_schema.as_ref(),
    )
}

fn values_equal(left: Option<&Value>, right: Option<&Value>) -> bool {
    match (left, right) {
        (None, None) => true,
        (Some(a), Some(b)) => canonicalize(a) == canonicalize(b),
        _ => false,
    }
}

fn canonicalize(value: &Value) -> Value {
    match value {
        Value::Object(map) => {
            let mut entries: Vec<(&String, &Value)> = map.iter().collect();
            entries.sort_by(|a, b| a.0.cmp(b.0));

            let mut out = Map::new();
            for (key, val) in entries {
                out.insert(key.clone(), canonicalize(val));
            }
            Value::Object(out)
        }
        Value::Array(arr) => Value::Array(arr.iter().map(canonicalize).collect()),
        _ => value.clone(),
    }
}

fn pct(numerator: usize, denominator: usize) -> f64 {
    if denominator == 0 {
        100.0
    } else {
        (numerator as f64 / denominator as f64) * 100.0
    }
}

fn render_keys(keys: &[OperationKey], limit: usize) -> Vec<String> {
    keys.iter()
        .take(limit)
        .map(|k| format!("{} {}", k.method, k.path))
        .collect()
}

fn render_markdown(report: &Report) -> String {
    let mut out = String::new();

    out.push_str("# OpenAPI Alignment Report\n\n");
    out.push_str(&format!("- Timestamp: `{}`\n", report.timestamp));
    out.push_str(&format!("- Reference: `{}`\n", report.reference.path));
    out.push_str(&format!("- Target: `{}`\n\n", report.target.path));

    out.push_str("## Summary\n\n");
    out.push_str(&format!(
        "- Reference operations: {}\n",
        report.reference.operations
    ));
    out.push_str(&format!(
        "- Target operations: {}\n",
        report.target.operations
    ));
    out.push_str(&format!(
        "- Matched / Missing / Extra: {} / {} / {}\n",
        report.summary.matched_operations,
        report.summary.missing_operations,
        report.summary.extra_operations
    ));
    out.push_str(&format!(
        "- Route coverage: `{:.2}%`\n",
        report.summary.route_coverage_pct
    ));
    out.push_str(&format!(
        "- Schema compatibility: `{:.2}%` (mismatches: {})\n",
        report.summary.schema_compatibility_pct, report.summary.schema_mismatch_operations
    ));
    out.push_str(&format!(
        "- Gate `Default complete`: `{}`\n\n",
        report.gates.default_plugin_complete
    ));

    out.push_str("## Plugin Breakdown\n\n");
    out.push_str(
        "| Plugin | Ref | Matched | Missing | Extra | SchemaMismatch | Route% | Schema% |\n",
    );
    out.push_str("|---|---:|---:|---:|---:|---:|---:|---:|\n");
    for p in &report.plugins {
        out.push_str(&format!(
            "| {} | {} | {} | {} | {} | {} | {:.2}% | {:.2}% |\n",
            p.plugin,
            p.reference_operations,
            p.matched_operations,
            p.missing_operations,
            p.extra_operations,
            p.schema_mismatch_operations,
            p.route_coverage_pct,
            p.schema_compatibility_pct
        ));
    }
    out.push('\n');

    out.push_str("## Top Missing\n\n");
    if report.top_missing.is_empty() {
        out.push_str("- None\n\n");
    } else {
        for item in &report.top_missing {
            out.push_str(&format!("- {}\n", item));
        }
        out.push('\n');
    }

    out.push_str("## Top Extra\n\n");
    if report.top_extra.is_empty() {
        out.push_str("- None\n\n");
    } else {
        for item in &report.top_extra {
            out.push_str(&format!("- {}\n", item));
        }
        out.push('\n');
    }

    out.push_str("## Top Schema Mismatch\n\n");
    if report.top_schema_mismatch.is_empty() {
        out.push_str("- None\n");
    } else {
        for item in &report.top_schema_mismatch {
            out.push_str(&format!("- {}\n", item));
        }
    }

    out
}
