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,
    /// Community membership lifecycle rows (M9p2).
    Membership = 0x0E,
    /// Retroactive-purge / remove-by-scope tombstones (M9p3, reused by M10p3).
    PurgeTombstone = 0x0F,
    /// Agent capability tokens (M10p2).
    Capability = 0x10,
    /// Agent capability revocations (M10p2).
    CapabilityRevocation = 0x11,
    /// Community governance policy versions (M10p1).
    GovernancePolicy = 0x12,
    /// Community signal-aggregate checkpoints (M9).
    Community = 0x13,
    /// Per-(user, item) hard-negative rows (skip/hide/dislike/block).
    ///
    /// Written write-through on the signal path so a `skip`/`dislike` survives a
    /// hard crash with ZERO loss window, and restored into the in-memory
    /// `HardNegIndex` on open. Closes the BLOCKER where hard negatives lived only
    /// in memory and were checkpoint-free entirely.
    HardNeg = 0x14,
    /// Per-(user, item) completion ratio and saved/liked/save-timestamp rows.
    ///
    /// Backs `UserStateIndex`'s completion / saved / liked / save-timestamp maps
    /// so the `InProgress` filter and `DateSaved` sort survive a crash. Written
    /// write-through on the signal path and restored on open.
    UserState = 0x15,
    /// Per-user preference (taste) vector + update count checkpoints.
    ///
    /// Periodically checkpointed (bounded post-crash loss window) and restored on
    /// open so personalized ranking does not snap back to cold-start after a crash.
    Preference = 0x16,
    /// Agent/writer-scoped purge tombstones (M10p3 remove-by-agent).
    ///
    /// Mirrors [`PurgeTombstone`](Self::PurgeTombstone) but keyed on
    /// `(community, writer_agent)` so an agent-scope purge survives a crash before
    /// the next community checkpoint, replayed on open exactly like the user
    /// tombstone.
    AgentPurgeTombstone = 0x17,
    /// Follower replication high-water-mark checkpoint (M8p2 durability).
    ///
    /// A single row at entity ID 0 holds the JSON-serialized per-shard
    /// `applied_seqno` ([`ReplicationState::to_checkpoint_bytes`]). The periodic
    /// checkpoint thread and shutdown persist it; open restores it via
    /// [`ReplicationState::from_checkpoint_bytes`] so a follower's idempotency
    /// boundary survives a crash. Without it, a restarted follower re-applies
    /// every re-shipped segment from seqno 0 and double-counts replicated state.
    ///
    /// [`ReplicationState::to_checkpoint_bytes`]: crate::replication::state::ReplicationState::to_checkpoint_bytes
    /// [`ReplicationState::from_checkpoint_bytes`]: crate::replication::state::ReplicationState::from_checkpoint_bytes
    ReplicationState = 0x18,
    /// Tenant migration routing-state checkpoint (M8p5 durability).
    ///
    /// A single row at entity ID 0 holds the JSON-serialized per-tenant
    /// dual-write pairs and pinned-shard map
    /// ([`TenantRouter::to_checkpoint_bytes`]). Persisted whenever a migration
    /// enters dual-write or finalizes, and restored on open via
    /// [`TenantRouter::restore_from_checkpoint_bytes`] so a crash mid-migration
    /// does not silently revert a finalized tenant to jump-hash routing.
    ///
    /// [`TenantRouter::to_checkpoint_bytes`]: crate::replication::tenant::TenantRouter::to_checkpoint_bytes
    /// [`TenantRouter::restore_from_checkpoint_bytes`]: crate::replication::tenant::TenantRouter::restore_from_checkpoint_bytes
    MigrationRouting = 0x19,
}

impl Tag {
    /// Every `Tag` variant, in discriminant order.
    ///
    /// Single source of truth for "all tags": the `all_byte_mappings_consistent`
    /// drift guard derives the valid discriminant range from this array and
    /// asserts that `as_byte` / `from_byte` agree for every variant, so adding a
    /// variant here (and to `from_byte`) is the only edit a new tag needs — the
    /// hand-maintained per-test arrays are gone.
    pub const ALL: [Self; 25] = [
        Self::Evt,
        Self::Sig,
        Self::Meta,
        Self::Rel,
        Self::Mv,
        Self::Idx,
        Self::Session,
        Self::Cohort,
        Self::CoEngagement,
        Self::Collection,
        Self::SavedSearch,
        Self::CohortDef,
        Self::SchemaFingerprint,
        Self::Membership,
        Self::PurgeTombstone,
        Self::Capability,
        Self::CapabilityRevocation,
        Self::GovernancePolicy,
        Self::Community,
        Self::HardNeg,
        Self::UserState,
        Self::Preference,
        Self::AgentPurgeTombstone,
        Self::ReplicationState,
        Self::MigrationRouting,
    ];

    /// 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),
            0x0E => Some(Self::Membership),
            0x0F => Some(Self::PurgeTombstone),
            0x10 => Some(Self::Capability),
            0x11 => Some(Self::CapabilityRevocation),
            0x12 => Some(Self::GovernancePolicy),
            0x13 => Some(Self::Community),
            0x14 => Some(Self::HardNeg),
            0x15 => Some(Self::UserState),
            0x16 => Some(Self::Preference),
            0x17 => Some(Self::AgentPurgeTombstone),
            0x18 => Some(Self::ReplicationState),
            0x19 => Some(Self::MigrationRouting),
            _ => 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());
    }

    /// Highest valid tag discriminant — derived from `Tag::ALL` so the proptests
    /// below stay in lockstep with the variant list and can never drift to a
    /// stale range (the prior bug: one proptest swept `1..=23` while another swept
    /// `1..=25`, silently skipping the two newest tags).
    const MAX_TAG_BYTE: u8 = Tag::ALL[Tag::ALL.len() - 1].as_byte();

    #[test]
    fn tag_byte_never_zero() {
        // The tag byte must never be 0x00 (NUL) to avoid collision with separator
        for tag in Tag::ALL {
            assert_ne!(tag.as_byte(), 0x00, "tag {tag:?} must not be NUL");
        }
    }

    #[test]
    fn tag_roundtrip_all_variants() {
        for tag in Tag::ALL {
            let byte = tag.as_byte();
            let parsed = Tag::from_byte(byte).expect("valid tag byte");
            assert_eq!(parsed, tag);
        }
    }

    #[test]
    fn all_byte_mappings_consistent() {
        // Drift guard for the hand-maintained as_byte/from_byte parallel tables.
        // `Tag::ALL` is the single source of truth. We assert the discriminants
        // are exactly the contiguous range [0x01, MAX_TAG_BYTE] (so a gap or an
        // out-of-order insert is caught), every byte in that range round-trips,
        // and no byte outside it parses — which keeps the proptest range
        // (1..=MAX_TAG_BYTE) provably exhaustive.
        let bytes: Vec<u8> = Tag::ALL.iter().map(|t| t.as_byte()).collect();
        let expected: Vec<u8> = (0x01..=MAX_TAG_BYTE).collect();
        assert_eq!(
            bytes, expected,
            "Tag::ALL discriminants must be the contiguous range 0x01..={MAX_TAG_BYTE:#04x} in order"
        );
        for b in 0x01..=MAX_TAG_BYTE {
            assert_eq!(
                Tag::from_byte(b).map(Tag::as_byte),
                Some(b),
                "byte {b:#04x} must round-trip through from_byte/as_byte"
            );
        }
        assert!(
            Tag::from_byte(MAX_TAG_BYTE + 1).is_none(),
            "byte just past the range must not parse"
        );
        assert!(
            Tag::from_byte(0x00).is_none(),
            "NUL must not parse as a 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 bytes: Vec<u8> = Tag::ALL.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 proptest::prelude::*;

        use super::*;

        proptest! {
            #[test]
            fn encode_parse_roundtrip(id_val: u64, tag_byte in 1u8..=MAX_TAG_BYTE, 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..=MAX_TAG_BYTE, 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));
            }
        }
    }
}
