use dioprism_resume::{
    ResumeBudgetLimit, ResumeDiagnosticSeverity, ResumeIntegrationOptions, ResumeManifest,
    ResumeSerializationFormat, batch_serialize_resume_fragments, explain_resume_integration,
    measure_resume_baseline, parallel_batch_serialize_resume_fragments, parse_handler_asset_spec,
    render_resume_explain, resume_artifact_cache_key, resume_compact_dictionary, resume_doctor,
    resume_manifest_fragment, resume_workertown_offload_tasks, serialize_resume_manifest,
    trace_resume_integration,
};
use std::collections::BTreeSet;
use std::fs;
use std::path::PathBuf;

pub fn run_from<I, S>(args: I) -> Result<(), String>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    run(args.into_iter().map(Into::into))
}

fn run<I>(args: I) -> Result<(), String>
where
    I: IntoIterator<Item = String>,
{
    let mut output: Option<PathBuf> = None;
    let mut mode = OutputMode::Manifest;
    let mut format = ResumeSerializationFormat::Pretty;
    let mut handlers = Vec::new();
    let mut prefetch = BTreeSet::new();
    let mut integration = ResumeIntegrationOptions::default();
    let mut budget = ResumeBudgetLimit::default();
    let mut parallel = false;
    let mut args = args.into_iter();

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "report" | "r" | "--report" => {
                mode = OutputMode::BudgetReport;
            }
            "explain" | "--explain" => {
                mode = OutputMode::Explain;
            }
            "doctor" | "--doctor" => {
                mode = OutputMode::Doctor;
            }
            "fragment" | "--fragment" => {
                mode = OutputMode::Fragment;
            }
            "baseline" | "--baseline" => {
                mode = OutputMode::Baseline;
            }
            "cache-key" | "--cache-key" => {
                mode = OutputMode::CacheKey;
            }
            "dictionary" | "compact-dictionary" | "--dictionary" => {
                mode = OutputMode::Dictionary;
            }
            "offload" | "--offload" => {
                mode = OutputMode::Offload;
            }
            "trace" | "--trace" => {
                mode = OutputMode::Trace;
            }
            "--parallel" => {
                parallel = true;
            }
            "--format" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--format requires stable, compact, or pretty".to_string())?;
                format = parse_format(&value)?;
            }
            "--out" => {
                output = args.next().map(PathBuf::from);
                if output.is_none() {
                    return Err("--out requires a path".to_string());
                }
            }
            "--handler" => {
                let handler = args
                    .next()
                    .ok_or_else(|| "--handler requires id=module#symbol".to_string())?;
                handlers.push(handler);
            }
            "--prefetch" => {
                let id = args
                    .next()
                    .ok_or_else(|| "--prefetch requires a handler id".to_string())?;
                prefetch.insert(id);
            }
            "--route" => {
                integration.route = Some(
                    args.next()
                        .ok_or_else(|| "--route requires a route path".to_string())?,
                );
            }
            "--label" => {
                let label = args
                    .next()
                    .ok_or_else(|| "--label requires a label".to_string())?;
                integration = integration.with_label(label);
            }
            "--tag" => {
                let tag = args
                    .next()
                    .ok_or_else(|| "--tag requires key=value".to_string())?;
                let (key, value) = tag
                    .split_once('=')
                    .ok_or_else(|| "--tag requires key=value".to_string())?;
                integration = integration.with_tag(key, value);
            }
            "--budget-handlers" => {
                budget =
                    budget.with_max_handlers(parse_usize_flag("--budget-handlers", args.next())?);
            }
            "--budget-stable-bytes" => {
                budget = budget.with_max_stable_manifest_bytes(parse_usize_flag(
                    "--budget-stable-bytes",
                    args.next(),
                )?);
            }
            "--budget-compact-bytes" => {
                budget = budget.with_max_compact_manifest_bytes(parse_usize_flag(
                    "--budget-compact-bytes",
                    args.next(),
                )?);
            }
            "--help" | "-h" => {
                print_help();
                return Ok(());
            }
            other => {
                return Err(format!("unknown argument '{other}'"));
            }
        }
    }
    integration = integration.with_budget(budget);

    let mut manifest = ResumeManifest::default();
    for spec in handlers {
        let (id, mut asset) = parse_handler_asset_spec(&spec).map_err(|error| error.to_string())?;
        asset.prefetch = prefetch.contains(&id);
        manifest.insert(id, asset);
    }

    let output_text = match mode {
        OutputMode::Manifest => serialize_resume_manifest(&manifest, format)
            .map_err(|error| format!("failed to serialize manifest: {error}"))?,
        OutputMode::BudgetReport => {
            let json = manifest
                .to_stable_json()
                .map_err(|error| format!("failed to serialize manifest: {error}"))?;
            resume_budget_report(&manifest, &json)?
        }
        OutputMode::Explain => {
            let report = explain_resume_integration(&manifest, None, &integration)
                .map_err(|error| format!("failed to explain manifest: {error}"))?;
            render_resume_explain(&report)
        }
        OutputMode::Doctor => {
            let report = resume_doctor(&manifest, None, &integration)
                .map_err(|error| format!("failed to run doctor: {error}"))?;
            render_doctor_report(&report)
        }
        OutputMode::Fragment => {
            let fragment = resume_manifest_fragment(&manifest, None, &integration);
            if parallel {
                parallel_batch_serialize_resume_fragments(vec![fragment], format)
                    .map_err(|error| format!("failed to serialize fragment: {error}"))?
            } else {
                batch_serialize_resume_fragments([fragment], format)
                    .map_err(|error| format!("failed to serialize fragment: {error}"))?
            }
        }
        OutputMode::Baseline => {
            let report = measure_resume_baseline(&manifest, None, &integration)
                .map_err(|error| format!("failed to measure baseline: {error}"))?;
            render_baseline_report(&report)
        }
        OutputMode::CacheKey => resume_artifact_cache_key(&manifest, None, &integration),
        OutputMode::Dictionary => {
            serde_json::to_string_pretty(&resume_compact_dictionary(&manifest, None))
                .map_err(|error| format!("failed to serialize compact dictionary: {error}"))?
        }
        OutputMode::Offload => serde_json::to_string_pretty(&resume_workertown_offload_tasks(
            &manifest,
            None,
            &integration,
        ))
        .map_err(|error| format!("failed to serialize offload tasks: {error}"))?,
        OutputMode::Trace => {
            let report = explain_resume_integration(&manifest, None, &integration)
                .map_err(|error| format!("failed to trace manifest: {error}"))?;
            let mut lines = Vec::new();
            trace_resume_integration(&report, |event| {
                lines.push(format!(
                    "{} {} {}",
                    event.span,
                    event.route.as_deref().unwrap_or("unscoped"),
                    event.detail
                ));
            });
            lines.join("\n")
        }
    };

    if let Some(output) = output {
        if let Some(parent) = output.parent()
            && !parent.as_os_str().is_empty()
        {
            fs::create_dir_all(parent)
                .map_err(|error| format!("failed to create {}: {error}", parent.display()))?;
        }
        fs::write(&output, output_text)
            .map_err(|error| format!("failed to write {}: {error}", output.display()))?;
    } else {
        println!("{output_text}");
    }

    Ok(())
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum OutputMode {
    Manifest,
    BudgetReport,
    Explain,
    Doctor,
    Fragment,
    Baseline,
    CacheKey,
    Dictionary,
    Offload,
    Trace,
}

fn parse_format(value: &str) -> Result<ResumeSerializationFormat, String> {
    match value.trim().to_ascii_lowercase().as_str() {
        "stable" | "json" => Ok(ResumeSerializationFormat::Stable),
        "compact" | "runtime" => Ok(ResumeSerializationFormat::Compact),
        "pretty" | "pretty-json" => Ok(ResumeSerializationFormat::Pretty),
        other => Err(format!("unknown --format '{other}'")),
    }
}

fn parse_usize_flag(flag: &str, value: Option<String>) -> Result<usize, String> {
    value
        .ok_or_else(|| format!("{flag} requires a number"))?
        .parse::<usize>()
        .map_err(|error| format!("{flag} requires a number: {error}"))
}

fn render_doctor_report(report: &dioprism_resume::ResumeDoctorReport) -> String {
    let mut output = format!("Dioxus Resume doctor\nok: {}", report.ok);
    for diagnostic in &report.diagnostics {
        let severity = match diagnostic.severity {
            ResumeDiagnosticSeverity::Info => "info",
            ResumeDiagnosticSeverity::Warning => "warning",
            ResumeDiagnosticSeverity::Error => "error",
        };
        output.push_str("\n");
        output.push_str(severity);
        output.push_str(": ");
        output.push_str(&diagnostic.code);
        output.push_str(" - ");
        output.push_str(&diagnostic.message);
        output.push_str(" | ");
        output.push_str(&diagnostic.remediation);
    }
    output
}

fn resume_budget_report(manifest: &ResumeManifest, stable_json: &str) -> Result<String, String> {
    let compact_json = manifest
        .to_compact_script_json()
        .map_err(|error| format!("failed to serialize compact manifest: {error}"))?;
    let mut modules = BTreeSet::new();
    let mut prefetched = 0usize;
    let mut classic_assets = 0usize;

    for asset in manifest.handlers.values() {
        modules.insert(asset.module.as_str());
        if asset.prefetch {
            prefetched += 1;
        }
        if asset.classic.is_some() {
            classic_assets += 1;
        }
    }

    let largest = manifest
        .handlers
        .iter()
        .max_by_key(|(id, asset)| id.len() + asset.module.len() + asset.symbol.len())
        .map(|(id, asset)| format!("{id} -> {}", asset.attr_value()))
        .unwrap_or_else(|| "none".to_string());

    Ok(format!(
        concat!(
            "Dioxus Resume budget report\n",
            "handlers: {handlers}\n",
            "uniqueModules: {modules}\n",
            "prefetchHandlers: {prefetched}\n",
            "classicFallbacks: {classic_assets}\n",
            "stableManifestBytes: {stable_bytes}\n",
            "compactRuntimeBytes: {compact_bytes}\n",
            "largestHandler: {largest}"
        ),
        handlers = manifest.handlers.len(),
        modules = modules.len(),
        prefetched = prefetched,
        classic_assets = classic_assets,
        stable_bytes = stable_json.len(),
        compact_bytes = compact_json.len(),
        largest = largest
    ))
}

fn render_baseline_report(report: &dioprism_resume::ResumeBaselineReport) -> String {
    format!(
        concat!(
            "Dioxus Resume baseline\n",
            "stableManifestBytes: {stable}\n",
            "compactManifestBytes: {compact}\n",
            "fragmentBytes: {fragment}\n",
            "dictionaryStrings: {strings}\n",
            "measuredMicros: {micros}"
        ),
        stable = report.stable_manifest_bytes,
        compact = report.compact_manifest_bytes,
        fragment = report.fragment_bytes,
        strings = report.dictionary_strings,
        micros = report.measured_micros
    )
}

fn print_help() {
    println!(
        "Usage: dxr-manifest [report|explain|doctor|fragment|baseline|cache-key|dictionary|offload|trace] [--parallel] [--format stable|compact|pretty] --handler id=/assets/chunk.js#symbol [--prefetch id] [--route /path] [--label name] [--tag key=value] [--out resume-manifest.json]"
    );
}

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

    #[test]
    fn accepts_empty_manifest() {
        run(Vec::<String>::new()).expect("empty manifest prints to stdout");
    }

    #[test]
    fn accepts_budget_report() {
        run(vec![
            "report".to_string(),
            "--handler".to_string(),
            "counter.increment=/assets/counter.js#increment".to_string(),
            "--prefetch".to_string(),
            "counter.increment".to_string(),
        ])
        .expect("budget report prints");
    }

    #[test]
    fn accepts_explain_and_fragment_modes() {
        run(vec![
            "explain".to_string(),
            "--route".to_string(),
            "/counter".to_string(),
            "--label".to_string(),
            "counter-route".to_string(),
            "--handler".to_string(),
            "counter.increment=/assets/counter.js#increment".to_string(),
        ])
        .expect("explain report prints");

        run(vec![
            "fragment".to_string(),
            "--format".to_string(),
            "compact".to_string(),
            "--tag".to_string(),
            "owner=resume".to_string(),
            "--handler".to_string(),
            "counter.increment=/assets/counter.js#increment".to_string(),
        ])
        .expect("fragment prints");
    }

    #[test]
    fn doctor_can_fail_budget_limits_without_panicking() {
        run(vec![
            "doctor".to_string(),
            "--budget-handlers".to_string(),
            "0".to_string(),
            "--handler".to_string(),
            "counter.increment=/assets/counter.js#increment".to_string(),
        ])
        .expect("doctor report prints budget failure");
    }

    #[test]
    fn accepts_cache_baseline_dictionary_offload_and_parallel_fragment_modes() {
        let handler = "counter.increment=/assets/counter.js#increment".to_string();
        run(vec![
            "baseline".to_string(),
            "--handler".to_string(),
            handler.clone(),
        ])
        .expect("baseline prints");

        run(vec![
            "cache-key".to_string(),
            "--route".to_string(),
            "/counter".to_string(),
            "--handler".to_string(),
            handler.clone(),
        ])
        .expect("cache key prints");

        run(vec![
            "dictionary".to_string(),
            "--handler".to_string(),
            handler.clone(),
        ])
        .expect("dictionary prints");

        run(vec![
            "offload".to_string(),
            "--handler".to_string(),
            handler.clone(),
        ])
        .expect("offload tasks print");

        run(vec![
            "trace".to_string(),
            "--route".to_string(),
            "/counter".to_string(),
            "--handler".to_string(),
            handler.clone(),
        ])
        .expect("trace spans print");

        run(vec![
            "fragment".to_string(),
            "--parallel".to_string(),
            "--format".to_string(),
            "stable".to_string(),
            "--handler".to_string(),
            handler,
        ])
        .expect("parallel fragment prints");
    }
}
