use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process;

use alef::cli::{cache, commands, dispatch, pipeline};

use super::args::*;
use super::dispatch::DispatchContext;
use super::helpers::*;

pub(crate) fn handle(command: Commands, context: &DispatchContext) -> Result<Option<Commands>> {
    let config_path = &context.config_path;
    match command {
        Commands::Init { lang, format } => {
            eprintln!("Initializing alef project");
            if let Some(langs) = &lang {
                eprintln!("  Languages: {}", langs.join(", "));
            }
            pipeline::init(config_path, lang.clone())?;
            eprintln!("  Created alef.toml");

            // Load the generated config and bootstrap the project
            let (_workspace, resolved) = load_config(config_path)?;
            let resolved_cfg = &resolved[0];
            let languages = resolve_languages(resolved_cfg, lang.as_deref())?;
            let base_dir = std::env::current_dir()?;

            // Extract API surface
            let api = pipeline::extract(resolved_cfg, config_path, false)?;
            let sources_hash = cache::sources_hash(&resolved_cfg.sources)?;

            // Generate bindings
            eprintln!("  Generating bindings...");
            let bindings = pipeline::generate(&api, resolved_cfg, &languages, false)?;
            let mut binding_count: usize = 0;
            let mut all_paths = std::collections::HashSet::new();
            for (lang_key, lang_files) in &bindings {
                for file in lang_files {
                    all_paths.insert(base_dir.join(&file.path));
                }
                let single = vec![(*lang_key, lang_files.clone())];
                binding_count += pipeline::write_files(&single, &base_dir)?;
            }

            // Scaffold package manifests and lint configs
            eprintln!("  Generating scaffolding...");
            let scaffold_files = pipeline::scaffold(&api, resolved_cfg, &languages)?;
            let scaffold_count = pipeline::write_scaffold_files(&scaffold_files, &base_dir)?;
            for file in &scaffold_files {
                all_paths.insert(base_dir.join(&file.path));
            }

            // Format generated code only when --format is requested.
            if format {
                eprintln!("  Formatting...");
                pipeline::fmt_post_generate(resolved_cfg, &languages);
            }

            // Finalise per-file hashes after formatting.
            let alef_toml_bytes = cache::read_alef_toml_bytes(config_path);
            pipeline::finalize_hashes(&all_paths, &sources_hash, &alef_toml_bytes)?;

            println!("Initialized: {binding_count} binding files, {scaffold_count} scaffold files");
            Ok(None)
        }
        Commands::Schema {
            output,
            schema_version,
            check,
        } => {
            let version = schema_version.as_deref().unwrap_or(env!("CARGO_PKG_VERSION"));
            if check {
                alef::core::config::check_alef_config_schema(&output, version)?;
                println!("Schema is up to date: {}", output.display());
            } else {
                alef::core::config::write_alef_config_schema(&output, version)?;
                println!("Wrote schema to {}", output.display());
            }
            Ok(None)
        }
        Commands::Migrate { path, write } => {
            let migrate_path = path.unwrap_or_else(|| context.config_path.clone());
            let options = commands::migrate::MigrateOptions {
                path: migrate_path,
                write,
            };
            commands::migrate::run(options)?;
            Ok(None)
        }
        Commands::E2e { action } => {
            let (_workspace, resolved) = load_config(config_path)?;
            let crates_to_process = dispatch::select_crates(&resolved, &context.crate_filter)?;
            // E2e operates on per-crate e2e config. Use first crate that has an
            // e2e section, or error if none has one. For multi-crate workspaces,
            // all crates with an e2e section are processed in the loop below.
            // The action dispatch still uses the first crate's e2e config for
            // non-Generate actions (Init, Scaffold, List, Validate) since those
            // are fixture-directory operations.
            let resolved_cfg = crates_to_process
                .iter()
                .find(|c| c.e2e.is_some())
                .copied()
                .unwrap_or_else(|| crates_to_process[0]);
            let e2e_config = resolved_cfg.e2e.as_ref().context("no [e2e] section in alef.toml")?;
            match action {
                E2eAction::Generate { lang, registry, format } => {
                    if registry {
                        eprintln!(
                            "warning: `alef e2e generate --registry` is deprecated. \
                             Use `alef test-apps generate` instead. \
                             `alef e2e generate` is local-mode only."
                        );
                    }
                    let config_toml = std::fs::read_to_string(config_path)?;
                    let base_dir = std::env::current_dir()?;
                    let mut grand_count: usize = 0;
                    for e2e_crate in &crates_to_process {
                        let Some(this_e2e_config) = e2e_crate.e2e.as_ref() else {
                            continue;
                        };
                        let fixtures_dir = std::path::Path::new(&this_e2e_config.fixtures);
                        let fixture_hash = cache::hash_directory(fixtures_dir).unwrap_or_default();
                        let api = pipeline::extract(e2e_crate, config_path, false)?;
                        let ir_json = serde_json::to_string(&api)?;
                        let cache_key = if registry { "e2e-registry" } else { "e2e" };
                        let stage_hash = cache::compute_stage_hash(&ir_json, cache_key, &config_toml, &fixture_hash);
                        if cache::is_stage_cached(&e2e_crate.name, cache_key, &stage_hash) {
                            println!("E2E tests up to date (cached)");
                            continue;
                        }
                        // When --registry is set (deprecated path), clone the e2e config
                        // and switch to registry dependency mode.
                        let effective_e2e_config;
                        let e2e_ref = if registry {
                            let mut cloned = this_e2e_config.clone();
                            cloned.dep_mode = alef::core::config::e2e::DependencyMode::Registry;
                            effective_e2e_config = cloned;
                            eprintln!("Generating e2e test apps (registry mode)...");
                            &effective_e2e_config
                        } else {
                            eprintln!("Generating e2e test suites...");
                            this_e2e_config
                        };
                        let languages = lang.as_deref();
                        let files = alef::e2e::generate_e2e(e2e_crate, e2e_ref, languages, &api.types, &api.enums)?;
                        let sources_hash = cache::sources_hash(&e2e_crate.sources)?;
                        let alef_toml_bytes = cache::read_alef_toml_bytes(config_path);
                        let count = pipeline::write_scaffold_files_with_overwrite(&files, &base_dir, true)?;

                        if format {
                            alef::e2e::format::run_formatters(&files, e2e_ref);
                        }

                        let output_paths: Vec<PathBuf> = files.iter().map(|f| base_dir.join(&f.path)).collect();
                        let path_set: std::collections::HashSet<PathBuf> = output_paths.iter().cloned().collect();
                        pipeline::finalize_hashes(&path_set, &sources_hash, &alef_toml_bytes)?;

                        // Sweep orphan alef-generated files scoped to effective_output() of
                        // the current mode — local mode sweeps e2e/, registry mode sweeps
                        // test_apps/. This prevents each mode from deleting the other's files.
                        let e2e_output_root = base_dir.join(e2e_ref.effective_output());
                        let sweep_roots: Vec<PathBuf> = if lang.is_some() {
                            // Derive sweep roots from the top-level subdirectories of the
                            // e2e output root that appear in the generated file set.  Each
                            // generator writes into `<output>/<lang>/`, so taking the first
                            // two path components relative to the e2e root gives us the
                            // per-language directory.
                            let mut seen = std::collections::HashSet::new();
                            for path in &output_paths {
                                if let Ok(rel) = path.strip_prefix(&e2e_output_root) {
                                    if let Some(top) = rel.components().next() {
                                        let lang_dir = e2e_output_root.join(top.as_os_str());
                                        seen.insert(lang_dir);
                                    }
                                }
                            }
                            seen.into_iter().collect()
                        } else {
                            vec![e2e_output_root]
                        };
                        pipeline::sweep_orphans(&sweep_roots, &path_set)?;

                        cache::write_stage_hash(&e2e_crate.name, cache_key, &stage_hash, &output_paths)?;
                        grand_count += count;
                    }
                    println!("Generated {grand_count} e2e files");
                    Ok(None)
                }
                E2eAction::Init => {
                    eprintln!("Initializing e2e fixtures directory...");
                    let created = alef::e2e::scaffold::init_fixtures(e2e_config, resolved_cfg)?;
                    for path in &created {
                        println!("  created {path}");
                    }
                    println!("Initialized {} file(s)", created.len());
                    Ok(None)
                }
                E2eAction::Scaffold {
                    id,
                    category,
                    description,
                } => {
                    let path =
                        alef::e2e::scaffold::scaffold_fixture(e2e_config, resolved_cfg, &id, &category, &description)?;
                    println!("Created {path}");
                    Ok(None)
                }
                E2eAction::List => {
                    let fixtures_dir = std::path::Path::new(&e2e_config.fixtures);
                    let fixtures = alef::e2e::fixture::load_fixtures(fixtures_dir)
                        .with_context(|| format!("failed to load fixtures from {}", fixtures_dir.display()))?;
                    let groups = alef::e2e::fixture::group_fixtures(&fixtures);

                    println!("Fixtures: {} total", fixtures.len());
                    for group in &groups {
                        println!("  {}: {} fixture(s)", group.category, group.fixtures.len());
                    }
                    Ok(None)
                }
                E2eAction::Validate => {
                    let fixtures_dir = std::path::Path::new(&e2e_config.fixtures);
                    eprintln!("Validating fixtures in {}...", fixtures_dir.display());

                    // Schema validation
                    let mut all_errors = alef::e2e::validate::validate_fixtures(fixtures_dir)
                        .with_context(|| format!("failed to validate fixtures from {}", fixtures_dir.display()))?;

                    // Semantic validation
                    let fixtures = alef::e2e::fixture::load_fixtures(fixtures_dir)
                        .with_context(|| format!("failed to load fixtures from {}", fixtures_dir.display()))?;
                    let semantic_errors =
                        alef::e2e::validate::validate_fixtures_semantic(&fixtures, e2e_config, &e2e_config.languages);
                    all_errors.extend(semantic_errors);

                    if all_errors.is_empty() {
                        println!("All fixtures are valid.");
                        Ok(None)
                    } else {
                        use alef::e2e::validate::Severity;
                        let error_count = all_errors.iter().filter(|e| e.severity == Severity::Error).count();
                        let warning_count = all_errors.iter().filter(|e| e.severity == Severity::Warning).count();
                        println!("Found {} error(s) and {} warning(s):", error_count, warning_count);
                        for err in &all_errors {
                            println!("  {err}");
                        }
                        if error_count > 0 {
                            process::exit(1);
                        }
                        Ok(None)
                    }
                }
            }
        }
        Commands::TestApps { action } => {
            let (_workspace, resolved) = load_config(config_path)?;
            let crates_to_process = dispatch::select_crates(&resolved, &context.crate_filter)?;
            let _resolved_cfg = crates_to_process
                .iter()
                .find(|c| c.e2e.is_some())
                .copied()
                .unwrap_or_else(|| crates_to_process[0]);
            // Validate that at least one crate has an [e2e] section.
            let _ = _resolved_cfg.e2e.as_ref().context("no [e2e] section in alef.toml")?;
            match action {
                TestAppsAction::Generate {
                    lang,
                    clean,
                    format,
                    jobs: _,
                } => {
                    let config_toml = std::fs::read_to_string(config_path)?;
                    let base_dir = std::env::current_dir()?;
                    let mut grand_count: usize = 0;
                    for e2e_crate in &crates_to_process {
                        let Some(this_e2e_config) = e2e_crate.e2e.as_ref() else {
                            continue;
                        };

                        // Build a registry-mode clone of the e2e config.
                        let mut registry_config = this_e2e_config.clone();
                        registry_config.dep_mode = alef::core::config::e2e::DependencyMode::Registry;
                        let e2e_ref = &registry_config;
                        let output_root = base_dir.join(e2e_ref.effective_output());

                        // --clean: delete the per-language directories before regen.
                        // Preserve lock files (go.sum, go.mod is regenerated, etc.) — generators
                        // should not own dependency lock files.
                        if clean {
                            let langs_to_clean: Vec<String> = lang
                                .as_deref()
                                .map(|ls| ls.iter().map(|s| s.to_string()).collect())
                                .unwrap_or_else(|| e2e_ref.languages.clone());
                            let lock_files = [
                                "go.sum",
                                "go.mod",
                                "package-lock.json",
                                "pnpm-lock.yaml",
                                "yarn.lock",
                                "Gemfile.lock",
                                "composer.lock",
                                "uv.lock",
                                "pubspec.lock",
                            ];
                            for lang_name in &langs_to_clean {
                                let lang_dir = output_root.join(lang_name);
                                if lang_dir.exists() {
                                    // Save lock files before deletion
                                    let mut saved_locks = std::collections::HashMap::new();
                                    for lock_file in &lock_files {
                                        let lock_path = lang_dir.join(lock_file);
                                        if lock_path.exists() {
                                            if let Ok(content) = std::fs::read(&lock_path) {
                                                saved_locks.insert(lock_path.clone(), content);
                                            }
                                        }
                                    }

                                    std::fs::remove_dir_all(&lang_dir)
                                        .with_context(|| format!("failed to remove {}", lang_dir.display()))?;

                                    // Restore lock files after deletion
                                    std::fs::create_dir_all(&lang_dir)
                                        .with_context(|| format!("failed to recreate {}", lang_dir.display()))?;
                                    for (lock_path, content) in saved_locks {
                                        std::fs::write(&lock_path, content).with_context(|| {
                                            format!("failed to restore lock file {}", lock_path.display())
                                        })?;
                                    }
                                }
                            }
                        }

                        let fixtures_dir = std::path::Path::new(&this_e2e_config.fixtures);
                        let fixture_hash = cache::hash_directory(fixtures_dir).unwrap_or_default();
                        let api = pipeline::extract(e2e_crate, config_path, false)?;
                        let ir_json = serde_json::to_string(&api)?;
                        let cache_key = "test-apps";
                        let stage_hash = cache::compute_stage_hash(&ir_json, cache_key, &config_toml, &fixture_hash);
                        if !clean && cache::is_stage_cached(&e2e_crate.name, cache_key, &stage_hash) {
                            println!("Test apps up to date (cached)");
                            continue;
                        }

                        eprintln!("Generating registry-mode test apps...");
                        let languages = lang.as_deref();
                        let files = alef::e2e::generate_e2e(e2e_crate, e2e_ref, languages, &api.types, &api.enums)?;
                        let sources_hash = cache::sources_hash(&e2e_crate.sources)?;
                        let alef_toml_bytes = cache::read_alef_toml_bytes(config_path);
                        let count = pipeline::write_scaffold_files_with_overwrite(&files, &base_dir, true)?;

                        if format {
                            alef::e2e::format::run_formatters(&files, e2e_ref);
                        }

                        let output_paths: Vec<PathBuf> = files.iter().map(|f| base_dir.join(&f.path)).collect();
                        let path_set: std::collections::HashSet<PathBuf> = output_paths.iter().cloned().collect();
                        pipeline::finalize_hashes(&path_set, &sources_hash, &alef_toml_bytes)?;

                        // Sweep orphans scoped to test_apps/ only — never touches e2e/.
                        let sweep_roots: Vec<PathBuf> = if lang.is_some() {
                            let mut seen = std::collections::HashSet::new();
                            for path in &output_paths {
                                if let Ok(rel) = path.strip_prefix(&output_root) {
                                    if let Some(top) = rel.components().next() {
                                        seen.insert(output_root.join(top.as_os_str()));
                                    }
                                }
                            }
                            seen.into_iter().collect()
                        } else {
                            vec![output_root]
                        };
                        pipeline::sweep_orphans(&sweep_roots, &path_set)?;

                        cache::write_stage_hash(&e2e_crate.name, cache_key, &stage_hash, &output_paths)?;
                        grand_count += count;
                    }
                    println!("Generated {grand_count} test-app files");
                    Ok(None)
                }
                TestAppsAction::Run { lang } => {
                    for e2e_crate in &crates_to_process {
                        let Some(this_e2e_config) = e2e_crate.e2e.as_ref() else {
                            continue;
                        };
                        let all_names: Vec<String> = if this_e2e_config.languages.is_empty() {
                            alef::e2e::default_e2e_languages(&e2e_crate.languages)
                        } else {
                            this_e2e_config.languages.clone()
                        };
                        let names: Vec<String> = match lang.as_deref() {
                            Some(filter) => all_names
                                .into_iter()
                                .filter(|n| filter.iter().any(|f| f == n))
                                .collect(),
                            None => all_names,
                        };
                        if names.is_empty() {
                            continue;
                        }
                        eprintln!("Running test apps for: {}", names.join(", "));
                        pipeline::test_apps_run(e2e_crate, &names)?;
                    }
                    Ok(None)
                }
            }
        }
        other => Ok(Some(other)),
    }
}
