//! FASTA serialisation for obikmer sequence types.
//!
//! Three public functions cover the main output cases:
//!
//! - [`write_scatter`]: super-kmers in scatter phase (minimizer annotation)
//! - [`write_count`]: super-kmers in count phase (occurrence count annotation)
//! - [`write_unitig`]: unitigs from the layered index (partition + index annotation)
//!
//! All produce OBITools-compatible FASTA:
//!
//! ```text
//! >ID {"key":value,...}
//! SEQUENCE
//! ```
//!
//! The lower-level primitive [`write_record`] and the [`JsonVal`] type are also
//! public for callers that need custom annotations.

#![deny(missing_docs)]

mod fasta;

use std::io::{self, Write};

use obikseq::{Minimizer, SuperKmer, Unitig};

pub use fasta::{JsonVal, annotation, seq_id, write_record};

// ── public API ────────────────────────────────────────────────────────────────

/// Write one super-kmer in FASTA format — **scatter phase**.
///
/// ID is the xxHash-64 of the sequence.  JSON annotation includes
/// `seq_length`, `kmer_size`, `minimizer_size`, `partition`, `minimizer`.
pub fn write_scatter<W: Write>(
    sk: &SuperKmer,
    out: &mut W,
    k: usize,
    m: usize,
    partition: usize,
    minimizer: Minimizer,
) -> io::Result<()> {
    let ascii = sk.to_ascii();
    let id = seq_id(&ascii);
    let min_seq = minimizer.to_ascii();
    let min_str = unsafe { std::str::from_utf8_unchecked(&min_seq) };
    write_record(
        &ascii,
        &id,
        &[
            ("seq_length",    JsonVal::Num(ascii.len() as u64)),
            ("kmer_size",     JsonVal::Num(k as u64)),
            ("minimizer_size",JsonVal::Num(m as u64)),
            ("partition",     JsonVal::Num(partition as u64)),
            ("minimizer",     JsonVal::Str(min_str)),
        ],
        out,
    )
}

/// Write one super-kmer in FASTA format — **count phase**.
///
/// ID is the xxHash-64 of the sequence.  JSON annotation includes
/// `seq_length`, `kmer_size`, `minimizer_size`, `partition`, `count`.
pub fn write_count<W: Write>(
    sk: &SuperKmer,
    out: &mut W,
    k: usize,
    m: usize,
    partition: u32,
) -> io::Result<()> {
    let ascii = sk.to_ascii();
    let id = seq_id(&ascii);
    write_record(
        &ascii,
        &id,
        &[
            ("seq_length",    JsonVal::Num(ascii.len() as u64)),
            ("kmer_size",     JsonVal::Num(k as u64)),
            ("minimizer_size",JsonVal::Num(m as u64)),
            ("partition",     JsonVal::Num(partition as u64)),
            ("count",         JsonVal::Num(sk.count() as u64)),
        ],
        out,
    )
}

/// Write one unitig in FASTA format.
///
/// ID is `part_PPPPP_unitig_IIIIII` where `P` is the partition index and `I`
/// is the unitig index within that partition.  JSON annotation includes
/// `seq_length`, `kmer_size`, `n_kmers`, `partition`, `unitig_index`.
pub fn write_unitig<W: Write>(
    unitig: &Unitig,
    k: usize,
    partition: usize,
    index: usize,
    out: &mut W,
) -> io::Result<()> {
    let ascii = unitig.to_ascii();
    let seql = unitig.seql();
    let id = format!("part_{partition:05}_unitig_{index:06}");
    write_record(
        &ascii,
        &id,
        &[
            ("seq_length",   JsonVal::Num(seql as u64)),
            ("kmer_size",    JsonVal::Num(k as u64)),
            ("n_kmers",      JsonVal::Num((seql - k + 1) as u64)),
            ("partition",    JsonVal::Num(partition as u64)),
            ("unitig_index", JsonVal::Num(index as u64)),
        ],
        out,
    )
}

// ── tests ─────────────────────────────────────────────────────────────────────

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

    fn make(seq: &[u8]) -> SuperKmer {
        SuperKmer::from_ascii(seq)
    }

    fn capture<F: Fn(&mut Vec<u8>) -> io::Result<()>>(f: F) -> String {
        let mut buf = Vec::new();
        f(&mut buf).unwrap();
        String::from_utf8(buf).unwrap()
    }

    // ── write_scatter ─────────────────────────────────────────────────────────

    #[test]
    fn scatter_header_contains_minimizer_field() {
        let sk = make(b"ACGTACGTACGT");
        let out = capture(|w| write_scatter(&sk, w, 4, 3, 7, Minimizer::from_raw_unchecked(0)));
        assert!(out.contains("\"minimizer\":\""));
        assert!(!out.contains("\"count\":"));
    }

    #[test]
    fn scatter_minimizer_decoded_from_hash() {
        obikseq::params::set_m(3);
        let sk = make(b"ACGTACGTACGT");
        let minimizer = Minimizer::from_raw_unchecked(6u64 << (64 - 2 * 3));
        let out = capture(|w| write_scatter(&sk, w, 4, 3, 0, minimizer));
        assert!(out.contains("\"minimizer\":\"ACG\""), "got: {out}");
    }

    #[test]
    fn scatter_fields_present() {
        let sk = make(b"ACGTACGTACGT");
        let out = capture(|w| write_scatter(&sk, w, 4, 3, 5, Minimizer::from_raw_unchecked(0)));
        assert!(out.contains("\"seq_length\":12"));
        assert!(out.contains("\"kmer_size\":4"));
        assert!(out.contains("\"minimizer_size\":3"));
        assert!(out.contains("\"partition\":5"));
    }

    #[test]
    fn scatter_sequence_line_correct() {
        let sk = make(b"ACGTACGT");
        let out = capture(|w| write_scatter(&sk, w, 4, 2, 0, Minimizer::from_raw_unchecked(0)));
        let lines: Vec<&str> = out.lines().collect();
        assert_eq!(lines[1], "ACGTACGT");
    }

    // ── write_count ───────────────────────────────────────────────────────────

    #[test]
    fn count_header_contains_count_field() {
        let mut sk = make(b"ACGTACGTACGT");
        sk.add(49);
        let out = capture(|w| write_count(&sk, w, 4, 3, 2));
        assert!(out.contains("\"count\":50"));
        assert!(!out.contains("\"minimizer\":"));
    }

    #[test]
    fn count_fields_present() {
        let sk = make(b"ACGTACGTACGT");
        let out = capture(|w| write_count(&sk, w, 4, 3, 9));
        assert!(out.contains("\"seq_length\":12"));
        assert!(out.contains("\"kmer_size\":4"));
        assert!(out.contains("\"minimizer_size\":3"));
        assert!(out.contains("\"partition\":9"));
        assert!(out.contains("\"count\":1"));
    }

    #[test]
    fn count_sequence_line_correct() {
        let sk = make(b"TTTTACGT");
        let out = capture(|w| write_count(&sk, w, 4, 2, 0));
        let lines: Vec<&str> = out.lines().collect();
        assert_eq!(lines[1], "ACGTAAAA");
    }

    // ── write_unitig ──────────────────────────────────────────────────────────

    #[test]
    fn unitig_id_format() {
        obikseq::params::set_k(4);
        let unitig = obikseq::packed_seq::PackedSeq::from_ascii(b"ACGTACGT");
        let out = capture(|w| write_unitig(&unitig, 4, 3, 17, w));
        let id = out.lines().next().unwrap();
        assert!(id.starts_with(">part_00003_unitig_000017"), "got: {id}");
    }

    #[test]
    fn unitig_annotation_fields() {
        obikseq::params::set_k(4);
        let unitig = obikseq::packed_seq::PackedSeq::from_ascii(b"ACGTACGT");
        let out = capture(|w| write_unitig(&unitig, 4, 2, 5, w));
        assert!(out.contains("\"partition\":2"));
        assert!(out.contains("\"unitig_index\":5"));
        assert!(out.contains("\"n_kmers\":5"));
        assert!(out.contains("\"kmer_size\":4"));
    }

    // ── ID stability ──────────────────────────────────────────────────────────

    #[test]
    fn same_sequence_same_id() {
        let sk1 = make(b"ACGTACGT");
        let sk2 = make(b"ACGTACGT");

        let id1 = capture(|w| write_scatter(&sk1, w, 4, 2, 0, Minimizer::from_raw_unchecked(0)))
            .lines()
            .next()
            .unwrap()
            .split_whitespace()
            .next()
            .unwrap()[1..]
            .to_string();
        let id2 = capture(|w| write_scatter(&sk2, w, 4, 2, 0, Minimizer::from_raw_unchecked(0)))
            .lines()
            .next()
            .unwrap()
            .split_whitespace()
            .next()
            .unwrap()[1..]
            .to_string();
        assert_eq!(id1, id2);
    }

    #[test]
    fn different_sequences_different_id() {
        let sk1 = make(b"ACGTACGT");
        let sk2 = make(b"TTTTTTTT");

        let id1 = capture(|w| write_scatter(&sk1, w, 4, 2, 0, Minimizer::from_raw_unchecked(0)))
            .lines().next().unwrap()
            .split_whitespace().next().unwrap()[1..].to_string();
        let id2 = capture(|w| write_scatter(&sk2, w, 4, 2, 0, Minimizer::from_raw_unchecked(0)))
            .lines().next().unwrap()
            .split_whitespace().next().unwrap()[1..].to_string();
        assert_ne!(id1, id2);
    }

    #[test]
    fn id_is_16_hex_digits() {
        let sk = make(b"ACGTACGT");
        let out = capture(|w| write_scatter(&sk, w, 4, 2, 0, Minimizer::from_raw_unchecked(0)));
        let id = &out.lines().next().unwrap()[1..17];
        assert_eq!(id.len(), 16);
        assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
    }
}
