use dioprism_shader::core::{
    DEFAULT_SHADER_RUNTIME_PATH, DEFAULT_SHADER_WORKER_PATH, FirstPaint, SHADERGRADIENT_PRESETS,
    ShaderBaselineReport, ShaderCacheBackend, ShaderExplainReport, ShaderFirstPaintBudget,
    ShaderIslandConfig, ShaderManifestFragment, ShaderPreset, ShaderPresetProfile,
    ShaderRuntimeEmissionPolicy, ShaderSerializationFormat, Upgrade, assert_lighthouse_safe_html,
    explain_shader_config, shader_baseline_fixture_report, shader_cached_manifest_json,
    shader_manifest_fragment, svg_poster,
};
use std::fs;
use std::path::PathBuf;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CliOutput {
    Html,
    Report,
    Manifest,
    Explain,
    Poster,
}

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 id = "shader-island".to_string();
    let mut label = "dioprism-shader".to_string();
    let mut preset = ShaderPreset::Halo;
    let mut first_paint = FirstPaint::InlineCss;
    let mut profile: Option<ShaderPresetProfile> = None;
    let mut assert_safe: Option<PathBuf> = None;
    let mut output = CliOutput::Html;
    let mut budget = ShaderFirstPaintBudget::default();
    let mut args = args.into_iter();

    while let Some(arg) = args.next() {
        match arg.as_str() {
            "report" | "r" | "--report" | "--budget" => output = CliOutput::Report,
            "manifest" | "--manifest" => output = CliOutput::Manifest,
            "explain" | "--explain" => output = CliOutput::Explain,
            "poster" | "--poster" => output = CliOutput::Poster,
            "--id" => {
                id = args
                    .next()
                    .ok_or_else(|| "--id requires a value".to_string())?
            }
            "--label" => {
                label = args
                    .next()
                    .ok_or_else(|| "--label requires a value".to_string())?
            }
            "--preset" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--preset requires a value".to_string())?;
                preset = ShaderPreset::parse(&value)
                    .ok_or_else(|| format!("unknown preset '{value}'"))?;
            }
            "--profile" => {
                let value = args
                    .next()
                    .ok_or_else(|| "--profile requires a value".to_string())?;
                profile = Some(parse_profile(&value)?);
            }
            "--first-paint" | "--fp" => {
                let value = args.next().ok_or_else(|| {
                    "--first-paint requires inline-css, inline-svg-poster, inline-live-canvas, or inline-live-gpu-canvas"
                        .to_string()
                })?;
                first_paint = match value.as_str() {
                    "inline-css" => FirstPaint::InlineCss,
                    "inline-svg-poster" => FirstPaint::InlineSvgPoster,
                    "inline-live-canvas" => FirstPaint::InlineLiveCanvas,
                    "inline-live-gpu-canvas" => FirstPaint::InlineLiveGpuCanvas,
                    _ => return Err(format!("unknown first paint mode '{value}'")),
                };
            }
            "--assert-safe" | "--safe" => {
                assert_safe =
                    Some(PathBuf::from(args.next().ok_or_else(|| {
                        "--assert-safe requires an HTML path".to_string()
                    })?));
            }
            "--max-html-bytes" => {
                budget.max_static_html_bytes = parse_usize_arg(&mut args, "--max-html-bytes")?;
            }
            "--max-config-bytes" => {
                budget.max_runtime_config_bytes = parse_usize_arg(&mut args, "--max-config-bytes")?;
            }
            "--max-fps" => {
                let value = parse_usize_arg(&mut args, "--max-fps")?;
                budget.max_fps_cap = u16::try_from(value)
                    .map_err(|_| "--max-fps is too large for u16".to_string())?;
            }
            "--strict-lcp" => {
                budget = ShaderFirstPaintBudget::strict_lcp();
            }
            "--help" | "-h" => {
                print_help();
                return Ok(());
            }
            other => return Err(format!("unknown argument '{other}'")),
        }
    }

    if let Some(path) = assert_safe {
        let html = fs::read_to_string(&path)
            .map_err(|error| format!("failed to read {}: {error}", path.display()))?;
        assert_lighthouse_safe_html(&html)
            .map_err(|violations| format!("{} failed: {violations:?}", path.display()))?;
        println!("{} is dioprism-shader lighthouse-safe", path.display());
        return Ok(());
    }

    let mut config = ShaderIslandConfig::new(id)
        .with_preset(preset)
        .with_first_paint(first_paint)
        .with_upgrade(Upgrade::FirstInteraction);
    if let Some(profile) = profile {
        config = config.with_profile(profile).with_preset(preset);
    }

    match output {
        CliOutput::Html => println!("{}", config.static_html(label)),
        CliOutput::Report => println!("{}", shader_budget_report(&config, &label, &budget)),
        CliOutput::Manifest => {
            let manifest = shader_manifest_fragment(
                [&config],
                std::iter::empty::<&dioprism_shader::core::ShaderGalleryConfig>(),
            );
            println!(
                "{}",
                manifest
                    .to_json(ShaderSerializationFormat::PrettyJson)
                    .map_err(|error| error.to_string())?
            );
        }
        CliOutput::Explain => {
            let explain = explain_shader_config(&config, &ShaderRuntimeEmissionPolicy::default());
            println!("{}", shader_explain_report(&explain));
        }
        CliOutput::Poster => println!("{}", shader_poster_asset(&config)),
    }
    Ok(())
}

pub fn shader_poster_asset(config: &ShaderIslandConfig) -> String {
    svg_poster(config.preset)
}

pub fn shader_cli_poster_asset(config: &ShaderIslandConfig) -> String {
    shader_poster_asset(config)
}

pub fn shader_cli_baseline_report() -> ShaderBaselineReport {
    shader_baseline_fixture_report()
}

pub fn shader_cli_manifest(config: &ShaderIslandConfig) -> ShaderManifestFragment {
    shader_manifest_fragment(
        [config],
        std::iter::empty::<&dioprism_shader::core::ShaderGalleryConfig>(),
    )
}

pub fn shader_cli_manifest_json(
    config: &ShaderIslandConfig,
    format: ShaderSerializationFormat,
) -> Result<String, String> {
    shader_cli_manifest(config)
        .to_json(format)
        .map_err(|error| error.to_string())
}

pub fn shader_cli_cached_manifest_json(
    config: &ShaderIslandConfig,
    cache: &mut impl ShaderCacheBackend,
) -> Result<String, String> {
    let manifest = shader_cli_manifest(config);
    shader_cached_manifest_json(
        &manifest,
        ShaderSerializationFormat::StableJson,
        cache,
        |_| {},
    )
    .map_err(|error| error.to_string())
}

pub fn shader_cli_explain(
    config: &ShaderIslandConfig,
    policy: &ShaderRuntimeEmissionPolicy,
) -> ShaderExplainReport {
    explain_shader_config(config, policy)
}

pub fn shader_cli_explain_report(
    config: &ShaderIslandConfig,
    policy: &ShaderRuntimeEmissionPolicy,
) -> String {
    shader_explain_report(&shader_cli_explain(config, policy))
}

pub fn shader_cli_budget_report(
    config: &ShaderIslandConfig,
    label: &str,
    budget: &ShaderFirstPaintBudget,
) -> String {
    shader_budget_report(config, label, budget)
}

pub fn shader_budget_report(
    config: &ShaderIslandConfig,
    label: &str,
    budget: &ShaderFirstPaintBudget,
) -> String {
    let compact = config.compact_runtime_config();
    let html = config.static_html(label);
    let budget_report = config.first_paint_budget_report(label, budget);
    let presets = SHADERGRADIENT_PRESETS
        .iter()
        .map(|preset| preset.as_str())
        .collect::<Vec<_>>()
        .join(",");

    format!(
        concat!(
            "Dioxus Shader budget report\n",
            "preset: {preset}\n",
            "presetLabel: {preset_label}\n",
            "availablePresets: {preset_count}\n",
            "presetSummary: {presets}\n",
            "firstPaint: {first_paint}\n",
            "staticHtmlBytes: {html_bytes}\n",
            "compactRuntimeConfigBytes: {config_bytes}\n",
            "budgetPassed: {budget_passed}\n",
            "budgetViolations: {budget_violations}\n",
            "runtimePath: {runtime_path}\n",
            "workerPath: {worker_path}"
        ),
        preset = config.preset.as_str(),
        preset_label = config.preset.label(),
        preset_count = SHADERGRADIENT_PRESETS.len(),
        presets = presets,
        first_paint = match config.first_paint {
            FirstPaint::InlineCss => "inline-css",
            FirstPaint::InlineSvgPoster => "inline-svg-poster",
            FirstPaint::InlineLiveCanvas => "inline-live-canvas",
            FirstPaint::InlineLiveGpuCanvas => "inline-live-gpu-canvas",
        },
        html_bytes = html.len(),
        config_bytes = compact.len(),
        budget_passed = budget_report.passed,
        budget_violations = budget_report.violations.len(),
        runtime_path = DEFAULT_SHADER_RUNTIME_PATH,
        worker_path = DEFAULT_SHADER_WORKER_PATH
    )
}

fn shader_explain_report(report: &dioprism_shader::core::ShaderExplainReport) -> String {
    format!(
        concat!(
            "Dioxus Shader explainer\n",
            "id: {id}\n",
            "preset: {preset}\n",
            "profile: {profile}\n",
            "renderer: {renderer}\n",
            "requiresRuntime: {requires_runtime}\n",
            "requiresWasm: {requires_wasm}\n",
            "emissionReason: {reason}\n",
            "diagnostics: {diagnostics}"
        ),
        id = report.id,
        preset = report.preset.as_str(),
        profile = report.profile.as_attr(),
        renderer = report.renderer.as_attr(),
        requires_runtime = report.requires_runtime,
        requires_wasm = report.requires_wasm,
        reason = report.emission.reason,
        diagnostics = report.diagnostics.len()
    )
}

fn parse_profile(value: &str) -> Result<ShaderPresetProfile, String> {
    match value {
        "conservative" => Ok(ShaderPresetProfile::Conservative),
        "balanced" => Ok(ShaderPresetProfile::Balanced),
        "aggressive" => Ok(ShaderPresetProfile::Aggressive),
        _ => Err(format!("unknown profile '{value}'")),
    }
}

fn parse_usize_arg<I>(args: &mut I, name: &str) -> Result<usize, String>
where
    I: Iterator<Item = String>,
{
    let value = args
        .next()
        .ok_or_else(|| format!("{name} requires a value"))?;
    value
        .parse::<usize>()
        .map_err(|error| format!("{name} expected an unsigned integer: {error}"))
}

fn print_help() {
    println!(
        "Usage: dioprism-shader [report|manifest|explain|poster] [--id hero] [--profile conservative|balanced|aggressive] [--preset halo|pensive|mint|interstella|nighty-night|viola|universe|sunset|mandarin|cotton-candy|glass-blue|glass-magenta|loop-demo] [--first-paint|--fp inline-css|inline-svg-poster|inline-live-canvas|inline-live-gpu-canvas] [--strict-lcp]\n       dioprism-shader --assert-safe|--safe path/to/page.html"
    );
}

#[cfg(test)]
mod tests {
    use super::{
        ShaderIslandConfig, run, shader_cli_baseline_report, shader_cli_budget_report,
        shader_cli_cached_manifest_json, shader_cli_explain, shader_cli_explain_report,
        shader_cli_manifest, shader_cli_manifest_json, shader_cli_poster_asset,
    };

    #[test]
    fn prints_default_island() {
        run(Vec::<String>::new()).expect("default island prints");
    }

    #[test]
    fn prints_budget_report() {
        run(vec![
            "report".to_string(),
            "--preset".to_string(),
            "mint".to_string(),
            "--first-paint".to_string(),
            "inline-svg-poster".to_string(),
        ])
        .expect("budget report prints");
    }

    #[test]
    fn prints_manifest_explain_and_poster() {
        run(vec![
            "manifest".to_string(),
            "--profile".to_string(),
            "balanced".to_string(),
        ])
        .expect("manifest prints");
        run(vec![
            "explain".to_string(),
            "--preset".to_string(),
            "universe".to_string(),
        ])
        .expect("explain prints");
        run(vec![
            "poster".to_string(),
            "--preset".to_string(),
            "halo".to_string(),
        ])
        .expect("poster prints");
    }

    #[test]
    fn exposes_cache_and_baseline_helpers_for_tooling() {
        let report = shader_cli_baseline_report();
        assert!(report.output_bytes > 0);
        assert!(report.operation_count > 0);

        let config = ShaderIslandConfig::new("cli-cache");
        let manifest = shader_cli_manifest(&config);
        assert_eq!(manifest.records.len(), 1);
        assert!(
            shader_cli_manifest_json(
                &config,
                dioprism_shader::core::ShaderSerializationFormat::StableJson,
            )
            .unwrap()
            .contains("dioprism-shader")
        );
        let explain = shader_cli_explain(
            &config,
            &dioprism_shader::core::ShaderRuntimeEmissionPolicy::default(),
        );
        assert_eq!(explain.id, "cli-cache");
        assert!(
            shader_cli_explain_report(
                &config,
                &dioprism_shader::core::ShaderRuntimeEmissionPolicy::default(),
            )
            .contains("Dioxus Shader explainer")
        );
        assert!(
            shader_cli_budget_report(
                &config,
                "CLI",
                &dioprism_shader::core::ShaderFirstPaintBudget::default(),
            )
            .contains("Dioxus Shader budget report")
        );
        assert!(shader_cli_poster_asset(&config).contains("<svg"));
        let mut cache = dioprism_shader::core::ShaderMemoryCache::new();
        let first = shader_cli_cached_manifest_json(&config, &mut cache).expect("manifest json");
        let second = shader_cli_cached_manifest_json(&config, &mut cache).expect("cached json");
        assert_eq!(first, second);
        assert!(first.contains("dioprism-shader"));
    }
}
