use std::fs;
use std::path::Path;
use obilayeredmap::{IndexMode, layer::Layer};
use obilayeredmap::meta::PartitionMeta;
use obisys::{Reporter, Stage, progress_bar};
use tracing::info;

use crate::error::{OKIError, OKIResult};
use crate::index::KmerIndex;
use crate::state::IndexState;

const EVIDENCE_FILE:    &str = "evidence.bin";
const FINGERPRINT_FILE: &str = "fingerprint.bin";
const UNITIG_IDX_FILE:  &str = "unitigs.bin.idx";

fn olm_to_oki(e: obilayeredmap::OLMError) -> OKIError {
    OKIError::InvalidInput(e.to_string())
}

impl KmerIndex {
    /// Convert every layer's evidence bundle to `target` in-place.
    ///
    /// - `Exact`   → builds `evidence.bin` + `unitigs.bin.idx`, removes `fingerprint.bin`
    /// - `Approx`  → builds `fingerprint.bin`, removes `evidence.bin` + `unitigs.bin.idx`
    ///
    /// The MPHF (`mphf.bin`) and unitigs (`unitigs.bin`) are never touched.
    /// `index.meta` is updated with the new evidence kind on success.
    pub fn reindex(
        &mut self,
        target: IndexMode,
        block_bits: u8,
        rep: &mut Reporter,
    ) -> OKIResult<()> {
        if self.state() != IndexState::Indexed {
            return Err(OKIError::NotIndexed(self.root_path.clone()));
        }

        let n = self.partition.n_partitions();
        info!(
            "reindex {} partition(s): {:?} → {:?}",
            n, self.meta.config.evidence, target,
        );

        let t = Stage::start("reindex");
        let pb = progress_bar("reindex", n as u64, "partitions");

        let order: Vec<usize> = (0..n).collect();
        let runner = crate::numa::PartitionRunner::new();
        runner.run(
            &order,
            |i| reindex_partition(&self.partition.part_dir(i).join("index"), &target, block_bits)
                .map_err(|e| OKIError::InvalidInput(format!("partition {i}: {e}"))),
            |_, _, _| { pb.inc(1); },
        )?;

        pb.finish_and_clear();

        self.meta.config.evidence = target;
        if matches!(self.meta.config.evidence, IndexMode::Exact) {
            self.meta.config.block_bits = block_bits;
        }
        self.meta.write(&self.root_path)?;
        rep.push(t.stop());
        Ok(())
    }
}

/// Process all layers of one partition's index directory.
fn reindex_partition(index_dir: &Path, target: &IndexMode, block_bits: u8) -> OKIResult<()> {
    if !index_dir.exists() {
        return Ok(());
    }
    let pm = PartitionMeta::load(index_dir).map_err(olm_to_oki)?;
    for layer_idx in 0..pm.n_layers {
        let layer_dir = index_dir.join(format!("layer_{layer_idx}"));
        reindex_layer(&layer_dir, target, block_bits)?;
    }
    Ok(())
}

fn reindex_layer(layer_dir: &Path, target: &IndexMode, block_bits: u8) -> OKIResult<()> {
    match target {
        IndexMode::Exact => {
            Layer::<()>::build_exact_evidence(layer_dir, block_bits).map_err(olm_to_oki)?;
        }
        IndexMode::Approx { b, z } | IndexMode::Hybrid { b, z } => {
            Layer::<()>::build_approx_evidence(layer_dir, *b, *z).map_err(olm_to_oki)?;
        }
    }
    remove_stale_evidence(layer_dir, target)
}

fn remove_stale_evidence(layer_dir: &Path, target: &IndexMode) -> OKIResult<()> {
    match target {
        IndexMode::Exact => {
            remove_if_exists(&layer_dir.join(FINGERPRINT_FILE));
        }
        IndexMode::Approx { .. } => {
            remove_if_exists(&layer_dir.join(EVIDENCE_FILE));
            remove_if_exists(&layer_dir.join(UNITIG_IDX_FILE));
        }
        IndexMode::Hybrid { .. } => {
            // both bundles kept — nothing to remove
        }
    }
    Ok(())
}

fn remove_if_exists(path: &Path) {
    if let Err(e) = fs::remove_file(path) {
        if e.kind() != std::io::ErrorKind::NotFound {
            eprintln!("warning: could not remove {}: {e}", path.display());
        }
    }
}
