use runmat_builtins::{
    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor, Value,
};
use runmat_macros::runtime_builtin;

const CALL_BOUND_METHOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "out",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Variadic,
    default: None,
    description: "Method return value(s).",
}];

const CALL_BOUND_METHOD_INPUTS: [BuiltinParamDescriptor; 4] = [
    BuiltinParamDescriptor {
        name: "base",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Object receiver.",
    },
    BuiltinParamDescriptor {
        name: "method",
        ty: BuiltinParamType::StringScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Method name.",
    },
    BuiltinParamDescriptor {
        name: "scope",
        ty: BuiltinParamType::StringScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Creator class scope for access checks.",
    },
    BuiltinParamDescriptor {
        name: "varargin",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Variadic,
        default: None,
        description: "Method arguments.",
    },
];

const CALL_BOUND_METHOD_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
    [BuiltinSignatureDescriptor {
        label: "[out] = __runmat_call_bound_method__(base, method, scope, varargin)",
        inputs: &CALL_BOUND_METHOD_INPUTS,
        outputs: &CALL_BOUND_METHOD_OUTPUT,
    }];

const CALL_BOUND_METHOD_ERRORS: [BuiltinErrorDescriptor; 1] = [BuiltinErrorDescriptor {
    code: "RM.CALL_BOUND_METHOD.SCOPE_INVALID",
    identifier: Some("RunMat:CallMethodScopeInvalid"),
    when: "Scope argument is not a string scalar.",
    message: "__runmat_call_bound_method__: invalid scope argument",
}];

pub const CALL_BOUND_METHOD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &CALL_BOUND_METHOD_SIGNATURES,
    output_mode: BuiltinOutputMode::ByRequestedOutputCount,
    completion_policy: BuiltinCompletionPolicy::HiddenInternal,
    errors: &CALL_BOUND_METHOD_ERRORS,
};

#[runtime_builtin(
    name = "__runmat_call_bound_method__",
    category = "introspection",
    summary = "Internal bound-method closure invocation with creator class scope.",
    keywords = "classdef,method,dispatch,closure,internal",
    descriptor(crate::builtins::introspection::call_bound_method::CALL_BOUND_METHOD_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::call_bound_method"
)]
pub async fn call_bound_method_builtin(
    base: Value,
    method: String,
    scope: Value,
    rest: Vec<Value>,
) -> crate::BuiltinResult<Value> {
    let scope_class = match scope {
        Value::String(text) => {
            let trimmed = text.trim();
            if trimmed.is_empty() {
                None
            } else {
                Some(trimmed.to_string())
            }
        }
        other => {
            return Err(crate::runtime_descriptor_error_with_detail(
                "__runmat_call_bound_method__",
                &CALL_BOUND_METHOD_ERRORS[0],
                format!("expected scope string, got {other:?}"),
            ))
        }
    };
    let _scope_guard =
        scope_class.map(|class_name| crate::push_class_access_context(Some(class_name)));
    let class_name = crate::object_receiver_class_name(&base).ok_or_else(|| {
        crate::runtime_descriptor_error(
            "__runmat_call_bound_method__",
            &crate::builtins::introspection::call_method::CALL_METHOD_ERROR_RECEIVER_INVALID,
        )
    })?;
    let method = method.trim().to_string();
    if method.is_empty() {
        return Err(crate::runtime_descriptor_error(
            "__runmat_call_bound_method__",
            &crate::builtins::introspection::call_method::CALL_METHOD_ERROR_NAME_INVALID,
        ));
    }
    let mut args = Vec::with_capacity(1 + rest.len());
    args.push(base);
    args.extend(rest);
    let requested_outputs = crate::current_requested_outputs();
    if let Some((_resolved, owner)) = runmat_builtins::lookup_method(&class_name, &method) {
        return crate::dispatch_object_external_member(owner, &method, args, requested_outputs)
            .await;
    }
    crate::dispatch_object_external_member(class_name, &method, args, requested_outputs).await
}
