package tools

import (
	"context"
	"fmt"
	"strings"

	"security-center-mcp/internal/client"
)

// caasmToolNames is the set of tools executed by this file (P4-T4).
// The remaining four registered tools belong to P4-T5 (vuln.go) and
// are routed through callVulnTool instead.
var caasmToolNames = map[string]bool{
	"search_assets":          true,
	"get_asset":              true,
	"list_ports":             true,
	"list_high_risk_ports":   true,
	"list_dns_records":       true,
	"list_domains":           true,
	"get_relationship_graph": true,
	"list_cloud_accounts":    true,
	"list_sync_jobs":         true,
	"list_api_route_assets":  true,
}

// callCAASMTool executes one of the 10 CAASM tools. Mode switches (per
// the registry's merge conventions): id-bearing tools query their
// detail/comments/events sub-endpoints when id is set, the list
// endpoint otherwise; search_assets switches to the IP-asset dimension
// when an IP-only filter is present; get_asset and the cloud-account
// detail aggregate multiple endpoints into one result; the
// parameterless auxiliary endpoints (stats, services, environments,
// registrars, config/active) are selected by the dispatch-only "view"
// argument.
func callCAASMTool(ctx context.Context, c *client.Client, tool Tool, args map[string]any) (any, error) {
	switch tool.Name {
	case "search_assets":
		// view switches to the enum-value endpoints (no parameters).
		view, err := viewArg(tool.Name, args, "services", "environments")
		if err != nil {
			return nil, err
		}
		switch view {
		case "services":
			return getData(ctx, c, tool.Endpoints[2], args)
		case "environments":
			return getData(ctx, c, tool.Endpoints[3], args)
		}
		// region/exposure/asset_service are declared only by the
		// ip-assets endpoint: their presence selects that dimension.
		if hasAny(args, "region", "exposure", "asset_service") {
			return getData(ctx, c, tool.Endpoints[1], args)
		}
		return getData(ctx, c, tool.Endpoints[0], args)

	case "get_asset":
		// Aggregate detail + tags into {"asset": ..., "tags": ...}.
		asset, err := getData(ctx, c, tool.Endpoints[0], args)
		if err != nil {
			return nil, err
		}
		tags, err := getData(ctx, c, tool.Endpoints[1], args)
		if err != nil {
			return nil, err
		}
		return map[string]any{"asset": asset, "tags": tags}, nil

	case "list_ports":
		// view switches to stats (no parameters) or the asset-dimension
		// alias (same filters as the primary list); it is mutually
		// exclusive with the id detail switch.
		view, err := viewArg(tool.Name, args, "stats", "by_asset")
		if err != nil {
			return nil, err
		}
		if view != "" {
			if has(args, "id") {
				return nil, &ArgError{Msg: "list_ports: id 与 view 互斥，只能传一个"}
			}
			if view == "stats" {
				return getData(ctx, c, tool.Endpoints[2], args)
			}
			return getData(ctx, c, tool.Endpoints[3], args) // assets/ports/search
		}
		if has(args, "id") {
			return getData(ctx, c, tool.Endpoints[1], args) // ports/{id}
		}
		return getData(ctx, c, tool.Endpoints[0], args)

	case "list_dns_records":
		if has(args, "id") {
			return getData(ctx, c, tool.Endpoints[1], args) // dns-records/{id}/comments
		}
		return getData(ctx, c, tool.Endpoints[0], args)

	case "list_domains":
		view, err := viewArg(tool.Name, args, "stats", "registrars")
		if err != nil {
			return nil, err
		}
		switch view {
		case "stats":
			return getData(ctx, c, tool.Endpoints[1], args)
		case "registrars":
			return getData(ctx, c, tool.Endpoints[2], args)
		}
		return getData(ctx, c, tool.Endpoints[0], args)

	case "list_api_route_assets":
		view, err := viewArg(tool.Name, args, "config")
		if err != nil {
			return nil, err
		}
		if view == "config" {
			return getData(ctx, c, tool.Endpoints[1], args) // api-assets/config/active
		}
		return getData(ctx, c, tool.Endpoints[0], args)

	case "list_cloud_accounts":
		if has(args, "id") {
			// Aggregate account detail + its sync jobs into
			// {"account": ..., "jobs": ...}; page/page_size apply to
			// the jobs sub-list.
			account, err := getData(ctx, c, tool.Endpoints[1], args)
			if err != nil {
				return nil, err
			}
			jobs, err := getData(ctx, c, tool.Endpoints[2], args)
			if err != nil {
				return nil, err
			}
			return map[string]any{"account": account, "jobs": jobs}, nil
		}
		return getData(ctx, c, tool.Endpoints[0], args)

	case "list_sync_jobs":
		return callSyncJobs(ctx, c, tool, args)

	default:
		// list_high_risk_ports, get_relationship_graph: no schema
		// switch — primary endpoint.
		return getData(ctx, c, tool.Endpoints[0], args)
	}
}

// callSyncJobs routes list_sync_jobs between its three endpoints: the
// event stream when id comes with event-paging arguments, the detail
// when only id is set, and the plain list otherwise. The
// limit/before_id/after_id arguments are declared by the events
// endpoint only, so they require id; before_id and after_id are
// mutually exclusive (both rules are enforced here, not just in the
// schema description).
func callSyncJobs(ctx context.Context, c *client.Client, tool Tool, args map[string]any) (any, error) {
	if has(args, "before_id") && has(args, "after_id") {
		return nil, &ArgError{Msg: "list_sync_jobs: before_id 与 after_id 互斥，只能传一个"}
	}
	if hasAny(args, "limit", "before_id", "after_id") {
		if !has(args, "id") {
			return nil, &ArgError{Msg: "list_sync_jobs: limit/before_id/after_id 需与 id 同传（事件流模式）"}
		}
		return getData(ctx, c, tool.Endpoints[2], args) // sync-jobs/{id}/events
	}
	if has(args, "id") {
		return getData(ctx, c, tool.Endpoints[1], args) // sync-jobs/{id}
	}
	return getData(ctx, c, tool.Endpoints[0], args)
}

// has reports whether the argument is present and non-nil.
func has(args map[string]any, key string) bool {
	v, ok := args[key]
	return ok && v != nil
}

// hasAny reports whether any of the arguments is present and non-nil.
func hasAny(args map[string]any, keys ...string) bool {
	for _, key := range keys {
		if has(args, key) {
			return true
		}
	}
	return false
}

// viewArg reads the dispatch-only "view" argument: "" when absent, an
// *ArgError listing the legal values when set to anything else. view is
// mapped by no endpoint's ParamMap, so it never reaches the backend
// query string (same convention as the vuln tools' kind).
func viewArg(toolName string, args map[string]any, legal ...string) (string, error) {
	v, ok := args["view"]
	if !ok || v == nil {
		return "", nil
	}
	s, ok := v.(string)
	if ok {
		for _, l := range legal {
			if s == l {
				return s, nil
			}
		}
	}
	return "", &ArgError{Msg: fmt.Sprintf("%s: 参数 view 取值 %v 非法，合法值：%s", toolName, v, strings.Join(legal, "/"))}
}
