/**************************************************************************************** 
 Copyright © 2003-2012 hbasesoft Corporation. All rights reserved. Reproduction or       <br>
 transmission in whole or in part, in any form or by any means, electronic, mechanical <br>
 or otherwise, is prohibited without the prior written consent of the copyright owner. <br>
 ****************************************************************************************/
package com.hbasesoft.framework.ai.agent.tool;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.hbasesoft.framework.common.ServiceException;
import com.hbasesoft.framework.common.utils.logger.LoggerUtil;

/**
 * <Description> Adapter for converting annotated methods to ToolCallBiFunctionDef <br>
 * 
 * @author 王伟<br>
 * @version 1.0<br>
 * @taskId <br>
 * @CreateDate 2025年8月31日 <br>
 * @since V1.0<br>
 * @see com.hbasesoft.framework.ai.agent.tool <br>
 */
public class AnnotatedMethodToolAdapter extends AbstractBaseTool<Map<String, Object>> {

    private final Object targetObject;

    private final Method method;

    private final Action actionAnnotation;

    private final ObjectMapper objectMapper;

    private final String parameters;

    public AnnotatedMethodToolAdapter(Object targetObject, Method method, Action actionAnnotation,
        ObjectMapper objectMapper) {
        this.targetObject = targetObject;
        this.method = method;
        this.actionAnnotation = actionAnnotation;
        this.objectMapper = objectMapper;
        this.parameters = initParameters();
    }

    @Override
    public String getName() {
        // Use class name + method name as tool name to prevent conflicts
        return method.getName();
    }

    @Override
    public String getDescription() {
        // Use annotation description or generate default
        return actionAnnotation.description().isEmpty() ? "Tool for executing method: " + method.getName()
            : actionAnnotation.description();
    }

    @Override
    public String getServiceGroup() {
        return actionAnnotation.serviceGroup();
    }

    @Override
    public boolean isReturnDirect() {
        return actionAnnotation.returnDirect();
    }

    @Override
    public String getParameters() {
        return parameters;
    }

    @Override
    public Class<Map<String, Object>> getInputType() {
        return (Class<Map<String, Object>>) (Class<?>) Map.class;
    }

    @Override
    public String getCurrentToolStateString() {
        return "AnnotatedMethodToolAdapter for method: " + method.getName();
    }

    @Override
    public void cleanup(String planId) {
        // No specific cleanup needed for method-based tools
    }

    @Override
    public ToolExecuteResult run(Map<String, Object> input) {
        try {
            // Convert input parameters to method arguments
            Parameter[] methodParameters = method.getParameters();

            // If this is a Spring AOP proxy, get parameter names from the target method
            Parameter[] targetMethodParameters = methodParameters;
            if (AopUtils.isAopProxy(targetObject)) {
                try {
                    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(targetObject);
                    Method targetMethod = targetClass.getMethod(method.getName(), method.getParameterTypes());
                    targetMethodParameters = targetMethod.getParameters();
                }
                catch (NoSuchMethodException e) {
                    // If we can't find the method in the target class, use the proxy method parameters
                }
            }

            Object[] args = new Object[methodParameters.length];

            for (int i = 0; i < methodParameters.length; i++) {
                Parameter param = methodParameters[i];
                String paramName = (i < targetMethodParameters.length) ? targetMethodParameters[i].getName()
                    : param.getName();
                Object value = input.get(paramName);

                // Handle type conversion
                if (value != null) {
                    args[i] = convertValue(value, param.getType());
                }
                else {
                    // Check if parameter is required
                    ActionParam actionParam = getActionParamAnnotation(param);
                    if (actionParam != null && actionParam.required()) {
                        throw new IllegalArgumentException("Required parameter '" + paramName + "' is missing");
                    }
                    args[i] = null;
                }
            }

            // Invoke the method
            Object result = method.invoke(targetObject, args);

            // Return result as ToolExecuteResult
            return new ToolExecuteResult(result != null ? result.toString() : "", false);
        }
        catch (Exception e) {

            LoggerUtil.error(e);
            return new ToolExecuteResult("Error executing tool: " + e.getMessage(), true);
        }
    }

    /**
     * Get ActionParam annotation from parameter, handling Spring proxy objects
     * 
     * @param param The parameter to get annotation from
     * @return The ActionParam annotation or null if not found
     */
    private ActionParam getActionParamAnnotation(Parameter param) {
        // First try to get annotation directly from the parameter
        ActionParam actionParam = param.getAnnotation(ActionParam.class);
        if (actionParam != null) {
            return actionParam;
        }

        // If not found and targetObject is a Spring proxy, try to get from target method
        if (AopUtils.isAopProxy(targetObject)) {
            try {
                // Get the target class
                Class<?> targetClass = AopProxyUtils.ultimateTargetClass(targetObject);

                // Find the corresponding method in the target class
                Method targetMethod = targetClass.getMethod(method.getName(), method.getParameterTypes());

                // Get the parameter from the target method at the same index
                Parameter[] targetParameters = targetMethod.getParameters();
                int paramIndex = java.util.Arrays.asList(method.getParameters()).indexOf(param);

                if (paramIndex >= 0 && paramIndex < targetParameters.length) {
                    return targetParameters[paramIndex].getAnnotation(ActionParam.class);
                }
            }
            catch (NoSuchMethodException e) {
                // If we can't find the method in the target class, return null
                return null;
            }
        }

        return null;
    }

    private String initParameters() {

        ObjectNode parameters = objectMapper.createObjectNode();
        ObjectNode properties = objectMapper.createObjectNode();
        parameters.put("type", "object");

        // Add method parameters as tool parameters
        Parameter[] methodParameters = method.getParameters();

        // If this is a Spring AOP proxy, get parameter names from the target method
        Parameter[] targetMethodParameters = methodParameters;
        if (AopUtils.isAopProxy(targetObject)) {
            try {
                Class<?> targetClass = AopProxyUtils.ultimateTargetClass(targetObject);
                Method targetMethod = targetClass.getMethod(method.getName(), method.getParameterTypes());
                targetMethodParameters = targetMethod.getParameters();
            }
            catch (NoSuchMethodException e) {
                LoggerUtil.error(e);
            }
        }

        // Process parameters
        for (int i = 0; i < methodParameters.length; i++) {
            Parameter param = methodParameters[i];
            String paramName = (i < targetMethodParameters.length) ? targetMethodParameters[i].getName()
                : param.getName();
            ObjectNode paramNode = objectMapper.createObjectNode();
            ActionParam actionParam = getActionParamAnnotation(param);
            processParameterOrField(param, paramName, paramNode, actionParam);
            properties.set(paramName, paramNode);
        }

        parameters.set("properties", properties);
        // Add required parameters
        ArrayNode requiredArray = objectMapper.createArrayNode();
        for (int i = 0; i < methodParameters.length; i++) {
            Parameter param = methodParameters[i];
            String paramName = (i < targetMethodParameters.length) ? targetMethodParameters[i].getName()
                : param.getName();
            ActionParam actionParam = getActionParamAnnotation(param);
            if (actionParam == null || actionParam.required()) {
                requiredArray.add(paramName);
            }
        }

        if (requiredArray.size() > 0) {
            parameters.set("required", requiredArray);
        }

        return parameters.toString();

    }

    /**
     * Process type information and populate the parameter node
     * 
     * @param type The type to process
     * @param node The node to populate
     */
    private void processType(Class<?> type, ObjectNode node) {
        if (type == String.class) {
            node.put("type", "string");
        }
        else if (type == Integer.class || type == int.class) {
            node.put("type", "integer");
        }
        else if (type == Long.class || type == long.class) {
            node.put("type", "integer");
        }
        else if (type == Double.class || type == double.class) {
            node.put("type", "number");
        }
        else if (type == Boolean.class || type == boolean.class) {
            node.put("type", "boolean");
        }
        else if (type == Date.class || type == LocalDate.class || type == LocalDateTime.class) {
            node.put("type", "string");
            node.put("format", "date-time");
        }
        else if (type.isArray()) {
            // Handle array types
            node.put("type", "array");
            ObjectNode itemsNode = objectMapper.createObjectNode();
            Class<?> componentType = type.getComponentType();
            if (isPrimitiveType(componentType)) {
                processType(componentType, itemsNode);
            }
            else if (componentType == Date.class || componentType == LocalDate.class
                || componentType == LocalDateTime.class) {
                processType(componentType, itemsNode);
            }
            else {
                // Handle complex object types in arrays
                addObjectProperties(componentType, itemsNode);
            }
            node.set("items", itemsNode);
        }
        else if (Collection.class.isAssignableFrom(type)) {
            // Handle Collection types (List, Set, etc.)
            node.put("type", "array");
            ObjectNode itemsNode = objectMapper.createObjectNode();
            // For Collection types, we can't easily determine the generic type at runtime
            // So we'll use a generic object type for items
            itemsNode.put("type", "object");
            node.set("items", itemsNode);
        }
        else if (type.isPrimitive()) {
            // Handle other primitive types
            processPrimitiveType(type, node);
        }
        else {
            // Handle complex object types recursively
            addObjectProperties(type, node);
        }
    }

    /**
     * Check if a type is a primitive type
     * 
     * @param type The type to check
     * @return true if the type is a primitive type
     */
    private boolean isPrimitiveType(Class<?> type) {
        return type == String.class || type == Integer.class || type == int.class || type == Long.class
            || type == long.class || type == Double.class || type == double.class || type == Boolean.class
            || type == boolean.class || type == byte.class || type == short.class || type == float.class
            || type == char.class;
    }

    /**
     * Process primitive types
     * 
     * @param type The primitive type
     * @param node The node to populate
     */
    private void processPrimitiveType(Class<?> type, ObjectNode node) {
        if (type == byte.class || type == short.class) {
            node.put("type", "integer");
        }
        else if (type == float.class) {
            node.put("type", "number");
        }
        else if (type == char.class) {
            node.put("type", "string");
        }
        else {
            // Default to string for other primitives
            node.put("type", "string");
        }
    }

    /**
     * Process a parameter or field, handling type information and metadata
     * 
     * @param paramOrField The parameter or field to process
     * @param name The name of the parameter or field
     * @param node The node to populate
     */
    private void processParameterOrField(Object paramOrField, String name, ObjectNode node, ActionParam actionParam) {
        if (paramOrField instanceof Parameter param) {
            // Process parameter type
            processType(param.getType(), node);
        }
        else if (paramOrField instanceof Field field) {
            // Process field type
            processType(field.getType(), node);
        }

        if (actionParam != null) {
            if (!actionParam.description().isEmpty()) {
                node.put("description", actionParam.description());
            }
            node.put("required", actionParam.required());
        }
        else {
            node.put("description", "Field " + name);
        }
    }

    /**
     * Recursively add object properties to the parameter node
     * 
     * @param clazz The class to analyze
     * @param paramNode The parameter node to add properties to
     */
    private void addObjectProperties(Class<?> clazz, ObjectNode paramNode) {
        // For object types, we need to recursively add their properties
        paramNode.put("type", "object");
        ObjectNode properties = objectMapper.createObjectNode();

        // Get all declared fields of the class
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // Only process fields with ActionParam annotation
            ActionParam actionParam = field.getAnnotation(ActionParam.class);
            if (actionParam != null) {
                ObjectNode fieldNode = objectMapper.createObjectNode();
                // Process field
                processParameterOrField(field, field.getName(), fieldNode, actionParam);

                properties.set(field.getName(), fieldNode);
            }
        }

        paramNode.set("properties", properties);
    }

    /**
     * Convert a value to the specified type
     * 
     * @param value The value to convert
     * @param type The target type
     * @return The converted value
     */
    private Object convertValue(Object value, Class<?> type) {
        if (type == String.class) {
            return value.toString();
        }
        else if (type == Integer.class || type == int.class) {
            return Integer.valueOf(value.toString());
        }
        else if (type == Long.class || type == long.class) {
            return Long.valueOf(value.toString());
        }
        else if (type == Double.class || type == double.class) {
            return Double.valueOf(value.toString());
        }
        else if (type == Boolean.class || type == boolean.class) {
            return Boolean.valueOf(value.toString());
        }
        else if (type == Date.class) {
            // Handle Date type
            if (value instanceof String) {
                // Try to parse as ISO date format
                try {
                    return Date.from(java.time.Instant.parse((String) value));
                }
                catch (Exception e) {
                    return new Date(value.toString());
                }
            }
            else {
                return new Date(value.toString());
            }
        }
        else if (type == LocalDate.class) {
            // Handle LocalDate type
            if (value instanceof String) {
                return LocalDate.parse((String) value, DateTimeFormatter.ISO_LOCAL_DATE);
            }
            else {
                return LocalDate.parse(value.toString(), DateTimeFormatter.ISO_LOCAL_DATE);
            }
        }
        else if (type == LocalDateTime.class) {
            // Handle LocalDateTime type
            if (value instanceof String) {
                return LocalDateTime.parse((String) value, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
            }
            else {
                return LocalDateTime.parse(value.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME);
            }
        }
        else if (type.isArray()) {
            return convertToArray(value, type);
        }
        else if (Collection.class.isAssignableFrom(type)) {
            return convertToCollection(value);
        }
        else {
            // For complex types, try to convert from JSON
            return objectMapper.convertValue(value, type);
        }
    }

    /**
     * Convert a value to an array of the specified type
     * 
     * @param value The value to convert
     * @param arrayType The target array type
     * @return The converted array
     */
    private Object convertToArray(Object value, Class<?> arrayType) {

        if (value instanceof Collection it) {
            Iterable<?> iterable = (Iterable<?>) value;
            Class<?> componentType = arrayType.getComponentType();

            // Convert to array
            if (componentType == String.class) {
                if (iterable instanceof Collection) {
                    return ((Collection<?>) iterable).toArray(new String[0]);
                }
                else {
                    // Handle non-Collection Iterable
                    StringBuilder sb = new StringBuilder();
                    for (Object item : iterable) {
                        sb.append(item.toString());
                    }
                    return new String[] {
                        sb.toString()
                    };
                }
            }
            else if (componentType == Integer.class || componentType == int.class) {
                int[] intArray = new int[iterable instanceof Collection ? ((Collection<?>) iterable).size() : 10];
                int index = 0;
                for (Object item : iterable) {
                    intArray[index++] = Integer.valueOf(item.toString());
                }
                return intArray;
            }
            else if (componentType == Long.class || componentType == long.class) {
                long[] longArray = new long[iterable instanceof Collection ? ((Collection<?>) iterable).size() : 10];
                int index = 0;
                for (Object item : iterable) {
                    longArray[index++] = Long.valueOf(item.toString());
                }
                return longArray;
            }
            else if (componentType == Double.class || componentType == double.class) {
                double[] doubleArray = new double[iterable instanceof Collection ? ((Collection<?>) iterable).size()
                    : 10];
                int index = 0;
                for (Object item : iterable) {
                    doubleArray[index++] = Double.valueOf(item.toString());
                }
                return doubleArray;
            }
            else if (componentType == Boolean.class || componentType == boolean.class) {
                boolean[] booleanArray = new boolean[iterable instanceof Collection ? ((Collection<?>) iterable).size()
                    : 10];
                int index = 0;
                for (Object item : iterable) {
                    booleanArray[index++] = Boolean.valueOf(item.toString());
                }
                return booleanArray;
            }
            else {
                var objectArray = Array.newInstance(componentType, it.size());
                int index = 0;
                for (Object item : it) {
                    Array.set(objectArray, index++, objectMapper.convertValue(item, componentType));
                }
                return objectArray;
            }
        }
        else if (value instanceof String s) {
            try {
                if (s.startsWith("[")) {
                    return objectMapper.readValue(s, arrayType);
                }
                else {
                    Class<?> componentType = arrayType.getComponentType();
                    return objectMapper.readValue(s, componentType);
                }
            }
            catch (JsonProcessingException e) {
                throw new ServiceException(e);
            }
        }
        else {
            return objectMapper.convertValue(value, arrayType);
        }
    }

    /**
     * Convert a value to a collection
     * 
     * @param value The value to convert
     * @return The converted collection
     */
    private Object convertToCollection(Object value) {
        // Handle Collection types (List, Set, etc.)
        if (value instanceof Iterable) {
            Iterable<?> iterable = (Iterable<?>) value;
            // Convert to List
            if (iterable instanceof Collection) {
                return iterable;
            }
            else {
                // Convert non-Collection Iterable to List
                List<Object> list = new ArrayList<>();
                for (Object item : iterable) {
                    list.add(item);
                }
                return list;
            }
        }
        else {
            // Handle as single value collection
            List<Object> list = new ArrayList<>();
            list.add(value);
            return list;
        }
    }
}
