use std::collections::BTreeMap;
use std::path::Path;

use runx_contracts::{AuthorityVerb, JsonObject, Receipt};
use runx_parser::GraphStep;

use crate::RuntimeError;
use crate::adapter::SkillOutput;
use crate::effects::{
    EffectAdmission, EffectOutputRequest, EffectReceiptRequest, EffectReplay,
    EffectReplayOutputRequest, EffectReplayReceiptRequest, EffectStepRequest, RuntimeEffectError,
    RuntimeEffectRegistry,
};

pub(super) fn find_effect_replay(
    step: &GraphStep,
    inputs: &JsonObject,
    env: &BTreeMap<String, String>,
    graph_dir: &Path,
    effects: &RuntimeEffectRegistry,
) -> Result<Option<EffectReplay>, RuntimeError> {
    effects
        .find_replay(EffectStepRequest {
            step,
            inputs,
            env,
            graph_dir,
        })
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn recover_pending_effects(
    step: &GraphStep,
    inputs: &JsonObject,
    env: &BTreeMap<String, String>,
    graph_dir: &Path,
    effects: &RuntimeEffectRegistry,
) -> Result<(), RuntimeError> {
    effects
        .recover_pending(EffectStepRequest {
            step,
            inputs,
            env,
            graph_dir,
        })
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn enforce_step_authority_admission(
    step: &GraphStep,
    inputs: &JsonObject,
    env: &BTreeMap<String, String>,
    graph_dir: &Path,
    effects: &RuntimeEffectRegistry,
) -> Result<Option<StepAuthorityContext>, RuntimeError> {
    effects
        .admit(EffectStepRequest {
            step,
            inputs,
            env,
            graph_dir,
        })
        .map(|admission| admission.map(StepAuthorityContext::new))
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn prepare_effect_output_before_gate(
    step: &GraphStep,
    authority: Option<&StepAuthorityContext>,
    claim: &JsonObject,
    output: &mut SkillOutput,
    effects: &RuntimeEffectRegistry,
) -> Result<(), RuntimeError> {
    let Some(authority) = authority else {
        return Ok(());
    };
    effects
        .prepare_output(EffectOutputRequest {
            step,
            admission: &authority.admission,
            claim,
            output,
        })
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn finalize_effect_output_before_success(
    context: EffectReceiptContext<'_>,
) -> Result<(), RuntimeError> {
    let Some(authority) = context.authority else {
        return Ok(());
    };
    let effects = context.effects;
    let step = context.step;
    effects
        .finalize_output(effect_receipt_request(context, authority))
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn persist_effect_state_for_step(
    context: EffectReceiptContext<'_>,
) -> Result<(), RuntimeError> {
    let Some(authority) = context.authority else {
        return Ok(());
    };
    let effects = context.effects;
    let step = context.step;
    effects
        .persist(effect_receipt_request(context, authority))
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn prepare_replay_output(
    step: &GraphStep,
    replay: &EffectReplay,
    output: &mut SkillOutput,
    effects: &RuntimeEffectRegistry,
) -> Result<(), RuntimeError> {
    effects
        .prepare_replay_output(EffectReplayOutputRequest {
            step,
            replay,
            output,
        })
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn validate_replayed_effect(
    step: &GraphStep,
    replay: &EffectReplay,
    receipt: &runx_contracts::Receipt,
    output: &SkillOutput,
    claim: &JsonObject,
    effects: &RuntimeEffectRegistry,
) -> Result<(), RuntimeError> {
    effects
        .validate_replay(EffectReplayReceiptRequest {
            step,
            replay,
            receipt,
            output,
            claim,
        })
        .map_err(|source| runtime_effect_error(step, source))
}

pub(super) fn authority_denied(
    step: &GraphStep,
    verb: AuthorityVerb,
    reason: String,
) -> RuntimeError {
    RuntimeError::AuthorityDenied {
        verb,
        step_id: step.id.clone(),
        reason,
    }
}

pub(super) struct EffectReceiptContext<'a> {
    pub(super) step: &'a GraphStep,
    pub(super) graph_dir: &'a Path,
    pub(super) authority: Option<&'a StepAuthorityContext>,
    pub(super) claim: &'a JsonObject,
    pub(super) output: &'a mut SkillOutput,
    pub(super) receipt: &'a Receipt,
    pub(super) env: &'a BTreeMap<String, String>,
    pub(super) effects: &'a RuntimeEffectRegistry,
}

fn effect_receipt_request<'a>(
    context: EffectReceiptContext<'a>,
    authority: &'a StepAuthorityContext,
) -> EffectReceiptRequest<'a> {
    EffectReceiptRequest {
        step: context.step,
        graph_dir: context.graph_dir,
        admission: &authority.admission,
        claim: context.claim,
        output: context.output,
        receipt: context.receipt,
        env: context.env,
    }
}

fn runtime_effect_error(step: &GraphStep, source: RuntimeEffectError) -> RuntimeError {
    match source {
        RuntimeEffectError::Denied { verb, message, .. } => authority_denied(step, verb, message),
        RuntimeEffectError::Failed {
            operation, message, ..
        } if operation.contains("state") => RuntimeError::effect_state(operation, message),
        other => RuntimeError::ReceiptInvalid {
            message: other.to_string(),
        },
    }
}

#[derive(Clone, Debug)]
pub(super) struct StepAuthorityContext {
    admission: EffectAdmission,
}

impl StepAuthorityContext {
    fn new(admission: EffectAdmission) -> Self {
        Self { admission }
    }

    pub(super) fn admission_witness(&self) -> &runx_core::state_machine::AuthorityAdmissionWitness {
        self.admission.witness()
    }

    pub(super) fn authority_grant_refs(
        &self,
        effects: &RuntimeEffectRegistry,
    ) -> Result<Vec<runx_contracts::Reference>, RuntimeError> {
        effects
            .authority_grant_refs(&self.admission)
            .map_err(|source| RuntimeError::ReceiptInvalid {
                message: source.to_string(),
            })
    }
}
