package mcpserver

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"unicode"
	"unicode/utf8"

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

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

// submitDescriptionSuffix is appended to every submitting tool so the model
// cannot miss what submitting does and does not do.
const submitDescriptionSuffix = " This does NOT execute anything directly: it creates an operation instance " +
	"that enters the DevOps approval chain, and a human approver must approve it before any action runs. " +
	"Poll get_operation_instance with the returned instance_id to follow it."

func (s *server) registerPreviewTool(srv *mcp.Server) {
	mcp.AddTool(srv, &mcp.Tool{
		Name: "preview_operation",
		Description: "Run the DevOps server-side pre-checks for an operation and return their advisories, " +
			"without creating anything. Call this before submitting so that a bad target or a blocking " +
			"cluster state is caught before a human is asked to approve.",
		Annotations: readOnlyAnnotations("Preview an operation"),
	}, s.previewOperation)
}

func (s *server) registerWriteTools(srv *mcp.Server) error {
	mcp.AddTool(srv, &mcp.Tool{
		Name: "submit_operation",
		Description: "Submit any allowlisted DevOps operation by name, with its params as a map." +
			submitDescriptionSuffix,
		Annotations: &mcp.ToolAnnotations{
			Title:           "Submit an operation for approval",
			DestructiveHint: boolPtr(true),
			OpenWorldHint:   boolPtr(true),
		},
	}, s.submitOperation)

	mcp.AddTool(srv, &mcp.Tool{
		Name: "cancel_operation_instance",
		Description: "Cancel an operation instance that has not finished yet, for example one submitted " +
			"by mistake or made obsolete because the alert recovered on its own.",
		Annotations: &mcp.ToolAnnotations{
			Title:           "Cancel an operation instance",
			DestructiveHint: boolPtr(false),
			IdempotentHint:  true,
			OpenWorldHint:   boolPtr(true),
		},
	}, s.cancelInstance)

	for _, name := range s.allowedList {
		if err := s.registerOperationTool(srv, name); err != nil {
			return err
		}
	}
	return nil
}

// registerOperationTool generates a dedicated tool for one allowlisted
// operation, so the model gets the operation's params as first-class arguments
// instead of an untyped map.
func (s *server) registerOperationTool(srv *mcp.Server, name string) error {
	op, ok := s.catalog.Get(name)
	if !ok {
		return errUnknownOperation(name)
	}

	schema, err := operationInputSchema(op)
	if err != nil {
		return err
	}

	description := op.Description
	if description == "" {
		description = op.Alias
	}
	if description == "" {
		description = "Submit the " + op.Name + " operation."
	}
	description = capitalize(description)
	if !strings.HasSuffix(description, ".") {
		description += "."
	}
	description = fmt.Sprintf("%s (DevOps operation %s.)%s", description, op.Name, submitDescriptionSuffix)

	srv.AddTool(&mcp.Tool{
		Name:        toolNameFor(op.Name),
		Description: description,
		InputSchema: schema,
		Annotations: &mcp.ToolAnnotations{
			Title:           "Submit " + op.Name + " for approval",
			DestructiveHint: boolPtr(true),
			OpenWorldHint:   boolPtr(true),
		},
	}, func(ctx context.Context, req *mcp.CallToolRequest) (*mcp.CallToolResult, error) {
		args := map[string]any{}
		if len(req.Params.Arguments) > 0 {
			if err := json.Unmarshal(req.Params.Arguments, &args); err != nil {
				return errorResult(fmt.Errorf("invalid arguments: %w", err)), nil
			}
		}

		reason, _ := args[fieldReason].(string)
		policy, _ := args[fieldStartPolicy].(string)
		scheduleAt, err := optionalUnixSeconds(args[fieldStartScheduleAt])
		if err != nil {
			return errorResult(err), nil
		}
		delete(args, fieldReason)
		delete(args, fieldStartPolicy)
		delete(args, fieldStartScheduleAt)

		result, err := s.submit(ctx, op.Name, args, reason, policy, scheduleAt, nil)
		if err != nil {
			return errorResult(err), nil
		}
		res, _, err := jsonResult(result)
		return res, err
	})

	return nil
}

// capitalize upper-cases the first rune, leaving the rest untouched.
func capitalize(s string) string {
	for i, r := range s {
		return string(unicode.ToUpper(r)) + s[i+utf8.RuneLen(r):]
	}
	return s
}

type actionArg struct {
	Name            string `json:"name" jsonschema:"action name as listed by describe_operation"`
	StartPolicy     string `json:"start_policy,omitempty" jsonschema:"immediate, manual or schedule; defaults to immediate"`
	StartScheduleAt *int64 `json:"start_schedule_at,omitempty" jsonschema:"unix timestamp in seconds, required when start_policy is schedule"`
}

type submitOperationArgs struct {
	Operation       string         `json:"operation" jsonschema:"operation name, e.g. RestartChangefeed"`
	Params          map[string]any `json:"params" jsonschema:"operation params keyed by param name, as listed by describe_operation"`
	Reason          string         `json:"reason" jsonschema:"why this operation is needed. Shown verbatim to the human approver, so name the alert or ticket you are acting on"`
	StartPolicy     string         `json:"start_policy,omitempty" jsonschema:"when the actions start once approved: immediate (default), manual or schedule"`
	StartScheduleAt *int64         `json:"start_schedule_at,omitempty" jsonschema:"unix timestamp in seconds, required when start_policy is schedule"`
	Actions         []actionArg    `json:"actions,omitempty" jsonschema:"override which actions run and how they start; defaults to every non-optional action of the operation"`
}

type previewOperationArgs struct {
	Operation string         `json:"operation" jsonschema:"operation name, e.g. RestartChangefeed"`
	Params    map[string]any `json:"params" jsonschema:"operation params keyed by param name, as listed by describe_operation"`
	Reason    string         `json:"reason,omitempty" jsonschema:"why this operation is needed; optional for a preview"`
}

type submitResult struct {
	InstanceID   uint64                        `json:"instance_id"`
	Operation    string                        `json:"operation"`
	Phase        devops.InstancePhase          `json:"phase,omitempty"`
	PhaseHint    string                        `json:"phase_hint,omitempty"`
	OpsPortalURL string                        `json:"ops_portal_url,omitempty"`
	Approval     []devops.ApprovalNodeSummary  `json:"approval_chain,omitempty"`
	Params       map[string]string             `json:"params"`
	Actions      []devops.CreateInstanceAction `json:"actions"`
}

func (s *server) previewOperation(ctx context.Context, _ *mcp.CallToolRequest, args previewOperationArgs) (*mcp.CallToolResult, any, error) {
	op, err := s.checkAllowed(args.Operation)
	if err != nil {
		return errorResult(err), nil, nil
	}

	body, err := s.buildRequest(op, args.Params, args.Reason, "", nil, nil)
	if err != nil {
		return errorResult(err), nil, nil
	}

	advisories, err := s.cli.PreviewInstance(ctx, body)
	if err != nil {
		return errorResult(err), nil, nil
	}

	return jsonResult(map[string]any{
		"operation":  op.Name,
		"params":     body.Params,
		"actions":    body.Actions,
		"advisories": advisories,
	})
}

func (s *server) submitOperation(ctx context.Context, _ *mcp.CallToolRequest, args submitOperationArgs) (*mcp.CallToolResult, any, error) {
	overrides := make([]devops.CreateInstanceAction, 0, len(args.Actions))
	for _, a := range args.Actions {
		overrides = append(overrides, devops.CreateInstanceAction{
			Name:            a.Name,
			StartPolicy:     devops.ActionStartPolicy(a.StartPolicy),
			StartScheduleAt: a.StartScheduleAt,
		})
	}

	result, err := s.submit(ctx, args.Operation, args.Params, args.Reason, args.StartPolicy, args.StartScheduleAt, overrides)
	if err != nil {
		return errorResult(err), nil, nil
	}
	return jsonResult(result)
}

// submit validates and creates an operation instance. The instance enters the
// server-side approval chain; this code never approves anything.
func (s *server) submit(
	ctx context.Context,
	operation string,
	params map[string]any,
	reason string,
	startPolicy string,
	startScheduleAt *int64,
	actionOverrides []devops.CreateInstanceAction,
) (*submitResult, error) {
	op, err := s.checkAllowed(operation)
	if err != nil {
		return nil, err
	}

	body, err := s.buildRequest(op, params, reason, devops.ActionStartPolicy(startPolicy), startScheduleAt, actionOverrides)
	if err != nil {
		return nil, err
	}
	if strings.TrimSpace(body.Reason) == "" {
		return nil, fmt.Errorf("reason is required: the human approver sees it as the only explanation of why " +
			"this operation was requested, so name the alert or ticket it comes from")
	}

	id, err := s.cli.CreateInstance(ctx, body)
	if err != nil {
		return nil, err
	}

	result := &submitResult{
		InstanceID:   id,
		Operation:    op.Name,
		OpsPortalURL: s.cli.InstanceURL(id),
		Approval:     op.Approval.Summarize(),
		Params:       body.Params,
		Actions:      body.Actions,
	}

	// Report the phase the server actually put the instance in. A failure here
	// is not fatal: the instance exists either way and can be polled by ID.
	if ins, err := s.cli.GetInstance(ctx, id); err == nil {
		result.Phase = ins.Phase
		result.PhaseHint = phaseHint(ins.Phase)
	}

	return result, nil
}

func (s *server) buildRequest(
	op *devops.Operation,
	params map[string]any,
	reason string,
	startPolicy devops.ActionStartPolicy,
	startScheduleAt *int64,
	actionOverrides []devops.CreateInstanceAction,
) (devops.CreateInstanceReq, error) {
	built, err := devops.BuildParams(op, params)
	if err != nil {
		return devops.CreateInstanceReq{}, err
	}

	var actions []devops.CreateInstanceAction
	if len(actionOverrides) > 0 {
		actions, err = devops.ResolveActions(op, actionOverrides)
	} else {
		actions, err = devops.BuildActions(op, startPolicy, startScheduleAt)
	}
	if err != nil {
		return devops.CreateInstanceReq{}, err
	}

	return devops.CreateInstanceReq{
		Type:      op.Target,
		Operation: op.Name,
		Reason:    reason,
		Params:    built,
		Actions:   actions,
		IsDraft:   false,
	}, nil
}

type cancelInstanceArgs struct {
	InstanceID uint64 `json:"instance_id" jsonschema:"operation instance id to cancel"`
}

func (s *server) cancelInstance(ctx context.Context, _ *mcp.CallToolRequest, args cancelInstanceArgs) (*mcp.CallToolResult, any, error) {
	if err := s.cli.CancelInstance(ctx, args.InstanceID); err != nil {
		return errorResult(err), nil, nil
	}

	out := map[string]any{
		"instance_id": args.InstanceID,
		"canceled":    true,
	}
	if ins, err := s.cli.GetInstance(ctx, args.InstanceID); err == nil {
		out["phase"] = ins.Phase
		out["phase_hint"] = phaseHint(ins.Phase)
	}
	return jsonResult(out)
}

// optionalUnixSeconds reads a start_schedule_at value out of raw tool
// arguments, where JSON numbers arrive as float64.
func optionalUnixSeconds(raw any) (*int64, error) {
	switch v := raw.(type) {
	case nil:
		return nil, nil
	case float64:
		seconds := int64(v)
		return &seconds, nil
	case int64:
		return &v, nil
	case json.Number:
		seconds, err := v.Int64()
		if err != nil {
			return nil, fmt.Errorf("%s must be a unix timestamp in seconds: %w", fieldStartScheduleAt, err)
		}
		return &seconds, nil
	default:
		return nil, fmt.Errorf("%s must be a unix timestamp in seconds, got %T", fieldStartScheduleAt, raw)
	}
}
