package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"

	"github.com/pardnchiu/agenvoy/configs"
	"github.com/pardnchiu/agenvoy/extensions"
	"github.com/pardnchiu/agenvoy/internal/filesystem"
	"github.com/pardnchiu/agenvoy/internal/runtime"
	apiAdapter "github.com/pardnchiu/agenvoy/internal/toolAdapter/api"
	scriptAdapter "github.com/pardnchiu/agenvoy/internal/toolAdapter/script"
	toolRegister "github.com/pardnchiu/agenvoy/internal/tools/register"
	toolTypes "github.com/pardnchiu/agenvoy/internal/tools/types"
)

func NewExecutor(workPath, sessionID string, scanner *runtime.SkillScanner) (*toolTypes.Executor, error) {
	var tools []toolTypes.Tool
	if err := json.Unmarshal(toolRegister.JSON(), &tools); err != nil {
		return nil, fmt.Errorf("json.Unmarshal: %w", err)
	}

	var commands []string
	if err := json.Unmarshal(configs.WhiteList, &commands); err != nil {
		return nil, fmt.Errorf("json.Unmarshal: %w", err)
	}

	allowedCommand := make(map[string]bool, len(commands))
	for _, cmd := range commands {
		allowedCommand[cmd] = true
	}

	apiToolbox := apiAdapter.New()
	apiToolbox.LoadFS(extensions.APIs, "apis")

	for _, dir := range []string{
		filesystem.LegacyAPIToolsDir,
		filesystem.LegacyWorkAPIToolsDir,
		filesystem.APIToolsDir,
		filesystem.WorkAPIToolsDir,
	} {
		apiToolbox.Load(dir)
	}

	for _, tool := range apiToolbox.GetTools() {
		data, err := json.Marshal(tool)
		if err != nil {
			continue
		}
		var t toolTypes.Tool
		if err := json.Unmarshal(data, &t); err != nil {
			continue
		}
		tools = append(tools, t)
	}

	for _, name := range apiToolbox.AlwaysAllowNames() {
		toolRegister.MarkAlwaysAllow(name)
	}

	scriptToolbox := scriptAdapter.New()
	for _, dir := range []string{
		filesystem.SystemToolsDir,
		filesystem.LegacyScriptToolsDir,
		filesystem.LegacyWorkScriptToolsDir,
		filesystem.ScriptToolsDir,
		filesystem.WorkScriptToolsDir,
	} {
		scriptToolbox.Scan(dir)
	}

	for _, tool := range scriptToolbox.GetTools() {
		data, err := json.Marshal(tool)
		if err != nil {
			continue
		}
		var t toolTypes.Tool
		if err := json.Unmarshal(data, &t); err != nil {
			continue
		}
		tools = append(tools, t)
	}

	for _, name := range scriptToolbox.AlwaysAllowNames() {
		toolRegister.MarkAlwaysAllow(name)
	}

	// * order fixed, for cache hit
	sort.Slice(tools, func(i, j int) bool {
		return tools[i].Function.Name < tools[j].Function.Name
	})

	// * use claude code idea, use one tool to search and insert
	stubParams := json.RawMessage(`{"type":"object","properties":{}}`)
	stubTools := make(map[string]bool, len(tools))
	initial := make([]toolTypes.Tool, 0, len(tools))
	for _, t := range tools {
		if toolRegister.IsAlwaysLoad(t.Function.Name) {
			initial = append(initial, t)
		} else {
			stubTools[t.Function.Name] = true
			initial = append(initial, toolTypes.Tool{
				Type: t.Type,
				Function: toolTypes.ToolFunction{
					Name:        t.Function.Name,
					Description: t.Function.Description,
					Parameters:  stubParams,
				},
			})
		}
	}

	return &toolTypes.Executor{
		WorkDir:        workPath,
		SessionID:      sessionID,
		AllowedCommand: allowedCommand,
		Tools:          initial,
		AllTools:       tools,
		StubTools:      stubTools,
		APIToolbox:     apiToolbox,
		ScriptToolbox:  scriptToolbox,
		SkillScanner:   scanner,
	}, nil
}

func normalizeArgs(args json.RawMessage) json.RawMessage {
	var m map[string]any
	if err := json.Unmarshal(args, &m); err != nil {
		return args
	}
	for k, v := range m {
		if s, ok := v.(string); ok {
			var unquoted string
			if err := json.Unmarshal([]byte(`"`+s+`"`), &unquoted); err == nil {
				m[k] = unquoted
			}
		}
	}
	if out, err := json.Marshal(m); err == nil {
		return out
	}
	return args
}

func Execute(ctx context.Context, e *toolTypes.Executor, name string, args json.RawMessage) (string, error) {
	args = normalizeArgs(args)

	if e.StubTools[name] {
		activateArgs, _ := json.Marshal(map[string]any{"query": "select:" + name})
		_, _ = toolRegister.Dispatch(ctx, e, "search_tools", activateArgs)
		delete(e.StubTools, name)
	}

	return toolRegister.Dispatch(ctx, e, name, args)
}
