//! Filter and plugin capability registry types.

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

use crate::{
    ClipMedia, ConcurrencyClass, DependencyPattern, ErrorCategory, ErrorCode, FilterCompatibility,
    MetadataKind, MetadataSchema, PixelFlowError, Rational, Result,
};

/// Generic script-provided option value for filter planning.
#[derive(Clone, Debug, PartialEq)]
pub enum FilterOptionValue {
    /// Rhai `()` / `none()` option.
    None,
    /// String option.
    String(String),
    /// Boolean option.
    Bool(bool),
    /// Integer option.
    Int(i64),
    /// Floating-point option.
    Float(f64),
    /// Array option.
    Array(Vec<FilterOptionValue>),
    /// Rational option.
    Rational(Rational),
    /// Binary blob option.
    Blob(Arc<[u8]>),
}

/// Deterministically sorted filter option map.
pub type FilterOptions = BTreeMap<String, FilterOptionValue>;

/// Filter planner callback used during graph construction.
pub type FilterPlanner = fn(FilterPlanRequest<'_>) -> Result<FilterPlan>;

/// Borrowed inputs for planning one filter node.
#[derive(Clone, Copy, Debug)]
pub struct FilterPlanRequest<'a> {
    input_media: &'a [ClipMedia],
    options: &'a FilterOptions,
    metadata_schema: &'a MetadataSchema,
}

impl<'a> FilterPlanRequest<'a> {
    /// Creates planner request from input media and generic options.
    #[must_use]
    pub const fn new(
        input_media: &'a [ClipMedia],
        options: &'a FilterOptions,
        metadata_schema: &'a MetadataSchema,
    ) -> Self {
        Self {
            input_media,
            options,
            metadata_schema,
        }
    }

    /// Returns media for all input clips in call order.
    #[must_use]
    pub const fn input_media(self) -> &'a [ClipMedia] {
        self.input_media
    }

    /// Returns generic filter options.
    #[must_use]
    pub const fn options(self) -> &'a FilterOptions {
        self.options
    }

    /// Returns metadata schema active for this registry.
    #[must_use]
    pub const fn metadata_schema(self) -> &'a MetadataSchema {
        self.metadata_schema
    }
}

/// Planned graph-node metadata for one filter call.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FilterPlan {
    output_media: ClipMedia,
    compatibility: FilterCompatibility,
    dependencies: DependencyPattern,
    concurrency: ConcurrencyClass,
}

impl FilterPlan {
    /// Creates same-frame stateless plan.
    #[must_use]
    pub const fn new(output_media: ClipMedia, compatibility: FilterCompatibility) -> Self {
        Self {
            output_media,
            compatibility,
            dependencies: DependencyPattern::same_frame(),
            concurrency: ConcurrencyClass::Stateless,
        }
    }

    /// Returns this plan with explicit scheduling metadata.
    #[must_use]
    pub const fn with_schedule(
        mut self,
        dependencies: DependencyPattern,
        concurrency: ConcurrencyClass,
    ) -> Self {
        self.dependencies = dependencies;
        self.concurrency = concurrency;
        self
    }

    /// Returns planned output media.
    #[must_use]
    pub const fn output_media(&self) -> &ClipMedia {
        &self.output_media
    }

    /// Consumes plan and returns output media.
    #[must_use]
    pub fn into_output_media(self) -> ClipMedia {
        self.output_media
    }

    /// Returns graph compatibility policy.
    #[must_use]
    pub const fn compatibility(&self) -> FilterCompatibility {
        self.compatibility
    }

    /// Returns upstream dependency contract.
    #[must_use]
    pub const fn dependencies(&self) -> &DependencyPattern {
        &self.dependencies
    }

    /// Returns scheduler concurrency class.
    #[must_use]
    pub const fn concurrency(&self) -> ConcurrencyClass {
        self.concurrency
    }

    /// Consumes plan into graph-builder arguments.
    #[must_use]
    pub fn into_parts(
        self,
    ) -> (
        ClipMedia,
        FilterCompatibility,
        DependencyPattern,
        ConcurrencyClass,
    ) {
        (
            self.output_media,
            self.compatibility,
            self.dependencies,
            self.concurrency,
        )
    }
}

/// Declares one registered filter capability.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FilterDescriptor {
    name: String,
    publisher: String,
    plugin: String,
}

impl FilterDescriptor {
    /// Creates a filter descriptor from stable diagnostic fields.
    #[must_use]
    pub fn new(
        name: impl Into<String>,
        publisher: impl Into<String>,
        plugin: impl Into<String>,
    ) -> Self {
        Self {
            name: name.into(),
            publisher: publisher.into(),
            plugin: plugin.into(),
        }
    }

    /// Returns stable filter name.
    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Returns publisher namespace.
    #[must_use]
    pub fn publisher(&self) -> &str {
        &self.publisher
    }

    /// Returns plugin namespace.
    #[must_use]
    pub fn plugin(&self) -> &str {
        &self.plugin
    }
}

#[derive(Clone, Debug)]
struct RegisteredFilter {
    descriptor: FilterDescriptor,
    planner: Option<FilterPlanner>,
}

/// Per-core registry for filters and metadata extension keys.
#[derive(Clone, Debug)]
pub struct FilterRegistry {
    filters: BTreeMap<String, RegisteredFilter>,
    metadata_schema: MetadataSchema,
}

impl Default for FilterRegistry {
    fn default() -> Self {
        Self {
            filters: BTreeMap::new(),
            metadata_schema: MetadataSchema::core(),
        }
    }
}

impl FilterRegistry {
    /// Creates an empty registry with core metadata keys installed.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Registers one filter descriptor.
    pub fn register_filter(&mut self, descriptor: FilterDescriptor) -> Result<()> {
        self.insert_filter(descriptor, None)
    }

    /// Registers one filter descriptor with graph-construction planner.
    pub fn register_filter_planner(
        &mut self,
        descriptor: FilterDescriptor,
        planner: FilterPlanner,
    ) -> Result<()> {
        self.insert_filter(descriptor, Some(planner))
    }

    fn insert_filter(
        &mut self,
        descriptor: FilterDescriptor,
        planner: Option<FilterPlanner>,
    ) -> Result<()> {
        validate_filter_identity(&descriptor)?;

        if self.filters.contains_key(descriptor.name()) {
            return Err(PixelFlowError::new(
                ErrorCategory::Plugin,
                ErrorCode::new("plugin.duplicate_filter"),
                format!("filter '{}' is already registered", descriptor.name()),
            ));
        }

        self.filters.insert(
            descriptor.name.clone(),
            RegisteredFilter {
                descriptor,
                planner,
            },
        );
        Ok(())
    }

    /// Plans one registered filter call.
    pub fn plan_filter(
        &self,
        name: &str,
        input_media: &[ClipMedia],
        options: &FilterOptions,
    ) -> Result<FilterPlan> {
        let Some(filter) = self.filters.get(name) else {
            return Err(PixelFlowError::new(
                ErrorCategory::Graph,
                ErrorCode::new("graph.unknown_filter"),
                format!("filter '{name}' is not registered"),
            ));
        };
        let Some(planner) = filter.planner else {
            return plan_descriptor_only_filter(name, input_media);
        };

        planner(FilterPlanRequest::new(
            input_media,
            options,
            &self.metadata_schema,
        ))
    }

    /// Resolves `plugin.<namespace>.<filter>` script sugar to a registered filter name.
    pub fn resolve_filter_name_for_plugin_call(
        &self,
        plugin_namespace: &str,
        filter_name: &str,
    ) -> Result<&str> {
        if !is_filter_namespace_segment(plugin_namespace) || !is_filter_name_path(filter_name) {
            return Err(PixelFlowError::new(
                ErrorCategory::Graph,
                ErrorCode::new("graph.unknown_filter"),
                format!("filter namespace '{plugin_namespace}.{filter_name}' is not registered"),
            ));
        }

        let dotted_name = format!("{plugin_namespace}.{filter_name}");
        let mut matches = Vec::new();

        for filter in self.filters.values() {
            let descriptor = &filter.descriptor;
            if descriptor.plugin() == plugin_namespace && descriptor.name() == filter_name {
                matches.push(descriptor.name());
            }
        }

        if let Some(filter) = self.filters.get(&dotted_name)
            && !matches.contains(&filter.descriptor.name())
        {
            matches.push(filter.descriptor.name());
        }

        match matches.as_slice() {
            [name] => Ok(name),
            [] => Err(PixelFlowError::new(
                ErrorCategory::Graph,
                ErrorCode::new("graph.unknown_filter"),
                format!("filter namespace '{plugin_namespace}.{filter_name}' is not registered"),
            )),
            _ => Err(PixelFlowError::new(
                ErrorCategory::Graph,
                ErrorCode::new("graph.ambiguous_filter_namespace"),
                format!(
                    "filter namespace '{plugin_namespace}.{filter_name}' matches multiple filters"
                ),
            )),
        }
    }

    /// Registers one plugin metadata key.
    pub fn register_metadata_key(&mut self, key: &str, kind: MetadataKind) -> Result<()> {
        self.metadata_schema.register_plugin_key(key, kind)
    }

    /// Returns descriptor for registered filter.
    #[must_use]
    pub fn filter_descriptor(&self, name: &str) -> Option<&FilterDescriptor> {
        self.filters.get(name).map(|filter| &filter.descriptor)
    }

    /// Returns true when filter exists.
    #[must_use]
    pub fn contains_filter(&self, name: &str) -> bool {
        self.filters.contains_key(name)
    }

    /// Returns filter names in deterministic order.
    #[must_use]
    pub fn filter_names(&self) -> Vec<&str> {
        self.filters.keys().map(String::as_str).collect()
    }

    /// Returns metadata schema including plugin extension keys.
    #[must_use]
    pub const fn metadata_schema(&self) -> &MetadataSchema {
        &self.metadata_schema
    }
}

fn plan_descriptor_only_filter(name: &str, input_media: &[ClipMedia]) -> Result<FilterPlan> {
    let Some(first_input) = input_media.first() else {
        return Err(PixelFlowError::new(
            ErrorCategory::Graph,
            ErrorCode::new("graph.invalid_filter_inputs"),
            format!("filter '{name}' requires at least one input clip"),
        ));
    };

    Ok(FilterPlan::new(
        first_input.clone(),
        FilterCompatibility::Custom,
    ))
}

fn is_filter_namespace_segment(value: &str) -> bool {
    let mut bytes = value.bytes();

    matches!(bytes.next(), Some(first) if first.is_ascii_alphabetic() || first == b'_')
        && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
}

fn is_filter_name_path(value: &str) -> bool {
    !value.is_empty() && value.split('.').all(is_filter_namespace_segment)
}

fn validate_filter_identity(descriptor: &FilterDescriptor) -> Result<()> {
    for (field, value) in [
        ("name", descriptor.name()),
        ("publisher", descriptor.publisher()),
        ("plugin", descriptor.plugin()),
    ] {
        if value.trim().is_empty() {
            return Err(PixelFlowError::new(
                ErrorCategory::Plugin,
                ErrorCode::new("plugin.invalid_filter"),
                format!("filter {field} must not be empty"),
            ));
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    #![expect(clippy::indexing_slicing, reason = "allow in tests")]

    use std::collections::BTreeMap;

    use crate::{
        ClipFormat, ClipMedia, ClipResolution, ConcurrencyClass, DependencyPattern, ErrorCategory,
        ErrorCode, FilterCompatibility, FilterDescriptor, FilterOptionValue, FilterPlan,
        FilterPlanRequest, FilterRegistry, FrameCount, FrameRate, MetadataKind, Rational,
        resolve_format_alias,
    };

    #[test]
    fn filter_registry_records_filters_and_metadata_keys() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("acme.blur", "acme", "blur"))
            .expect("filter should register");
        registry
            .register_metadata_key("acme/blur:strength", MetadataKind::Float)
            .expect("metadata key should register");

        assert!(registry.contains_filter("acme.blur"));
        assert!(
            registry
                .metadata_schema()
                .contains_key("acme/blur:strength")
        );
    }

    #[test]
    fn filter_registry_rejects_duplicate_filters() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("acme.blur", "acme", "blur"))
            .expect("first filter should register");

        let error = registry
            .register_filter(FilterDescriptor::new("acme.blur", "acme", "blur"))
            .expect_err("duplicate filter should fail");

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

    #[test]
    fn filter_registry_rejects_empty_filter_identity_fields() {
        let mut registry = FilterRegistry::new();
        let error = registry
            .register_filter(FilterDescriptor::new("", "acme", "blur"))
            .expect_err("empty filter name should fail");
        assert_eq!(error.category(), ErrorCategory::Plugin);
        assert_eq!(error.code(), ErrorCode::new("plugin.invalid_filter"));

        let mut registry = FilterRegistry::new();
        let error = registry
            .register_filter(FilterDescriptor::new("acme.blur", "", "blur"))
            .expect_err("empty publisher should fail");
        assert_eq!(error.category(), ErrorCategory::Plugin);
        assert_eq!(error.code(), ErrorCode::new("plugin.invalid_filter"));

        let mut registry = FilterRegistry::new();
        let error = registry
            .register_filter(FilterDescriptor::new("acme.blur", "acme", ""))
            .expect_err("empty plugin should fail");
        assert_eq!(error.category(), ErrorCategory::Plugin);
        assert_eq!(error.code(), ErrorCode::new("plugin.invalid_filter"));
    }

    #[test]
    fn filter_registry_plans_registered_filter_calls() {
        fn planner(request: FilterPlanRequest<'_>) -> crate::Result<FilterPlan> {
            assert_eq!(request.input_media().len(), 1);
            assert_eq!(
                request.options().get("strength"),
                Some(&FilterOptionValue::Float(0.5))
            );
            Ok(FilterPlan::new(
                request.input_media()[0].clone(),
                FilterCompatibility::Preserve,
            ))
        }

        let media = ClipMedia::new(
            ClipFormat::Fixed(resolve_format_alias("yuv420p8").expect("format exists")),
            ClipResolution::Fixed {
                width: 640,
                height: 360,
            },
            FrameCount::Finite(2),
            FrameRate::Cfr(Rational {
                numerator: 24,
                denominator: 1,
            }),
        );
        let mut registry = FilterRegistry::new();
        registry
            .register_filter_planner(FilterDescriptor::new("blur", "test", "test"), planner)
            .expect("planner registers");

        let mut options = BTreeMap::new();
        options.insert("strength".to_owned(), FilterOptionValue::Float(0.5));
        let plan = registry
            .plan_filter("blur", std::slice::from_ref(&media), &options)
            .expect("registered planner should run");

        assert_eq!(plan.output_media(), &media);
        assert_eq!(plan.compatibility(), FilterCompatibility::Preserve);
    }

    #[test]
    fn filter_registry_reports_unknown_filters() {
        let registry = FilterRegistry::new();
        let error = registry
            .plan_filter("missing", &[], &BTreeMap::new())
            .expect_err("unknown filter should fail");

        assert_eq!(error.category(), ErrorCategory::Graph);
        assert_eq!(error.code(), ErrorCode::new("graph.unknown_filter"));
    }

    #[test]
    fn filter_registry_uses_opaque_plan_for_unplanned_filters() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("blur", "test", "test"))
            .expect("filter should register");

        let media = fixed_media("gray8");
        let plan = registry
            .plan_filter("blur", std::slice::from_ref(&media), &BTreeMap::new())
            .expect("descriptor-only filter should produce opaque plan");

        assert_eq!(plan.output_media(), &media);
        assert_eq!(plan.compatibility(), FilterCompatibility::Custom);
    }

    #[test]
    fn filter_registry_plans_descriptor_only_filters_as_opaque_custom_filters() {
        let media = fixed_media("yuv420p8");
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("acme.blur", "acme", "blur"))
            .expect("descriptor-only filter should register");

        let plan = registry
            .plan_filter("acme.blur", std::slice::from_ref(&media), &BTreeMap::new())
            .expect("descriptor-only filter should produce opaque plan");

        assert_eq!(plan.output_media(), &media);
        assert_eq!(plan.compatibility(), FilterCompatibility::Custom);
    }

    #[test]
    fn filter_registry_rejects_descriptor_only_filters_without_inputs() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new(
                "acme.source_like",
                "acme",
                "source_like",
            ))
            .expect("descriptor-only filter should register");

        let error = registry
            .plan_filter("acme.source_like", &[], &BTreeMap::new())
            .expect_err("descriptor-only filters require at least one input");

        assert_eq!(error.category(), ErrorCategory::Graph);
        assert_eq!(error.code(), ErrorCode::new("graph.invalid_filter_inputs"));
    }

    #[test]
    fn filter_registry_resolves_plugin_namespace_calls_by_descriptor_plugin() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("blur", "acme", "effects"))
            .expect("filter should register");

        assert_eq!(
            registry
                .resolve_filter_name_for_plugin_call("effects", "blur")
                .expect("namespace call should resolve"),
            "blur"
        );
    }

    #[test]
    fn filter_registry_resolves_plugin_namespace_calls_by_dotted_filter_name() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("acme.blur", "acme", "blur"))
            .expect("filter should register");

        assert_eq!(
            registry
                .resolve_filter_name_for_plugin_call("acme", "blur")
                .expect("namespace call should resolve"),
            "acme.blur"
        );
    }

    #[test]
    fn filter_registry_resolves_std_namespace_calls_by_plugin_namespace() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("merge_planes", "pixelflow", "std"))
            .expect("filter should register");

        assert_eq!(
            registry
                .resolve_filter_name_for_plugin_call("std", "merge_planes")
                .expect("std namespace call should resolve"),
            "merge_planes"
        );
    }

    #[test]
    fn filter_registry_reports_unknown_plugin_namespace_calls() {
        let registry = FilterRegistry::new();

        let error = registry
            .resolve_filter_name_for_plugin_call("missing", "blur")
            .expect_err("unknown namespace call should fail");

        assert_eq!(error.category(), ErrorCategory::Graph);
        assert_eq!(error.code(), ErrorCode::new("graph.unknown_filter"));
    }

    #[test]
    fn filter_registry_reports_ambiguous_plugin_namespace_calls() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("blur", "acme", "fx"))
            .expect("plugin-name filter should register");
        registry
            .register_filter(FilterDescriptor::new("fx.blur", "acme", "blur"))
            .expect("dotted-name filter should register");

        let error = registry
            .resolve_filter_name_for_plugin_call("fx", "blur")
            .expect_err("ambiguous namespace should fail");

        assert_eq!(error.category(), ErrorCategory::Graph);
        assert_eq!(
            error.code(),
            ErrorCode::new("graph.ambiguous_filter_namespace")
        );
    }

    #[test]
    fn filter_options_support_metadata_array_and_blob_values() {
        let array = FilterOptionValue::Array(vec![
            FilterOptionValue::Int(1),
            FilterOptionValue::String("x".to_owned()),
        ]);
        let blob = FilterOptionValue::Blob(vec![1_u8, 2, 3].into());

        assert_eq!(
            array,
            FilterOptionValue::Array(vec![
                FilterOptionValue::Int(1),
                FilterOptionValue::String("x".to_owned()),
            ])
        );
        assert_eq!(blob, FilterOptionValue::Blob(vec![1_u8, 2, 3].into()));
    }

    #[test]
    fn filter_planner_receives_registry_metadata_schema() {
        fn planner(request: FilterPlanRequest<'_>) -> crate::Result<FilterPlan> {
            assert_eq!(
                request.metadata_schema().kind("acme/filter:enabled"),
                Some(MetadataKind::Bool)
            );
            Ok(FilterPlan::new(
                request.input_media()[0].clone(),
                FilterCompatibility::Preserve,
            ))
        }

        let media = fixed_media("gray8");
        let mut registry = FilterRegistry::new();
        registry
            .register_metadata_key("acme/filter:enabled", MetadataKind::Bool)
            .expect("metadata key should register");
        registry
            .register_filter_planner(FilterDescriptor::new("check", "test", "check"), planner)
            .expect("filter should register");

        registry
            .plan_filter("check", &[media], &BTreeMap::new())
            .expect("planner should see schema");
    }

    #[test]
    fn filter_plan_with_schedule_roundtrips_into_parts() {
        let media = ClipMedia::new(
            ClipFormat::Fixed(resolve_format_alias("yuv420p8").expect("format exists")),
            ClipResolution::Fixed {
                width: 640,
                height: 360,
            },
            FrameCount::Finite(2),
            FrameRate::Cfr(Rational {
                numerator: 24,
                denominator: 1,
            }),
        );

        let plan = FilterPlan::new(media.clone(), FilterCompatibility::Preserve).with_schedule(
            DependencyPattern::window(2, 1),
            ConcurrencyClass::OrderedStateful,
        );

        let (output_media, compatibility, dependencies, concurrency) = plan.into_parts();

        assert_eq!(output_media, media);
        assert_eq!(compatibility, FilterCompatibility::Preserve);
        assert_eq!(dependencies, DependencyPattern::window(2, 1));
        assert_eq!(concurrency, ConcurrencyClass::OrderedStateful);
    }

    fn fixed_media(format: &str) -> ClipMedia {
        ClipMedia::new(
            ClipFormat::Fixed(resolve_format_alias(format).expect("format exists")),
            ClipResolution::Fixed {
                width: 640,
                height: 360,
            },
            FrameCount::Finite(2),
            FrameRate::Cfr(Rational {
                numerator: 24,
                denominator: 1,
            }),
        )
    }

    #[test]
    fn filter_registry_filter_names_stay_sorted() {
        let mut registry = FilterRegistry::new();
        registry
            .register_filter(FilterDescriptor::new("gamma", "test", "gamma"))
            .expect("gamma should register");
        registry
            .register_filter(FilterDescriptor::new("alpha", "test", "alpha"))
            .expect("alpha should register");
        registry
            .register_filter(FilterDescriptor::new("beta", "test", "beta"))
            .expect("beta should register");

        assert_eq!(registry.filter_names(), vec!["alpha", "beta", "gamma"]);
    }
}
