use strum::IntoStaticStr;

use crate::{
    executors::error::SubgraphExecutorError, headers::errors::HeaderRuleRuntimeError,
    projection::error::ProjectionError, response::graphql_error::GraphQLError,
};

#[derive(thiserror::Error, Debug, IntoStaticStr)]
pub enum PlanExecutionErrorKind {
    #[error("Projection failure: {0}")]
    #[strum(serialize = "PROJECTION_FAILURE")]
    ProjectionFailure(#[from] ProjectionError),

    #[error(transparent)]
    #[strum(serialize = "HEADER_PROPAGATION_FAILURE")]
    HeaderPropagation(#[from] HeaderRuleRuntimeError),

    #[error(transparent)]
    #[strum(serialize = "SUBGRAPH_EXECUTION_FAILURE")]
    SubgraphExecutor(#[from] SubgraphExecutorError),
}

/// The central error type for all query plan execution failures.
///
/// This struct combines a specific `PlanExecutionErrorKind` with a
/// `PlanExecutionErrorContext` that holds shared, dynamic information
/// like the subgraph name or affected GraphQL path.
#[derive(thiserror::Error, Debug)]
#[error("{kind}")]
pub struct PlanExecutionError {
    #[source]
    kind: PlanExecutionErrorKind,
    context: PlanExecutionErrorContext,
}

#[derive(Debug, Clone)]
pub struct PlanExecutionErrorContext {
    subgraph_name: Option<String>,
    affected_path: Option<String>,
}

pub struct LazyPlanContext<SN, AP> {
    pub subgraph_name: SN,
    pub affected_path: AP,
}

impl PlanExecutionError {
    pub(crate) fn new<SN, AP>(
        kind: PlanExecutionErrorKind,
        lazy_context: LazyPlanContext<SN, AP>,
    ) -> Self
    where
        SN: FnOnce() -> Option<String>,
        AP: FnOnce() -> Option<String>,
    {
        Self {
            kind,
            context: PlanExecutionErrorContext {
                subgraph_name: (lazy_context.subgraph_name)(),
                affected_path: (lazy_context.affected_path)(),
            },
        }
    }

    pub fn error_code(&self) -> &'static str {
        if let PlanExecutionErrorKind::SubgraphExecutor(subgraph_error) = &self.kind {
            return subgraph_error.error_code();
        }
        (&self.kind).into()
    }

    pub fn subgraph_name(&self) -> &Option<String> {
        &self.context.subgraph_name
    }

    pub fn affected_path(&self) -> &Option<String> {
        &self.context.affected_path
    }
}

// This is needed for individual fetch node error handling
// Individual fetch node errors are not propagated as PipelineError
// but converted directly to GraphQLError
// and added to `errors` field in GraphQL response
// So failing plan nodes do not fail the whole operation
// See `error_handling_e2e_tests` for reproduction
impl From<&PlanExecutionError> for GraphQLError {
    fn from(val: &PlanExecutionError) -> Self {
        let mut error = GraphQLError::from_message_and_code(val.to_string(), val.error_code());

        // We destructure the context to take ownership of the Option<String> values.
        // Then we move owned Strings directly into builder methods.
        // This way we avoid cloning through Into<String> in those methods.

        if let Some(subgraph_name) = &val.context.subgraph_name {
            error = error.add_subgraph_name(subgraph_name);
        }
        if let Some(affected_path) = &val.context.affected_path {
            error = error.add_affected_path(affected_path);
        }
        error
    }
}

/// An extension trait for `Result` types that can be converted into a `PlanExecutionError`.
///
/// This trait provides a lazy, performant way to add contextual information to
/// an error, only performing work (like cloning strings) if the `Result` is an `Err`.
pub trait IntoPlanExecutionError<T> {
    fn with_plan_context<SN, AP>(
        self,
        context: LazyPlanContext<SN, AP>,
    ) -> Result<T, PlanExecutionError>
    where
        SN: FnOnce() -> Option<String>,
        AP: FnOnce() -> Option<String>;
}

impl<T> IntoPlanExecutionError<T> for Result<T, ProjectionError> {
    fn with_plan_context<SN, AP>(
        self,
        context: LazyPlanContext<SN, AP>,
    ) -> Result<T, PlanExecutionError>
    where
        SN: FnOnce() -> Option<String>,
        AP: FnOnce() -> Option<String>,
    {
        self.map_err(|source| {
            let kind = PlanExecutionErrorKind::ProjectionFailure(source);
            PlanExecutionError::new(kind, context)
        })
    }
}

impl<T> IntoPlanExecutionError<T> for Result<T, HeaderRuleRuntimeError> {
    fn with_plan_context<SN, AP>(
        self,
        context: LazyPlanContext<SN, AP>,
    ) -> Result<T, PlanExecutionError>
    where
        SN: FnOnce() -> Option<String>,
        AP: FnOnce() -> Option<String>,
    {
        self.map_err(|source| {
            let kind = PlanExecutionErrorKind::HeaderPropagation(source);
            PlanExecutionError::new(kind, context)
        })
    }
}

impl<T> IntoPlanExecutionError<T> for Result<T, SubgraphExecutorError> {
    fn with_plan_context<SN, AP>(
        self,
        context: LazyPlanContext<SN, AP>,
    ) -> Result<T, PlanExecutionError>
    where
        SN: FnOnce() -> Option<String>,
        AP: FnOnce() -> Option<String>,
    {
        self.map_err(|source| {
            let kind = PlanExecutionErrorKind::SubgraphExecutor(source);
            PlanExecutionError::new(kind, context)
        })
    }
}
