pub mod clarity;
pub mod depth;
pub mod focus;
pub mod harmonic;
pub mod mode;
pub mod narrow;
pub mod refract;
pub mod wide;

use forge::budget::SignalBudget;

#[derive(Debug, Clone)]
pub struct LensContext {
    pub task_hint: Option<String>,
    pub budget: SignalBudget,
    pub ecosystem_hint: Option<String>,
    pub exit_code: i32,
    /// First argument / sub-command (e.g. "log", "diff", "build").
    /// Set by the FilterMesh from the args slice before calling preprocessors.
    pub subcmd: String,
}

impl LensContext {
    pub fn new(limit: usize) -> Self {
        Self {
            task_hint: None,
            budget: SignalBudget::new(limit),
            ecosystem_hint: None,
            exit_code: 0,
            subcmd: String::new(),
        }
    }

    pub fn with_task(mut self, task: impl Into<String>) -> Self {
        self.task_hint = Some(task.into());
        self
    }

    pub fn with_exit_code(mut self, code: i32) -> Self {
        self.exit_code = code;
        self
    }

    pub fn with_subcmd(mut self, subcmd: impl Into<String>) -> Self {
        self.subcmd = subcmd.into();
        self
    }
}

#[derive(Debug, Clone)]
pub struct LensOutput {
    pub content: String,
    pub tokens_before: usize,
    pub tokens_after: usize,
    pub applied: Vec<String>,
}

impl LensOutput {
    pub fn passthrough(content: impl Into<String>) -> Self {
        let content = content.into();
        let t = forge::budget::estimator::TokenEstimator::count_nonblocking(&content);
        Self {
            content,
            tokens_before: t,
            tokens_after: t,
            applied: vec![],
        }
    }

    pub fn savings_pct(&self) -> f64 {
        forge::budget::estimator::TokenEstimator::savings_pct(self.tokens_before, self.tokens_after)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LensId {
    Clarity,
    Depth,
    Focus,
    Narrow,
    Wide,
    Refract,
    Harmonic,
}

impl LensId {
    pub fn name(&self) -> &'static str {
        match self {
            Self::Clarity => "clarity",
            Self::Depth => "depth",
            Self::Focus => "focus",
            Self::Narrow => "narrow",
            Self::Wide => "wide",
            Self::Refract => "refract",
            Self::Harmonic => "harmonic",
        }
    }
}

pub trait Lens: Send + Sync {
    fn id(&self) -> LensId;
    fn apply(&self, input: &str, ctx: &LensContext) -> LensOutput;
    fn token_budget_hint(&self) -> Option<usize> {
        None
    }
}

/// Apply a stack of lenses in order, threading the output through.
pub fn apply_stack(lenses: &[Box<dyn Lens>], input: &str, ctx: &LensContext) -> LensOutput {
    if lenses.is_empty() {
        return LensOutput::passthrough(input);
    }

    let tokens_before = forge::budget::estimator::TokenEstimator::count_nonblocking(input);
    let mut current = input.to_string();
    let mut applied = Vec::new();

    for lens in lenses {
        let out = lens.apply(&current, ctx);
        applied.push(lens.id().name().to_string());
        current = out.content;
    }

    let tokens_after = forge::budget::estimator::TokenEstimator::count_nonblocking(&current);

    // Never emit "compressed" output larger than the original. On tiny or already-dense
    // inputs the lens framing/annotation overhead can exceed the payload, which would
    // surface as negative savings; fall back to the raw input so savings are never < 0.
    if tokens_after >= tokens_before {
        return LensOutput {
            content: input.to_string(),
            tokens_before,
            tokens_after: tokens_before,
            applied,
        };
    }

    LensOutput {
        content: current,
        tokens_before,
        tokens_after,
        applied,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A lens that always makes its input bigger — used to verify the never-expand guard.
    struct ExpandingLens;
    impl Lens for ExpandingLens {
        fn id(&self) -> LensId {
            LensId::Clarity
        }
        fn apply(&self, input: &str, _ctx: &LensContext) -> LensOutput {
            LensOutput::passthrough(format!("{input} {input} {input} extra padding words here"))
        }
    }

    #[test]
    fn apply_stack_never_expands_output() {
        let lenses: Vec<Box<dyn Lens>> = vec![Box::new(ExpandingLens)];
        let ctx = LensContext::new(2000);
        let out = apply_stack(&lenses, "short input", &ctx);
        assert!(
            out.tokens_after <= out.tokens_before,
            "compressed output must never exceed the original"
        );
        assert_eq!(
            out.content, "short input",
            "must fall back to the raw input when the lens stack would expand it"
        );
    }
}
