use std::collections::HashSet;
use std::path::Path;

use once_cell::sync::Lazy;
use rand::seq::SliceRandom;
use rand::Rng;
use regex::Regex;

use super::seed::SeedCsvAccess;

const SEED_CSV_SAMPLES: usize = 40;

static RE_WS: Lazy<Regex> = Lazy::new(|| Regex::new(r"\s+").unwrap());
static RE_TRIM_TOK: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?u)^[\W_]+|[\W_]+$").unwrap());

fn skip_name_tokens() -> &'static HashSet<&'static str> {
    static SET: Lazy<HashSet<&'static str>> = Lazy::new(|| {
        [
            "b", "bv", "b.v", "b.v.", "nv", "vof", "cv", "ab", "kb", "as", "asa", "oy", "oyj",
            "oü", "ou", "ltd", "llc", "inc", "plc", "gmbh", "ag", "sas", "sa", "srl", "spa", "sl",
            "slu", "ooo", "zoo", "see",
            // NL / EU registry noise — keeps windows on substantive tokens under tight max_len
            "stichting",
            "vereniging",
            "maatschappij",
            "holding",
            "federatie",
            "coöperatie",
            "cooperatie",
            "onderlinge",
        ]
        .into_iter()
        .collect()
    });
    &SET
}

fn bad_query_whole() -> &'static HashSet<&'static str> {
    static SET: Lazy<HashSet<&'static str>> = Lazy::new(|| {
        ["b.v.", "bv", "nv", "vof", "ab", "oy", "ltd", "llc", "inc"]
            .into_iter()
            .collect()
    });
    &SET
}

fn normalize_query_token(t: &str) -> String {
    RE_TRIM_TOK.replace_all(t, "").to_string().to_lowercase()
}

fn meaningful_tokens_from_name(name: &str) -> Vec<String> {
    let name = name.trim();
    if name.is_empty() {
        return vec![];
    }
    let skip = skip_name_tokens();
    let mut out = Vec::new();
    for p in RE_WS.split(name) {
        if p.is_empty() {
            continue;
        }
        let norm = normalize_query_token(p);
        if norm.is_empty() || skip.contains(norm.as_str()) {
            continue;
        }
        if norm.len() == 1 && !norm.chars().next().is_some_and(|c| c.is_alphanumeric()) {
            continue;
        }
        out.push(p.trim().to_string());
    }
    out
}

/// Contiguous runs of tokens whose joined length is in `[min_len, max_len]` — no mid-word cuts.
fn query_candidates_token_windows(tokens: &[String], min_len: usize, max_len: usize) -> Vec<String> {
    let bad = bad_query_whole();
    let mut seen_local: HashSet<String> = HashSet::new();
    let mut cands: Vec<String> = Vec::new();

    let mut add = |s: &str| {
        let s = s.trim();
        let c_len = s.chars().count();
        if c_len < min_len || c_len > max_len {
            return;
        }
        let low = s.to_lowercase();
        if bad.contains(low.as_str()) {
            return;
        }
        if !s.chars().any(|c| c.is_alphabetic()) {
            return;
        }
        if seen_local.insert(low) {
            cands.push(s.to_string());
        }
    };

    for i in 0..tokens.len() {
        let mut parts: Vec<&str> = Vec::new();
        let mut total = 0usize;
        for t in tokens.iter().skip(i) {
            let t = t.as_str();
            let tlen = t.chars().count();
            let next = if parts.is_empty() {
                tlen
            } else {
                total + 1 + tlen
            };
            if next > max_len {
                break;
            }
            parts.push(t);
            total = next;
            let chunk = parts.join(" ");
            add(&chunk);
        }
    }
    cands
}

fn query_candidates_from_name(name: &str, min_len: usize, max_len: usize) -> Vec<String> {
    let tokens = meaningful_tokens_from_name(name);
    if !tokens.is_empty() {
        return query_candidates_token_windows(&tokens, min_len, max_len);
    }
    let stripped = name.trim();
    if stripped.is_empty() {
        return vec![];
    }
    let c_len = stripped.chars().count();
    if c_len < min_len || c_len > max_len {
        return vec![];
    }
    let low = stripped.to_lowercase();
    if bad_query_whole().contains(low.as_str()) || !stripped.chars().any(|c| c.is_alphabetic()) {
        return vec![];
    }
    vec![stripped.to_string()]
}

fn query_is_acceptable(q: &str, min_len: usize, max_len: usize) -> bool {
    let q = q.trim();
    let c_len = q.chars().count();
    if c_len < min_len || c_len > max_len {
        return false;
    }
    if bad_query_whole().contains(q.to_lowercase().as_str()) {
        return false;
    }
    q.chars().any(|c| c.is_alphabetic())
}

pub fn load_words_file(path: &Path) -> Vec<String> {
    if !path.is_file() {
        return vec![];
    }
    let Ok(data) = std::fs::read_to_string(path) else {
        return vec![];
    };
    data.lines()
        .map(|l| l.trim())
        .filter(|l| !l.is_empty() && !l.starts_with('#'))
        .map(String::from)
        .collect()
}

pub struct QueryGenerator<'a> {
    words: Vec<String>,
    seed_access: Option<&'a SeedCsvAccess>,
    text_seed: String,
    seen: HashSet<String>,
}

impl<'a> QueryGenerator<'a> {
    pub fn new(
        words: Vec<String>,
        seed_access: Option<&'a SeedCsvAccess>,
        text_seed: String,
    ) -> Self {
        Self {
            words,
            seed_access,
            text_seed,
            seen: HashSet::new(),
        }
    }

    pub fn mark_seen(&mut self, query: String) {
        self.seen.insert(query.to_lowercase());
    }

    pub fn seen_count(&self) -> usize {
        self.seen.len()
    }

    pub fn next(
        &mut self,
        last_result_names: &[String],
        min_len: usize,
        max_len: usize,
        rng: &mut impl Rng,
    ) -> (String, bool) {
        let mut pool: Vec<String> = Vec::new();
        for name in last_result_names {
            pool.extend(query_candidates_from_name(name, min_len, max_len));
        }
        pool.shuffle(rng);
        for c in pool {
            if !self.seen.contains(&c.to_lowercase()) && query_is_acceptable(&c, min_len, max_len) {
                return (c, false);
            }
        }

        if let Some(access) = self.seed_access {
            for _ in 0..SEED_CSV_SAMPLES {
                let Some(row) = access.random_row(rng) else {
                    continue;
                };
                let name = row
                    .get("name")
                    .or_else(|| row.get("naam"))
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty());
                let Some(name) = name else {
                    continue;
                };
                let mut cands = query_candidates_from_name(&name, min_len, max_len);
                cands.shuffle(rng);
                for c in cands {
                    if !self.seen.contains(&c.to_lowercase()) && query_is_acceptable(&c, min_len, max_len)
                    {
                        return (c, false);
                    }
                }
            }
        }

        let mut bag = self.words.clone();
        bag.shuffle(rng);
        for w in bag {
            let c = w.trim().chars().take(max_len).collect::<String>();
            let c = c.trim().to_string();
            if !self.seen.contains(&c.to_lowercase()) && query_is_acceptable(&c, min_len, max_len) {
                return (c, false);
            }
        }

        for c in query_candidates_from_name(&self.text_seed, min_len, max_len) {
            if !self.seen.contains(&c.to_lowercase()) {
                return (c, false);
            }
        }

        for _ in 0..1000 {
            let n: u64 = rng.gen_range(100_000..1_000_000_000);
            let c = n.to_string();
            let c = c.chars().take(max_len).collect::<String>();
            if c.len() >= min_len && !self.seen.contains(&c.to_lowercase()) {
                return (c, true);
            }
        }

        let c = format!(
            "q{}",
            std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .map(|d| d.as_nanos())
                .unwrap_or(0)
        )
        .chars()
        .take(max_len)
        .collect::<String>();

        let c = if c.len() < min_len {
            format!("{c}{}", "0".repeat(min_len - c.len()))
        } else {
            c
        };
        (c, true)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn no_mid_word_char_prefix_fragments() {
        let c = query_candidates_from_name("Alphabetabase Secondco", 3, 8);
        assert!(
            !c.iter().any(|q| q.starts_with("Alphabet") && *q != "Alphabetabase"),
            "no char-trimmed prefix of long token: {:?}",
            c
        );
        assert!(
            c.contains(&"Secondco".to_string()),
            "expected full token window, got {:?}",
            c
        );
    }

    #[test]
    fn skips_oversized_leading_token_starts_windows_on_next_tokens() {
        let c = query_candidates_from_name("Stichting Foo Bar", 3, 8);
        assert!(
            c.iter().any(|q| q == "Foo Bar"),
            "want token window after skipped legal head, got {:?}",
            c
        );
    }

    #[test]
    fn multi_token_name_yields_bounded_windows() {
        let c = query_candidates_from_name("Twen Technics V.O.F.", 3, 8);
        assert!(c.contains(&"Twen".to_string()));
        assert!(c.contains(&"Technics".to_string()));
    }
}
