use std::collections::HashSet;
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use serde_json::{Map, Value};

pub struct PipelineCache {
    hash_cache: Arc<Mutex<HashSet<String>>>,
    ee_cache: Option<Arc<Mutex<HashSet<String>>>>,
    output_csv: PathBuf,
}

impl PipelineCache {
    pub fn new(output_csv: PathBuf, country_code: &str) -> Self {
        PipelineCache {
            hash_cache: Arc::new(Mutex::new(Self::load_hash_cache_from_csv(&output_csv))),
            ee_cache: if country_code.to_uppercase() == "EE" {
                Some(Arc::new(Mutex::new(HashSet::new())))
            } else {
                None
            },
            output_csv,
        }
    }

    pub fn has_hash(&self, hash: &str) -> bool {
        self.hash_cache.lock().unwrap().contains(hash)
    }

    pub fn insert_hash(&self, hash: String) {
        self.hash_cache.lock().unwrap().insert(hash);
    }

    pub fn insert_ee(&self, rk: String) {
        if let Some(c) = &self.ee_cache {
            c.lock().unwrap().insert(rk);
        }
    }

    pub fn has_ee(&self, rk: &str) -> bool {
        if let Some(c) = &self.ee_cache {
            c.lock().unwrap().contains(rk)
        } else {
            false
        }
    }

    pub fn ee_cache_ref(&self) -> Option<Arc<Mutex<HashSet<String>>>> {
        self.ee_cache.clone()
    }

    pub fn hash_cache_ref(&self) -> Arc<Mutex<HashSet<String>>> {
        self.hash_cache.clone()
    }

    fn load_hash_cache_from_csv(path: &Path) -> HashSet<String> {
        let Ok(f) = File::open(path) else {
            return HashSet::new();
        };
        let mut rdr = csv::Reader::from_reader(f);
        let headers = match rdr.headers() {
            Ok(h) => h.clone(),
            Err(_) => return HashSet::new(),
        };
        let hash_idx = headers.iter().position(|x| x == "hash");
        let Some(hash_idx) = hash_idx else {
            return HashSet::new();
        };
        let mut out = HashSet::new();
        for rec in rdr.records().flatten() {
            if let Some(v) = rec.get(hash_idx) {
                if !v.is_empty() {
                    out.insert(v.to_string());
                }
            }
        }
        out
    }

    pub fn save_rows_to_csv(
        &self,
        rows: &[Map<String, Value>],
        append: bool,
    ) -> std::io::Result<()> {
        if rows.is_empty() {
            tracing::warn!("No rows to save to CSV.");
            return Ok(());
        }
        let mut fieldnames: Vec<String> = rows
            .iter()
            .flat_map(|r| r.keys().cloned())
            .collect::<HashSet<_>>()
            .into_iter()
            .collect();
        fieldnames.sort();
        let file_exists = self.output_csv.is_file()
            && self
                .output_csv
                .metadata()
                .map(|m| m.len() > 0)
                .unwrap_or(false);
        let write_header = !append || !file_exists;

        // Ensure parent directory exists if necessary! (optional, depending on setup)

        let mut wtr = csv::WriterBuilder::new().has_headers(false).from_writer(
            OpenOptions::new()
                .create(true)
                .append(append && file_exists)
                .truncate(!append || !file_exists)
                .write(true)
                .open(&self.output_csv)?,
        );
        if write_header {
            wtr.write_record(&fieldnames)?;
        }
        for r in rows {
            let vals: Vec<String> = fieldnames
                .iter()
                .map(|k| {
                    r.get(k)
                        .map(|v| match v {
                            Value::Null => String::new(),
                            Value::String(s) => s.clone(),
                            other => other.to_string(),
                        })
                        .unwrap_or_default()
                })
                .collect();
            wtr.write_record(&vals)?;
        }
        wtr.flush()?;
        tracing::info!(
            "Saved {} row(s) to '{}'.",
            rows.len(),
            self.output_csv.display()
        );
        Ok(())
    }
}
