package mcpserver

import (
	"context"
	"sort"

	"github.com/modelcontextprotocol/go-sdk/mcp"

	"github.com/tidbcloud/aisupport/apps/cli/operation-mcp/devops"
)

func boolPtr(b bool) *bool { return &b }

func readOnlyAnnotations(title string) *mcp.ToolAnnotations {
	return &mcp.ToolAnnotations{
		Title:         title,
		ReadOnlyHint:  true,
		OpenWorldHint: boolPtr(true),
	}
}

func (s *server) registerReadTools(srv *mcp.Server) {
	mcp.AddTool(srv, &mcp.Tool{
		Name: "list_operations",
		Description: "List the DevOps operations defined on the server. " +
			"Use it to find the operation name for an action such as restarting a changefeed, " +
			"then call describe_operation for its params. `submittable` marks the operations this " +
			"server is allowed to submit; the rest have to go through Ops Portal by hand.",
		Annotations: readOnlyAnnotations("List operations"),
	}, s.listOperations)

	mcp.AddTool(srv, &mcp.Tool{
		Name: "describe_operation",
		Description: "Describe one DevOps operation: its params (with types, enums and which are required), " +
			"the actions it runs, and the approval chain that gates it.",
		Annotations: readOnlyAnnotations("Describe an operation"),
	}, s.describeOperation)

	mcp.AddTool(srv, &mcp.Tool{
		Name: "get_operation_instance",
		Description: "Get one operation instance by ID, including its current phase, its actions and " +
			"its Ops Portal link. Poll this after submitting to see whether the approval went through.",
		Annotations: readOnlyAnnotations("Get an operation instance"),
	}, s.getInstance)

	mcp.AddTool(srv, &mcp.Tool{
		Name: "list_operation_instances",
		Description: "List operation instances, optionally filtered by operation, phase, applicant or target. " +
			"Use it to check whether an operation is already in flight before submitting a duplicate.",
		Annotations: readOnlyAnnotations("List operation instances"),
	}, s.listInstances)
}

type listOperationsArgs struct {
	Category        string `json:"category,omitempty" jsonschema:"only return operations carrying this category, e.g. dedicated, serverless, cdc-meta"`
	Target          string `json:"target,omitempty" jsonschema:"only return operations acting on this target type: cluster, tenant, project, user, region, resource_pool or undefined"`
	IncludeInternal bool   `json:"include_internal,omitempty" jsonschema:"also list internal operations, which only the DevOps server itself may create"`
}

type operationSummary struct {
	Name        string                 `json:"name"`
	Alias       string                 `json:"alias,omitempty"`
	Target      devops.OperationTarget `json:"target"`
	Categories  []string               `json:"categories,omitempty"`
	Description string                 `json:"description,omitempty"`
	Params      []string               `json:"params,omitempty"`
	Internal    bool                   `json:"internal,omitempty"`
	Submittable bool                   `json:"submittable"`
}

func (s *server) listOperations(_ context.Context, _ *mcp.CallToolRequest, args listOperationsArgs) (*mcp.CallToolResult, any, error) {
	ops := s.catalog.List(devops.CatalogFilter{
		Category:        args.Category,
		Target:          args.Target,
		IncludeInternal: args.IncludeInternal,
	})

	out := make([]operationSummary, 0, len(ops))
	for _, op := range ops {
		out = append(out, operationSummary{
			Name:        op.Name,
			Alias:       op.Alias,
			Target:      op.Target,
			Categories:  op.Categories,
			Description: op.Description,
			Params:      sortedParamNames(op),
			Internal:    op.Internal,
			Submittable: s.allowed[op.Name],
		})
	}

	return jsonResult(map[string]any{
		"total":      len(out),
		"operations": out,
	})
}

func sortedParamNames(op *devops.Operation) []string {
	names := make([]string, 0, len(op.Params))
	for name := range op.Params {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

type describeOperationArgs struct {
	Operation   string `json:"operation" jsonschema:"operation name, e.g. RestartChangefeed"`
	TargetID    string `json:"target_id,omitempty" jsonschema:"target resource id (cluster/tenant/project/resource pool). Supplying it lets the server fill in runtime param values such as the node sizes actually available"`
	TargetType  string `json:"target_type,omitempty" jsonschema:"target type of target_id: cluster, tenant, project, user, region, resource_pool or undefined"`
	ServicePlan string `json:"service_plan,omitempty" jsonschema:"service plan used for runtime param injection"`
}

type paramView struct {
	Name      string           `json:"name"`
	Type      devops.ParamType `json:"type"`
	Required  bool             `json:"required"`
	Desc      string           `json:"desc,omitempty"`
	Default   *string          `json:"default,omitempty"`
	Enum      []string         `json:"enum,omitempty"`
	MultiEnum []string         `json:"multi_enum,omitempty"`
}

type actionView struct {
	Name              string `json:"name"`
	Optional          bool   `json:"optional,omitempty"`
	PreferStartPolicy string `json:"prefer_start_policy,omitempty"`
}

type operationDetail struct {
	Name         string                       `json:"name"`
	Alias        string                       `json:"alias,omitempty"`
	Target       devops.OperationTarget       `json:"target"`
	Description  string                       `json:"description,omitempty"`
	Categories   []string                     `json:"categories,omitempty"`
	Internal     bool                         `json:"internal,omitempty"`
	Submittable  bool                         `json:"submittable"`
	SubmitTool   string                       `json:"submit_tool,omitempty"`
	Params       []paramView                  `json:"params"`
	Actions      []actionView                 `json:"actions"`
	ApprovalTips string                       `json:"approval_tips,omitempty"`
	Approval     []devops.ApprovalNodeSummary `json:"approval_chain"`
	ApprovalNote string                       `json:"approval_note"`
	RuntimeData  map[string]string            `json:"runtime_data,omitempty"`
}

func (s *server) describeOperation(ctx context.Context, _ *mcp.CallToolRequest, args describeOperationArgs) (*mcp.CallToolResult, any, error) {
	op, ok := s.catalog.Get(args.Operation)
	if !ok {
		return errorResult(errUnknownOperation(args.Operation)), nil, nil
	}

	// Ask the server for a param-injected copy only when the caller gave it
	// enough context to inject anything; otherwise the cached catalog is equal.
	if args.TargetID != "" || args.ServicePlan != "" {
		injected, err := s.cli.GetOperation(ctx, args.Operation, devops.GetOperationParams{
			InjectParam: true,
			TargetType:  args.TargetType,
			TargetID:    args.TargetID,
			ServicePlan: args.ServicePlan,
		})
		if err != nil {
			return errorResult(err), nil, nil
		}
		op = injected
	}

	params := make([]paramView, 0, len(op.Params))
	for _, name := range sortedParamNames(op) {
		def := op.Params[name]
		if def.Const != nil {
			// Fixed server-side; not something a caller can or should set.
			continue
		}
		params = append(params, paramView{
			Name:      name,
			Type:      def.Type,
			Required:  !def.Optional && def.Default == nil,
			Desc:      def.Desc,
			Default:   def.Default,
			Enum:      def.Enum,
			MultiEnum: def.MultiEnum,
		})
	}

	actions := make([]actionView, 0, len(op.Actions))
	for _, a := range op.Actions {
		view := actionView{Name: a.Name, Optional: a.Optional}
		if a.PreferStartPolicy != nil {
			view.PreferStartPolicy = string(*a.PreferStartPolicy)
		}
		actions = append(actions, view)
	}

	chain := op.Approval.Summarize()
	note := "Submitting this operation creates an instance that waits for the approvers above; " +
		"no action runs until a human approves it."
	if len(chain) == 0 {
		note = "This operation has no approval nodes configured and is approved as soon as it is created."
	}

	detail := operationDetail{
		Name:         op.Name,
		Alias:        op.Alias,
		Target:       op.Target,
		Description:  op.Description,
		Categories:   op.Categories,
		Internal:     op.Internal,
		Submittable:  s.allowed[op.Name],
		Params:       params,
		Actions:      actions,
		ApprovalTips: op.ApprovalTips,
		Approval:     chain,
		ApprovalNote: note,
		RuntimeData:  op.RuntimeData,
	}
	if detail.Submittable {
		detail.SubmitTool = toolNameFor(op.Name)
	}

	return jsonResult(detail)
}

type getInstanceArgs struct {
	InstanceID uint64 `json:"instance_id" jsonschema:"operation instance id returned when the operation was submitted"`
}

func (s *server) getInstance(ctx context.Context, _ *mcp.CallToolRequest, args getInstanceArgs) (*mcp.CallToolResult, any, error) {
	ins, err := s.cli.GetInstance(ctx, args.InstanceID)
	if err != nil {
		return errorResult(err), nil, nil
	}
	return jsonResult(s.viewInstance(ins))
}

type listInstancesArgs struct {
	Operation    string `json:"operation,omitempty" jsonschema:"filter by operation name, e.g. RestartChangefeed"`
	Phase        string `json:"phase,omitempty" jsonschema:"filter by phase: draft, created, approving, withdrawn, pending, running, succeeded, failed or canceled"`
	Applicant    string `json:"applicant,omitempty" jsonschema:"filter by the applicant's email"`
	TargetID     string `json:"target_id,omitempty" jsonschema:"filter by target resource id, e.g. a cluster id"`
	Type         string `json:"type,omitempty" jsonschema:"filter by target type: cluster, tenant, project, user, region, resource_pool or undefined"`
	CreateAfter  *int64 `json:"create_after,omitempty" jsonschema:"only instances created at or after this unix timestamp in seconds"`
	CreateBefore *int64 `json:"create_before,omitempty" jsonschema:"only instances created at or before this unix timestamp in seconds"`
	Page         int    `json:"page,omitempty" jsonschema:"1-based page number, default 1"`
	PerPage      int    `json:"per_page,omitempty" jsonschema:"page size, 1 to 100, default 100"`
}

func (s *server) listInstances(ctx context.Context, _ *mcp.CallToolRequest, args listInstancesArgs) (*mcp.CallToolResult, any, error) {
	instances, total, err := s.cli.ListInstances(ctx, devops.ListInstancesParams{
		Operation:    args.Operation,
		Phase:        args.Phase,
		Applicant:    args.Applicant,
		TargetID:     args.TargetID,
		Type:         args.Type,
		CreateAfter:  args.CreateAfter,
		CreateBefore: args.CreateBefore,
		Page:         args.Page,
		PerPage:      args.PerPage,
	})
	if err != nil {
		return errorResult(err), nil, nil
	}

	views := make([]instanceView, 0, len(instances))
	for _, ins := range instances {
		views = append(views, s.viewInstance(ins))
	}

	return jsonResult(map[string]any{
		"total":     total,
		"instances": views,
	})
}

type instanceView struct {
	*devops.Instance
	OpsPortalURL string `json:"ops_portal_url,omitempty"`
	PhaseHint    string `json:"phase_hint,omitempty"`
}

func (s *server) viewInstance(ins *devops.Instance) instanceView {
	return instanceView{
		Instance:     ins,
		OpsPortalURL: s.cli.InstanceURL(ins.ID),
		PhaseHint:    phaseHint(ins.Phase),
	}
}

// phaseHint explains a phase in terms of what still has to happen.
func phaseHint(phase devops.InstancePhase) string {
	switch phase {
	case devops.PhaseDraft:
		return "Saved as a draft; it has not been submitted for approval."
	case devops.PhaseCreated:
		return "Created; the approval request is being raised."
	case devops.PhaseApproving:
		return "Waiting for a human approver in Ops Portal. Nothing has run yet."
	case devops.PhaseWithdrawn:
		return "The approval request was withdrawn, so the operation will not run."
	case devops.PhasePending:
		return "Approved and waiting to start. Actions with start_policy `manual` need a human to press Start in Ops Portal."
	case devops.PhaseRunning:
		return "Approved and currently executing."
	case devops.PhaseSucceeded:
		return "Finished successfully."
	case devops.PhaseFailed:
		return "Finished with a failure; check the action outputs."
	case devops.PhaseCanceled:
		return "Canceled before completion."
	default:
		return ""
	}
}
