/*
 * AquaVM Workflow Engine
 *
 * Copyright (C) 2024 Fluence DAO
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation version 3 of the
 * License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use super::ExecutionCtx;
use super::ExecutionResult;
use super::TraceHandler;
use crate::execution_step::execution_context::check_error_object;
use crate::execution_step::resolver::Resolvable;
use crate::execution_step::CatchableError;
use crate::execution_step::RcSecurityTetraplet;
use crate::log_instruction;
use crate::ExecutionError;
use crate::JValue;

use air_interpreter_data::Provenance;
use air_parser::ast;
use air_parser::ast::Fail;
use polyplets::SecurityTetraplet;

use std::rc::Rc;

impl<'i> super::ExecutableInstruction<'i> for Fail<'i> {
    fn execute(&self, exec_ctx: &mut ExecutionCtx<'i>, trace_ctx: &mut TraceHandler) -> ExecutionResult<()> {
        log_instruction!(fail, exec_ctx, trace_ctx);

        match self {
            Fail::Scalar(scalar) => fail_with_scalar(scalar, exec_ctx),
            Fail::ScalarWithLambda(scalar) => fail_with_scalar_wl(scalar, exec_ctx),
            &Fail::Literal {
                ret_code,
                error_message,
            } => fail_with_literals(ret_code, error_message, self, exec_ctx),
            Fail::CanonStreamWithLambda(canon_stream) => fail_with_canon_stream(canon_stream, exec_ctx),
            // bubble last error up
            Fail::LastError => fail_with_last_error(exec_ctx),
            Fail::Error => fail_with_error(exec_ctx),
        }
    }
}

fn fail_with_scalar<'i>(scalar: &ast::Scalar<'i>, exec_ctx: &mut ExecutionCtx<'i>) -> ExecutionResult<()> {
    let (value, mut tetraplet, provenance) = scalar.resolve(exec_ctx)?;
    // tetraplets always have one element here and it'll be refactored after boxed value
    let tetraplet = tetraplet.remove(0);
    check_error_object(&value).map_err(CatchableError::InvalidErrorObjectError)?;

    fail_with_error_object(exec_ctx, value, Some(tetraplet), provenance)
}

fn fail_with_scalar_wl<'i>(scalar: &ast::ScalarWithLambda<'i>, exec_ctx: &mut ExecutionCtx<'i>) -> ExecutionResult<()> {
    let (value, mut tetraplet, provenance) = scalar.resolve(exec_ctx)?;
    // tetraplets always have one element here and it'll be refactored after boxed value
    let tetraplet = tetraplet.remove(0);
    check_error_object(&value).map_err(CatchableError::InvalidErrorObjectError)?;

    fail_with_error_object(exec_ctx, value, Some(tetraplet), provenance)
}

fn fail_with_literals(
    error_code: i64,
    error_message: &str,
    fail: &Fail<'_>,
    exec_ctx: &mut ExecutionCtx<'_>,
) -> ExecutionResult<()> {
    let error_object = crate::execution_step::execution_context::error_from_raw_fields_w_peerid(
        error_code,
        error_message,
        &fail.to_string(),
        exec_ctx.run_parameters.init_peer_id.as_ref(),
    );

    let literal_tetraplet = SecurityTetraplet::literal_tetraplet(exec_ctx.run_parameters.init_peer_id.as_ref());
    let literal_tetraplet = Rc::new(literal_tetraplet);
    // in (fail x y), x and y are always literals
    let provenance = Provenance::literal();

    fail_with_error_object(exec_ctx, error_object, Some(literal_tetraplet), provenance)
}

fn fail_with_canon_stream(
    ast_canon: &ast::CanonStreamWithLambda<'_>,
    exec_ctx: &mut ExecutionCtx<'_>,
) -> ExecutionResult<()> {
    let (value, mut tetraplets, provenance) = ast_canon.resolve(exec_ctx)?;

    // tetraplets always have one element here and it'll be refactored after boxed value
    check_error_object(&value).map_err(CatchableError::InvalidErrorObjectError)?;

    fail_with_error_object(exec_ctx, value, Some(tetraplets.remove(0)), provenance)
}

fn fail_with_last_error(exec_ctx: &mut ExecutionCtx<'_>) -> ExecutionResult<()> {
    use crate::execution_step::InstructionError;

    let InstructionError {
        error,
        tetraplet,
        provenance,
        ..
    } = exec_ctx.last_error_descriptor.error();

    check_error_object(error).map_err(CatchableError::InvalidErrorObjectError)?;

    // to avoid warnings from https://github.com/rust-lang/rust/issues/59159
    let error = error.clone();
    let tetraplet = tetraplet.clone();

    fail_with_error_object(exec_ctx, error, tetraplet, provenance.clone())
}

fn fail_with_error(exec_ctx: &mut ExecutionCtx<'_>) -> ExecutionResult<()> {
    use crate::execution_step::InstructionError;

    let InstructionError {
        error,
        tetraplet,
        provenance,
        orig_catchable,
    } = exec_ctx.error_descriptor.error();

    check_error_object(error).map_err(CatchableError::InvalidErrorObjectError)?;

    let result = match orig_catchable {
        Some(orig_catchable) => {
            let catchable_to_return = orig_catchable.clone();
            let _ = fail_with_error_object(exec_ctx, error.clone(), tetraplet.clone(), provenance.clone());
            Err(ExecutionError::Catchable(catchable_to_return.into()))
        }
        None => fail_with_error_object(exec_ctx, error.clone(), tetraplet.clone(), provenance.clone()),
    };
    exec_ctx.error_descriptor.disable_error_setting();
    result
}

fn fail_with_error_object(
    exec_ctx: &mut ExecutionCtx<'_>,
    error: JValue,
    tetraplet: Option<RcSecurityTetraplet>,
    provenance: Provenance,
) -> ExecutionResult<()> {
    exec_ctx
        .last_error_descriptor
        .set_from_error_object(error.clone(), tetraplet, provenance);
    exec_ctx.make_subgraph_incomplete();

    Err(ExecutionError::Catchable(Rc::new(CatchableError::UserError { error })))
}
