//! Typed frame metadata values and schema enforcement.

use std::collections::BTreeMap;
use std::sync::Arc;

use crate::{ErrorCategory, ErrorCode, PixelFlowError, Result};

const CORE_KEYS: [(&str, MetadataKind); 10] = [
    ("core:matrix", MetadataKind::String),
    ("core:transfer", MetadataKind::String),
    ("core:primaries", MetadataKind::String),
    ("core:range", MetadataKind::String),
    ("core:chroma_siting", MetadataKind::String),
    ("core:field_order", MetadataKind::String),
    ("core:frame_number", MetadataKind::Int),
    ("core:duration", MetadataKind::Rational),
    ("core:timecode", MetadataKind::String),
    ("core:source_path", MetadataKind::String),
];

/// Rational metadata value.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Rational {
    /// Numerator.
    pub numerator: i64,
    /// Denominator.
    pub denominator: i64,
}

/// Typed metadata value.
#[derive(Clone, Debug, PartialEq)]
pub enum MetadataValue {
    /// Non-applicable value.
    None,
    /// Boolean value.
    Bool(bool),
    /// Integer value.
    Int(i64),
    /// Floating point value.
    Float(f64),
    /// String value.
    String(String),
    /// Nested array value.
    Array(Vec<MetadataValue>),
    /// Rational value.
    Rational(Rational),
    /// Binary blob value.
    Blob(Arc<[u8]>),
}

impl MetadataValue {
    /// Returns metadata kind for non-None values.
    #[must_use]
    pub const fn kind(&self) -> Option<MetadataKind> {
        match self {
            Self::None => None,
            Self::Bool(_) => Some(MetadataKind::Bool),
            Self::Int(_) => Some(MetadataKind::Int),
            Self::Float(_) => Some(MetadataKind::Float),
            Self::String(_) => Some(MetadataKind::String),
            Self::Array(_) => Some(MetadataKind::Array),
            Self::Rational(_) => Some(MetadataKind::Rational),
            Self::Blob(_) => Some(MetadataKind::Blob),
        }
    }
}

/// Declared metadata type for schema entries.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MetadataKind {
    /// Boolean value.
    Bool,
    /// Integer value.
    Int,
    /// Floating point value.
    Float,
    /// String value.
    String,
    /// Array value.
    Array,
    /// Rational value.
    Rational,
    /// Binary blob value.
    Blob,
}

/// Metadata schema for core keys and plugin extension keys.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MetadataSchema {
    core: BTreeMap<String, MetadataKind>,
    plugin: BTreeMap<String, MetadataKind>,
}

impl MetadataSchema {
    /// Creates schema containing all `core:*` keys.
    #[must_use]
    pub fn core() -> Self {
        let core = CORE_KEYS
            .into_iter()
            .map(|(key, kind)| (key.to_owned(), kind))
            .collect();
        Self {
            core,
            plugin: BTreeMap::new(),
        }
    }

    /// Registers a plugin metadata key in `publisher/plugin:key` format.
    pub fn register_plugin_key(&mut self, key: &str, kind: MetadataKind) -> Result<()> {
        if !is_plugin_key(key) {
            return Err(PixelFlowError::new(
                ErrorCategory::Plugin,
                ErrorCode::new("metadata.invalid_plugin_key"),
                format!("invalid plugin metadata key '{key}'"),
            ));
        }

        self.plugin.insert(key.to_owned(), kind);
        Ok(())
    }

    /// Returns true when key is registered in schema.
    #[must_use]
    pub fn contains_key(&self, key: &str) -> bool {
        self.kind_for(key).is_some()
    }

    /// Returns declared metadata kind for registered key.
    #[must_use]
    pub fn kind(&self, key: &str) -> Option<MetadataKind> {
        self.kind_for(key).map(|(kind, _is_core)| kind)
    }

    /// Returns true when key is one of built-in `core:*` keys.
    #[must_use]
    pub fn is_core_key(&self, key: &str) -> bool {
        self.core.contains_key(key)
    }

    /// Validates that key is registered and value matches declared kind.
    pub fn validate_value(&self, key: &str, value: &MetadataValue) -> Result<()> {
        let Some((expected_kind, is_core)) = self.kind_for(key) else {
            return Err(PixelFlowError::new(
                ErrorCategory::Plugin,
                ErrorCode::new("metadata.unregistered_key"),
                format!("metadata key '{key}' is not registered"),
            ));
        };

        if let Some(actual_kind) = value.kind()
            && actual_kind != expected_kind
        {
            let category = if is_core {
                ErrorCategory::Core
            } else {
                ErrorCategory::Plugin
            };
            return Err(PixelFlowError::new(
                category,
                ErrorCode::new("metadata.type_mismatch"),
                format!(
                    "metadata key '{key}' expects {:?}, got {:?}",
                    expected_kind, actual_kind
                ),
            ));
        }

        Ok(())
    }

    pub(crate) fn kind_for(&self, key: &str) -> Option<(MetadataKind, bool)> {
        if let Some(kind) = self.core.get(key).copied() {
            return Some((kind, true));
        }
        self.plugin.get(key).copied().map(|kind| (kind, false))
    }

    pub(crate) fn core_keys(&self) -> impl Iterator<Item = &str> {
        self.core.keys().map(String::as_str)
    }
}

/// Typed metadata map validated against a [`MetadataSchema`].
#[derive(Clone, Debug, PartialEq)]
pub struct Metadata {
    values: BTreeMap<String, MetadataValue>,
}

impl Metadata {
    /// Creates metadata map with every core key pre-populated as `None`.
    #[must_use]
    pub fn new(schema: &MetadataSchema) -> Self {
        let values = schema
            .core_keys()
            .map(|key| (key.to_owned(), MetadataValue::None))
            .collect();
        Self { values }
    }

    /// Returns metadata value by key.
    #[must_use]
    pub fn get(&self, key: &str) -> Option<&MetadataValue> {
        self.values.get(key)
    }

    /// Clears registered metadata key by writing `None`.
    pub fn clear(&mut self, schema: &MetadataSchema, key: &str) -> Result<()> {
        self.set(schema, key, MetadataValue::None)
    }

    /// Iterates metadata entries in deterministic key order.
    pub fn iter(&self) -> impl Iterator<Item = (&str, &MetadataValue)> + '_ {
        self.values.iter().map(|(key, value)| (key.as_str(), value))
    }

    /// Writes metadata value after registration and type validation.
    pub fn set(&mut self, schema: &MetadataSchema, key: &str, value: MetadataValue) -> Result<()> {
        schema.validate_value(key, &value)?;
        self.values.insert(key.to_owned(), value);
        Ok(())
    }
}

fn is_plugin_key(key: &str) -> bool {
    let Some((namespace, field)) = key.split_once(':') else {
        return false;
    };
    let Some((publisher, plugin)) = namespace.split_once('/') else {
        return false;
    };
    is_key_component(publisher) && is_key_component(plugin) && is_key_component(field)
}

fn is_key_component(component: &str) -> bool {
    !component.is_empty()
        && component
            .bytes()
            .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
}

#[cfg(test)]
mod tests {
    use crate::{ErrorCategory, ErrorCode};

    use super::{Metadata, MetadataKind, MetadataSchema, MetadataValue, Rational};

    #[test]
    fn core_metadata_keys_are_always_present_as_none() {
        let schema = MetadataSchema::core();
        let metadata = Metadata::new(&schema);

        for key in [
            "core:matrix",
            "core:transfer",
            "core:primaries",
            "core:range",
            "core:chroma_siting",
            "core:field_order",
            "core:frame_number",
            "core:duration",
            "core:timecode",
            "core:source_path",
        ] {
            assert_eq!(metadata.get(key), Some(&MetadataValue::None));
        }
    }

    #[test]
    fn plugin_metadata_write_requires_registered_key() {
        let schema = MetadataSchema::core();
        let mut metadata = Metadata::new(&schema);

        let error = metadata
            .set(&schema, "acme/filter:strength", MetadataValue::Float(0.5))
            .expect_err("unregistered plugin key should fail");

        assert_eq!(error.category(), ErrorCategory::Plugin);
        assert_eq!(error.code(), ErrorCode::new("metadata.unregistered_key"));
    }

    #[test]
    fn plugin_metadata_write_accepts_registered_key_and_type() {
        let mut schema = MetadataSchema::core();
        schema
            .register_plugin_key("acme/filter:strength", MetadataKind::Float)
            .expect("plugin key should register");
        let mut metadata = Metadata::new(&schema);

        metadata
            .set(&schema, "acme/filter:strength", MetadataValue::Float(0.5))
            .expect("registered key should accept matching value");

        assert_eq!(
            metadata.get("acme/filter:strength"),
            Some(&MetadataValue::Float(0.5))
        );
    }

    #[test]
    fn mismatched_metadata_type_returns_structured_error() {
        let schema = MetadataSchema::core();
        let mut metadata = Metadata::new(&schema);

        let error = metadata
            .set(
                &schema,
                "core:frame_number",
                MetadataValue::String("zero".to_owned()),
            )
            .expect_err("wrong type should fail");

        assert_eq!(error.category(), ErrorCategory::Core);
        assert_eq!(error.code(), ErrorCode::new("metadata.type_mismatch"));
    }

    #[test]
    fn metadata_supports_rational_array_and_blob_values() {
        let mut schema = MetadataSchema::core();
        schema
            .register_plugin_key("acme/filter:ratios", MetadataKind::Array)
            .expect("array key should register");
        schema
            .register_plugin_key("acme/filter:payload", MetadataKind::Blob)
            .expect("blob key should register");
        let mut metadata = Metadata::new(&schema);

        metadata
            .set(
                &schema,
                "acme/filter:ratios",
                MetadataValue::Array(vec![MetadataValue::Rational(Rational {
                    numerator: 1,
                    denominator: 2,
                })]),
            )
            .expect("array value should be accepted");
        metadata
            .set(
                &schema,
                "acme/filter:payload",
                MetadataValue::Blob(vec![1_u8, 2, 3].into()),
            )
            .expect("blob value should be accepted");

        assert!(matches!(
            metadata.get("acme/filter:ratios"),
            Some(MetadataValue::Array(_))
        ));
        assert!(matches!(
            metadata.get("acme/filter:payload"),
            Some(MetadataValue::Blob(_))
        ));
    }

    #[test]
    fn metadata_schema_exposes_registered_kind_and_namespace() {
        let mut schema = MetadataSchema::core();
        schema
            .register_plugin_key("acme/filter:enabled", MetadataKind::Bool)
            .expect("plugin key should register");

        assert_eq!(schema.kind("core:frame_number"), Some(MetadataKind::Int));
        assert_eq!(schema.kind("acme/filter:enabled"), Some(MetadataKind::Bool));
        assert!(schema.is_core_key("core:frame_number"));
        assert!(!schema.is_core_key("acme/filter:enabled"));
        assert_eq!(schema.kind("missing"), None);
    }

    #[test]
    fn metadata_clear_sets_registered_key_to_none() {
        let mut schema = MetadataSchema::core();
        schema
            .register_plugin_key("acme/filter:enabled", MetadataKind::Bool)
            .expect("plugin key should register");
        let mut metadata = Metadata::new(&schema);
        metadata
            .set(&schema, "acme/filter:enabled", MetadataValue::Bool(true))
            .expect("registered key should set");

        metadata
            .clear(&schema, "acme/filter:enabled")
            .expect("registered key should clear");

        assert_eq!(
            metadata.get("acme/filter:enabled"),
            Some(&MetadataValue::None)
        );
    }

    #[test]
    fn metadata_iter_is_deterministic_and_sorted() {
        let mut schema = MetadataSchema::core();
        schema
            .register_plugin_key("acme/filter:enabled", MetadataKind::Bool)
            .expect("plugin key should register");
        let mut metadata = Metadata::new(&schema);
        metadata
            .set(&schema, "acme/filter:enabled", MetadataValue::Bool(true))
            .expect("registered key should set");

        let keys = metadata.iter().map(|(key, _value)| key).collect::<Vec<_>>();

        assert_eq!(keys.first().copied(), Some("acme/filter:enabled"));
        assert!(
            keys.windows(2)
                .all(|pair| matches!(pair, [left, right] if left < right))
        );
    }
}
