//! el-aop — Aspect-Oriented Programming for el-ui.
//!
//! Cross-cutting concerns as first-class language features. Not a library you
//! import. Built into the framework. Applied as decorators:
//!
//! ```text
//! @authenticate          ← applied by DEFAULT to every function
//! @authorize(role: "admin")
//! @cache(ttl: 300)
//! @rate_limit(requests: 100, per: 60)
//! component AdminDashboard { ... }
//!
//! @public                ← explicit opt-out of authentication
//! component LandingPage { ... }
//! ```
//!
//! ## Security-by-default
//!
//! `@authenticate` is the default. Functions without `@public` are protected.
//! This makes it as hard as possible to accidentally ship an unprotected endpoint.

pub mod aspects;
pub mod chain;
pub mod registry;

pub use aspects::{
    AuthenticateAspect, AuthorizeAspect, CacheAspect, LogAspect, RateLimitAspect, RetryAspect,
    TraceAspect, ValidateAspect,
};
pub use chain::AspectChain;
pub use public::PublicMarker;
pub use registry::AspectRegistry;

pub mod public;

#[cfg(test)]
mod tests;

use std::collections::HashMap;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AopError {
    #[error("authentication required")]
    Unauthenticated,
    #[error("forbidden: requires role '{role}'")]
    Forbidden { role: String },
    #[error("rate limit exceeded: {requests} requests per {per}s")]
    RateLimited { requests: u32, per: u64 },
    #[error("validation failed: {0}")]
    ValidationFailed(String),
    #[error("aspect error: {0}")]
    Aspect(String),
    #[error("all {attempts} retry attempts failed: {last_error}")]
    RetriesExhausted { attempts: u32, last_error: String },
}

pub type AopResult<T> = Result<T, AopError>;

/// Context passed through the aspect chain.
///
/// Contains the incoming arguments and metadata about the call.
/// Aspects can read and mutate this context as they execute.
#[derive(Debug, Clone)]
pub struct InvocationContext {
    /// The component or service being called.
    pub target: String,
    /// The method being called.
    pub method: String,
    /// Arguments passed to the method.
    pub args: HashMap<String, String>,
    /// Metadata added by aspects (e.g., the authenticated user, trace ID).
    pub metadata: HashMap<String, String>,
}

impl InvocationContext {
    pub fn new(target: impl Into<String>, method: impl Into<String>) -> Self {
        Self {
            target: target.into(),
            method: method.into(),
            args: HashMap::new(),
            metadata: HashMap::new(),
        }
    }

    pub fn with_arg(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.args.insert(key.into(), value.into());
        self
    }

    pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.metadata.insert(key.into(), value.into());
        self
    }

    pub fn get_meta(&self, key: &str) -> Option<&str> {
        self.metadata.get(key).map(|s| s.as_str())
    }
}

/// The result of invoking a method through an aspect chain.
#[derive(Debug, Clone)]
pub struct InvocationResult {
    pub value: String,
    pub metadata: HashMap<String, String>,
}

impl InvocationResult {
    pub fn new(value: impl Into<String>) -> Self {
        Self {
            value: value.into(),
            metadata: HashMap::new(),
        }
    }
}

/// A handler that performs the actual method invocation.
/// Aspects wrap around this.
pub type ProceedFn = Box<dyn Fn(InvocationContext) -> AopResult<InvocationResult> + Send + Sync>;

/// The core Aspect trait.
///
/// Each aspect implements `before`, `after`, or `around` advice.
/// The default implementations are no-ops — only override what you need.
pub trait Aspect: Send + Sync {
    /// The aspect's name (used for debugging and registry lookup).
    fn name(&self) -> &'static str;

    /// Before advice — runs before the method. Can reject the call.
    fn before(&self, ctx: &mut InvocationContext) -> AopResult<()> {
        let _ = ctx;
        Ok(())
    }

    /// After advice — runs after the method. Receives the result.
    /// Can modify the result or perform cleanup.
    fn after(
        &self,
        ctx: &InvocationContext,
        result: AopResult<InvocationResult>,
    ) -> AopResult<InvocationResult> {
        let _ = ctx;
        result
    }

    /// Around advice — wraps the entire invocation.
    /// The default implementation calls `before`, then `proceed`, then `after`.
    fn around(
        &self,
        mut ctx: InvocationContext,
        proceed: &ProceedFn,
    ) -> AopResult<InvocationResult> {
        self.before(&mut ctx)?;
        let result = proceed(ctx.clone());
        self.after(&ctx, result)
    }
}
