/*
 * Copyright 2025 CloudWeGo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package utils

import (
	"context"

	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/schema"
)

type ErrorHandler func(context.Context, error) string

// WrapToolWithErrorHandler wraps any BaseTool with custom error handling.
// This function detects the tool type (InvokableTool, StreamableTool, or both)
// and applies the appropriate error handling wrapper.
// When the wrapped tool returns an error, the error handler function 'h' will be called
// to convert the error into a string result, and no error will be returned from the wrapper.
//
// Parameters:
//   - t: The original BaseTool to be wrapped
//   - h: A function that converts an error to a string
//
// Returns:
//   - A wrapped BaseTool that handles errors internally based on its capabilities
func WrapToolWithErrorHandler(t tool.BaseTool, h ErrorHandler) tool.BaseTool {
	ih := &infoHelper{info: t.Info}
	var s tool.StreamableTool
	if st, ok := t.(tool.StreamableTool); ok {
		s = st
	}
	if it, ok := t.(tool.InvokableTool); ok {
		if s == nil {
			return WrapInvokableToolWithErrorHandler(it, h)
		} else {
			return &combinedErrorWrapper{
				infoHelper: ih,
				errorHelper: &errorHelper{
					i: it.InvokableRun,
					h: h,
				},
				streamErrorHelper: &streamErrorHelper{
					s: s.StreamableRun,
					h: h,
				},
			}
		}
	}
	if s != nil {
		return WrapStreamableToolWithErrorHandler(s, h)
	}
	return t
}

// WrapInvokableToolWithErrorHandler wraps an InvokableTool with custom error handling.
// When the wrapped tool returns an error, the error handler function 'h' will be called
// to convert the error into a string result, and no error will be returned from the wrapper.
//
// Parameters:
//   - tool: The original InvokableTool to be wrapped
//   - h: A function that converts an error to a string
//
// Returns:
//   - A wrapped InvokableTool that handles errors internally
func WrapInvokableToolWithErrorHandler(t tool.InvokableTool, h ErrorHandler) tool.InvokableTool {
	return &errorWrapper{
		infoHelper: &infoHelper{info: t.Info},
		errorHelper: &errorHelper{
			i: t.InvokableRun,
			h: h,
		},
	}
}

// WrapStreamableToolWithErrorHandler wraps a StreamableTool with custom error handling.
// When the wrapped tool returns an error, the error handler function 'h' will be called
// to convert the error into a string result, which will be returned as a single-item stream,
// and no error will be returned from the wrapper.
//
// Parameters:
//   - tool: The original StreamableTool to be wrapped
//   - h: A function that converts an error to a string
//
// Returns:
//   - A wrapped StreamableTool that handles errors internally
func WrapStreamableToolWithErrorHandler(t tool.StreamableTool, h ErrorHandler) tool.StreamableTool {
	return &streamErrorWrapper{
		infoHelper: &infoHelper{info: t.Info},
		streamErrorHelper: &streamErrorHelper{
			s: t.StreamableRun,
			h: h,
		},
	}
}

type errorWrapper struct {
	*infoHelper
	*errorHelper
}

type streamErrorWrapper struct {
	*infoHelper
	*streamErrorHelper
}

type combinedErrorWrapper struct {
	*infoHelper
	*errorHelper
	*streamErrorHelper
}

type infoHelper struct {
	info func(ctx context.Context) (*schema.ToolInfo, error)
}

func (i *infoHelper) Info(ctx context.Context) (*schema.ToolInfo, error) {
	return i.info(ctx)
}

type errorHelper struct {
	i func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error)
	h ErrorHandler
}

func (s *errorHelper) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
	result, err := s.i(ctx, argumentsInJSON, opts...)
	if err != nil {
		return s.h(ctx, err), nil
	}
	return result, nil
}

type streamErrorHelper struct {
	s func(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error)
	h ErrorHandler
}

func (s *streamErrorHelper) StreamableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (*schema.StreamReader[string], error) {
	result, err := s.s(ctx, argumentsInJSON, opts...)
	if err != nil {
		return schema.StreamReaderFromArray([]string{s.h(ctx, err)}), nil
	}
	return result, nil
}
