use anyhow::{Context, Result, bail};
use base64::{Engine as _, engine::general_purpose};
use clap::{Arg, Command as ClapCommand};
use octocrab::Octocrab;
use sha2::{Digest, Sha256};
use std::env;
use std::path::Path;
use std::process::Command;
use tokio::fs;

#[derive(Debug, Clone)]
struct GitHubConfig {
    token: String,
    owner: String,
    repo: String,
}

impl GitHubConfig {
    fn from_env() -> Result<Self> {
        let token =
            env::var("GITHUB_TOKEN").context("GITHUB_TOKEN environment variable not set")?;
        let owner = env::var("GITHUB_OWNER").unwrap_or_else(|_| "HelixDB".to_string());
        let repo = env::var("GITHUB_REPO").unwrap_or_else(|_| "helix-db".to_string());

        Ok(GitHubConfig { token, owner, repo })
    }
}

fn generate_error_hash(error_type: &str, error_message: &str, _file_num: u32) -> String {
    let mut hasher = Sha256::new();
    hasher.update(format!(
        "{}:{}",
        error_type,
        error_message.lines().take(5).collect::<Vec<_>>().join("\n")
    ));
    let hash = hasher.finalize();
    general_purpose::STANDARD.encode(hash)[0..12].to_string()
}

async fn check_issue_exists(github_config: &GitHubConfig, error_hash: &str) -> Result<bool> {
    println!("DEBUG: Checking if issue exists with hash: {error_hash}");

    let octocrab = Octocrab::builder()
        .personal_token(github_config.token.clone())
        .build()?;

    let search_query = format!(
        "repo:{}/{} is:issue ERROR_HASH:{}",
        github_config.owner, github_config.repo, error_hash
    );

    println!("DEBUG: GitHub search query: {search_query}");

    let issues = octocrab
        .search()
        .issues_and_pull_requests(&search_query)
        .send()
        .await?;

    let count = issues.total_count.unwrap_or(0);
    println!("DEBUG: Found {count} existing issues");

    Ok(count > 0)
}

async fn create_github_issue(
    github_config: &GitHubConfig,
    error_type: &str,
    error_message: &str,
    file_num: u32,
    error_hash: &str,
    query: &str,
    schema: &str,
    generated_rust_code: &str,
) -> Result<()> {
    println!(
        "DEBUG: Creating GitHub issue for {}/{}",
        github_config.owner, github_config.repo
    );

    let octocrab = Octocrab::builder()
        .personal_token(github_config.token.clone())
        .build()?;

    let title = format!("Auto-generated: {error_type} Error in file{file_num}");

    let body = format!(
        "## Automatic Error Report\n\n\
        **Error Type:** {error_type}\n\
        **File:** file{file_num}\n\
        **Error Hash:** ERROR_HASH:{error_hash}\n\n\
        ### Query\n\
        ```js\n{query}\n```\n\n\
        ### Schema\n\
        ```js\n{schema}\n```\n\n\
        ### Generated Rust Code\n\
        ```rust\n{generated_rust_code}\n```\n\n\
        ### Error Details\n\
        ```\n{error_message}\n```\n\n\
        ---\n\
        *This issue was automatically generated by the hql-tests runner.*"
    );

    let labels = vec![
        "bug".to_string(),
        "automated".to_string(),
        "hql-tests".to_string(),
    ];

    println!("DEBUG: Issue title: {title}");
    println!("DEBUG: Issue body length: {} chars", body.len());
    println!("DEBUG: Issue labels: {labels:?}");

    let issue = octocrab
        .issues(&github_config.owner, &github_config.repo)
        .create(&title)
        .body(&body)
        .labels(Some(labels))
        .send()
        .await?;

    println!(
        "Created GitHub issue #{} for {} error in file{}",
        issue.number, error_type, file_num
    );
    Ok(())
}

async fn handle_error_with_github(
    github_config: &GitHubConfig,
    error_type: &str,
    error_message: &str,
    file_num: u32,
    query: &str,
    schema: &str,
    generated_rust_code: &str,
) -> Result<()> {
    let error_hash = generate_error_hash(error_type, error_message, file_num);

    println!(
        "DEBUG: Handling error with GitHub - Type: {error_type}, File: {file_num}, Hash: {error_hash}"
    );

    match check_issue_exists(github_config, &error_hash).await {
        Ok(exists) => {
            println!("DEBUG: Issue exists check result: {exists}");
            if !exists {
                println!("DEBUG: Creating new GitHub issue...");
                if let Err(e) = create_github_issue(
                    github_config,
                    error_type,
                    error_message,
                    file_num,
                    &error_hash,
                    query,
                    schema,
                    generated_rust_code,
                )
                .await
                {
                    eprintln!("Failed to create GitHub issue: {e}");
                }
            } else {
                println!(
                    "Issue already exists for {error_type} error in file{file_num} (hash: {error_hash})"
                );
            }
        }
        Err(e) => {
            eprintln!("Failed to check existing issues: {e}");
            // Try to create the issue anyway if we can't check for duplicates
            println!("DEBUG: Attempting to create issue despite check failure...");
            if let Err(e) = create_github_issue(
                github_config,
                error_type,
                error_message,
                file_num,
                &error_hash,
                query,
                schema,
                generated_rust_code,
            )
            .await
            {
                eprintln!("Failed to create GitHub issue: {e}");
            }
        }
    }

    Ok(())
}

#[tokio::main]
async fn main() -> Result<()> {
    let matches = ClapCommand::new("queries-test")
        .about("Process helix files")
        .arg(
            Arg::new("file_number")
                .help("File number to process (1-100)")
                .value_parser(clap::value_parser!(u32))
                .required(false),
        )
        .arg(
            Arg::new("batch")
                .long("batch")
                .help("Enable batch processing with total batches and current batch")
                .num_args(2)
                .value_names(["TOTAL_BATCHES", "CURRENT_BATCH"])
                .value_parser(clap::value_parser!(u32))
                .required(false),
        )
        .arg(
            Arg::new("branch")
                .long("branch")
                .help("Branch to process")
                .value_parser(clap::value_parser!(String))
                .required(false),
        )
        .get_matches();

    let current_dir = env::current_dir().context("Failed to get current directory")?;

    // Initialize GitHub configuration (optional - will print warning if not available)
    let github_config = match GitHubConfig::from_env() {
        Ok(config) => {
            println!(
                "GitHub integration enabled for {}/{}",
                config.owner, config.repo
            );
            Some(config)
        }
        Err(e) => {
            println!("GitHub integration disabled: {e}");
            println!("Set GITHUB_TOKEN environment variable to enable automatic issue creation");
            None
        }
    };

    // pull repo to copy to all folders
    let temp_repo = env::temp_dir().join("temp_repo");
    if !temp_repo.exists() {
        fs::create_dir_all(&temp_repo)
            .await
            .context("Failed to create temp directory")?;
    }

    // copy source code from project root to temp_repo
    let project_root = match current_dir.parent() {
        Some(parent) if parent.join("helix-cli").exists() => parent.to_path_buf(),
        Some(_) if current_dir.join("helix-cli").exists() => current_dir.to_path_buf(),
        Some(parent) => bail!("Error: Failed to get project root: {}", parent.display()),
        None => bail!("Error: Failed to get project root"),
    };
    copy_dir_recursive(&project_root, &temp_repo).await?;

    // build rust cli from ./helix-db/helix-cli with sh build.sh dev
    println!("DEBUG: Building rust cli from ./helix-db/helix-cli with sh build.sh dev");
    let build_script_path = project_root.join("helix-cli/build.sh");
    println!("DEBUG: Build script path: {}", build_script_path.display());
    println!("DEBUG: Build script exists: {}", build_script_path.exists());

    let helix_cli_dir = project_root.join("helix-cli");
    println!("DEBUG: Helix CLI dir: {}", helix_cli_dir.display());
    println!("DEBUG: Helix CLI dir exists: {}", helix_cli_dir.exists());

    // Check if helix is already available
    let helix_check = Command::new("helix").arg("--version").output();

    match helix_check {
        Ok(output) => {
            if output.status.success() {
                println!(
                    "DEBUG: Helix already available: {}",
                    String::from_utf8_lossy(&output.stdout)
                );
            } else {
                println!("DEBUG: Helix not available or failed version check");
            }
        }
        Err(e) => {
            println!("DEBUG: Helix command not found: {e}");
        }
    }

    let output = Command::new("sh")
        .arg("build.sh")
        .arg("dev")
        .current_dir(&helix_cli_dir) // Change to helix-cli directory first
        .output()
        .context("Failed to execute build.sh")?;

    println!("DEBUG: build.sh exit code: {:?}", output.status.code());
    println!(
        "DEBUG: build.sh stdout: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    println!(
        "DEBUG: build.sh stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    if !output.status.success() {
        bail!(
            "❌ BUILD FAILED: helix-cli build.sh failed\nStderr: {}\nStdout: {}",
            String::from_utf8_lossy(&output.stderr),
            String::from_utf8_lossy(&output.stdout)
        );
    } else {
        println!("DEBUG: build.sh dev succeeded");

        // Check if helix is available after build
        let helix_check_after = Command::new("helix").arg("--version").output();

        match helix_check_after {
            Ok(output) => {
                if output.status.success() {
                    println!(
                        "DEBUG: Helix available after build: {}",
                        String::from_utf8_lossy(&output.stdout)
                    );
                } else {
                    println!("DEBUG: Helix still not available after build");
                    println!(
                        "DEBUG: Helix version check stderr: {}",
                        String::from_utf8_lossy(&output.stderr)
                    );
                }
            }
            Err(e) => {
                println!("DEBUG: Helix command still not found after build: {e}");
            }
        }
    }

    if let Some(file_num) = matches.get_one::<u32>("file_number") {
        // Process single file
        if *file_num < 1 || *file_num > 100 {
            bail!("Error: Please provide a number between 1 and 100");
        }

        process_file_parallel(*file_num, &current_dir, &temp_repo, &github_config).await?;
        println!("✅ Successfully processed file{file_num}");
    } else if let Some(batch_args) = matches.get_many::<u32>("batch") {
        // Process in batch mode
        let batch_values: Vec<u32> = batch_args.copied().collect();
        if batch_values.len() != 2 {
            bail!("Error: --batch requires exactly 2 arguments: total_batches current_batch");
        }

        let total_batches = batch_values[0];
        let current_batch = batch_values[1];

        if current_batch < 1 || current_batch > total_batches {
            bail!(
                "Error: Current batch ({}) must be between 1 and {}",
                current_batch,
                total_batches
            );
        }

        if total_batches == 0 {
            bail!("Error: Total batches must be greater than 0");
        }

        // Calculate which files this batch should process
        let files_per_batch = 100 / total_batches;
        let remainder = 100 % total_batches;

        // Calculate start and end for this batch
        let start_file = ((current_batch - 1) * files_per_batch) + 1;
        let mut end_file = current_batch * files_per_batch;

        // Add remainder files to the last batch
        if current_batch == total_batches {
            end_file += remainder;
        }

        println!(
            "Processing batch {current_batch}/{total_batches}: files {start_file}-{end_file}"
        );

        let tasks: Vec<_> = (start_file..=end_file)
            .map(|file_num| {
                let current_dir = current_dir.clone();
                let temp_repo = temp_repo.clone();
                let github_config = github_config.clone();
                tokio::spawn(async move {
                    process_file_parallel(file_num, &current_dir, &temp_repo, &github_config).await
                })
            })
            .collect();

        // Wait for all tasks to complete and collect results
        let mut failed_files = Vec::new();
        for (i, task) in tasks.into_iter().enumerate() {
            let file_num = start_file + i as u32;
            match task.await {
                Ok(Ok(())) => {
                    println!("Successfully processed file{file_num}");
                }
                Ok(Err(e)) => {
                    eprintln!("Error processing file{file_num}: {e}");
                    failed_files.push(file_num);
                }
                Err(e) => {
                    eprintln!("Task error for file{file_num}: {e}");
                    failed_files.push(file_num);
                }
            }
        }

        if !failed_files.is_empty() {
            bail!(
                "❌ BATCH PROCESSING FAILED: {} out of {} files failed compilation/check: {:?}",
                failed_files.len(),
                end_file - start_file + 1,
                failed_files
            );
        }

        println!(
            "✅ Finished processing batch {current_batch}/{total_batches} successfully"
        );
    } else {
        // Process all files in parallel (default behavior)
        println!("Processing all files 1-100 in parallel...");

        let tasks: Vec<_> = (1..=100)
            .map(|file_num| {
                let current_dir = current_dir.clone();
                let temp_repo = temp_repo.clone();
                let github_config = github_config.clone();
                tokio::spawn(async move {
                    process_file_parallel(file_num, &current_dir, &temp_repo, &github_config).await
                })
            })
            .collect();

        // Wait for all tasks to complete and collect results
        let mut failed_files = Vec::new();
        for (i, task) in tasks.into_iter().enumerate() {
            let file_num = i as u32 + 1;
            match task.await {
                Ok(Ok(())) => {
                    println!("Successfully processed file{file_num}");
                }
                Ok(Err(e)) => {
                    eprintln!("Error processing file{file_num}: {e}");
                    failed_files.push(file_num);
                }
                Err(e) => {
                    eprintln!("Task error for file{file_num}: {e}");
                    failed_files.push(file_num);
                }
            }
        }

        if !failed_files.is_empty() {
            bail!(
                "❌ PROCESSING FAILED: {} out of 100 files failed compilation/check: {:?}",
                failed_files.len(),
                failed_files
            );
        }

        println!("✅ Finished processing all 100 files successfully");
    }

    Ok(())
}

async fn process_file_parallel(
    file_num: u32,
    current_dir: &Path,
    temp_repo: &Path,
    github_config: &Option<GitHubConfig>,
) -> Result<()> {
    let folder = format!("file{file_num}");
    let folder_path = current_dir.join(&folder);

    if !folder_path.exists() {
        // Skip non-existent files silently in parallel mode
        return Ok(());
    }

    // if queries.hx is empty, skip
    let queries_hx_path = folder_path.join(format!("{folder}.hx"));
    let schema_hx_path = folder_path.join("schema.hx");
    if queries_hx_path.exists() && queries_hx_path.is_file() {
        let content = fs::read_to_string(&queries_hx_path).await?;
        if content.is_empty() {
            return Ok(());
        }
    }

    // Create a temporary directory for this file
    let temp_dir = env::temp_dir().join(format!("helix_temp_{file_num}"));
    // let temp_dir = current_dir.join(format!("helix_temp_{}", file_num));
    if temp_dir.exists() {
        fs::remove_dir_all(&temp_dir)
            .await
            .context("Failed to remove existing temp directory")?;
    }
    fs::create_dir_all(&temp_dir)
        .await
        .context("Failed to create temp directory")?;

    // Copy the file contents to temp directory
    copy_dir_recursive(&folder_path, &temp_dir).await?;
    // copy repo to folder
    copy_dir_recursive(temp_repo, &temp_dir).await?;

    // Run helix compile command
    let compile_output_path = temp_dir.join("helix-db/helix-container/src");
    fs::create_dir_all(&compile_output_path)
        .await
        .context("Failed to create compile output directory")?;

    let output = Command::new("helix")
        .arg("compile")
        .arg("--path")
        .arg(&temp_dir)
        .arg("--output")
        .arg(&compile_output_path)
        .output()
        .context("Failed to execute helix compile command")?;

    println!(
        "DEBUG: Helix compile output: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    println!(
        "DEBUG: Helix compile stderr: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    if !output.status.success() {
        fs::remove_dir_all(&temp_dir).await.ok();
        let stderr = String::from_utf8_lossy(&output.stderr);
        let stdout = String::from_utf8_lossy(&output.stdout);
        // For helix compilation, we'll show the raw output since it's not cargo format
        let error_message = format!(
            "❌ HELIX COMPILE FAILED for file{file_num}\nStderr: {stderr}\nStdout: {stdout}"
        );

        // Create GitHub issue if configuration is available
        if let Some(config) = github_config {
            println!("DEBUG: Helix compilation failed in parellel mode, creating GitHub issue...");
            let query_content = fs::read_to_string(&queries_hx_path).await.map_err(|e| {
                println!("DEBUG: Failed to read queries.hx: {e}");
                e
            })?;
            let schema_content = fs::read_to_string(&schema_hx_path).await.map_err(|e| {
                println!("DEBUG: Failed to read schema.hx: {e}");
                e
            })?;
            let generated_rust_code = fs::read_to_string(&compile_output_path.join("queries.rs"))
                .await
                .map_err(|e| {
                    println!("DEBUG: Failed to read queries.rs: {e}");
                    e
                })?;
            handle_error_with_github(
                config,
                "Helix Compilation",
                &error_message,
                file_num,
                &query_content,
                &schema_content,
                &generated_rust_code,
            )
            .await?;
        } else {
            println!("DEBUG: GitHub integration not configured, skipping issue creation");
        }

        bail!("Error: {}", error_message);
    }

    // Run cargo check on the helix container path
    let helix_container_path = temp_dir.join("helix-db/helix-container");
    if helix_container_path.exists() {
        let output = Command::new("cargo")
            .arg("check")
            .current_dir(&helix_container_path)
            .output()
            .context("Failed to execute cargo check")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            let _stdout = String::from_utf8_lossy(&output.stdout);
            // let filtered_errors = extract_cargo_errors(&stderr, &stdout);
            let error_message = format!("❌ CARGO CHECK FAILED for file{file_num}\n{stderr}");

            // Create GitHub issue if configuration is available
            if let Some(config) = github_config {
                println!("DEBUG: Cargo check failed in parallel mode, creating GitHub issue...");
                let query_content = fs::read_to_string(&queries_hx_path).await.map_err(|e| {
                    println!("DEBUG: Failed to read queries.hx: {e}");
                    e
                })?;
                let schema_content = fs::read_to_string(&schema_hx_path).await.map_err(|e| {
                    println!("DEBUG: Failed to read schema.hx: {e}");
                    e
                })?;
                let generated_rust_code =
                    fs::read_to_string(&compile_output_path.join("queries.rs"))
                        .await
                        .map_err(|e| {
                            println!("DEBUG: Failed to read queries.rs: {e}");
                            e
                        })?;
                handle_error_with_github(
                    config,
                    "Cargo Check",
                    &error_message,
                    file_num,
                    &query_content,
                    &schema_content,
                    &generated_rust_code,
                )
                .await?;
            } else {
                println!("DEBUG: GitHub integration not configured, skipping issue creation");
            }
            fs::remove_dir_all(&temp_dir).await.ok();
            bail!("Error: {}", error_message);
        }
    }

    println!("Cargo check passed for {file_num}");
    // Clean up temp directory
    fs::remove_dir_all(&temp_dir).await.ok();

    Ok(())
}

async fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
    if !dst.exists() {
        fs::create_dir_all(dst).await?;
    }

    let mut entries = fs::read_dir(src).await?;
    while let Some(entry) = entries.next_entry().await? {
        let path = entry.path();
        let file_name = path.file_name().unwrap();
        let dest_path = dst.join(file_name);

        if path.is_dir() {
            if IGNORE_DIRS.contains(&path.file_name().unwrap().to_str().unwrap()) {
                continue;
            }
            Box::pin(copy_dir_recursive(&path, &dest_path))
                .await
                .map_err(|e| {
                    println!("DEBUG: Failed to copy directory {}: {}", path.display(), e);
                    e
                })?;
        } else {
            fs::copy(&path, &dest_path).await.map_err(|e| {
                println!("DEBUG: Failed to copy file {}: {}", path.display(), e);
                e
            })?;
        }
    }

    Ok(())
}

const IGNORE_DIRS: [&str; 2] = ["target", ".git"];
