use crate::schema::EntityId;

/// Separator byte between entity ID and tag in encoded keys.
const NUL: u8 = 0x00;

/// Key tag discriminant.
///
/// Each tag maps to a single byte that identifies the data category
/// within an entity's key range. Tags are chosen to never collide
/// with the `NUL` separator byte (`0x00`).
///
/// Layout: `[entity_id: 8 bytes BE][0x00][tag: 1 byte][suffix bytes...]`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Tag {
    /// Raw signal event log.
    Evt = 0x01,
    /// Running decay scores, window counts.
    Sig = 0x02,
    /// Entity metadata.
    Meta = 0x03,
    /// Relationship edges.
    Rel = 0x04,
    /// Materialized view aggregates.
    Mv = 0x05,
    /// Secondary indexes.
    Idx = 0x06,
    /// Session archives (snapshots, audit logs).
    Session = 0x07,
    /// Cohort signal state checkpoints (M6).
    Cohort = 0x08,
    /// Co-engagement edge checkpoints (M6).
    CoEngagement = 0x09,
    /// User collections (M6p4).
    Collection = 0x0A,
    /// Saved searches (M6p4).
    SavedSearch = 0x0B,
    /// Cohort definitions (M6 persistence).
    CohortDef = 0x0C,
    /// Schema fingerprint sentinel (stored at entity ID 0).
    SchemaFingerprint = 0x0D,
}

impl Tag {
    /// Parse a tag byte back into a `Tag` variant.
    #[must_use]
    pub const fn from_byte(b: u8) -> Option<Self> {
        match b {
            0x01 => Some(Self::Evt),
            0x02 => Some(Self::Sig),
            0x03 => Some(Self::Meta),
            0x04 => Some(Self::Rel),
            0x05 => Some(Self::Mv),
            0x06 => Some(Self::Idx),
            0x07 => Some(Self::Session),
            0x08 => Some(Self::Cohort),
            0x09 => Some(Self::CoEngagement),
            0x0A => Some(Self::Collection),
            0x0B => Some(Self::SavedSearch),
            0x0C => Some(Self::CohortDef),
            0x0D => Some(Self::SchemaFingerprint),
            _ => None,
        }
    }

    /// The discriminant byte for this tag.
    #[must_use]
    pub const fn as_byte(self) -> u8 {
        self as u8
    }
}

/// Encode a storage key from its components.
///
/// Format: `[entity_id: 8 bytes BE][0x00][tag: 1 byte][suffix...]`
#[must_use]
#[allow(clippy::missing_const_for_fn)] // Vec allocation
pub fn encode_key(entity_id: EntityId, tag: Tag, suffix: &[u8]) -> Vec<u8> {
    let mut key = Vec::with_capacity(8 + 1 + 1 + suffix.len());
    key.extend_from_slice(&entity_id.to_be_bytes());
    key.push(NUL);
    key.push(tag.as_byte());
    key.extend_from_slice(suffix);
    key
}

/// Build the 9-byte entity prefix for prefix scans.
///
/// Returns `[entity_id: 8 bytes BE][0x00]` — all keys for this entity
/// start with this prefix.
#[must_use]
pub const fn entity_prefix(entity_id: EntityId) -> [u8; 9] {
    let be = entity_id.to_be_bytes();
    [be[0], be[1], be[2], be[3], be[4], be[5], be[6], be[7], NUL]
}

/// Build a 10-byte entity+tag prefix for tag-scoped scans.
///
/// Returns `[entity_id: 8 bytes BE][0x00][tag]`.
#[must_use]
pub const fn entity_tag_prefix(entity_id: EntityId, tag: Tag) -> [u8; 10] {
    let be = entity_id.to_be_bytes();
    [
        be[0],
        be[1],
        be[2],
        be[3],
        be[4],
        be[5],
        be[6],
        be[7],
        NUL,
        tag.as_byte(),
    ]
}

/// Parse a key back into its components.
///
/// Returns `(entity_id, tag, suffix)` or `None` if the key is malformed.
#[must_use]
pub fn parse_key(key: &[u8]) -> Option<(EntityId, Tag, &[u8])> {
    // Minimum: 8 (entity_id) + 1 (NUL) + 1 (tag) = 10 bytes
    if key.len() < 10 {
        return None;
    }

    // Verify separator
    if key[8] != NUL {
        return None;
    }

    let entity_id = EntityId::new(u64::from_be_bytes([
        key[0], key[1], key[2], key[3], key[4], key[5], key[6], key[7],
    ]));

    let tag = Tag::from_byte(key[9])?;
    let suffix = &key[10..];

    Some((entity_id, tag, suffix))
}

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

    #[test]
    fn encode_and_parse_roundtrip() {
        let id = EntityId::new(42);
        let suffix = b"hello";
        let key = encode_key(id, Tag::Evt, suffix);

        let (parsed_id, parsed_tag, parsed_suffix) = parse_key(&key).expect("valid key");
        assert_eq!(parsed_id, id);
        assert_eq!(parsed_tag, Tag::Evt);
        assert_eq!(parsed_suffix, suffix);
    }

    #[test]
    fn encode_key_length() {
        let id = EntityId::new(1);
        let key = encode_key(id, Tag::Meta, b"");
        // 8 (entity_id) + 1 (NUL) + 1 (tag) = 10
        assert_eq!(key.len(), 10);

        let key_with_suffix = encode_key(id, Tag::Sig, b"extra");
        assert_eq!(key_with_suffix.len(), 15);
    }

    #[test]
    fn entity_prefix_is_first_9_bytes() {
        let id = EntityId::new(1000);
        let prefix = entity_prefix(id);
        let key = encode_key(id, Tag::Evt, b"suffix");

        assert_eq!(&key[..9], &prefix);
    }

    #[test]
    fn entity_tag_prefix_is_first_10_bytes() {
        let id = EntityId::new(1000);
        let prefix = entity_tag_prefix(id, Tag::Sig);
        let key = encode_key(id, Tag::Sig, b"suffix");

        assert_eq!(&key[..10], &prefix);
    }

    #[test]
    fn keys_for_same_entity_share_prefix() {
        let id = EntityId::new(99);
        let prefix = entity_prefix(id);
        let k1 = encode_key(id, Tag::Evt, b"a");
        let k2 = encode_key(id, Tag::Sig, b"b");
        let k3 = encode_key(id, Tag::Meta, b"");

        assert!(k1.starts_with(&prefix));
        assert!(k2.starts_with(&prefix));
        assert!(k3.starts_with(&prefix));
    }

    #[test]
    fn different_entities_different_prefixes() {
        let p1 = entity_prefix(EntityId::new(1));
        let p2 = entity_prefix(EntityId::new(2));
        assert_ne!(p1, p2);
    }

    #[test]
    fn parse_key_too_short() {
        assert!(parse_key(b"short").is_none());
        assert!(parse_key(&[0; 9]).is_none()); // exactly 9 bytes, need 10
    }

    #[test]
    fn parse_key_bad_separator() {
        let mut key = encode_key(EntityId::new(1), Tag::Evt, b"");
        key[8] = 0xFF; // corrupt separator
        assert!(parse_key(&key).is_none());
    }

    #[test]
    fn parse_key_bad_tag() {
        let mut key = encode_key(EntityId::new(1), Tag::Evt, b"");
        key[9] = 0xFF; // invalid tag
        assert!(parse_key(&key).is_none());
    }

    #[test]
    fn tag_byte_never_zero() {
        // The tag byte must never be 0x00 (NUL) to avoid collision with separator
        let tags = [
            Tag::Evt,
            Tag::Sig,
            Tag::Meta,
            Tag::Rel,
            Tag::Mv,
            Tag::Idx,
            Tag::Session,
            Tag::Cohort,
            Tag::CoEngagement,
            Tag::Collection,
            Tag::SavedSearch,
            Tag::CohortDef,
            Tag::SchemaFingerprint,
        ];
        for tag in tags {
            assert_ne!(tag.as_byte(), 0x00, "tag {tag:?} must not be NUL");
        }
    }

    #[test]
    fn tag_roundtrip_all_variants() {
        let tags = [
            Tag::Evt,
            Tag::Sig,
            Tag::Meta,
            Tag::Rel,
            Tag::Mv,
            Tag::Idx,
            Tag::Session,
            Tag::Cohort,
            Tag::CoEngagement,
            Tag::Collection,
            Tag::SavedSearch,
            Tag::CohortDef,
            Tag::SchemaFingerprint,
        ];
        for tag in tags {
            let byte = tag.as_byte();
            let parsed = Tag::from_byte(byte).expect("valid tag byte");
            assert_eq!(parsed, tag);
        }
    }

    #[test]
    fn entity_prefix_ordering_matches_numeric() {
        let id_a = EntityId::new(100);
        let id_b = EntityId::new(200);
        let prefix_a = entity_prefix(id_a);
        let prefix_b = entity_prefix(id_b);
        assert!(prefix_a < prefix_b);
    }

    #[test]
    fn all_tags_have_unique_bytes() {
        let tags = [
            Tag::Evt,
            Tag::Sig,
            Tag::Meta,
            Tag::Rel,
            Tag::Mv,
            Tag::Idx,
            Tag::Session,
            Tag::Cohort,
            Tag::CoEngagement,
            Tag::Collection,
            Tag::SavedSearch,
            Tag::CohortDef,
            Tag::SchemaFingerprint,
        ];
        let bytes: Vec<u8> = tags.iter().map(|t| t.as_byte()).collect();
        let mut deduped = bytes.clone();
        deduped.sort_unstable();
        deduped.dedup();
        assert_eq!(bytes.len(), deduped.len(), "tag bytes must be unique");
    }

    mod proptests {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            #[test]
            fn encode_parse_roundtrip(id_val: u64, tag_byte in 1u8..=13u8, suffix in proptest::collection::vec(any::<u8>(), 0..100)) {
                let id = EntityId::new(id_val);
                let tag = Tag::from_byte(tag_byte).unwrap();
                let key = encode_key(id, tag, &suffix);

                let (parsed_id, parsed_tag, parsed_suffix) = parse_key(&key).unwrap();
                prop_assert_eq!(parsed_id, id);
                prop_assert_eq!(parsed_tag, tag);
                prop_assert_eq!(parsed_suffix, suffix.as_slice());
            }

            #[test]
            fn entity_prefix_ordering(a: u64, b: u64) {
                let prefix_a = entity_prefix(EntityId::new(a));
                let prefix_b = entity_prefix(EntityId::new(b));
                prop_assert_eq!(a.cmp(&b), prefix_a.cmp(&prefix_b));
            }

            #[test]
            fn keys_share_entity_prefix(id_val: u64, tag_byte in 1u8..=13u8, suffix in proptest::collection::vec(any::<u8>(), 0..50)) {
                let id = EntityId::new(id_val);
                let tag = Tag::from_byte(tag_byte).unwrap();
                let prefix = entity_prefix(id);
                let key = encode_key(id, tag, &suffix);
                prop_assert!(key.starts_with(&prefix));
            }
        }
    }
}
