use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use serde_json::Value;

use crate::data::Row;
use crate::llm::client::LlmClient;
use crate::llm::error::LlmError;
use crate::llm::logger::LlmLogger;
use crate::llm::parse_json::try_parse_json_object;
use crate::llm::pricing;
use crate::template::fill_template;
use crate::template::frontmatter::QualityCheckConfig;

use super::cards::ValidatedCard;

/// How many QC LLM calls to run in parallel.
const DEFAULT_CONCURRENCY: usize = 10;

#[derive(Clone)]
pub struct FlaggedCard {
    pub card: ValidatedCard,
    pub reason: String,
}

pub struct QualityRunResult {
    pub passed: Vec<ValidatedCard>,
    pub flagged: Vec<FlaggedCard>,
    pub cost: f64,
    pub input_tokens: u64,
    pub output_tokens: u64,
}

/// Run quality checks on cards. Returns passed cards, flagged cards (for review),
/// failed count, and cost. Does NOT do interactive review.
/// If check_config is None, all cards are passed through unchanged.
#[allow(clippy::too_many_arguments)]
pub fn run_quality_checks(
    cards: Vec<ValidatedCard>,
    check_config: Option<&QualityCheckConfig>,
    client: &LlmClient,
    model: &str,
    temperature: Option<f64>,
    max_tokens: Option<u64>,
    retries: u32,
    logger: Option<&LlmLogger>,
    on_progress: &(dyn Fn(&str) + Send + Sync),
) -> Result<QualityRunResult, anyhow::Error> {
    let config = match check_config {
        Some(c) => c,
        None => {
            return Ok(QualityRunResult {
                passed: cards,
                flagged: vec![],
                cost: 0.0,
                input_tokens: 0,
                output_tokens: 0,
            });
        }
    };

    let qc_model = config.model.as_deref().unwrap_or(model);
    let total_cards = cards.len();

    // Pre-compute prompts; fail early if a field is missing or template is invalid.
    let mut cards_opt: Vec<Option<ValidatedCard>> = Vec::with_capacity(total_cards);
    let mut prompts: Vec<String> = Vec::with_capacity(total_cards);

    for card in cards {
        let text = card
            .fields
            .get(&config.field)
            .ok_or_else(|| anyhow::anyhow!("Field \"{}\" not found on card", config.field))?;

        let mut row = Row::new();
        row.insert("text".into(), Value::String(text.clone()));
        let filled_prompt = fill_template(&config.prompt, &row)
            .map_err(|e| anyhow::anyhow!("Invalid quality check prompt template: {e}"))?;

        cards_opt.push(Some(card));
        prompts.push(filled_prompt);
    }

    on_progress(&format!(
        "Quality check 0/{total_cards} using {qc_model}..."
    ));

    // Each entry: (original_index, Ok((is_valid, reason, input_tokens, output_tokens, cost)) | Err(message))
    type CardResult = Result<(bool, String, u64, u64, f64), String>;
    let results: Arc<Mutex<Vec<(usize, CardResult)>>> =
        Arc::new(Mutex::new(Vec::with_capacity(total_cards)));

    let next_index = Arc::new(AtomicUsize::new(0));
    let done_count = Arc::new(AtomicUsize::new(0));

    let concurrency = DEFAULT_CONCURRENCY.min(total_cards);

    std::thread::scope(|s| {
        for _ in 0..concurrency {
            let next_index = Arc::clone(&next_index);
            let done_count = Arc::clone(&done_count);
            let results = Arc::clone(&results);
            let prompts = &prompts;

            s.spawn(move || {
                loop {
                    let idx = next_index.fetch_add(1, Ordering::SeqCst);
                    if idx >= total_cards {
                        break;
                    }

                    let outcome = check_single(
                        client,
                        qc_model,
                        &prompts[idx],
                        temperature,
                        max_tokens,
                        retries,
                        logger,
                    )
                    .map_err(|e| e.to_string());

                    done_count.fetch_add(1, Ordering::SeqCst);

                    results.lock().unwrap().push((idx, outcome));
                }
            });
        }
    });

    // Sort by original card index so flagged review is in input order.
    let mut results = Arc::try_unwrap(results).unwrap().into_inner().unwrap();
    results.sort_by_key(|(idx, _)| *idx);

    let mut total_cost = 0.0;
    let mut total_input_tokens = 0u64;
    let mut total_output_tokens = 0u64;
    let mut passed: Vec<ValidatedCard> = Vec::new();
    let mut flagged: Vec<FlaggedCard> = Vec::new();

    for (idx, result) in results {
        let card = cards_opt[idx].take().unwrap();
        match result {
            Ok((true, _, in_tok, out_tok, cost)) => {
                total_cost += cost;
                total_input_tokens += in_tok;
                total_output_tokens += out_tok;
                passed.push(card);
            }
            Ok((false, reason, in_tok, out_tok, cost)) => {
                total_cost += cost;
                total_input_tokens += in_tok;
                total_output_tokens += out_tok;
                flagged.push(FlaggedCard { card, reason });
            }
            Err(e) => {
                on_progress(&format!("  Quality check failed: {e}. Discarding card."));
            }
        }
    }

    if total_cost > 0.0 {
        on_progress(&format!(
            "  Quality check cost: {}",
            pricing::format_cost(total_cost)
        ));
    }

    Ok(QualityRunResult {
        passed,
        flagged,
        cost: total_cost,
        input_tokens: total_input_tokens,
        output_tokens: total_output_tokens,
    })
}

// Returns (is_valid, reason, input_tokens, output_tokens, cost)
fn check_single(
    client: &LlmClient,
    model: &str,
    prompt: &str,
    temperature: Option<f64>,
    max_tokens: Option<u64>,
    retries: u32,
    logger: Option<&LlmLogger>,
) -> Result<(bool, String, u64, u64, f64), anyhow::Error> {
    let mut last_error = String::new();

    for attempt in 0..=retries {
        if attempt > 0 {
            let backoff = Duration::from_millis(1000 * 2u64.pow(attempt - 1));
            std::thread::sleep(backoff.min(Duration::from_secs(30)));
        }

        match client.chat_completion(model, prompt, temperature, max_tokens) {
            Ok(result) => {
                if let Some(logger) = logger {
                    logger.log(prompt, &result.content);
                }

                let obj = try_parse_json_object(&result.content)
                    .ok_or_else(|| anyhow::anyhow!("Quality check response is not valid JSON"))?;

                let is_valid = obj
                    .get("is_valid")
                    .and_then(|v| v.as_bool())
                    .ok_or_else(|| anyhow::anyhow!("Missing is_valid in quality check response"))?;
                let reason = obj
                    .get("reason")
                    .and_then(|v| v.as_str())
                    .unwrap_or("No reason provided")
                    .to_string();

                let (in_tok, out_tok, cost) = result
                    .usage
                    .map(|u| {
                        (
                            u.prompt_tokens,
                            u.completion_tokens,
                            pricing::calculate_cost(model, u.prompt_tokens, u.completion_tokens),
                        )
                    })
                    .unwrap_or((0, 0, 0.0));

                return Ok((is_valid, reason, in_tok, out_tok, cost));
            }
            Err(e) => {
                last_error = e.to_string();
                if let LlmError::Api(_) = e {
                    break;
                }
            }
        }
    }

    anyhow::bail!("Quality check failed: {last_error}")
}
