use crate::error::MobError;
use crate::ids::{AgentIdentity, ProfileName};
use crate::machines::mob_machine as mob_dsl;
use crate::profile::Profile;
use serde::Serialize;
use sha2::{Digest, Sha256};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct AuthorizedSpawnProfileMaterial {
    pub(super) agent_identity: mob_dsl::AgentIdentity,
    pub(super) profile_name: String,
    pub(super) model: String,
    pub(super) profile_material_digest: String,
    pub(super) tool_config_digest: String,
    pub(super) skills_digest: String,
    pub(super) provider_params_digest: Option<String>,
    pub(super) output_schema_digest: Option<String>,
    pub(super) external_addressable: bool,
}

pub(super) fn authorize_spawn_profile_material(
    authority: &mut mob_dsl::MobMachineAuthority,
    agent_identity: &AgentIdentity,
    profile_name: &ProfileName,
    profile: &Profile,
    context: &str,
) -> Result<AuthorizedSpawnProfileMaterial, MobError> {
    let (input, expected) = authorize_spawn_profile_input(agent_identity, profile_name, profile)?;
    let input_debug = format!("{input:?}");
    let transition = mob_dsl::MobMachineMutator::apply(authority, input).map_err(|error| {
        MobError::Internal(format!(
            "{context}: MobMachine rejected spawn profile material {input_debug}: {error}"
        ))
    })?;
    require_authorized_effect(&transition, &expected, context)?;
    Ok(expected)
}

pub(super) fn authorize_spawn_profile_input(
    agent_identity: &AgentIdentity,
    profile_name: &ProfileName,
    profile: &Profile,
) -> Result<(mob_dsl::MobMachineInput, AuthorizedSpawnProfileMaterial), MobError> {
    let profile_material_digest = digest_serializable(&SpawnProfileMaterialDigest {
        profile_name: profile_name.as_str(),
        profile,
    })?;
    let tool_config_digest = digest_serializable(&profile.tools)?;
    let skills_digest = digest_serializable(&profile.skills)?;
    let provider_params_digest = optional_value_digest(profile.provider_params.as_ref())?;
    let output_schema_digest = optional_value_digest(
        profile
            .output_schema
            .as_ref()
            .map(meerkat_core::MeerkatSchema::as_value),
    )?;
    let dsl_identity =
        mob_dsl::AgentIdentity::from_domain(&AgentIdentity::from(agent_identity.as_str()));
    let expected = AuthorizedSpawnProfileMaterial {
        agent_identity: dsl_identity.clone(),
        profile_name: profile_name.as_str().to_string(),
        model: profile.model.clone(),
        profile_material_digest: profile_material_digest.clone(),
        tool_config_digest: tool_config_digest.clone(),
        skills_digest: skills_digest.clone(),
        provider_params_digest: provider_params_digest.clone(),
        output_schema_digest: output_schema_digest.clone(),
        external_addressable: profile.external_addressable,
    };
    Ok((
        mob_dsl::MobMachineInput::AuthorizeSpawnProfile {
            agent_identity: dsl_identity,
            profile_name: expected.profile_name.clone(),
            model: expected.model.clone(),
            profile_material_digest,
            tool_config_digest,
            skills_digest,
            provider_params_digest,
            output_schema_digest,
            external_addressable: profile.external_addressable,
        },
        expected,
    ))
}

pub(super) fn require_authorized_effect(
    transition: &mob_dsl::MobMachineTransition,
    expected: &AuthorizedSpawnProfileMaterial,
    context: &str,
) -> Result<(), MobError> {
    let authorized = transition.effects().iter().any(|effect| {
        matches!(
            effect,
            mob_dsl::MobMachineEffect::SpawnProfileAuthorized {
                agent_identity,
                profile_name,
                model,
                profile_material_digest,
                tool_config_digest,
                skills_digest,
                provider_params_digest,
                output_schema_digest,
                external_addressable,
            } if agent_identity == &expected.agent_identity
                && profile_name == &expected.profile_name
                && model == &expected.model
                && profile_material_digest == &expected.profile_material_digest
                && tool_config_digest == &expected.tool_config_digest
                && skills_digest == &expected.skills_digest
                && provider_params_digest == &expected.provider_params_digest
                && output_schema_digest == &expected.output_schema_digest
                && *external_addressable == expected.external_addressable
        )
    });
    if authorized {
        return Ok(());
    }
    Err(MobError::Internal(format!(
        "{context}: MobMachine accepted spawn profile material without matching typed authorization"
    )))
}

fn optional_value_digest<T: Serialize>(value: Option<&T>) -> Result<Option<String>, MobError> {
    value.map(digest_serializable).transpose()
}

fn digest_serializable(value: &impl Serialize) -> Result<String, MobError> {
    let encoded = serde_json::to_vec(value).map_err(|error| {
        MobError::Internal(format!(
            "failed to encode spawn profile material for authority digest: {error}"
        ))
    })?;
    Ok(format!("{:x}", Sha256::digest(encoded)))
}

#[derive(Serialize)]
struct SpawnProfileMaterialDigest<'a> {
    profile_name: &'a str,
    profile: &'a Profile,
}
