/*
 * Copyright 2025 coze-dev 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 plugin

import (
	"context"
	"fmt"
	"strconv"

	"github.com/getkin/kin-openapi/openapi3"
	"golang.org/x/exp/maps"

	"github.com/cloudwego/eino/compose"

	"github.com/cloudwego/eino/schema"

	"github.com/coze-dev/coze-studio/backend/api/model/crossdomain/plugin"
	workflow3 "github.com/coze-dev/coze-studio/backend/api/model/ocean/cloud/workflow"
	common "github.com/coze-dev/coze-studio/backend/api/model/plugin_develop_common"
	"github.com/coze-dev/coze-studio/backend/application/base/pluginutil"
	"github.com/coze-dev/coze-studio/backend/domain/plugin/entity"
	"github.com/coze-dev/coze-studio/backend/domain/plugin/service"
	"github.com/coze-dev/coze-studio/backend/domain/workflow"
	crossplugin "github.com/coze-dev/coze-studio/backend/domain/workflow/crossdomain/plugin"
	entity2 "github.com/coze-dev/coze-studio/backend/domain/workflow/entity"
	"github.com/coze-dev/coze-studio/backend/domain/workflow/entity/vo"
	"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
	"github.com/coze-dev/coze-studio/backend/pkg/errorx"
	"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
	"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
	"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
	"github.com/coze-dev/coze-studio/backend/pkg/sonic"
	"github.com/coze-dev/coze-studio/backend/types/errno"
)

type pluginService struct {
	client service.PluginService
	tos    storage.Storage
}

func NewPluginService(client service.PluginService, tos storage.Storage) crossplugin.Service {
	return &pluginService{client: client, tos: tos}
}

type pluginInfo struct {
	*entity.PluginInfo
	LatestVersion *string
}

func (t *pluginService) getPluginsWithTools(ctx context.Context, pluginEntity *crossplugin.Entity, toolIDs []int64, isDraft bool) (
	_ *pluginInfo, toolsInfo []*entity.ToolInfo, err error) {
	defer func() {
		if err != nil {
			err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err)
		}
	}()

	var pluginsInfo []*entity.PluginInfo
	var latestPluginInfo *entity.PluginInfo
	pluginID := pluginEntity.PluginID
	if isDraft {
		plugins, err := t.client.MGetDraftPlugins(ctx, []int64{pluginID})
		if err != nil {
			return nil, nil, err
		}
		pluginsInfo = plugins
	} else if pluginEntity.PluginVersion == nil || (pluginEntity.PluginVersion != nil && *pluginEntity.PluginVersion == "") {
		plugins, err := t.client.MGetOnlinePlugins(ctx, []int64{pluginID})
		if err != nil {
			return nil, nil, err
		}
		pluginsInfo = plugins

	} else {
		plugins, err := t.client.MGetVersionPlugins(ctx, []entity.VersionPlugin{
			{PluginID: pluginID, Version: *pluginEntity.PluginVersion},
		})
		if err != nil {
			return nil, nil, err
		}
		pluginsInfo = plugins

		onlinePlugins, err := t.client.MGetOnlinePlugins(ctx, []int64{pluginID})
		if err != nil {
			return nil, nil, err
		}
		for _, pi := range onlinePlugins {
			if pi.ID == pluginID {
				latestPluginInfo = pi
				break
			}
		}
	}

	var pInfo *entity.PluginInfo
	for _, p := range pluginsInfo {
		if p.ID == pluginID {
			pInfo = p
			break
		}
	}
	if pInfo == nil {
		return nil, nil, vo.NewError(errno.ErrPluginIDNotFound, errorx.KV("id", strconv.FormatInt(pluginID, 10)))
	}

	if isDraft {
		tools, err := t.client.MGetDraftTools(ctx, toolIDs)
		if err != nil {
			return nil, nil, err
		}
		toolsInfo = tools
	} else if pluginEntity.PluginVersion == nil || (pluginEntity.PluginVersion != nil && *pluginEntity.PluginVersion == "") {
		tools, err := t.client.MGetOnlineTools(ctx, toolIDs)
		if err != nil {
			return nil, nil, err
		}
		toolsInfo = tools
	} else {
		eVersionTools := slices.Transform(toolIDs, func(tid int64) entity.VersionTool {
			return entity.VersionTool{
				ToolID:  tid,
				Version: *pluginEntity.PluginVersion,
			}
		})
		tools, err := t.client.MGetVersionTools(ctx, eVersionTools)
		if err != nil {
			return nil, nil, err
		}
		toolsInfo = tools
	}

	if latestPluginInfo != nil {
		return &pluginInfo{PluginInfo: pInfo, LatestVersion: latestPluginInfo.Version}, toolsInfo, nil
	}

	return &pluginInfo{PluginInfo: pInfo}, toolsInfo, nil
}

func (t *pluginService) GetPluginToolsInfo(ctx context.Context, req *crossplugin.ToolsInfoRequest) (
	_ *crossplugin.ToolsInfoResponse, err error) {
	defer func() {
		if err != nil {
			err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err)
		}
	}()

	var toolsInfo []*entity.ToolInfo
	isDraft := req.IsDraft || (req.PluginEntity.PluginVersion != nil && *req.PluginEntity.PluginVersion == "0")
	pInfo, toolsInfo, err := t.getPluginsWithTools(ctx, &crossplugin.Entity{PluginID: req.PluginEntity.PluginID, PluginVersion: req.PluginEntity.PluginVersion}, req.ToolIDs, isDraft)
	if err != nil {
		return nil, err
	}

	url, err := t.tos.GetObjectUrl(ctx, pInfo.GetIconURI())
	if err != nil {
		return nil, vo.WrapIfNeeded(errno.ErrTOSError, err)
	}

	response := &crossplugin.ToolsInfoResponse{
		PluginID:      pInfo.ID,
		SpaceID:       pInfo.SpaceID,
		Version:       pInfo.GetVersion(),
		PluginName:    pInfo.GetName(),
		Description:   pInfo.GetDesc(),
		IconURL:       url,
		PluginType:    int64(pInfo.PluginType),
		ToolInfoList:  make(map[int64]crossplugin.ToolInfo),
		LatestVersion: pInfo.LatestVersion,
		IsOfficial:    pInfo.IsOfficial(),
		AppID:         pInfo.GetAPPID(),
	}

	for _, tf := range toolsInfo {
		inputs, err := tf.ToReqAPIParameter()
		if err != nil {
			return nil, err
		}
		outputs, err := tf.ToRespAPIParameter()
		if err != nil {
			return nil, err
		}
		toolExample := pInfo.GetToolExample(ctx, tf.GetName())

		var (
			requestExample  string
			responseExample string
		)
		if toolExample != nil {
			requestExample = toolExample.RequestExample
			responseExample = toolExample.RequestExample
		}

		response.ToolInfoList[tf.ID] = crossplugin.ToolInfo{
			ToolID:      tf.ID,
			ToolName:    tf.GetName(),
			Inputs:      slices.Transform(inputs, toWorkflowAPIParameter),
			Outputs:     slices.Transform(outputs, toWorkflowAPIParameter),
			Description: tf.GetDesc(),
			DebugExample: &crossplugin.DebugExample{
				ReqExample:  requestExample,
				RespExample: responseExample,
			},
		}

	}
	return response, nil
}

func (t *pluginService) GetPluginInvokableTools(ctx context.Context, req *crossplugin.ToolsInvokableRequest) (
	_ map[int64]crossplugin.InvokableTool, err error) {
	defer func() {
		if err != nil {
			err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err)
		}
	}()

	var toolsInfo []*entity.ToolInfo
	isDraft := req.IsDraft || (req.PluginEntity.PluginVersion != nil && *req.PluginEntity.PluginVersion == "0")
	pInfo, toolsInfo, err := t.getPluginsWithTools(ctx, &crossplugin.Entity{
		PluginID:      req.PluginEntity.PluginID,
		PluginVersion: req.PluginEntity.PluginVersion,
	}, maps.Keys(req.ToolsInvokableInfo), isDraft)
	if err != nil {
		return nil, err
	}

	result := map[int64]crossplugin.InvokableTool{}
	for _, tf := range toolsInfo {
		tl := &pluginInvokeTool{
			pluginEntity: crossplugin.Entity{
				PluginID:      pInfo.ID,
				PluginVersion: pInfo.Version,
			},
			client:   t.client,
			toolInfo: tf,
			IsDraft:  isDraft,
		}

		if r, ok := req.ToolsInvokableInfo[tf.ID]; ok && (r.RequestAPIParametersConfig != nil && r.ResponseAPIParametersConfig != nil) {
			reqPluginCommonAPIParameters := slices.Transform(r.RequestAPIParametersConfig, toPluginCommonAPIParameter)
			respPluginCommonAPIParameters := slices.Transform(r.ResponseAPIParametersConfig, toPluginCommonAPIParameter)

			tl.toolOperation, err = pluginutil.APIParamsToOpenapiOperation(reqPluginCommonAPIParameters, respPluginCommonAPIParameters)
			if err != nil {
				return nil, err
			}

			tl.toolOperation.OperationID = tf.Operation.OperationID
			tl.toolOperation.Summary = tf.Operation.Summary
		}

		result[tf.ID] = tl
	}
	return result, nil
}

func (t *pluginService) ExecutePlugin(ctx context.Context, input map[string]any, pe *crossplugin.Entity,
	toolID int64, cfg crossplugin.ExecConfig) (map[string]any, error) {
	args, err := sonic.MarshalString(input)
	if err != nil {
		return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
	}

	req := &service.ExecuteToolRequest{
		UserID:          conv.Int64ToStr(cfg.Operator),
		PluginID:        pe.PluginID,
		ToolID:          toolID,
		ExecScene:       plugin.ExecSceneOfWorkflow,
		ArgumentsInJson: args,
		ExecDraftTool:   pe.PluginVersion == nil || *pe.PluginVersion == "0",
	}
	execOpts := []entity.ExecuteToolOpt{
		plugin.WithInvalidRespProcessStrategy(plugin.InvalidResponseProcessStrategyOfReturnDefault),
	}

	if pe.PluginVersion != nil {
		execOpts = append(execOpts, plugin.WithToolVersion(*pe.PluginVersion))
	}

	r, err := t.client.ExecuteTool(ctx, req, execOpts...)
	if err != nil {
		if extra, ok := compose.IsInterruptRerunError(err); ok {
			pluginTIE, ok := extra.(*plugin.ToolInterruptEvent)
			if !ok {
				return nil, vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("expects ToolInterruptEvent, got %T", extra))
			}

			var eventType workflow3.EventType
			switch pluginTIE.Event {
			case plugin.InterruptEventTypeOfToolNeedOAuth:
				eventType = workflow3.EventType_WorkflowOauthPlugin
			default:
				return nil, vo.WrapError(errno.ErrPluginAPIErr,
					fmt.Errorf("unsupported interrupt event type: %s", pluginTIE.Event))
			}

			id, err := workflow.GetRepository().GenID(ctx)
			if err != nil {
				return nil, vo.WrapError(errno.ErrIDGenError, err)
			}

			ie := &entity2.InterruptEvent{
				ID:            id,
				InterruptData: pluginTIE.ToolNeedOAuth.Message,
				EventType:     eventType,
			}

			// temporarily replace interrupt with real error, until frontend can handle plugin oauth interrupt
			interruptData := ie.InterruptData
			return nil, vo.NewError(errno.ErrAuthorizationRequired, errorx.KV("extra", interruptData))
		}
		return nil, err
	}

	var output map[string]any
	err = sonic.UnmarshalString(r.RawResp, &output)
	if err != nil {
		return nil, vo.WrapError(errno.ErrSerializationDeserializationFail, err)
	}

	return output, nil
}

type pluginInvokeTool struct {
	pluginEntity  crossplugin.Entity
	client        service.PluginService
	toolInfo      *entity.ToolInfo
	toolOperation *openapi3.Operation
	IsDraft       bool
}

func (p *pluginInvokeTool) Info(ctx context.Context) (_ *schema.ToolInfo, err error) {
	defer func() {
		if err != nil {
			err = vo.WrapIfNeeded(errno.ErrPluginAPIErr, err)
		}
	}()

	var parameterInfo map[string]*schema.ParameterInfo
	if p.toolOperation != nil {
		parameterInfo, err = plugin.NewOpenapi3Operation(p.toolOperation).ToEinoSchemaParameterInfo(ctx)
	} else {
		parameterInfo, err = p.toolInfo.Operation.ToEinoSchemaParameterInfo(ctx)
	}

	if err != nil {
		return nil, err
	}

	return &schema.ToolInfo{
		Name:        p.toolInfo.GetName(),
		Desc:        p.toolInfo.GetDesc(),
		ParamsOneOf: schema.NewParamsOneOfByParams(parameterInfo),
	}, nil
}

func (p *pluginInvokeTool) PluginInvoke(ctx context.Context, argumentsInJSON string, cfg crossplugin.ExecConfig) (string, error) {
	req := &service.ExecuteToolRequest{
		UserID:          conv.Int64ToStr(cfg.Operator),
		PluginID:        p.pluginEntity.PluginID,
		ToolID:          p.toolInfo.ID,
		ExecScene:       plugin.ExecSceneOfWorkflow,
		ArgumentsInJson: argumentsInJSON,
		ExecDraftTool:   p.IsDraft,
	}
	execOpts := []entity.ExecuteToolOpt{
		plugin.WithInvalidRespProcessStrategy(plugin.InvalidResponseProcessStrategyOfReturnDefault),
	}

	if p.pluginEntity.PluginVersion != nil {
		execOpts = append(execOpts, plugin.WithToolVersion(*p.pluginEntity.PluginVersion))
	}

	if p.toolOperation != nil {
		execOpts = append(execOpts, plugin.WithOpenapiOperation(plugin.NewOpenapi3Operation(p.toolOperation)))
	}

	r, err := p.client.ExecuteTool(ctx, req, execOpts...)
	if err != nil {
		if extra, ok := compose.IsInterruptRerunError(err); ok {
			pluginTIE, ok := extra.(*plugin.ToolInterruptEvent)
			if !ok {
				return "", vo.WrapError(errno.ErrPluginAPIErr, fmt.Errorf("expects ToolInterruptEvent, got %T", extra))
			}

			var eventType workflow3.EventType
			switch pluginTIE.Event {
			case plugin.InterruptEventTypeOfToolNeedOAuth:
				eventType = workflow3.EventType_WorkflowOauthPlugin
			default:
				return "", vo.WrapError(errno.ErrPluginAPIErr,
					fmt.Errorf("unsupported interrupt event type: %s", pluginTIE.Event))
			}

			id, err := workflow.GetRepository().GenID(ctx)
			if err != nil {
				return "", vo.WrapError(errno.ErrIDGenError, err)
			}

			ie := &entity2.InterruptEvent{
				ID:            id,
				InterruptData: pluginTIE.ToolNeedOAuth.Message,
				EventType:     eventType,
			}

			tie := &entity2.ToolInterruptEvent{
				ToolCallID:     compose.GetToolCallID(ctx),
				ToolName:       p.toolInfo.GetName(),
				InterruptEvent: ie,
			}

			// temporarily replace interrupt with real error, until frontend can handle plugin oauth interrupt
			_ = tie
			interruptData := ie.InterruptData
			return "", vo.NewError(errno.ErrAuthorizationRequired, errorx.KV("extra", interruptData))
		}
		return "", err
	}
	return r.TrimmedResp, nil
}

func toPluginCommonAPIParameter(parameter *workflow3.APIParameter) *common.APIParameter {
	if parameter == nil {
		return nil
	}
	p := &common.APIParameter{
		ID:            parameter.ID,
		Name:          parameter.Name,
		Desc:          parameter.Desc,
		Type:          common.ParameterType(parameter.Type),
		Location:      common.ParameterLocation(parameter.Location),
		IsRequired:    parameter.IsRequired,
		GlobalDefault: parameter.GlobalDefault,
		GlobalDisable: parameter.GlobalDisable,
		LocalDefault:  parameter.LocalDefault,
		LocalDisable:  parameter.LocalDisable,
		VariableRef:   parameter.VariableRef,
	}
	if parameter.SubType != nil {
		p.SubType = ptr.Of(common.ParameterType(*parameter.SubType))
	}

	if parameter.DefaultParamSource != nil {
		p.DefaultParamSource = ptr.Of(common.DefaultParamSource(*parameter.DefaultParamSource))
	}
	if parameter.AssistType != nil {
		p.AssistType = ptr.Of(common.AssistParameterType(*parameter.AssistType))
	}

	if len(parameter.SubParameters) > 0 {
		p.SubParameters = make([]*common.APIParameter, 0, len(parameter.SubParameters))
		for _, subParam := range parameter.SubParameters {
			p.SubParameters = append(p.SubParameters, toPluginCommonAPIParameter(subParam))
		}
	}

	return p
}

func toWorkflowAPIParameter(parameter *common.APIParameter) *workflow3.APIParameter {
	if parameter == nil {
		return nil
	}
	p := &workflow3.APIParameter{
		ID:            parameter.ID,
		Name:          parameter.Name,
		Desc:          parameter.Desc,
		Type:          workflow3.ParameterType(parameter.Type),
		Location:      workflow3.ParameterLocation(parameter.Location),
		IsRequired:    parameter.IsRequired,
		GlobalDefault: parameter.GlobalDefault,
		GlobalDisable: parameter.GlobalDisable,
		LocalDefault:  parameter.LocalDefault,
		LocalDisable:  parameter.LocalDisable,
		VariableRef:   parameter.VariableRef,
	}
	if parameter.SubType != nil {
		p.SubType = ptr.Of(workflow3.ParameterType(*parameter.SubType))
	}

	if parameter.DefaultParamSource != nil {
		p.DefaultParamSource = ptr.Of(workflow3.DefaultParamSource(*parameter.DefaultParamSource))
	}
	if parameter.AssistType != nil {
		p.AssistType = ptr.Of(workflow3.AssistParameterType(*parameter.AssistType))
	}

	if len(parameter.SubParameters) > 0 {
		p.SubParameters = make([]*workflow3.APIParameter, 0, len(parameter.SubParameters))
		for _, subParam := range parameter.SubParameters {
			p.SubParameters = append(p.SubParameters, toWorkflowAPIParameter(subParam))
		}
	}

	return p
}
