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

use anyhow::Result;
use clap::Args;
use serde::Serialize;
use tracing::{info, warn};

use rh_codegen::quality::run_quality_checks;
use rh_codegen::{
    generate_crate_readme, generate_crate_structure, generate_module_files, parse_package_metadata,
    CodeGenerator, CodegenConfig, CrateGenerationParams, QualityConfig,
};
use rh_foundation::loader::{LoaderConfig, PackageLoader};

use crate::output::{Envelope, OutputContext, OutputFormat};

#[derive(Serialize)]
struct CodegenManifest {
    package: String,
    version: String,
    crate_name: String,
    output_dir: String,
    files_generated: usize,
}

fn print_envelope<T: Serialize>(ctx: &OutputContext, envelope: &Envelope<T>) -> Result<()> {
    let json = if matches!(ctx.format, OutputFormat::Json) {
        serde_json::to_string_pretty(envelope)?
    } else {
        serde_json::to_string(envelope)?
    };
    println!("{json}");
    Ok(())
}

fn count_generated_files(path: &Path) -> Result<usize> {
    let mut count = 0usize;
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let entry_path = entry.path();
        if entry_path.is_dir() {
            count += count_generated_files(&entry_path)?;
        } else {
            count += 1;
        }
    }
    Ok(count)
}

fn resolve_generated_crate_name(output_path: &Path, fallback: &str) -> Result<String> {
    let cargo_toml = fs::read_to_string(output_path.join("Cargo.toml"))?;
    for line in cargo_toml.lines() {
        let trimmed = line.trim();
        if let Some(name) = trimmed.strip_prefix("name = ") {
            return Ok(name.trim_matches('"').to_string());
        }
    }
    Ok(fallback.to_string())
}

/// Check if a directory exists and is not empty
fn is_directory_non_empty(path: &Path) -> Result<bool> {
    if !path.exists() {
        return Ok(false);
    }

    if !path.is_dir() {
        return Ok(false);
    }

    let mut entries = fs::read_dir(path)?;
    Ok(entries.next().is_some())
}

#[derive(Args)]
pub struct CodegenArgs {
    /// Package name (e.g., "hl7.fhir.r4.core")
    pub package: String,
    /// Package version (e.g., "4.0.1"). If not provided, uses the latest version
    pub version: Option<String>,

    /// Output directory for generated crate
    #[arg(short, long, default_value = "./generated")]
    pub output: String,

    /// Package directory for downloaded packages (defaults to ~/.fhir/packages)
    #[arg(long)]
    pub package_dir: Option<String>,

    /// Registry URL
    #[arg(long, default_value = "https://packages.fhir.org")]
    pub registry: String,

    /// Overwrite package if it already exists
    #[arg(long)]
    pub overwrite: bool,

    /// Force overwrite of non-empty output directory
    #[arg(long)]
    pub force: bool,

    /// Override the generated crate name (e.g., "rh-hl7-fhir-r4-core").
    /// When not provided, the name is auto-derived from the FHIR package name.
    #[arg(long)]
    pub crate_name: Option<String>,

    /// Refresh README.md for an already generated crate without rewriting sources.
    #[arg(long, hide = true)]
    pub refresh_readme: bool,
}

pub async fn handle_command(args: CodegenArgs, ctx: &OutputContext) -> Result<()> {
    // Set up loader configuration first to resolve version if needed
    let token = std::env::var("RH_REGISTRY_TOKEN").ok();
    let loader_config = LoaderConfig {
        registry_url: args.registry.clone(),
        auth_token: token,
        timeout_seconds: 30,
        max_retries: 3,
        verify_checksums: false,
        overwrite_existing: args.overwrite,
    };

    let loader = PackageLoader::new(loader_config)?;

    // Resolve version - use provided version or get latest
    let version = match args.version {
        Some(v) => v,
        None => {
            info!(
                "No version specified, fetching latest version for {}",
                args.package
            );
            loader.get_latest_version(&args.package).await?
        }
    };

    info!(
        "Installing package {}@{} and generating organized crate",
        args.package, version
    );

    let output_path = Path::new(&args.output);

    // Check if output directory exists and is not empty before doing any work
    if !args.refresh_readme && is_directory_non_empty(output_path)? {
        if !args.force {
            return Err(anyhow::anyhow!(
                "Output directory '{}' is not empty. Use --force to overwrite existing files.",
                output_path.display()
            ));
        } else {
            warn!(
                "Will remove existing contents of output directory: {}",
                output_path.display()
            );
        }
    }

    // Set up package directory
    let pkg_dir = if let Some(dir) = args.package_dir {
        PathBuf::from(dir)
    } else {
        PackageLoader::get_default_packages_dir()
            .map_err(|e| anyhow::anyhow!("Failed to get default packages directory: {e}"))?
    };

    // Download package if not already downloaded
    if !loader.is_package_downloaded(&args.package, &version, &pkg_dir)? {
        loader
            .download_package(&args.package, &version, &pkg_dir)
            .await?;
    }

    // Use default configuration with optional crate name override
    let config = CodegenConfig {
        crate_name: args.crate_name.clone(),
        ..CodegenConfig::default()
    };

    // Set up package directory
    let package_dir = pkg_dir
        .join(format!("{}#{}", args.package, version))
        .join("package");

    let effective_package_dir = if package_dir.exists() {
        &package_dir
    } else {
        &pkg_dir
    };

    // Create generator with ValueSet directory
    let mut generator = CodeGenerator::new_with_value_set_directory(config, effective_package_dir);

    // Remove existing output directory if --force was specified and directory was non-empty.
    // Preserve hand-authored directories (tests/, examples/) that codegen doesn't touch.
    if !args.refresh_readme && args.force && is_directory_non_empty(output_path)? {
        info!(
            "Removing existing contents of output directory: {}",
            output_path.display()
        );
        if output_path.exists() {
            let preserved = ["tests", "examples", "benches"];
            for entry in fs::read_dir(output_path)? {
                let entry = entry?;
                let name = entry.file_name();
                if preserved.contains(&name.to_str().unwrap_or("")) {
                    continue; // keep hand-authored directories
                }
                if entry.path().is_dir() {
                    fs::remove_dir_all(entry.path())?;
                } else {
                    fs::remove_file(entry.path())?;
                }
            }
        }
    }

    // Create output directory
    fs::create_dir_all(output_path)?;

    // Generate complete crate structure metadata first
    let command_invoked = format!(
        "rh codegen {} {} --output {}{}{}{}{}",
        args.package,
        version,
        args.output,
        if args.registry != "https://packages.fhir.org" {
            format!(" --registry {}", args.registry)
        } else {
            String::new()
        },
        if args.overwrite { " --overwrite" } else { "" },
        if args.force { " --force" } else { "" },
        if let Some(cn) = &args.crate_name {
            format!(" --crate-name {cn}")
        } else {
            String::new()
        },
    );

    let (canonical, author, description) =
        parse_package_metadata(&effective_package_dir.join("package.json"))
            .map_err(|e| anyhow::anyhow!("Failed to parse package metadata: {e}"))?;

    info!("Using canonical URL from package.json: {}", canonical);

    if args.refresh_readme {
        generate_crate_readme(&CrateGenerationParams {
            output: output_path,
            package: &args.package,
            version: &version,
            canonical_url: &canonical,
            author: &author,
            description: &description,
            command_invoked: &command_invoked,
            crate_name: args.crate_name.as_deref(),
        })?;
        info!("Refreshed README.md at: {}", output_path.display());
        return Ok(());
    }

    // Generate crate structure (Cargo.toml, lib.rs, directory structure) BEFORE processing files
    generate_crate_structure(CrateGenerationParams {
        output: output_path,
        package: &args.package,
        version: &version,
        canonical_url: &canonical,
        author: &author,
        description: &description,
        command_invoked: &command_invoked,
        crate_name: args.crate_name.as_deref(),
    })?;

    info!("Generated initial crate structure with Cargo.toml and directories");

    // Pre-register all ValueSet enums before processing resources
    // This ensures correct import paths for enum types referenced by resources
    generator
        .pre_register_value_set_enums(effective_package_dir)
        .map_err(|e| anyhow::anyhow!("Failed to pre-register ValueSet enums: {e}"))?;

    info!("Pre-registered ValueSet enums for correct import path generation");

    // Process JSON files using organized structure (includes enum generation and Phase 3)
    process_json_files_organized(&mut generator, effective_package_dir, output_path)?;

    // Refresh README after generation so package statistics reflect the populated crate.
    generate_crate_readme(&CrateGenerationParams {
        output: output_path,
        package: &args.package,
        version: &version,
        canonical_url: &canonical,
        author: &author,
        description: &description,
        command_invoked: &command_invoked,
        crate_name: args.crate_name.as_deref(),
    })?;

    info!(
        "Successfully generated complete crate at: {}",
        output_path.display()
    );

    if ctx.is_json() {
        let crate_name_fallback = args
            .crate_name
            .clone()
            .unwrap_or_else(|| args.package.replace('.', "-"));
        let manifest = CodegenManifest {
            package: args.package,
            version,
            crate_name: resolve_generated_crate_name(output_path, &crate_name_fallback)?,
            output_dir: output_path.display().to_string(),
            files_generated: count_generated_files(output_path)?,
        };
        print_envelope(ctx, &Envelope::ok(manifest, "codegen"))?;
    }

    Ok(())
}

/// Process JSON files in a directory using organized structure
fn process_json_files_organized(
    generator: &mut CodeGenerator,
    input_dir: &Path,
    output_dir: &Path,
) -> Result<()> {
    let entries = fs::read_dir(input_dir)?;
    let mut structure_definitions = Vec::new();

    // Phase 1: Load all StructureDefinitions and register them in TypeRegistry
    info!("Phase 1: Loading and registering all StructureDefinitions...");
    for entry in entries {
        let entry = entry?;
        let path = entry.path();

        if path.is_file() && path.extension().is_some_and(|ext| ext == "json") {
            // Quick check for resourceType before attempting to load
            let content = match fs::read_to_string(&path) {
                Ok(content) => content,
                Err(e) => {
                    warn!("Failed to read {}: {}", path.display(), e);
                    continue;
                }
            };

            // Parse JSON to check resourceType
            let json_value: serde_json::Value = match serde_json::from_str(&content) {
                Ok(value) => value,
                Err(e) => {
                    warn!("Skipping {}: Invalid JSON - {}", path.display(), e);
                    continue;
                }
            };

            // Skip if resourceType is not "StructureDefinition"
            if json_value.get("resourceType").and_then(|v| v.as_str())
                != Some("StructureDefinition")
            {
                continue;
            }

            // Try to load StructureDefinition
            match generator.load_structure_definition(&path) {
                Ok(structure_def) => {
                    info!(
                        "Loaded StructureDefinition: {} ({})",
                        structure_def.name, structure_def.id
                    );

                    // Skip if status is retired
                    if structure_def.status == "retired" {
                        info!(
                            "Skipping retired StructureDefinition: {}",
                            structure_def.name
                        );
                        continue;
                    }

                    // Register in TypeRegistry for import path resolution
                    rh_codegen::generators::type_registry::TypeRegistry::register_from_structure_definition(&structure_def);
                    info!("Registered {} in TypeRegistry", structure_def.name);

                    structure_definitions.push(structure_def);
                }
                Err(e) => {
                    // This might not be a StructureDefinition file, so just skip it
                    warn!("Skipping {}: {}", path.display(), e);
                }
            }
        }
    }

    // Phase 1.5: Pre-generate base definitions (Element, BackboneElement, DomainResource, Resource)
    // to populate the type_cache before parallel generation, reducing lock contention
    info!("Pre-generating base definitions...");
    generator.pre_generate_base_definitions(&structure_definitions);

    // Phase 2: Generate code for all StructureDefinitions
    info!(
        "Phase 2: Generating code for {} StructureDefinitions (parallel)...",
        structure_definitions.len()
    );

    // Generate all structs in parallel using rayon
    let results = generator.generate_structs_parallel(&structure_definitions);
    let success_count = results.iter().filter(|(_, r)| r.is_ok()).count();
    let error_count = results.len() - success_count;
    for (name, result) in &results {
        match result {
            Ok(_) => info!("Generated {name}"),
            Err(e) => warn!("Failed to generate {name}: {e}"),
        }
    }
    info!("Phase 2 complete: {success_count} succeeded, {error_count} failed");

    // Write all generated structs to files
    info!("Writing generated struct files...");
    let write_results = generator.write_all_generated_files(&structure_definitions, output_dir);
    let write_success = write_results.iter().filter(|r| r.is_ok()).count();
    info!("Wrote {write_success} struct files");

    // Write all trait files for resources and profiles
    info!("Writing trait files...");
    let trait_results = generator.write_all_trait_files(&structure_definitions, output_dir);
    let trait_success = trait_results.iter().filter(|r| r.is_ok()).count();
    info!("Wrote {trait_success} trait files");

    // Generate ValueSet enums before finalizing module files
    info!("Generating ValueSet enums...");
    let bindings_dir = output_dir.join("src").join("bindings");
    generator
        .generate_enum_files(&bindings_dir)
        .map_err(|e| anyhow::anyhow!("Failed to generate enum files: {e}"))?;

    generator
        .generate_enums_mod_file(&bindings_dir)
        .map_err(|e| anyhow::anyhow!("Failed to generate enums mod file: {e}"))?;

    info!("Generated ValueSet enums to: {}", bindings_dir.display());

    // Phase 2.5: Generate metadata module (split by category for faster incremental compilation)
    info!("Generating metadata module...");
    let metadata_registry = rh_codegen::generators::build_metadata_registry(&structure_definitions);
    let metadata_files = rh_codegen::generators::generate_metadata_code_split(
        &metadata_registry,
        &structure_definitions,
    );
    let metadata_dir = output_dir.join("src").join("metadata");

    // Remove old single-file metadata.rs if it exists (migration from monolith to split)
    let old_metadata_file = output_dir.join("src").join("metadata.rs");
    if old_metadata_file.exists() {
        fs::remove_file(&old_metadata_file)
            .map_err(|e| anyhow::anyhow!("Failed to remove old metadata.rs: {e}"))?;
        info!("Removed old monolithic metadata.rs");
    }

    fs::create_dir_all(&metadata_dir)
        .map_err(|e| anyhow::anyhow!("Failed to create metadata directory: {e}"))?;
    for (filename, content) in &metadata_files {
        let file_path = metadata_dir.join(filename);
        fs::write(&file_path, content)
            .map_err(|e| anyhow::anyhow!("Failed to write metadata/{}: {e}", filename))?;
    }
    info!(
        "Generated {} metadata files to: {}",
        metadata_files.len(),
        metadata_dir.display()
    );

    // Phase 3: Finalize crate by regenerating all mod.rs files
    info!("Phase 3: Finalizing crate structure...");
    let src_dir = output_dir.join("src");
    let resource_dir = src_dir.join("resources");
    let datatypes_dir = src_dir.join("datatypes");
    let extensions_dir = src_dir.join("extensions");
    let primitives_dir = src_dir.join("primitives");
    let traits_dir = src_dir.join("traits");
    let bindings_dir = src_dir.join("bindings");
    let profiles_dir = src_dir.join("profiles");

    match generate_module_files(
        &resource_dir,
        &datatypes_dir,
        &extensions_dir,
        &primitives_dir,
        &traits_dir,
        &bindings_dir,
        &profiles_dir,
    ) {
        Ok(()) => {
            info!("Successfully regenerated all module files");
        }
        Err(e) => {
            warn!("Failed to regenerate module files: {}", e);
        }
    }

    // Run quality checks on the generated crate
    info!("Running quality checks on generated crate...");
    let quality_config = QualityConfig::default();
    match run_quality_checks(output_dir, &quality_config) {
        Ok(()) => {
            info!("✅ Quality checks completed successfully");
        }
        Err(e) => {
            warn!("⚠️  Quality checks completed with issues: {}", e);
        }
    }

    Ok(())
}
