package main

import (
	"fmt"
	"os"
	"strconv"
	"time"

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

	"github.com/tidbcloud/aisupport/apps/cli/operation-mcp/devops"
	"github.com/tidbcloud/aisupport/apps/cli/operation-mcp/mcpserver"
	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/auth/consts"
)

// version is reported to MCP clients. Overridden at build time with
// -ldflags "-X main.version=...".
var version = "dev"

// defaultAllowedOperations is the operation allowlist this server starts with.
// Operations outside it stay readable but cannot be submitted.
var defaultAllowedOperations = []string{"RestartChangefeed"}

const (
	envEnv            = "DEVOPS_ENV"
	envBaseDomain     = "DEVOPS_BASE_DOMAIN"
	envOpsPortalURL   = "DEVOPS_OPS_PORTAL_URL"
	envOperationsYAML = "DEVOPS_OPERATIONS_FILE"
	envEnableWrite    = "DEVOPS_OPERATION_MCP_ENABLE_WRITE"
)

type options struct {
	env            string
	baseURL        string
	opsPortalURL   string
	timeout        time.Duration
	operationsFile string
	allowed        []string
	enableWrite    bool

	client  *devops.Client
	catalog *devops.Catalog
}

// NewRootCommand builds the operation-mcp CLI root command.
func NewRootCommand() *cobra.Command {
	opts := &options{
		env:            envOrDefault(envEnv, string(devops.EnvProd)),
		baseURL:        os.Getenv(envBaseDomain),
		opsPortalURL:   os.Getenv(envOpsPortalURL),
		timeout:        30 * time.Second,
		operationsFile: os.Getenv(envOperationsYAML),
		allowed:        defaultAllowedOperations,
		enableWrite:    envBool(envEnableWrite),
	}

	root := &cobra.Command{
		Use:   "operation-mcp",
		Short: "Expose the DevOps operation API as a stdio MCP server",
		Long: fmt.Sprintf(`operation-mcp exposes the DevOps (Ops Portal) operation API over MCP.

It can read the operation catalog and submit instances of allowlisted operations.
Submitting never bypasses approval: the DevOps server puts every instance into its
configured approval chain, and this CLI does not talk to the approval API at all.

Requires one of %s, %s, %s to be set for API authentication.
Use a credential dedicated to the agent so the DevOps audit log can tell agent-submitted
operations apart from human ones.`, consts.EnvDevOpsAccessToken, consts.EnvDevOpsAPIToken, consts.EnvDevOpsAPIKey),
		SilenceUsage:  true,
		SilenceErrors: true,
		PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
			return opts.setup(cmd)
		},
	}

	root.PersistentFlags().StringVar(&opts.env, "env", opts.env,
		fmt.Sprintf("DevOps environment %v", devops.EnvValues))
	root.PersistentFlags().StringVar(&opts.baseURL, "base-url", opts.baseURL,
		"DevOps API base URL, overrides --env")
	root.PersistentFlags().StringVar(&opts.opsPortalURL, "ops-portal-url", opts.opsPortalURL,
		"Ops Portal base URL used for instance links, overrides --env")
	root.PersistentFlags().DurationVar(&opts.timeout, "timeout", opts.timeout,
		"HTTP request timeout")
	root.PersistentFlags().StringVar(&opts.operationsFile, "operations-file", opts.operationsFile,
		"read the operation catalog from an operations.yaml file instead of the DevOps API")
	root.PersistentFlags().StringSliceVar(&opts.allowed, "allow", opts.allowed,
		"operations that may be submitted; everything else is read-only")
	root.PersistentFlags().BoolVar(&opts.enableWrite, "enable-write", opts.enableWrite,
		"register the tools that submit and cancel operations")

	root.AddCommand(newServeCommand(opts))
	root.AddCommand(newToolsCommand(opts))

	return root
}

// setup resolves the endpoints and loads the operation catalog shared by the
// subcommands.
func (o *options) setup(cmd *cobra.Command) error {
	baseURL, opsPortalURL, err := o.resolveEndpoints()
	if err != nil {
		return err
	}

	o.client, err = devops.NewClient(devops.Config{
		BaseURL:     baseURL,
		FrontendURL: opsPortalURL,
		Timeout:     o.timeout,
	})
	if err != nil {
		return err
	}

	if o.operationsFile != "" {
		o.catalog, err = devops.LoadCatalogFromFile(o.operationsFile)
	} else {
		o.catalog, err = devops.LoadCatalog(cmd.Context(), o.client)
	}
	if err != nil {
		return err
	}

	return nil
}

func (o *options) resolveEndpoints() (baseURL, opsPortalURL string, err error) {
	baseURL, opsPortalURL = o.baseURL, o.opsPortalURL

	if baseURL == "" || opsPortalURL == "" {
		env := devops.Env(o.env)
		if baseURL == "" {
			if baseURL, err = devops.BackendURL(env); err != nil {
				return "", "", err
			}
		}
		if opsPortalURL == "" {
			if opsPortalURL, err = devops.FrontendURL(env); err != nil {
				return "", "", err
			}
		}
	}

	return baseURL, opsPortalURL, nil
}

// newServer builds the MCP server from the resolved options.
func (o *options) newServer() (*mcp.Server, error) {
	return mcpserver.New(mcpserver.Options{
		Client:      o.client,
		Catalog:     o.catalog,
		Allowed:     o.allowed,
		EnableWrite: o.enableWrite,
		Version:     version,
	})
}

func envOrDefault(key, fallback string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return fallback
}

func envBool(key string) bool {
	value, err := strconv.ParseBool(os.Getenv(key))
	return err == nil && value
}
