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

const GETMETHOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
    name: "fh",
    ty: BuiltinParamType::Any,
    arity: BuiltinParamArity::Required,
    default: None,
    description: "Bound method closure/handle.",
}];

const GETMETHOD_INPUTS: [BuiltinParamDescriptor; 2] = [
    BuiltinParamDescriptor {
        name: "obj_or_class",
        ty: BuiltinParamType::Any,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Object receiver or class reference.",
    },
    BuiltinParamDescriptor {
        name: "name",
        ty: BuiltinParamType::StringScalar,
        arity: BuiltinParamArity::Required,
        default: None,
        description: "Method name.",
    },
];

const GETMETHOD_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
    label: "fh = getmethod(obj_or_class, name)",
    inputs: &GETMETHOD_INPUTS,
    outputs: &GETMETHOD_OUTPUT,
}];

const GETMETHOD_ERROR_NAME_INVALID: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.GETMETHOD.NAME_INVALID",
    identifier: Some("RunMat:GetMethodNameInvalid"),
    when: "Method name is empty.",
    message: "getmethod: method name must not be empty",
};

const GETMETHOD_ERROR_RECEIVER_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.GETMETHOD.RECEIVER_UNSUPPORTED",
    identifier: Some("RunMat:GetMethodReceiverUnsupported"),
    when: "Receiver is neither object nor class reference.",
    message: "getmethod: unsupported receiver",
};

const GETMETHOD_ERROR_METHOD_PRIVATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
    code: "RM.GETMETHOD.METHOD_PRIVATE",
    identifier: Some("RunMat:MethodPrivate"),
    when: "Resolved method exists but is inaccessible from the current class scope.",
    message: "getmethod: method is not accessible from current scope",
};

const GETMETHOD_ERRORS: [BuiltinErrorDescriptor; 3] = [
    GETMETHOD_ERROR_NAME_INVALID,
    GETMETHOD_ERROR_RECEIVER_UNSUPPORTED,
    GETMETHOD_ERROR_METHOD_PRIVATE,
];

pub const GETMETHOD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
    signatures: &GETMETHOD_SIGNATURES,
    output_mode: BuiltinOutputMode::Fixed,
    completion_policy: BuiltinCompletionPolicy::Public,
    errors: &GETMETHOD_ERRORS,
};

pub(crate) fn dispatch_getmethod(obj: Value, name: String) -> crate::BuiltinResult<Value> {
    fn ensure_method_accessible(class_name: &str, method_name: &str) -> crate::BuiltinResult<()> {
        let Some((method, owner)) = runmat_builtins::lookup_method(class_name, method_name) else {
            return Ok(());
        };
        let caller_class = crate::class_access_context();
        let access_allowed = match method.access {
            runmat_builtins::Access::Public => true,
            runmat_builtins::Access::Private => caller_class.as_deref() == Some(owner.as_str()),
            runmat_builtins::Access::Protected => caller_class
                .as_deref()
                .is_some_and(|caller| runmat_builtins::is_class_or_subclass(caller, &owner)),
        };
        if access_allowed {
            return Ok(());
        }
        Err(crate::runtime_descriptor_error_with_detail(
            "getmethod",
            &GETMETHOD_ERROR_METHOD_PRIVATE,
            format!("{}.{}", class_name, method_name),
        ))
    }

    let method_name = name.trim();
    if method_name.is_empty() {
        return Err(crate::runtime_descriptor_error(
            "getmethod",
            &GETMETHOD_ERROR_NAME_INVALID,
        ));
    }
    let caller_scope = crate::class_access_context()
        .map(Value::String)
        .unwrap_or_else(|| Value::String(String::new()));
    match obj {
        Value::Object(o) => {
            ensure_method_accessible(&o.class_name, method_name)?;
            if let Some((resolved, _owner)) =
                runmat_builtins::lookup_method(&o.class_name, method_name)
            {
                return Ok(Value::Closure(runmat_builtins::Closure {
                    function_name: resolved.function_name.clone(),
                    bound_function: crate::user_functions::resolve_semantic_function_by_name(
                        &resolved.function_name,
                    ),
                    captures: vec![Value::Object(o)],
                }));
            }
            Ok(Value::Closure(runmat_builtins::Closure {
                function_name: crate::CALL_BOUND_METHOD_BUILTIN_NAME.to_string(),
                bound_function: None,
                captures: vec![
                    Value::Object(o),
                    Value::String(method_name.to_string()),
                    caller_scope.clone(),
                ],
            }))
        }
        Value::HandleObject(h) => {
            ensure_method_accessible(&h.class_name, method_name)?;
            if let Some((resolved, _owner)) =
                runmat_builtins::lookup_method(&h.class_name, method_name)
            {
                return Ok(Value::Closure(runmat_builtins::Closure {
                    function_name: resolved.function_name.clone(),
                    bound_function: crate::user_functions::resolve_semantic_function_by_name(
                        &resolved.function_name,
                    ),
                    captures: vec![Value::HandleObject(h)],
                }));
            }
            Ok(Value::Closure(runmat_builtins::Closure {
                function_name: crate::CALL_BOUND_METHOD_BUILTIN_NAME.to_string(),
                bound_function: None,
                captures: vec![
                    Value::HandleObject(h),
                    Value::String(method_name.to_string()),
                    caller_scope,
                ],
            }))
        }
        Value::ClassRef(cls) => {
            ensure_method_accessible(&cls, method_name)?;
            crate::builtins::introspection::function_handle_text::dispatch_str2func(Value::String(
                format!("@{cls}.{method_name}"),
            ))
        }
        other => Err(crate::runtime_descriptor_error_with_detail(
            "getmethod",
            &GETMETHOD_ERROR_RECEIVER_UNSUPPORTED,
            format!("{other:?}"),
        )),
    }
}

#[runtime_builtin(
    name = "getmethod",
    category = "introspection",
    summary = "Create a method-bound function handle from object/class and method name.",
    keywords = "method,function_handle,classdef,dispatch",
    descriptor(crate::builtins::introspection::getmethod::GETMETHOD_DESCRIPTOR),
    builtin_path = "crate::builtins::introspection::getmethod"
)]
pub async fn getmethod_builtin(obj: Value, name: String) -> crate::BuiltinResult<Value> {
    dispatch_getmethod(obj, name)
}
