use std::fs;
use std::path::{Path, PathBuf};
use std::process;

use serde::Serialize;

use crate::cli::CheckOutputFormat;
use crate::json_envelope::{to_string_pretty, JsonEnvelope};
use crate::package::{
    find_nearest_manifest, normalize_connector_capability, read_manifest_from_path,
    ConnectorCapabilities, Manifest,
};

pub(crate) const CONNECTOR_MATRIX_SCHEMA_VERSION: u32 = 1;

const DEFAULT_CONNECTOR_MATRIX_SOURCE: &str = "conformance/fixtures/connectors/contract-v1";

#[derive(Debug, Clone, Serialize)]
pub(crate) struct ConnectorCapabilityMatrixRow {
    pub provider: String,
    pub package: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub repository: Option<String>,
    pub webhook: bool,
    pub oauth: bool,
    pub rate_limit: bool,
    pub pagination: bool,
    pub graphql: bool,
    pub streaming: bool,
    pub manifest_path: String,
}

pub(crate) fn run(
    format: CheckOutputFormat,
    filter: Option<&str>,
    targets: &[String],
    deprecated_json_format: bool,
) {
    let rows = filtered_rows(load_rows_for_cli(targets), filter);
    match format {
        CheckOutputFormat::Text => print_text(&rows),
        CheckOutputFormat::Json => print_json(&rows, deprecated_json_format),
        CheckOutputFormat::Markdown => print!("{}", generate_markdown(&rows)),
    }
}

pub(crate) fn run_docs(output_path: &str, sources: &[String], check_only: bool) {
    let rows = load_rows_for_docs(sources);
    let generated = generate_markdown(&rows);
    let path = Path::new(output_path);

    if check_only {
        let existing = match fs::read_to_string(path) {
            Ok(s) => s,
            Err(e) => {
                eprintln!("error: cannot read {}: {e}", path.display());
                eprintln!("hint: run `make gen-connector-matrix` to regenerate.");
                process::exit(1);
            }
        };
        if normalize_line_endings(&existing) != normalize_line_endings(&generated) {
            eprintln!(
                "error: {} is stale relative to the connector capability matrix.",
                path.display()
            );
            eprintln!("hint: run `make gen-connector-matrix` to regenerate.");
            process::exit(1);
        }
        return;
    }

    if let Some(parent) = path.parent() {
        if let Err(e) = fs::create_dir_all(parent) {
            eprintln!("error: cannot create {}: {e}", parent.display());
            process::exit(1);
        }
    }
    if let Err(e) = fs::write(path, &generated) {
        eprintln!("error: cannot write {}: {e}", path.display());
        process::exit(1);
    }
    println!("wrote {}", path.display());
}

pub(crate) fn generate_markdown(rows: &[ConnectorCapabilityMatrixRow]) -> String {
    let mut out = String::new();
    out.push_str("# Connector parity matrix\n\n");
    out.push_str("<!-- GENERATED by `harn dump-connector-matrix` -- do not edit by hand. -->\n");
    out.push_str("<!-- Source of truth: connector package harn.toml [[providers]].capabilities declarations. -->\n\n");
    out.push_str("<!-- markdownlint-disable MD013 -->\n\n");
    out.push_str(
        "This table is generated from connector package manifests. A checked feature means the package declares support for that connector surface; missing support highlights either an intentional scope boundary or a package gap.\n\n",
    );
    out.push_str("Regenerate with `make gen-connector-matrix` and verify with `make check-connector-matrix`.\n\n");
    out.push_str(
        "| Provider | Package | Webhook | OAuth | Rate limit | Pagination | GraphQL | Streaming |\n",
    );
    out.push_str("|---|---|---:|---:|---:|---:|---:|---:|\n");
    for row in rows {
        out.push_str(&format!(
            "| `{}` | {} | {} | {} | {} | {} | {} | {} |\n",
            row.provider,
            package_cell(row),
            yes_no(row.webhook),
            yes_no(row.oauth),
            yes_no(row.rate_limit),
            yes_no(row.pagination),
            yes_no(row.graphql),
            yes_no(row.streaming),
        ));
    }
    out
}

fn normalize_line_endings(text: &str) -> String {
    text.replace("\r\n", "\n").replace('\r', "\n")
}

fn load_rows_for_cli(targets: &[String]) -> Vec<ConnectorCapabilityMatrixRow> {
    let sources = if targets.is_empty() {
        default_sources_for_cli()
    } else {
        targets.iter().map(PathBuf::from).collect()
    };
    load_rows_from_sources(&sources)
}

fn load_rows_for_docs(sources: &[String]) -> Vec<ConnectorCapabilityMatrixRow> {
    let sources = if sources.is_empty() {
        vec![PathBuf::from(DEFAULT_CONNECTOR_MATRIX_SOURCE)]
    } else {
        sources.iter().map(PathBuf::from).collect()
    };
    let rows = load_rows_from_sources(&sources);
    if rows.is_empty() {
        eprintln!(
            "error: no connector providers with manifests found under {}",
            sources
                .iter()
                .map(|source| source.display().to_string())
                .collect::<Vec<_>>()
                .join(", ")
        );
        process::exit(1);
    }
    rows
}

fn default_sources_for_cli() -> Vec<PathBuf> {
    let fixture_source = PathBuf::from(DEFAULT_CONNECTOR_MATRIX_SOURCE);
    if fixture_source.exists() {
        return vec![fixture_source];
    }
    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    if let Some((_, manifest_dir)) = find_nearest_manifest(&cwd) {
        return vec![manifest_dir.join("harn.toml")];
    }
    eprintln!(
        "error: no harn.toml found for connector matrix; pass package manifests or package directories"
    );
    process::exit(1);
}

fn load_rows_from_sources(sources: &[PathBuf]) -> Vec<ConnectorCapabilityMatrixRow> {
    let mut manifests = Vec::new();
    for source in sources {
        collect_manifest_paths(source, &mut manifests);
    }
    manifests.sort();
    manifests.dedup();

    let mut rows = Vec::new();
    for manifest_path in manifests {
        let manifest = match read_manifest_from_path(&manifest_path) {
            Ok(manifest) => manifest,
            Err(error) => {
                eprintln!("error: failed to read {}: {error}", manifest_path.display());
                process::exit(1);
            }
        };
        rows.extend(rows_from_manifest(&manifest, &manifest_path));
    }
    rows.sort_by(|left, right| {
        left.provider
            .cmp(&right.provider)
            .then_with(|| left.package.cmp(&right.package))
            .then_with(|| left.manifest_path.cmp(&right.manifest_path))
    });
    rows
}

fn collect_manifest_paths(source: &Path, manifests: &mut Vec<PathBuf>) {
    if source.is_file() {
        if source.file_name().is_some_and(|name| name == "harn.toml") {
            manifests.push(source.to_path_buf());
        } else {
            eprintln!(
                "error: connector matrix source {} is not a harn.toml manifest",
                source.display()
            );
            process::exit(1);
        }
        return;
    }
    if !source.is_dir() {
        eprintln!(
            "error: connector matrix source {} does not exist",
            source.display()
        );
        process::exit(1);
    }

    let direct_manifest = source.join("harn.toml");
    if direct_manifest.is_file() {
        manifests.push(direct_manifest);
        return;
    }

    let entries = match fs::read_dir(source) {
        Ok(entries) => entries,
        Err(error) => {
            eprintln!("error: cannot read {}: {error}", source.display());
            process::exit(1);
        }
    };
    for entry in entries {
        let entry = match entry {
            Ok(entry) => entry,
            Err(error) => {
                eprintln!("error: cannot read directory entry: {error}");
                process::exit(1);
            }
        };
        let path = entry.path();
        if path.is_file() && path.file_name().is_none_or(|name| name != "harn.toml") {
            continue;
        }
        if should_skip_dir(&path) {
            continue;
        }
        collect_manifest_paths(&path, manifests);
    }
}

fn should_skip_dir(path: &Path) -> bool {
    if !path.is_dir() {
        return false;
    }
    matches!(
        path.file_name().and_then(|name| name.to_str()),
        Some(".git" | ".harn" | "node_modules" | "target")
    )
}

fn rows_from_manifest(
    manifest: &Manifest,
    manifest_path: &Path,
) -> Vec<ConnectorCapabilityMatrixRow> {
    let package = manifest
        .package
        .as_ref()
        .and_then(|package| package.name.clone())
        .unwrap_or_else(|| "-".to_string());
    let repository = manifest
        .package
        .as_ref()
        .and_then(|package| package.repository.clone());
    manifest
        .providers
        .iter()
        .map(|provider| row_from_provider(&package, repository.clone(), manifest_path, provider))
        .collect()
}

fn row_from_provider(
    package: &str,
    repository: Option<String>,
    manifest_path: &Path,
    provider: &crate::package::ProviderManifestEntry,
) -> ConnectorCapabilityMatrixRow {
    let capabilities = provider.capabilities;
    ConnectorCapabilityMatrixRow {
        provider: provider.id.as_str().to_string(),
        package: package.to_string(),
        repository,
        webhook: capabilities.webhook,
        oauth: capabilities.oauth,
        rate_limit: capabilities.rate_limit,
        pagination: capabilities.pagination,
        graphql: capabilities.graphql,
        streaming: capabilities.streaming,
        manifest_path: manifest_path.display().to_string(),
    }
}

fn filtered_rows(
    rows: Vec<ConnectorCapabilityMatrixRow>,
    filter: Option<&str>,
) -> Vec<ConnectorCapabilityMatrixRow> {
    let Some(feature) = filter else {
        return rows;
    };
    let normalized = normalize_connector_capability(feature);
    if !ConnectorCapabilities::FEATURES.contains(&normalized.as_str()) {
        eprintln!(
            "error: unknown connector matrix feature `{feature}`. Expected one of: {}",
            ConnectorCapabilities::FEATURES.join(", ")
        );
        process::exit(2);
    }
    rows.into_iter()
        .filter(|row| row_supports_feature(row, &normalized))
        .collect()
}

fn row_supports_feature(row: &ConnectorCapabilityMatrixRow, feature: &str) -> bool {
    match feature {
        "webhook" => row.webhook,
        "oauth" => row.oauth,
        "rate_limit" => row.rate_limit,
        "pagination" => row.pagination,
        "graphql" => row.graphql,
        "streaming" => row.streaming,
        _ => false,
    }
}

fn print_text(rows: &[ConnectorCapabilityMatrixRow]) {
    let table_rows: Vec<[String; 8]> = rows
        .iter()
        .map(|row| {
            [
                row.provider.clone(),
                row.package.clone(),
                yes_no(row.webhook).to_string(),
                yes_no(row.oauth).to_string(),
                yes_no(row.rate_limit).to_string(),
                yes_no(row.pagination).to_string(),
                yes_no(row.graphql).to_string(),
                yes_no(row.streaming).to_string(),
            ]
        })
        .collect();
    let headers = [
        "provider".to_string(),
        "package".to_string(),
        "webhook".to_string(),
        "oauth".to_string(),
        "rate_limit".to_string(),
        "pagination".to_string(),
        "graphql".to_string(),
        "streaming".to_string(),
    ];
    let mut widths: Vec<usize> = headers.iter().map(String::len).collect();
    for row in &table_rows {
        for (index, value) in row.iter().enumerate() {
            widths[index] = widths[index].max(value.len());
        }
    }
    print_row(&headers, &widths);
    for row in table_rows {
        print_row(&row, &widths);
    }
}

fn print_row<const N: usize>(cells: &[String; N], widths: &[usize]) {
    for (index, cell) in cells.iter().enumerate() {
        if index > 0 {
            print!("  ");
        }
        print!("{cell:<width$}", width = widths[index]);
    }
    println!();
}

fn print_json(rows: &[ConnectorCapabilityMatrixRow], deprecated_json_format: bool) {
    let mut envelope = JsonEnvelope::ok(CONNECTOR_MATRIX_SCHEMA_VERSION, rows);
    if deprecated_json_format {
        envelope = envelope.with_warning(
            "deprecated.flag",
            "`harn check --connector-matrix --format=json` is deprecated; use `--json`",
        );
    }
    println!("{}", to_string_pretty(&envelope));
}

fn package_cell(row: &ConnectorCapabilityMatrixRow) -> String {
    match &row.repository {
        Some(repository) if !repository.is_empty() => {
            format!("[`{}`]({})", row.package, repository)
        }
        _ => format!("`{}`", row.package),
    }
}

fn yes_no(value: bool) -> &'static str {
    if value {
        "yes"
    } else {
        "no"
    }
}

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

    #[test]
    fn manifest_list_capabilities_generate_rows() {
        let manifest: Manifest = toml::from_str(
            r#"
[package]
name = "harn-acme-connector"
repository = "https://example.com/acme"

[[providers]]
id = "acme"
connector = { harn = "src/lib.harn" }
capabilities = ["webhook", "rate-limit", "graphql"]
"#,
        )
        .unwrap();

        let rows = rows_from_manifest(&manifest, Path::new("harn.toml"));
        assert_eq!(rows.len(), 1);
        assert_eq!(rows[0].provider, "acme");
        assert!(rows[0].webhook);
        assert!(rows[0].rate_limit);
        assert!(rows[0].graphql);
        assert!(!rows[0].oauth);
    }

    #[test]
    fn manifest_table_capabilities_generate_rows() {
        let manifest: Manifest = toml::from_str(
            r#"
[[providers]]
id = "acme"
connector = { harn = "src/lib.harn" }
capabilities = { webhook = true, oauth = true, rate_limit = true }
"#,
        )
        .unwrap();

        let rows = rows_from_manifest(&manifest, Path::new("harn.toml"));
        assert!(rows[0].webhook);
        assert!(rows[0].oauth);
        assert!(rows[0].rate_limit);
        assert!(!rows[0].streaming);
    }

    #[test]
    fn markdown_includes_connector_columns() {
        let rows = vec![ConnectorCapabilityMatrixRow {
            provider: "acme".to_string(),
            package: "harn-acme-connector".to_string(),
            repository: None,
            webhook: true,
            oauth: false,
            rate_limit: true,
            pagination: false,
            graphql: false,
            streaming: false,
            manifest_path: "harn.toml".to_string(),
        }];

        let markdown = generate_markdown(&rows);
        assert!(markdown.contains("Connector parity matrix"));
        assert!(markdown.contains("Source of truth"));
        assert!(markdown.contains(
            "| Provider | Package | Webhook | OAuth | Rate limit | Pagination | GraphQL | Streaming |"
        ));
        assert!(
            markdown.contains("| `acme` | `harn-acme-connector` | yes | no | yes | no | no | no |")
        );
    }

    #[test]
    fn generated_markdown_comparison_ignores_platform_line_endings() {
        let generated = "# Connector parity matrix\n\n| Provider |\n";
        let on_disk = generated.replace('\n', "\r\n");

        assert_eq!(
            normalize_line_endings(&on_disk),
            normalize_line_endings(generated)
        );
    }
}
