use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::Path;

use obilayeredmap::IndexMode;
use serde::{Deserialize, Serialize};

pub const META_FILENAME: &str = "index.meta";
const META_VERSION: u32 = 1;

/// Per-genome label + categorical metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenomeInfo {
    pub label: String,
    #[serde(default)]
    pub meta: HashMap<String, String>,
}

impl GenomeInfo {
    pub fn new(label: impl Into<String>) -> Self {
        Self { label: label.into(), meta: HashMap::new() }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexConfig {
    pub kmer_size: usize,
    pub minimizer_size: usize,
    pub n_bits: usize,
    pub with_counts: bool,
    #[serde(default)]
    pub evidence: IndexMode,
    /// Block size for the unitig index as a power-of-two exponent.
    /// The `.idx` block covers 2^block_bits consecutive unitigs.
    /// 0 = one entry per unitig (O(1) access, largest `.idx`).
    #[serde(default)]
    pub block_bits: u8,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexMeta {
    pub version: u32,
    pub config: IndexConfig,
    /// Ordered list of genomes indexed here (label + optional categorical metadata).
    pub genomes: Vec<GenomeInfo>,
}

impl IndexMeta {
    pub fn new(config: IndexConfig) -> Self {
        Self { version: META_VERSION, config, genomes: Vec::new() }
    }

    pub fn write(&self, root: &Path) -> io::Result<()> {
        let file = fs::File::create(root.join(META_FILENAME))?;
        serde_json::to_writer_pretty(file, self).map_err(io::Error::other)
    }

    pub fn read(root: &Path) -> io::Result<Self> {
        let file = fs::File::open(root.join(META_FILENAME))?;
        serde_json::from_reader(file).map_err(io::Error::other)
    }

    pub fn exists(root: &Path) -> bool {
        root.join(META_FILENAME).exists()
    }

    /// Iterate over genome labels only.
    pub fn genome_labels(&self) -> impl Iterator<Item = &str> {
        self.genomes.iter().map(|g| g.label.as_str())
    }
}

/// Validate a user-supplied genome label.
///
/// Forbidden: `/` (filesystem separator), `=` (--new-label parser separator),
/// `\0` (null byte), `\n`, `\r`, `\t` (break CSV output).
/// Empty labels are also rejected.
pub fn validate_label(label: &str) -> Result<(), String> {
    if label.is_empty() {
        return Err("genome label must not be empty".into());
    }
    const FORBIDDEN: &[char] = &['/', '=', '\0', '\n', '\r', '\t'];
    if let Some(c) = label.chars().find(|c| FORBIDDEN.contains(c)) {
        let display = match c {
            '\0' => "\\0 (null)".to_string(),
            '\n' => "\\n (newline)".to_string(),
            '\r' => "\\r (carriage return)".to_string(),
            '\t' => "\\t (tab)".to_string(),
            c    => format!("'{c}'"),
        };
        return Err(format!("genome label contains forbidden character {display}"));
    }
    Ok(())
}
