//
//  Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
//  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 tool

import (
	"fmt"
	"math"
	"strconv"
	"strings"

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

// Factory builds a tool instance by DSL / Agent-visible name and
// optional node-level configuration. The config map belongs to the
// Agent node / DSL, not to the model-emitted function-call args.
type Factory func(params map[string]any) (einotool.BaseTool, error)

var registry = map[string]Factory{
	"akshare":               buildAkShareTool,
	"arxiv":                 buildArxivTool,
	"bgpt":                  buildBGPTTool,
	"code_exec":             noConfig("code_exec", func() einotool.BaseTool { return NewCodeExecTool() }),
	"crawler":               noConfig("crawler", func() einotool.BaseTool { return NewCrawlerTool() }),
	"deepl":                 noConfig("deepl", func() einotool.BaseTool { return NewDeepLTool() }),
	"duckduckgo":            buildDuckDuckGoTool,
	"email":                 buildEmailTool,
	"execute_sql":           buildExeSQLTool,
	"exesql":                buildExeSQLTool,
	"github":                buildGitHubTool,
	"google":                buildGoogleTool,
	"google_scholar":        buildGoogleScholarTool,
	"google_scholar_search": buildGoogleScholarTool,
	"jin10":                 noConfig("jin10", func() einotool.BaseTool { return NewJin10Tool() }),
	"keenable":              buildKeenableTool,
	"pubmed":                buildPubMedTool,
	"qweather":              noConfig("qweather", func() einotool.BaseTool { return NewQWeatherTool() }),
	"querit":                buildQueritTool,
	"querit_search":         buildQueritTool,
	"queritsearch":          buildQueritTool,
	"retrieval":             buildRetrievalTool,
	"search_my_dataset":     buildRetrievalTool,
	"search_my_dateset":     buildRetrievalTool,
	"searxng":               buildSearXNGTool,
	"tavily":                buildTavilyTool,
	// Agent DSL tool lists carry the Python Canvas component_name verbatim.
	// BuildByName lower-cases names, so register those component names too.
	"tavilysearch":     buildTavilyTool,
	"tavily_extract":   buildTavilyExtractTool,
	"tavilyextract":    buildTavilyExtractTool,
	"tushare":          noConfig("tushare", func() einotool.BaseTool { return NewTushareTool() }),
	"wencai":           buildWencaiTool,
	"web_crawler":      noConfig("web_crawler", func() einotool.BaseTool { return NewCrawlerTool() }),
	"wikipedia":        buildWikipediaTool,
	"wikipedia_search": buildWikipediaTool,
	"yahoo_finance":    buildYahooFinanceTool,
}

func noConfig(name string, fn func() einotool.BaseTool) Factory {
	return func(params map[string]any) (einotool.BaseTool, error) {
		if len(params) != 0 {
			return nil, fmt.Errorf("agent tool: tool %q does not accept node-level params", name)
		}
		return fn(), nil
	}
}

// BuildByName resolves a tool name into an Eino BaseTool.
func BuildByName(name string, params map[string]any) (einotool.BaseTool, error) {
	key := strings.ToLower(strings.TrimSpace(name))
	if key == "" {
		return nil, fmt.Errorf("agent tool: empty tool name")
	}
	factory, ok := registry[key]
	if !ok {
		return nil, fmt.Errorf("agent tool: unsupported tool %q", name)
	}
	if factory == nil {
		return nil, fmt.Errorf("agent tool: nil factory for %q", name)
	}
	return factory(params)
}

// BuildAll resolves a list of tool names into Eino BaseTool instances.
// perToolParams is keyed by the Agent-visible tool name.
func BuildAll(names []string, perToolParams map[string]map[string]any) ([]einotool.BaseTool, error) {
	if len(names) == 0 {
		return nil, nil
	}
	tools := make([]einotool.BaseTool, 0, len(names))
	for _, name := range names {
		var params map[string]any
		if perToolParams != nil {
			params = perToolParams[strings.ToLower(strings.TrimSpace(name))]
			if params == nil {
				params = perToolParams[name]
			}
		}
		t, err := BuildByName(name, params)
		if err != nil {
			return nil, err
		}
		tools = append(tools, t)
	}
	return tools, nil
}

func buildAkShareTool(params map[string]any) (einotool.BaseTool, error) {
	topN := defaultAkShareTopN
	if len(params) != 0 {
		for key := range params {
			if key != "top_n" {
				return nil, fmt.Errorf("agent tool: tool %q only accepts node-level param top_n", "akshare")
			}
		}
		if v, ok := intParam(params, "top_n"); ok {
			topN = v
		}
		if topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "akshare")
		}
	}
	return NewAkShareToolWithTopN(nil, topN), nil
}

func buildArxivTool(params map[string]any) (einotool.BaseTool, error) {
	topN := defaultArxivTopN
	sortBy := defaultArxivSortBy
	if v, ok := intParam(params, "top_n"); ok {
		topN = v
	}
	if topN <= 0 {
		return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "arxiv")
	}
	if v, ok := stringParam(params, "sort_by"); ok {
		sortBy = v
	}
	if !ArxivSortBySupported(sortBy) {
		return nil, fmt.Errorf("agent tool: tool %q has unsupported sort_by %q", "arxiv", sortBy)
	}
	return NewArxivToolWithParams(nil, topN, sortBy), nil
}

func buildBGPTTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := bgptParams{}
	if value, ok := params["api_key"]; ok {
		apiKey, valid := value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param api_key", "bgpt")
		}
		defaults.APIKey = apiKey
	}
	if value, ok := params["top_n"]; ok {
		topN, valid := strictInt(value)
		if text, isString := value.(string); isString {
			parsed, err := strconv.Atoi(strings.TrimSpace(text))
			topN, valid = parsed, err == nil
		}
		if !valid || topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "bgpt")
		}
		defaults.TopN = topN
	}
	if value, ok := params["days_back"]; ok && value != nil && value != "" {
		daysBack, valid := strictInt(value)
		if !valid || daysBack <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param days_back", "bgpt")
		}
		defaults.DaysBack = daysBack
	}
	return newBGPTTool(nil, defaults), nil
}

func buildExeSQLTool(params map[string]any) (einotool.BaseTool, error) {
	conn, err := decodeExeSQLConnParams(params)
	if err != nil {
		return nil, err
	}
	return NewExeSQLTool(conn), nil
}

func buildEmailTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := emailParams{SMTPPort: 465}
	stringFields := map[string]*string{
		"smtp_server":   &defaults.SMTPServer,
		"email":         &defaults.Email,
		"smtp_username": &defaults.SMTPUsername,
		"password":      &defaults.Password,
		"sender_name":   &defaults.SenderName,
		"to_email":      &defaults.ToEmail,
		"cc_email":      &defaults.CCEmail,
		"content":       &defaults.Content,
		"subject":       &defaults.Subject,
	}
	for key, destination := range stringFields {
		value, exists := params[key]
		if !exists {
			continue
		}
		text, valid := value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param %s", "email", key)
		}
		*destination = text
	}
	if value, exists := params["smtp_port"]; exists {
		port, valid := strictInt(value)
		if text, isString := value.(string); isString {
			parsed, err := strconv.Atoi(strings.TrimSpace(text))
			port, valid = parsed, err == nil
		}
		if !valid || port <= 0 || port > 65535 {
			return nil, fmt.Errorf("agent tool: tool %q requires integer node-level param smtp_port in [1, 65535]", "email")
		}
		defaults.SMTPPort = port
	}
	return newEmailTool(defaults), nil
}

func buildDuckDuckGoTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := duckduckgoParams{}
	if value, ok := params["top_n"]; ok {
		topN, valid := strictInt(value)
		if !valid || topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "duckduckgo")
		}
		defaults.TopN = topN
	}
	if value, ok := params["channel"]; ok {
		channel, valid := value.(string)
		if !valid || (channel != "text" && channel != "general" && channel != "news") {
			return nil, fmt.Errorf("agent tool: tool %q has unsupported channel %q", "duckduckgo", channel)
		}
		defaults.Channel = normalizeDuckDuckGoChannel(channel)
	}
	return newDuckDuckGoTool(nil, defaults), nil
}

func buildGoogleTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := googleParams{}
	if v, ok := stringParam(params, "api_key"); ok {
		defaults.APIKey = v
	}
	if v, ok := stringParam(params, "country"); ok {
		defaults.Country = v
	}
	if v, ok := stringParam(params, "language"); ok {
		defaults.Language = v
	}
	if v, ok := stringParam(params, "q"); ok {
		defaults.Q = v
	}
	if v, ok := intParam(params, "start"); ok {
		defaults.Start = v
	}
	if v, ok := intParam(params, "num"); ok {
		defaults.Num = v
	}
	return NewGoogleToolWithDefaults(nil, defaults), nil
}

func buildGitHubTool(params map[string]any) (einotool.BaseTool, error) {
	topN := defaultGitHubTopN
	if raw, exists := params["top_n"]; exists {
		value, ok := intParam(params, "top_n")
		if !ok {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "github")
		}
		if decimal, ok := raw.(float64); ok && math.Trunc(decimal) != decimal {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "github")
		}
		topN = value
	}
	if topN <= 0 {
		return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "github")
	}
	if topN > maxGitHubTopN {
		return nil, fmt.Errorf("agent tool: tool %q requires node-level param top_n to be at most %d", "github", maxGitHubTopN)
	}
	return NewGitHubToolWithDefaults(nil, githubParams{TopN: topN}), nil
}

func buildGoogleScholarTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := googleScholarParams{}
	if v, ok := stringParam(params, "query"); ok {
		defaults.Query = v
	}
	if value, ok := params["top_n"]; ok {
		topN, valid := strictInt(value)
		if !valid || topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "google_scholar")
		}
		defaults.TopN = topN
	}
	if value, ok := params["sort_by"]; ok {
		sortBy, valid := value.(string)
		if !valid || (sortBy != "date" && sortBy != "relevance") {
			return nil, fmt.Errorf("agent tool: tool %q has unsupported sort_by %q", "google_scholar", sortBy)
		}
		defaults.SortBy = sortBy
	}
	if value, ok := params["year_low"]; ok && value != nil {
		yearLow, valid := strictInt(value)
		if !valid || yearLow <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param year_low", "google_scholar")
		}
		defaults.YearLow = yearLow
	}
	if value, ok := params["year_high"]; ok && value != nil {
		yearHigh, valid := strictInt(value)
		if !valid || yearHigh <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param year_high", "google_scholar")
		}
		defaults.YearHigh = yearHigh
	}
	if v, ok := boolParam(params, "patents"); ok {
		defaults.Patents = &v
	}
	if value, ok := params["patents"]; ok {
		if _, valid := value.(bool); !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires boolean node-level param patents", "google_scholar")
		}
	}
	return NewGoogleScholarToolWithDefaults(nil, defaults), nil
}

func buildPubMedTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := pubmedParams{}
	if value, ok := params["top_n"]; ok {
		topN, valid := strictInt(value)
		if !valid || topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "pubmed")
		}
		defaults.TopN = topN
	}
	if value, ok := params["email"]; ok {
		email, valid := value.(string)
		if !valid || strings.TrimSpace(email) == "" {
			return nil, fmt.Errorf("agent tool: tool %q requires non-empty string node-level param email", "pubmed")
		}
		defaults.Email = email
	}
	return NewPubMedToolWithDefaults(nil, defaults), nil
}

func buildRetrievalTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := retrievalArgs{}
	for key := range params {
		switch key {
		case "dataset_ids", "kb_ids", "top_n", "top_k", "similarity_threshold",
			"keywords_similarity_weight", "use_kg", "rerank_id", "empty_response",
			"toc_enhance", "meta_data_filter", "retrieval_from", "memory_ids",
			"kb_vars", "cross_languages", "function_name", "description", "meta",
			"inputs", "outputs":
		default:
			return nil, fmt.Errorf("agent tool: retrieval tool does not accept node-level param %s", key)
		}
	}

	if ids, ok, err := stringSliceParam(params, "dataset_ids"); err != nil {
		return nil, fmt.Errorf("agent tool: retrieval config: %w", err)
	} else if ok {
		defaults.DatasetIDs = ids
	}
	if ids, ok, err := stringSliceParam(params, "kb_ids"); err != nil {
		return nil, fmt.Errorf("agent tool: retrieval config: %w", err)
	} else if ok {
		defaults.KBIDs = ids
		if len(defaults.DatasetIDs) == 0 {
			defaults.DatasetIDs = ids
		}
	}
	if v, ok := intParam(params, "top_n"); ok {
		defaults.TopN = v
	}
	if raw, exists := params["top_k"]; exists {
		value, ok := strictInt(raw)
		if !ok || value <= 0 {
			return nil, fmt.Errorf("agent tool: retrieval tool requires positive integer node-level param top_k")
		}
		defaults.TopK = value
	}
	if v, ok := boolParam(params, "use_kg"); ok {
		defaults.UseKG = v
	}
	if v, ok := floatParam(params, "similarity_threshold"); ok {
		defaults.SimilarityThreshold = v
	}
	if v, ok := floatParam(params, "keywords_similarity_weight"); ok {
		if v < 0 || v > 1 {
			return nil, fmt.Errorf("agent tool: retrieval tool requires node-level param keywords_similarity_weight in [0,1]")
		}
		defaults.KeywordsSimilarityWeight = &v
	}
	if value, ok := params["empty_response"]; ok {
		emptyResponse, ok := value.(string)
		if !ok {
			return nil, fmt.Errorf("agent tool: retrieval tool requires string node-level param empty_response")
		}
		defaults.EmptyResponse = emptyResponse
	}
	return NewRetrievalToolWithDefaults(defaults), nil
}

func buildSearXNGTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := defaultSearXNGParams()
	if value, ok := params["top_n"]; ok {
		topN, valid := parseSearXNGTopN(value)
		if !valid || topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "searxng")
		}
		defaults.TopN = topN
	}
	if value, ok := params["searxng_url"]; ok {
		searxngURL, valid := value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param searxng_url", "searxng")
		}
		defaults.SearXNGURL = searxngURL
	}
	return newSearXNGToolWithDefaults(nil, defaults), nil
}

func buildWencaiTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := wencaiParams{}
	if value, ok := params["top_n"]; ok {
		topN, valid := strictInt(value)
		if !valid || topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "wencai")
		}
		defaults.TopN = topN
	}
	if value, ok := params["query_type"]; ok {
		queryType, valid := value.(string)
		if !valid || !isWencaiQueryTypeSupported(queryType) {
			return nil, fmt.Errorf("agent tool: tool %q has unsupported query_type %q", "wencai", queryType)
		}
		defaults.QueryType = queryType
	}
	return newWencaiTool(defaults), nil
}

func buildTavilyExtractTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := tavilyExtractParams{}
	if value, ok := params["api_key"]; ok {
		apiKey, valid := value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param api_key", "tavily_extract")
		}
		defaults.APIKey = apiKey
	}
	if value, ok := params["urls"]; ok {
		defaults.URLs = value
	}
	if value, ok := params["extract_depth"]; ok {
		extractDepth, valid := value.(string)
		if !valid || (extractDepth != "basic" && extractDepth != "advanced") {
			return nil, fmt.Errorf("agent tool: tool %q has unsupported extract_depth %q", "tavily_extract", extractDepth)
		}
		defaults.ExtractDepth = extractDepth
	}
	if value, ok := params["format"]; ok {
		format, valid := value.(string)
		if !valid || (format != "markdown" && format != "text") {
			return nil, fmt.Errorf("agent tool: tool %q has unsupported format %q", "tavily_extract", format)
		}
		defaults.Format = format
	}
	return newTavilyExtractTool(nil, nil, defaults), nil
}

func buildTavilyTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := tavilyParams{}
	if value, ok := params["api_key"]; ok {
		apiKey, valid := value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param api_key", "tavily")
		}
		defaults.APIKey = apiKey
	}
	if value, ok := params["search_depth"]; ok {
		searchDepth, valid := value.(string)
		if !valid || (searchDepth != "basic" && searchDepth != "advanced") {
			return nil, fmt.Errorf("agent tool: tool %q has unsupported search_depth %q", "tavily", searchDepth)
		}
		defaults.SearchDepth = searchDepth
	}
	if value, ok := params["max_results"]; ok {
		maxResults, valid := strictInt(value)
		if !valid || maxResults <= 0 || maxResults > 20 {
			return nil, fmt.Errorf("agent tool: tool %q requires integer node-level param max_results within [1, 20]", "tavily")
		}
		defaults.MaxResults = maxResults
	}
	if value, ok := params["days"]; ok {
		days, valid := strictInt(value)
		if !valid || days <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param days", "tavily")
		}
		defaults.Days = days
	}
	for _, key := range []string{"include_answer", "include_raw_content", "include_images", "include_image_descriptions"} {
		value, ok := params[key]
		if !ok {
			continue
		}
		flag, valid := value.(bool)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires boolean node-level param %s", "tavily", key)
		}
		switch key {
		case "include_answer":
			defaults.IncludeAnswer = flag
		case "include_raw_content":
			defaults.IncludeRawContent = flag
		case "include_images":
			defaults.IncludeImages = flag
		case "include_image_descriptions":
			defaults.IncludeImageDescriptions = flag
		}
	}
	return newTavilyTool(nil, nil, defaults), nil
}

func buildQueritTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := queritParams{}
	stringFields := map[string]*string{
		"api_key":    &defaults.APIKey,
		"query":      &defaults.Query,
		"time_range": &defaults.TimeRange,
	}
	for key, destination := range stringFields {
		value, exists := params[key]
		if !exists {
			continue
		}
		text, valid := value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param %s", "querit_search", key)
		}
		*destination = text
	}
	if value, exists := params["count"]; exists {
		count, valid := strictInt(value)
		if !valid || count < 1 {
			return nil, fmt.Errorf("agent tool: tool %q requires integer node-level param count of at least 1", "querit_search")
		}
		defaults.Count = count
	}
	if value, exists := params["chunks_per_doc"]; exists {
		chunksPerDoc, valid := strictInt(value)
		if !valid || chunksPerDoc < 1 || chunksPerDoc > 3 {
			return nil, fmt.Errorf("agent tool: tool %q requires integer node-level param chunks_per_doc within [1, 3]", "querit_search")
		}
		defaults.ChunksPerDoc = queritInt(chunksPerDoc)
	}
	listFields := map[string]*[]string{
		"site_include":     &defaults.SiteInclude,
		"site_exclude":     &defaults.SiteExclude,
		"country_include":  &defaults.CountryInclude,
		"language_include": &defaults.LanguageInclude,
	}
	for key, destination := range listFields {
		value, exists := params[key]
		if !exists {
			continue
		}
		items, valid := queritStringSlice(value)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string array node-level param %s", "querit_search", key)
		}
		*destination = items
	}
	if !isValidQueritTimeRange(strings.TrimSpace(defaults.TimeRange)) {
		return nil, fmt.Errorf("agent tool: tool %q has invalid node-level param time_range", "querit_search")
	}
	return newQueritTool(nil, nil, defaults, nil), nil
}

func buildKeenableTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := keenableParams{}
	apiKey := ""
	if value, ok := params["api_key"]; ok {
		var valid bool
		apiKey, valid = value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param api_key", "keenable")
		}
	}
	if value, ok := params["mode"]; ok {
		mode, valid := value.(string)
		if !valid || (mode != "pro" && mode != "realtime") {
			return nil, fmt.Errorf("agent tool: tool %q has unsupported mode %q", "keenable", mode)
		}
		defaults.Mode = mode
	}
	if value, ok := params["top_n"]; ok {
		topN, valid := strictInt(value)
		if !valid || topN <= 0 {
			return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "keenable")
		}
		defaults.TopN = topN
	}
	if value, ok := params["site"]; ok {
		site, valid := value.(string)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires string node-level param site", "keenable")
		}
		defaults.Site = site
	}
	if defaults.Mode == "realtime" && strings.TrimSpace(apiKey) == "" {
		return nil, fmt.Errorf("agent tool: tool %q requires api_key for realtime mode", "keenable")
	}
	return newKeenableTool(nil, nil, apiKey, defaults), nil
}

func buildWikipediaTool(params map[string]any) (einotool.BaseTool, error) {
	topN := defaultWikipediaTopN
	language := defaultWikipediaLanguage
	if v, ok := intParam(params, "top_n"); ok {
		topN = v
	}
	if topN <= 0 {
		return nil, fmt.Errorf("agent tool: tool %q requires positive integer node-level param top_n", "wikipedia")
	}
	if v, ok := stringParam(params, "language"); ok {
		language = strings.TrimSpace(v)
	}
	if language == "" {
		return nil, fmt.Errorf("agent tool: tool %q requires non-empty string node-level param language", "wikipedia")
	}
	if !WikipediaLanguageSupported(language) {
		return nil, fmt.Errorf("agent tool: tool %q unsupported node-level param language %q", "wikipedia", language)
	}
	return NewWikipediaToolWithParams(nil, topN, language), nil
}

func buildYahooFinanceTool(params map[string]any) (einotool.BaseTool, error) {
	defaults := defaultYahooFinanceParams()
	boolFields := map[string]*bool{
		"info":                &defaults.Info,
		"history":             &defaults.History,
		"count":               &defaults.Count,
		"financials":          &defaults.Financials,
		"income_stmt":         &defaults.IncomeStmt,
		"balance_sheet":       &defaults.BalanceSheet,
		"cash_flow_statement": &defaults.CashFlowStatement,
		"news":                &defaults.News,
	}
	for key, target := range boolFields {
		value, exists := params[key]
		if !exists {
			continue
		}
		flag, valid := value.(bool)
		if !valid {
			return nil, fmt.Errorf("agent tool: tool %q requires boolean node-level param %s", "yahoo_finance", key)
		}
		*target = flag
	}
	return NewYahooFinanceToolWithDefaults(nil, defaults), nil
}

func decodeExeSQLConnParams(params map[string]any) (exesqlConnParams, error) {
	if len(params) == 0 {
		return exesqlConnParams{}, fmt.Errorf(
			"agent tool: execute_sql requires node-level params " +
				"(db_type/host/port/database/username/password)",
		)
	}
	conn, err := NewExeSQLConnParams(params)
	if err != nil {
		return exesqlConnParams{}, fmt.Errorf("agent tool: execute_sql config: %w", err)
	}
	if err := conn.check(); err != nil {
		return exesqlConnParams{}, fmt.Errorf("agent tool: execute_sql config: %w", err)
	}
	return conn, nil
}

func stringParam(params map[string]any, key string) (string, bool) {
	v, ok := params[key]
	if !ok {
		return "", false
	}
	s, ok := v.(string)
	return s, ok
}

func intParam(params map[string]any, key string) (int, bool) {
	v, ok := params[key]
	if !ok {
		return 0, false
	}
	switch x := v.(type) {
	case int:
		return x, true
	case int32:
		return int(x), true
	case int64:
		return int(x), true
	case float64:
		return int(x), true
	default:
		return 0, false
	}
}

func floatParam(params map[string]any, key string) (float64, bool) {
	v, ok := params[key]
	if !ok {
		return 0, false
	}
	switch x := v.(type) {
	case float64:
		return x, true
	case float32:
		return float64(x), true
	case int:
		return float64(x), true
	case int32:
		return float64(x), true
	case int64:
		return float64(x), true
	default:
		return 0, false
	}
}

func stringSliceParam(params map[string]any, key string) ([]string, bool, error) {
	v, ok := params[key]
	if !ok {
		return nil, false, nil
	}
	switch x := v.(type) {
	case []string:
		return append([]string(nil), x...), true, nil
	case []any:
		out := make([]string, 0, len(x))
		for _, item := range x {
			s, ok := item.(string)
			if !ok {
				return nil, true, fmt.Errorf("%s must be a string list", key)
			}
			if strings.TrimSpace(s) != "" {
				out = append(out, strings.TrimSpace(s))
			}
		}
		return out, true, nil
	default:
		return nil, true, fmt.Errorf("%s must be a string list", key)
	}
}

func strictInt(value any) (int, bool) {
	switch x := value.(type) {
	case int:
		return x, true
	case int32:
		return int(x), true
	case int64:
		if int64(int(x)) != x {
			return 0, false
		}
		return int(x), true
	case float64:
		maxInt := int(^uint(0) >> 1)
		minInt := -maxInt - 1
		if math.Trunc(x) != x || x >= float64(maxInt) || x <= float64(minInt) {
			return 0, false
		}
		return int(x), true
	default:
		return 0, false
	}
}

func boolParam(params map[string]any, key string) (bool, bool) {
	v, ok := params[key]
	if !ok {
		return false, false
	}
	b, ok := v.(bool)
	return b, ok
}
