//! loomx-core
//!
//! Framework-agnostic core for loomx.
//!
//! This crate contains the component registry, rendering entrypoints,
//! props parsing helpers, and validation utilities. It intentionally has
//! **no HTTP framework dependencies**.
//!
//! Most users will depend on the umbrella crate `loomx` instead.
//!

use std::collections::HashMap;

use tracing::{debug, warn};
use askama::Template;
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value;

#[derive(thiserror::Error, Debug)]
/// Errors produced by loomx during component lookup, props decoding, or rendering.
///
/// These errors are transport-agnostic. Integrations (e.g. `loomx-axum`) map them
/// to HTTP responses.
pub enum LoomxError {
    #[error("unknown component: {0}")]
    UnknownComponent(String),

    #[error("invalid props for component: {0}")]
    InvalidProps(String),

    #[error("template render error: {0}")]
    Render(String),

    #[error("duplicate component registrations: {0:?}")]
    DuplicateComponents(Vec<&'static str>),
}


/// A renderable component.
///
/// Components are defined as typed Rust structs with an Askama template attached.
/// The struct must be deserializable from JSON (`DeserializeOwned`) so the runtime
/// can instantiate it from dynamic props when rendering by name.
///
/// Each component has a stable string identifier (`NAME`) used for registry lookup.
pub trait Component: Template + DeserializeOwned + Send + Sync + 'static {
    const NAME: &'static str;
}

/// A type-erased component registration record stored in the global registry.
///
/// Instances of this struct are submitted via `inventory` from component crates.
/// The `render_from_json` function is responsible for validating and rendering props.
pub struct ComponentDecl {
    pub name: &'static str,
    pub render_from_json: fn(Value) -> Result<String, LoomxError>,
}

inventory::collect!(ComponentDecl);

/// Render a registered component by name.
///
/// `name` must match a `Component::NAME` that was registered at compile time.
/// `props` is a JSON value that will be deserialized into the component's props struct.
///
/// Returns the rendered HTML as a `String`.
///
/// For typed props, prefer [`render_with`].
pub fn render(name: &str, props: Value) -> Result<String, LoomxError> {
    debug!(component = name, "render request");

    for decl in inventory::iter::<ComponentDecl> {
        if decl.name == name {
            debug!(component = name, "component found");
            return (decl.render_from_json)(props);
        }
    }

    warn!(component = name, "unknown component");
    Err(LoomxError::UnknownComponent(name.to_string()))
}

/// Render a registered component by name using **typed props**.
///
/// This is the ergonomic, application-facing companion to [`render`]. It converts
/// `props` (any `T: Serialize`) into JSON, then dispatches through the normal
/// registry-based renderer.
///
/// Prefer this in application code and examples.
pub fn render_with<T: Serialize>(name: &str, props: &T) -> Result<String, LoomxError> {
    let value: Value =
        serde_json::to_value(props).map_err(|e| LoomxError::InvalidProps(e.to_string()))?;
    render(name, value)
}

pub fn render_component<C>(props: &C) -> Result<String, LoomxError>
where
    C: Component + Serialize,
{
    render_with(C::NAME, props)
}

/// Return the list of currently-registered component names.
///
/// Useful for debugging, diagnostics endpoints, or building a component catalog.
pub fn known_components() -> Vec<&'static str> {
    inventory::iter::<ComponentDecl>
        .into_iter()
        .map(|d| d.name)
        .collect()
}

// Helper used by the macro crate (kept public but minimal)
/// Render a component from JSON props when the concrete component type is known.
///
/// This is primarily used by `loomx-macros` to implement type-erased rendering.
pub fn render_typed<T: Component>(props: Value) -> Result<String, LoomxError> {
    let instance: T = serde_json::from_value(props)
        .map_err(|e| LoomxError::InvalidProps(e.to_string()))?;
    instance
        .render()
        .map_err(|e| LoomxError::Render(e.to_string()))
}


/// Validate the global component registry.
///
/// Currently checks for duplicate component names and returns an error if any are found.
/// Call this once during application startup.
pub fn validate_registry() -> Result<(), LoomxError> {
    let mut counts: HashMap<&'static str, usize> = HashMap::new();

    for decl in inventory::iter::<ComponentDecl> {
        *counts.entry(decl.name).or_insert(0) += 1;
    }

    let mut dups: Vec<&'static str> = counts
        .into_iter()
        .filter_map(|(name, n)| (n > 1).then_some(name))
        .collect();

    dups.sort_unstable();

    if !dups.is_empty() {
        tracing::error!(duplicates = ?dups, "duplicate component registrations detected");
        return Err(LoomxError::DuplicateComponents(dups));
    }

    Ok(())
}

/// Panic if the registry contains duplicate component names.
///
/// This is a convenience wrapper around [`validate_registry`]. It is intended for binaries
/// that prefer fail-fast startup behavior.
pub fn assert_unique_registry() {
    if let Err(e) = validate_registry() {
        panic!("{e}");
    }
}

pub mod props;
pub use inventory;
pub use tracing;
pub use props::parse_props;