use std::{fs, path::PathBuf, process::ExitCode, time::Instant};

use clap::{Parser, Subcommand};
use zipdiv::{variants_catalog, Client, Composition, Engine, TelemetryEvent};

#[derive(Parser)]
#[command(
    name = "zipdiv",
    version,
    about = "Runtime-diversified ZIP reader/extractor"
)]
struct Cli {
    /// Local composition JSON. Omit to use safe defaults.
    #[arg(long, global = true, env = "ZIPDIV_COMPOSITION")]
    composition: Option<PathBuf>,
    /// Distribution service base URL (for example http://127.0.0.1:8000).
    #[arg(long, global = true, env = "ZIPDIV_SERVER")]
    server: Option<String>,
    #[arg(long, global = true, env = "ZIPDIV_RELEASE_ID", default_value_t = 1)]
    release_id: u64,
    #[arg(long, global = true, env = "ZIPDIV_INSTALLATION_ID")]
    installation_id: Option<String>,
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    List {
        archive: PathBuf,
        #[arg(long)]
        json: bool,
    },
    Extract {
        archive: PathBuf,
        output: PathBuf,
    },
    /// Print implementation metadata suitable for release registration.
    Variants {
        #[arg(long)]
        json: bool,
        #[arg(long)]
        include_experimental: bool,
    },
    /// Check that an archive can be listed and fully extracted into a temporary directory.
    Verify {
        archive: PathBuf,
    },
}

fn main() -> ExitCode {
    match run() {
        Ok(()) => ExitCode::SUCCESS,
        Err(error) => {
            eprintln!("zipdiv: {error}");
            ExitCode::FAILURE
        }
    }
}

fn run() -> zipdiv::Result<()> {
    let cli = Cli::parse();
    if let Command::Variants {
        json,
        include_experimental,
    } = &cli.command
    {
        let values: Vec<_> = variants_catalog()
            .into_iter()
            .filter(|v| *include_experimental || !v.experimental)
            .collect();
        if *json {
            let mut grouped = std::collections::BTreeMap::<&str, Vec<_>>::new();
            for item in &values {
                grouped
                    .entry(item.function)
                    .or_default()
                    .push(serde_json::json!({
                        "id": format!("{}.{}", item.function, item.variant),
                        "name": item.variant,
                        "provenance": {"source": "built-in", "path": "rust/src/variants.rs", "experimental": item.experimental}
                    }));
            }
            let spec_functions: Vec<_> = grouped.into_iter().map(|(name, implementations)| serde_json::json!({"name": name, "implementations": implementations})).collect();
            println!(
                "{}",
                serde_json::to_string_pretty(
                    &serde_json::json!({"application":"zipdiv", "version": env!("CARGO_PKG_VERSION"), "spec_functions": spec_functions})
                )?
            );
        } else {
            for item in values {
                println!(
                    "{}\t{}{}",
                    item.function,
                    item.variant,
                    if item.experimental {
                        "\texperimental"
                    } else {
                        ""
                    }
                );
            }
        }
        return Ok(());
    }
    let mut remote = if let Some(server) = &cli.server {
        Some(Client::register(
            server,
            cli.release_id,
            cli.installation_id.as_deref(),
            "balanced",
        )?)
    } else {
        None
    };
    let composition = if let Some(client) = &remote {
        client.composition.clone()
    } else if let Some(path) = &cli.composition {
        Composition::from_json(&fs::read_to_string(path)?)?
    } else {
        Composition::default()
    };
    let mut engine = Engine::new(composition)?;
    let started = Instant::now();
    let operation = match &cli.command {
        Command::List { .. } => "list",
        Command::Extract { .. } => "extract",
        Command::Verify { .. } => "verify",
        Command::Variants { .. } => unreachable!(),
    };
    let result: zipdiv::Result<()> = match &cli.command {
        Command::List { archive, json } => engine.list_path(archive).map(|report| {
            if *json {
                println!("{}", serde_json::to_string_pretty(&report.value).unwrap());
            } else {
                for entry in report.value {
                    println!("{:>10}  {}", entry.uncompressed_size, entry.name);
                }
            }
        }),
        Command::Extract { archive, output } => engine
            .extract_path(archive, output)
            .map(|report| println!("extracted {} files", report.value.len())),
        Command::Verify { archive } => {
            let dir = tempfile_dir()?;
            let result = engine
                .extract_path(archive, &dir)
                .map(|_| println!("archive verified"));
            let _ = fs::remove_dir_all(dir);
            result
        }
        Command::Variants { .. } => unreachable!(),
    };
    if let Some(client) = remote.as_mut() {
        let success = result.is_ok();
        let error_kind = result.as_ref().err().map(classify_error);
        let execution = client.report_execution(
            operation,
            success,
            started.elapsed().as_secs_f64() * 1000.0,
            error_kind,
            &engine.recent_calls(),
        );
        if let Err(error) = &result {
            let _execution_id = execution.ok();
            let (spec_function, implementation_id) = engine
                .recent_calls()
                .last()
                .and_then(|s| s.split_once(':'))
                .map(|(a, b)| (a.to_owned(), b.to_owned()))
                .unwrap_or_else(|| {
                    (
                        "list_archive".into(),
                        engine.composition().variant("list_archive").into(),
                    )
                });
            let event = TelemetryEvent {
                client_id: client.client_id.clone(),
                release_id: client.release_id.clone(),
                composition_revision: client.revision,
                event_type: event_type(error).into(),
                spec_function,
                implementation_id,
                message_code: Some(classify_error(error).into()),
                recent_calls: zipdiv::calls_for_event(&engine.recent_calls(), false),
            };
            let _ = client.report_event(&event);
        } else {
            execution?;
        }
    }
    result
}

fn classify_error(error: &zipdiv::ZipError) -> &'static str {
    match error {
        zipdiv::ZipError::CrcMismatch { .. } => "crc",
        zipdiv::ZipError::UnsafePath(_) => "unsafe_path",
        _ => "zip_error",
    }
}
fn event_type(error: &zipdiv::ZipError) -> &'static str {
    match error {
        zipdiv::ZipError::CrcMismatch { .. } => "integrity_failure",
        _ => "extraction_failure",
    }
}
fn tempfile_dir() -> zipdiv::Result<PathBuf> {
    let path = std::env::temp_dir().join(format!("zipdiv-verify-{}", std::process::id()));
    fs::create_dir_all(&path)?;
    Ok(path)
}
