//! Consensus-related SAM tags for reads generated by consensus calling tools.
//!
//! This module encapsulates the various consensus related tags that are added to consensus reads
//! at both the per-read and per-base level.
//!
//! Contains tags for both single-strand and duplex consensus reads. The first letter in the tag is:
//! - `c` if it relates to the final consensus read
//! - `a` if it relates to the AB/top-strand of a duplex consensus read
//! - `b` if it relates to the BA/bottom-strand of a duplex consensus read
//!
//! The second letter in the tag is:
//! - Lower case if it is per-base
//! - Upper case if it is per-read
//!
//! ```text
//! Value                 AB  BA  Final
//! --------------------- --  --  -----
//! per-read-depth        aD  bD  cD
//! per-read-min-depth    aM  bM  cM
//! per-read-error-rate   aE  bE  cE
//! per-base-depth        ad  bd  cd
//! per-base-error-count  ae  be  ce
//! per-base-bases        ac  bc  bases
//! per-base-quals        aq  bq  quals
//! ```

use noodles::sam::alignment::record::data::field::Tag;
use noodles::sam::alignment::record_buf::data::field::value::Value;

/// Converts a two-character tag string to a noodles `Tag`.
///
/// # Panics
///
/// Panics if `s` is not exactly 2 ASCII bytes.
#[must_use]
pub fn tag(s: &str) -> Tag {
    let bytes = s.as_bytes();
    assert!(
        bytes.len() == 2 && bytes[0].is_ascii() && bytes[1].is_ascii(),
        "SAM tag must be exactly 2 ASCII bytes: {s:?}"
    );
    Tag::from([bytes[0], bytes[1]])
}

/// Converts a sequence (`Vec<u8>`) to a String value for SAM tags.
///
/// This matches fgbio's format for ac/bc tags (Z string type).
#[must_use]
pub fn sequence_to_tag_value(bases: &[u8]) -> Value {
    Value::from(String::from_utf8_lossy(bases).to_string())
}

/// Converts quality scores (`Vec<u8>`) to an ASCII Phred+33 String value for SAM tags.
///
/// This matches fgbio's format for aq/bq tags (Z string type).
/// Each quality score is converted to its Phred+33 ASCII representation.
#[must_use]
pub fn qualities_to_tag_value(quals: &[u8]) -> Value {
    let ascii_quals: String = quals.iter().map(|&q| (q + 33) as char).collect();
    Value::from(ascii_quals)
}

/// Default field in which to look for UMI sequences
pub const UMI_BASES: &str = "RX";

/// Default field in which to look for UMI qualities
pub const UMI_QUALS: &str = "QX";

/// Field in which the original UMI bases are stored post-correction
pub const ORIGINAL_UMI_BASES: &str = "OX";

/// Field in which the original UMI qualities are stored post-correction
pub const ORIGINAL_UMI_QUALS: &str = "BZ";

/// Post-grouping ID that is file-wide unique per source molecule
pub const MOLECULAR_ID: &str = "MI";

/// Per-base consensus tags
pub mod per_base {
    /// Converts a tag string to a noodles Tag (delegates to top-level [`super::tag`])
    #[must_use]
    pub fn tag(s: &str) -> super::Tag {
        super::tag(s)
    }

    /// The per-base number of raw-reads contributing to the consensus (stored as a short[])
    pub const RAW_READ_COUNT: &str = "cd"; // consensus depth

    /// The number of bases at each position that disagreed with the final consensus call (stored as a short[])
    /// If the final consensus call is a no call (N), then we use the most likely consensus base instead
    pub const RAW_READ_ERRORS: &str = "ce"; // consensus errors

    // Duplex versions of the above tags for the two single strand consensus reads
    /// The per-base number of raw reads contributing to the AB single-strand consensus
    pub const AB_RAW_READ_COUNT: &str = "ad";

    /// The per-base number of raw reads contributing to the BA single-strand consensus
    pub const BA_RAW_READ_COUNT: &str = "bd";

    /// The number of bases disagreeing with the AB single-strand consensus
    pub const AB_RAW_READ_ERRORS: &str = "ae";

    /// The number of bases disagreeing with the BA single-strand consensus
    pub const BA_RAW_READ_ERRORS: &str = "be";

    // Duplex-specific tags
    /// The single-stranded consensus bases from the AB raw reads
    pub const AB_CONSENSUS_BASES: &str = "ac";

    /// The single-stranded consensus bases from the BA raw reads
    pub const BA_CONSENSUS_BASES: &str = "bc";

    /// The phred-scaled qualities (as phred-33 ascii values) of the AB single-strand consensus
    pub const AB_CONSENSUS_QUALS: &str = "aq";

    /// The phred-scaled qualities (as phred-33 ascii values) of the BA single-strand consensus
    pub const BA_CONSENSUS_QUALS: &str = "bq";

    /// All per-base tags that need to be reversed for negative-strand reads
    pub const TAGS_TO_REVERSE: &[&str] = &[
        RAW_READ_COUNT,
        RAW_READ_ERRORS,
        AB_RAW_READ_COUNT,
        AB_RAW_READ_ERRORS,
        BA_RAW_READ_COUNT,
        BA_RAW_READ_ERRORS,
        AB_CONSENSUS_QUALS,
        BA_CONSENSUS_QUALS,
    ];

    /// All per-base tags that need to be reverse complemented for negative-strand reads
    pub const TAGS_TO_REVERSE_COMPLEMENT: &[&str] = &[AB_CONSENSUS_BASES, BA_CONSENSUS_BASES];

    /// All per-base tags
    pub const ALL_TAGS: &[&str] = &[
        RAW_READ_COUNT,
        RAW_READ_ERRORS,
        AB_RAW_READ_COUNT,
        AB_RAW_READ_ERRORS,
        BA_RAW_READ_COUNT,
        BA_RAW_READ_ERRORS,
        AB_CONSENSUS_QUALS,
        BA_CONSENSUS_QUALS,
        AB_CONSENSUS_BASES,
        BA_CONSENSUS_BASES,
    ];

    /// Returns all per-base tags that need to be reversed for negative-strand reads
    #[must_use]
    pub fn tags_to_reverse() -> &'static [&'static str] {
        TAGS_TO_REVERSE
    }

    /// Returns all per-base tags that need to be reverse complemented for negative-strand reads
    #[must_use]
    pub fn tags_to_reverse_complement() -> &'static [&'static str] {
        TAGS_TO_REVERSE_COMPLEMENT
    }

    /// Returns all per-base tags
    #[must_use]
    pub fn all_tags() -> &'static [&'static str] {
        ALL_TAGS
    }
}

/// Per-read consensus tags
pub mod per_read {
    /// Converts a tag string to a noodles Tag (delegates to top-level [`super::tag`])
    #[must_use]
    pub fn tag(s: &str) -> super::Tag {
        super::tag(s)
    }

    /// The number of raw reads that contributed to the consensus (max of per-base counts)
    pub const RAW_READ_COUNT: &str = "cD"; // consensus Depth

    /// The minimum number of raw reads contributing to a consensus call anywhere in the read
    pub const MIN_RAW_READ_COUNT: &str = "cM"; // consensus Min-depth

    /// The fraction of bases in raw reads that disagreed with the consensus call
    pub const RAW_READ_ERROR_RATE: &str = "cE"; // consensus Error rate

    // Duplex versions of the above tags for the two single strand consensus reads
    /// The max depth of raw reads contributing to the AB single-strand consensus
    pub const AB_RAW_READ_COUNT: &str = "aD";

    /// The max depth of raw reads contributing to the BA single-strand consensus
    pub const BA_RAW_READ_COUNT: &str = "bD";

    /// The min depth of raw reads contributing to the AB single-strand consensus
    pub const AB_MIN_RAW_READ_COUNT: &str = "aM";

    /// The min depth of raw reads contributing to the BA single-strand consensus
    pub const BA_MIN_RAW_READ_COUNT: &str = "bM";

    /// The error rate for the AB single-strand consensus
    pub const AB_RAW_READ_ERROR_RATE: &str = "aE";

    /// The error rate for the BA single-strand consensus
    pub const BA_RAW_READ_ERROR_RATE: &str = "bE";

    /// All per-read tags
    pub const ALL_TAGS: &[&str] = &[
        RAW_READ_COUNT,
        MIN_RAW_READ_COUNT,
        RAW_READ_ERROR_RATE,
        AB_RAW_READ_COUNT,
        BA_RAW_READ_COUNT,
        AB_MIN_RAW_READ_COUNT,
        BA_MIN_RAW_READ_COUNT,
        AB_RAW_READ_ERROR_RATE,
        BA_RAW_READ_ERROR_RATE,
    ];

    /// Returns all per-read tags
    #[must_use]
    pub fn all_tags() -> &'static [&'static str] {
        ALL_TAGS
    }
}

/// Checks if a read appears to be a simplex consensus read.
///
/// A simplex consensus read has the `cD` (`RawReadCount`) tag but NOT the duplex tags (`aD` and `bD`).
#[must_use]
pub fn is_simplex_consensus(rec: &impl noodles::sam::alignment::Record) -> bool {
    let has_cd = rec.data().get(&per_read::tag("cD")).is_some();
    has_cd && !is_duplex_consensus(rec)
}

/// Checks if a read appears to be a duplex consensus read.
///
/// A duplex consensus read has both the `aD` (`AbRawReadCount`) and `bD` (`BaRawReadCount`) tags.
#[must_use]
#[expect(
    clippy::similar_names,
    reason = "aD/bD tag names are domain-specific duplex strand identifiers"
)]
pub fn is_duplex_consensus(rec: &impl noodles::sam::alignment::Record) -> bool {
    let has_ad = rec.data().get(&per_read::tag("aD")).is_some();
    let has_bd = rec.data().get(&per_read::tag("bD")).is_some();
    has_ad && has_bd
}

/// Checks if a read appears to be any kind of consensus read (simplex or duplex).
///
/// This matches fgbio's `Umis.isFgbioStyleConsensus()` function.
#[must_use]
pub fn is_consensus(rec: &impl noodles::sam::alignment::Record) -> bool {
    is_simplex_consensus(rec) || is_duplex_consensus(rec)
}

#[cfg(test)]
mod tests {
    use super::*;
    use bstr::ByteSlice;
    use fgumi_sam::builder::RecordBuilder;

    #[test]
    fn test_tag_constants() {
        assert_eq!(UMI_BASES, "RX");
        assert_eq!(MOLECULAR_ID, "MI");
        assert_eq!(per_base::RAW_READ_COUNT, "cd");
        assert_eq!(per_read::RAW_READ_COUNT, "cD");
    }

    #[test]
    fn test_per_base_tags() {
        let tags = per_base::all_tags();
        assert!(tags.contains(&"cd"));
        assert!(tags.contains(&"ce"));
        assert!(tags.contains(&"ad"));
        assert!(tags.contains(&"bd"));
        assert!(tags.contains(&"ac"));
        assert!(tags.contains(&"bc"));
    }

    #[test]
    fn test_per_read_tags() {
        let tags = per_read::all_tags();
        assert!(tags.contains(&"cD"));
        assert!(tags.contains(&"cM"));
        assert!(tags.contains(&"cE"));
        assert!(tags.contains(&"aD"));
        assert!(tags.contains(&"bD"));
    }

    #[test]
    fn test_tag_conversion() {
        let tag = per_base::tag("cd");
        assert_eq!(tag, Tag::from([b'c', b'd']));

        let tag = per_read::tag("cD");
        assert_eq!(tag, Tag::from([b'c', b'D']));
    }

    #[test]
    fn test_is_simplex_consensus() {
        // Create a record with cD tag (simplex consensus)
        let rec = RecordBuilder::new().sequence("ACGT").tag("cD", 10_i32).build();

        assert!(is_simplex_consensus(&rec));
        assert!(!is_duplex_consensus(&rec));
        assert!(is_consensus(&rec));
    }

    #[test]
    fn test_is_duplex_consensus() {
        // Create a record with aD and bD tags (duplex consensus)
        let rec = RecordBuilder::new().sequence("ACGT").tag("aD", 10_i32).tag("bD", 5_i32).build();

        assert!(!is_simplex_consensus(&rec));
        assert!(is_duplex_consensus(&rec));
        assert!(is_consensus(&rec));
    }

    #[test]
    fn test_is_not_consensus() {
        // Create a record without consensus tags (raw read)
        let rec = RecordBuilder::new().sequence("ACGT").tag("RX", "ACGT").build();

        assert!(!is_simplex_consensus(&rec));
        assert!(!is_duplex_consensus(&rec));
        assert!(!is_consensus(&rec));
    }

    // =====================================================================
    // Tests for top-level tag() function
    // =====================================================================

    #[test]
    fn test_top_level_tag_conversion() {
        let t = tag("MI");
        assert_eq!(t, Tag::from([b'M', b'I']));

        let t = tag("RX");
        assert_eq!(t, Tag::from([b'R', b'X']));
    }

    // =====================================================================
    // Tests for sequence_to_tag_value and qualities_to_tag_value
    // =====================================================================

    #[test]
    fn test_sequence_to_tag_value() {
        let bases = b"ACGT";
        let value = sequence_to_tag_value(bases);
        match value {
            noodles::sam::alignment::record_buf::data::field::value::Value::String(s) => {
                assert_eq!(s, "ACGT");
            }
            _ => panic!("Expected String value"),
        }
    }

    #[test]
    fn test_sequence_to_tag_value_empty() {
        let bases = b"";
        let value = sequence_to_tag_value(bases);
        match value {
            noodles::sam::alignment::record_buf::data::field::value::Value::String(s) => {
                assert_eq!(s, "");
            }
            _ => panic!("Expected String value"),
        }
    }

    #[test]
    fn test_qualities_to_tag_value() {
        // Q0 -> '!' (33), Q10 -> '+' (43), Q30 -> '?' (63)
        let quals = vec![0, 10, 30];
        let value = qualities_to_tag_value(&quals);
        match value {
            noodles::sam::alignment::record_buf::data::field::value::Value::String(s) => {
                assert_eq!(s.as_bytes(), &[33, 43, 63]);
            }
            _ => panic!("Expected String value"),
        }
    }

    #[test]
    fn test_qualities_to_tag_value_empty() {
        let quals: Vec<u8> = vec![];
        let value = qualities_to_tag_value(&quals);
        match value {
            noodles::sam::alignment::record_buf::data::field::value::Value::String(s) => {
                assert_eq!(s, "");
            }
            _ => panic!("Expected String value"),
        }
    }

    // =====================================================================
    // Tests for per_base helper functions
    // =====================================================================

    #[test]
    fn test_per_base_tags_to_reverse() {
        let tags = per_base::tags_to_reverse();
        assert!(tags.contains(&"cd"));
        assert!(tags.contains(&"ce"));
        assert!(tags.contains(&"ad"));
        assert!(tags.contains(&"ae"));
        assert!(tags.contains(&"bd"));
        assert!(tags.contains(&"be"));
        assert!(tags.contains(&"aq"));
        assert!(tags.contains(&"bq"));
    }

    #[test]
    fn test_per_base_tags_to_reverse_complement() {
        let tags = per_base::tags_to_reverse_complement();
        assert!(tags.contains(&"ac"));
        assert!(tags.contains(&"bc"));
        assert_eq!(tags.len(), 2);
    }

    // =====================================================================
    // Tests for per_read tag constants
    // =====================================================================

    #[test]
    fn test_per_read_tag_constants() {
        assert_eq!(per_read::RAW_READ_COUNT, "cD");
        assert_eq!(per_read::MIN_RAW_READ_COUNT, "cM");
        assert_eq!(per_read::RAW_READ_ERROR_RATE, "cE");
        assert_eq!(per_read::AB_RAW_READ_COUNT, "aD");
        assert_eq!(per_read::BA_RAW_READ_COUNT, "bD");
        assert_eq!(per_read::AB_MIN_RAW_READ_COUNT, "aM");
        assert_eq!(per_read::BA_MIN_RAW_READ_COUNT, "bM");
        assert_eq!(per_read::AB_RAW_READ_ERROR_RATE, "aE");
        assert_eq!(per_read::BA_RAW_READ_ERROR_RATE, "bE");
    }

    #[test]
    fn test_per_base_tag_constants() {
        assert_eq!(per_base::RAW_READ_COUNT, "cd");
        assert_eq!(per_base::RAW_READ_ERRORS, "ce");
        assert_eq!(per_base::AB_RAW_READ_COUNT, "ad");
        assert_eq!(per_base::BA_RAW_READ_COUNT, "bd");
        assert_eq!(per_base::AB_RAW_READ_ERRORS, "ae");
        assert_eq!(per_base::BA_RAW_READ_ERRORS, "be");
        assert_eq!(per_base::AB_CONSENSUS_BASES, "ac");
        assert_eq!(per_base::BA_CONSENSUS_BASES, "bc");
        assert_eq!(per_base::AB_CONSENSUS_QUALS, "aq");
        assert_eq!(per_base::BA_CONSENSUS_QUALS, "bq");
    }

    // =====================================================================
    // Tests for tag string constants
    // =====================================================================

    #[test]
    fn test_umi_tag_constants() {
        assert_eq!(UMI_BASES, "RX");
        assert_eq!(UMI_QUALS, "QX");
        assert_eq!(ORIGINAL_UMI_BASES, "OX");
        assert_eq!(ORIGINAL_UMI_QUALS, "BZ");
        assert_eq!(MOLECULAR_ID, "MI");
    }

    // =====================================================================
    // Tests for per_read::tag and per_base::tag delegation
    // =====================================================================

    #[test]
    fn test_per_read_tag_function() {
        let t = per_read::tag("cD");
        assert_eq!(t, Tag::from([b'c', b'D']));
    }

    #[test]
    fn test_per_base_tag_function() {
        let t = per_base::tag("cd");
        assert_eq!(t, Tag::from([b'c', b'd']));
    }

    // =====================================================================
    // Test consensus detection with both simplex and duplex tags present
    // =====================================================================

    #[test]
    fn test_is_consensus_with_all_tags() {
        // Record with cD, aD, and bD is duplex, not simplex
        let rec = RecordBuilder::new()
            .sequence("ACGT")
            .tag("cD", 10_i32)
            .tag("aD", 5_i32)
            .tag("bD", 3_i32)
            .build();

        assert!(!is_simplex_consensus(&rec));
        assert!(is_duplex_consensus(&rec));
        assert!(is_consensus(&rec));
    }

    #[test]
    fn test_is_not_duplex_with_only_one_strand() {
        // Record with only aD (no bD) is not duplex
        let rec = RecordBuilder::new().sequence("ACGT").tag("aD", 5_i32).build();

        assert!(!is_simplex_consensus(&rec));
        assert!(!is_duplex_consensus(&rec));
        assert!(!is_consensus(&rec));
    }
}
