// Package mcpserver exposes the DevOps operation API as MCP tools.
//
// The server is deliberately narrow: it can read the operation catalog, submit
// operation instances that are on an explicit allowlist, and follow instances
// it created. It never touches the approval API, so every operation it submits
// is still gated by whatever approval chain the DevOps server has configured.
package mcpserver

import (
	"encoding/json"
	"fmt"
	"slices"
	"sort"
	"strings"

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

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

// ServerName is the MCP server name reported to clients.
const ServerName = "devops-operation"

// Options configures the MCP server.
type Options struct {
	// Client talks to the DevOps operation API.
	Client *devops.Client
	// Catalog holds the operation definitions used to build tool schemas.
	Catalog *devops.Catalog
	// Allowed lists the operations that may be submitted. Everything outside
	// it is readable but not submittable.
	Allowed []string
	// EnableWrite registers the submitting and cancelling tools. When false the
	// server is read-only apart from the side-effect-free preview.
	EnableWrite bool
	// Version is reported to clients.
	Version string
}

type server struct {
	cli         *devops.Client
	catalog     *devops.Catalog
	allowed     map[string]bool
	allowedList []string
}

// New builds the MCP server and registers its tools.
func New(opts Options) (*mcp.Server, error) {
	if opts.Client == nil {
		return nil, fmt.Errorf("mcpserver: client is required")
	}
	if opts.Catalog == nil {
		return nil, fmt.Errorf("mcpserver: catalog is required")
	}

	s := &server{
		cli:         opts.Client,
		catalog:     opts.Catalog,
		allowed:     make(map[string]bool, len(opts.Allowed)),
		allowedList: slices.Clone(opts.Allowed),
	}
	sort.Strings(s.allowedList)
	for _, name := range s.allowedList {
		op, ok := opts.Catalog.Get(name)
		if !ok {
			return nil, fmt.Errorf("allowlisted operation %q does not exist in the DevOps operation catalog", name)
		}
		if op.Internal {
			return nil, fmt.Errorf("operation %q is internal and can only be created by the DevOps server itself", name)
		}
		s.allowed[name] = true
	}

	version := opts.Version
	if version == "" {
		version = "dev"
	}

	mcpServer := mcp.NewServer(&mcp.Implementation{
		Name:    ServerName,
		Version: version,
	}, &mcp.ServerOptions{
		Instructions: s.instructions(opts.EnableWrite),
	})

	s.registerReadTools(mcpServer)
	s.registerPreviewTool(mcpServer)
	if opts.EnableWrite {
		if err := s.registerWriteTools(mcpServer); err != nil {
			return nil, err
		}
	}

	return mcpServer, nil
}

func (s *server) instructions(enableWrite bool) string {
	var b strings.Builder
	b.WriteString("Tools for TiDB Cloud DevOps (Ops Portal) operations.\n\n")
	b.WriteString("Submitting an operation does NOT execute it: the DevOps server puts the instance " +
		"into its approval chain, and a human approver has to approve it before any action runs. " +
		"Always write a `reason` that names the alert or ticket you are acting on, because the approver " +
		"sees nothing else about your intent.\n\n")

	if !enableWrite {
		b.WriteString("This server is running read-only: operations can be listed, described and previewed, but not submitted.\n")
		return b.String()
	}

	if len(s.allowedList) == 0 {
		b.WriteString("No operation is allowlisted, so nothing can be submitted.\n")
		return b.String()
	}

	b.WriteString("Submittable operations: " + strings.Join(s.allowedList, ", ") + ".\n")
	b.WriteString("Everything else in the catalog is read-only here and has to go through Ops Portal.\n")
	return b.String()
}

func errUnknownOperation(name string) error {
	return fmt.Errorf("unknown operation %q; use list_operations to see the catalog", name)
}

// checkAllowed reports whether an operation may be submitted or previewed.
func (s *server) checkAllowed(name string) (*devops.Operation, error) {
	op, ok := s.catalog.Get(name)
	if !ok {
		return nil, errUnknownOperation(name)
	}
	if !s.allowed[name] {
		allowed := "none"
		if len(s.allowedList) > 0 {
			allowed = strings.Join(s.allowedList, ", ")
		}
		return nil, fmt.Errorf("operation %q is not allowlisted on this server; submittable operations: %s", name, allowed)
	}
	return op, nil
}

// jsonResult renders a tool result as pretty-printed JSON text.
func jsonResult(v any) (*mcp.CallToolResult, any, error) {
	encoded, err := json.MarshalIndent(v, "", "  ")
	if err != nil {
		return nil, nil, err
	}
	return &mcp.CallToolResult{
		Content: []mcp.Content{&mcp.TextContent{Text: string(encoded)}},
	}, nil, nil
}

// errorResult reports a failure to the model without failing the protocol.
func errorResult(err error) *mcp.CallToolResult {
	return &mcp.CallToolResult{
		IsError: true,
		Content: []mcp.Content{&mcp.TextContent{Text: err.Error()}},
	}
}
