Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

use crate::bindings::attributes::AttributesBindingAdapter;
use crate::context::attributes::AttributesContext;
use crate::context::authentication::AuthenticationContext;
use crate::context::input::InputContext;
use crate::context::merged::MergedContext;
use crate::context::payload::PayloadContext;
use crate::context::reference::TransientContext;
use crate::context::vars::VarsContext;
use crate::context::StreamSolver;
use crate::value::IntoValue;
use crate::{AttributesBinding, AuthenticationBinding, Format, PayloadBinding, Value};
use pel::expression::Expression as PelExpression;
use pel::runtime::value::Value as PelValue;
use pel::runtime::{Context, Evaluation, Runtime, RuntimeError};
use std::cell::RefCell;
use std::collections::HashMap;
use std::convert::TryFrom;
use thiserror::Error;

#[cfg(feature = "stream_body")]
use classy::hl::BodyStream;

// For rustdoc.
#[allow(unused)]
use crate::Script;

/// An evaluator for [`Script`]s.
pub struct Evaluator<'a> {
    stream_solver: StreamSolver,
    evaluation: Option<PelExpression>,
    result: Option<PelValue>,
    error: Option<RuntimeError>,

    input_context: &'a InputContext,
    transient_context: TransientContext,
    vars: HashMap<String, Value>,

    content_type: RefCell<Option<Format>>,
}

impl<'a> Evaluator<'a> {
    pub(crate) fn new(
        stream_solver: StreamSolver,
        evaluation: Option<PelExpression>,
        result: Option<PelValue>,
        context: &'a InputContext,
    ) -> Self {
        Self {
            stream_solver,
            evaluation,
            result,
            error: None,
            input_context: context,
            transient_context: TransientContext::default(),
            vars: HashMap::new(),
            content_type: RefCell::new(None),
        }
    }

    /// Binds a set of values to a variable indentified by the `name` argument,
    /// nested into the `vars` namespace.
    pub fn bind_vars<K: IntoValue>(&mut self, name: &str, value: K) {
        if !self.is_ready() {
            self.vars.insert(name.to_string(), value.into_value());
            if self
                .input_context
                .vars()
                .iter()
                .all(|item| self.vars.contains_key(item))
            {
                let vars = self.vars.drain().collect();
                self.do_eval(&VarsContext::new(self.input_context, vars));
            }
        }
    }

    /// Binds a payload to the `payload` variable.
    pub fn bind_payload(&mut self, binding: &dyn PayloadBinding) {
        if !self.is_ready() {
            let content_type = *self.content_type.borrow();
            self.do_eval(&PayloadContext::new(
                self.input_context,
                binding,
                content_type,
            ))
        }
    }

    #[cfg(feature = "stream_body")]
    /// Binds a payload to the `payload` variable.
    pub async fn bind_stream_body(&mut self, body: BodyStream<'_>) {
        if !self.is_ready() {
            let content_type = *self.content_type.borrow();

            let body = self.stream_solver.bind_body_stream(body).await;

            self.do_eval(&PayloadContext::new(
                self.input_context,
                &body,
                content_type,
            ))
        }
    }

    /// Binds a set of actual attributes to the `attributes` namespace.
    pub fn bind_attributes(&mut self, binding: &dyn AttributesBinding) {
        if !self.is_ready() {
            Self::extract_format(binding)
                .map(|content_type| self.content_type.borrow_mut().replace(content_type));

            self.stream_solver.bind_attributes(binding);
            let adapter = AttributesBindingAdapter::new(binding);
            self.do_eval(&AttributesContext::new(self.input_context, &adapter))
        }
    }

    /// Binds a set of actual authentication properties to the `authentication` namespace.
    pub fn bind_authentication(&mut self, binding: &dyn AuthenticationBinding) {
        if !self.is_ready() {
            self.do_eval(&AuthenticationContext::new(self.input_context, binding))
        }
    }

    fn extract_format(binding: &dyn AttributesBinding) -> Option<Format> {
        binding.extract_header("content-type").and_then(|header| {
            if header.contains("/xml") {
                Some(Format::Xml)
            } else if header.contains("/json") {
                Some(Format::Json)
            } else if header.contains("text/plain") {
                Some(Format::PlainText)
            } else {
                None
            }
        })
    }

    fn do_eval(&mut self, bound_context: &'_ dyn Context) {
        let local_context = MergedContext::new(self.input_context, &self.transient_context);
        let merged_context = MergedContext::new(&local_context, bound_context);
        if let Some(expr) = self.evaluation.take() {
            match Runtime::new().eval_with_context(&expr, &merged_context) {
                Ok(eval) => match eval {
                    Evaluation::Complete(_, v) => {
                        self.result = Some(v);
                    }
                    Evaluation::Partial(exp) => {
                        self.transient_context.detach_pending(&exp, bound_context);
                        self.evaluation = Some(exp);
                    }
                },
                Err(error) => {
                    self.error = Some(error);
                }
            }
        }
    }

    /// Returns `[true]` if the Evaluator is ready to be evaluated.
    pub fn is_ready(&self) -> bool {
        self.result.is_some() || self.error.is_some()
    }

    /// Evals the script with the given bindings and returns the resulting [`Value`].
    /// Returns an [`EvaluationError`] when errors occurs during the evaluation.
    pub fn eval(self) -> Result<Value, EvaluationError> {
        if let Some(error) = self.error {
            return Err(EvaluationError::Error(error));
        }

        self.result
            .ok_or(EvaluationError::PartialEvaluation)
            .and_then(|val| Value::try_from(&val))
    }
}

/// Represents an evaluation error.
#[derive(Error, Debug)]
pub enum EvaluationError {
    /// Represents an incomplete evaluation error.
    #[error("Evaluation is incomplete")]
    PartialEvaluation,

    /// Represents a type mismatch error.
    #[error("Unsupported DataType")]
    TypeMismatch,

    /// Represents a runtime error.
    #[error("Error evaluating expression: {0}")]
    Error(RuntimeError),
}