use crate::benchmark::get_or_generate_cached_code;
use crate::channel::{AwgnChannel, Channel};
use crate::code::GenerationMethod;
use crate::decoder::{build_decoder, DecoderConfig, DecoderMethod};
use crate::encoder::{build_encoder, EncodingMethod};
use crate::Result as LdpcResult;

use indicatif::{ProgressBar, ProgressStyle};
use rand::Rng;
use rayon::prelude::*;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::Instant;

#[derive(Clone)]
pub struct CodeScenario {
    pub n: usize,
    pub k: usize,
    pub wc: usize,
    pub wr: usize,
    pub method: GenerationMethod,
    pub name: String,
}

pub struct CampaignConfig {
    pub scenarios: Vec<CodeScenario>,
    pub snr_range: Vec<f64>,
    pub n_trials: usize,
    pub max_iterations: usize,
    pub output_csv: String,
    pub export_graph: bool,
}

pub fn run_massive_campaign(config: CampaignConfig) -> LdpcResult<()> {
    println!("TEST)");
    println!("Fichier de sortie : {}", config.output_csv);
    println!("Scenarios a tester: {}", config.scenarios.len());
    println!("SNRs par scenario : {}", config.snr_range.len());
    println!("Trames par point  : {}\n", config.n_trials);

    let file_exists = std::path::Path::new(&config.output_csv).exists();
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(&config.output_csv)
        .expect("Impossible d'ouvrir le fichier CSV");

    if !file_exists {
        writeln!(
            file,
            "Scenario,N,K,Rate,Wc,Wr,Method,Girth,Density_pct,SNR_dB,Capacity,Decoder,FER_pct,BER_pct,Frames,Time_ms"
        ).unwrap();
    }

    let total_steps = (config.scenarios.len() * config.snr_range.len()) as u64;
    let pb = ProgressBar::new(total_steps);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.cyan} [{elapsed_precise}] [{bar:40.green/blue}] {pos}/{len} ({eta}) - {msg}")
            .unwrap()
            .progress_chars("=>-"),
    );

    for scenario in config.scenarios {
        pb.set_message(format!("Generation Matrice: {}", scenario.name));

        let mut code = get_or_generate_cached_code(
            scenario.n,
            scenario.k,
            scenario.wc,
            scenario.wr,
            scenario.method.clone(),
        )?;

        if config.export_graph {
            let dot_filename = format!("{}_tanner.dot", scenario.name);
            let mut dot_file = std::fs::File::create(&dot_filename).expect("Erreur creation .dot");

            writeln!(dot_file, "graph TannerGraph {{").unwrap();
            writeln!(dot_file, "    rankdir=TB;").unwrap();
            writeln!(dot_file, "    nodesep=0.5;").unwrap();
            writeln!(dot_file, "    ranksep=2.0;").unwrap();

            writeln!(dot_file, "    node [style=filled, fontname=\"Arial\"];").unwrap();

            writeln!(dot_file, "    {{ rank=same;").unwrap();
            for v in 0..code.n() {
                writeln!(
                    dot_file,
                    "        v{} [shape=circle, fillcolor=lightblue, label=\"v{}\"];",
                    v, v
                )
                .unwrap();
            }
            writeln!(dot_file, "    }}").unwrap();

            writeln!(dot_file, "    {{ rank=same;").unwrap();
            for c in 0..code.m() {
                writeln!(
                    dot_file,
                    "        c{} [shape=square, fillcolor=lightcoral, label=\"c{}\"];",
                    c, c
                )
                .unwrap();
            }
            writeln!(dot_file, "    }}").unwrap();

            for c in 0..code.m() {
                for &v in code.graph.chk_neighbors(c) {
                    writeln!(dot_file, "    v{} -- c{};", v, c).unwrap();
                }
            }
            writeln!(dot_file, "}}").unwrap();
        }

        let rate = code.rate();
        let girth = code.girth();
        let density = code.h.density() * 100.0;
        let method_str = match scenario.method {
            GenerationMethod::Gallager => "Gallager",
            GenerationMethod::MacKayNeal { .. } => "MacKayNeal",
        };

        pb.set_message(format!("Initialisation DSP: {}", scenario.name));
        let encoder = build_encoder(&mut code, EncodingMethod::Systematic)?;

        let dec_config = DecoderConfig {
            max_iterations: config.max_iterations,
            early_stopping: true,
        };
        let dec_sp = build_decoder(&code, DecoderMethod::SumProduct, dec_config.clone());
        let dec_ms = build_decoder(
            &code,
            DecoderMethod::MinSum {
                scaling_factor: 0.8,
            },
            dec_config.clone(),
        );
        let dec_bf = build_decoder(&code, DecoderMethod::BitFlipping, dec_config);

        for &snr in &config.snr_range {
            pb.set_message(format!("Test: {} | SNR: {:.2} dB", scenario.name, snr));
            let channel = AwgnChannel::new(snr, rate)?;
            let cap = channel.capacity();

            let start_snr_time = Instant::now();

            let results: Vec<_> = (0..config.n_trials)
                .into_par_iter()
                .map(|_| {
                    let mut rng = rand::thread_rng();
                    let message: Vec<u8> = (0..scenario.k).map(|_| rng.gen::<u8>() & 1).collect();
                    let codeword = encoder.encode(&message).unwrap();
                    let rx_llr = channel.transmit(&codeword, &mut rng);

                    let eval_decoder = |decoder: &dyn crate::decoder::Decoder| -> (usize, usize) {
                        if let Some(decoded) = decoder.decode(&rx_llr).codeword() {
                            let errs = codeword
                                .iter()
                                .zip(decoded.iter())
                                .filter(|(a, b)| a != b)
                                .count();
                            (if errs > 0 { 1 } else { 0 }, errs)
                        } else {
                            (1, scenario.n)
                        }
                    };

                    let (sp_f, sp_b) = eval_decoder(&*dec_sp);
                    let (ms_f, ms_b) = eval_decoder(&*dec_ms);
                    let (bf_f, bf_b) = eval_decoder(&*dec_bf);

                    (sp_f, sp_b, ms_f, ms_b, bf_f, bf_b)
                })
                .collect();

            let mut sp_err = (0, 0);
            let mut ms_err = (0, 0);
            let mut bf_err = (0, 0);
            for r in results {
                sp_err.0 += r.0;
                sp_err.1 += r.1;
                ms_err.0 += r.2;
                ms_err.1 += r.3;
                bf_err.0 += r.4;
                bf_err.1 += r.5;
            }

            let elapsed_ms = start_snr_time.elapsed().as_millis();
            let total_f = config.n_trials as f64;
            let total_b = (config.n_trials * scenario.n) as f64;

            let mut write_csv_line = |dec_name: &str, f_err: usize, b_err: usize| {
                writeln!(
                    file,
                    "{},{},{},{:.3},{},{},{},{},{:.4},{:.2},{:.4},{},{:.4},{:.4e},{},{}",
                    scenario.name,
                    scenario.n,
                    scenario.k,
                    rate,
                    scenario.wc,
                    scenario.wr,
                    method_str,
                    girth,
                    density,
                    snr,
                    cap,
                    dec_name,
                    (f_err as f64 / total_f) * 100.0,
                    (b_err as f64 / total_b) * 100.0,
                    config.n_trials,
                    elapsed_ms
                )
                .unwrap();
            };

            write_csv_line("SumProduct", sp_err.0, sp_err.1);
            write_csv_line("MinSum_0.8", ms_err.0, ms_err.1);
            write_csv_line("BitFlipping", bf_err.0, bf_err.1);

            file.flush().unwrap();
            pb.inc(1);
        }
    }

    pb.finish_with_message("Campagne terminee avec succes !");
    Ok(())
}
