package cmd

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"sort"

	"github.com/spf13/cobra"
	"github.com/tidbcloud/clinic-cli/internal/clinic"
	"github.com/tidbcloud/clinic-cli/internal/config"
)

type Client interface {
	Do(context.Context, clinic.Request, any) error
}

type RuntimeContext struct {
	SelectedEnv             string
	SelectedEnvSource       string
	SelectedProfileName     string
	SelectedProfileSource   string
	SelectedProfile         *config.ProfileRecord
	SelectedProfileDefaults config.ProfileDefaults
	Config                  *config.Config
	Client                  Client
	Verbose                 bool
	DiagnosticsOut          io.Writer
}

type commandHandler func(*RuntimeContext, []string, io.Writer) error

type rootRunner struct {
	loadConfig func() (*config.Config, error)
	newClient  func(config.Environment) (Client, error)
	handlers   map[string]commandHandler
}

func Run(args []string) error {
	return defaultRootRunner().runWithStderr(args, os.Stdout, os.Stderr)
}

func RunWithIO(args []string, stdout io.Writer) error {
	return defaultRootRunner().run(args, stdout)
}

func (r rootRunner) run(args []string, stdout io.Writer) error {
	return r.runWithStderr(args, stdout, io.Discard)
}

func (r rootRunner) runWithStderr(args []string, stdout, stderr io.Writer) error {
	command := r.newRootCommand(stdout, stderr)
	if command == nil {
		return nil
	}

	if err := executeCobraCommand(command, args); err != nil {
		return normalizeRootCommandError(err)
	}
	return nil
}

func resolveSelectedEnv(cfg *config.Config, override string) (string, config.Environment, error) {
	selectedEnv := override
	if selectedEnv == "" {
		if cfg != nil {
			selectedEnv = cfg.ActiveEnv
		}
	}

	if cfg == nil {
		return "", config.Environment{}, fmt.Errorf("config not loaded")
	}

	env, err := cfg.ResolveEnv(selectedEnv)
	if err != nil {
		return "", config.Environment{}, err
	}

	return selectedEnv, env, nil
}

func resolveSelectedProfile(cfg *config.Config, selectedEnv, override string) (string, *config.ProfileRecord, error) {
	if cfg == nil {
		return "", nil, fmt.Errorf("config not loaded")
	}

	if override != "" {
		_, profile, err := loadProfileForEnv(cfg, selectedEnv, override)
		if err != nil {
			return "", nil, err
		}
		return override, profile, nil
	}

	container, ok := cfg.EnvProfiles[selectedEnv]
	if !ok || container.ActiveProfile == "" {
		return "", nil, nil
	}

	_, profile, err := loadProfileForEnv(cfg, selectedEnv, container.ActiveProfile)
	if err != nil {
		return "", nil, err
	}

	return container.ActiveProfile, profile, nil
}

func defaultRootRunner() rootRunner {
	return rootRunner{
		loadConfig: config.LoadOrInit,
		newClient:  defaultClientFactory,
		handlers: map[string]commandHandler{
			"auth":       authCommandHandler,
			"cluster":    clusterCommandHandler,
			"completion": completionCommandHandler,
			"data":       dataCommandHandler,
			"diagnose":   diagnoseCommandHandler,
			"env":        envCommandHandler,
			"events":     eventsCommandHandler,
			"loki":       lokiCommandHandler,
			"metrics":    metricsCommandHandler,
			"profile":    profileCommandHandler,
			"raw":        rawCommandHandler,
			"switch":     switchCommandHandler,
		},
	}
}

func (r rootRunner) newRootCommand(stdout, stderr io.Writer) *cobra.Command {
	var (
		envOverride     string
		profileOverride string
		verbose         bool
		runtimeReady    bool
		runtimeCtx      *RuntimeContext
		runtimeErr      error
	)

	runtime := func() (*RuntimeContext, error) {
		if runtimeReady {
			return runtimeCtx, runtimeErr
		}
		runtimeReady = true
		runtimeCtx, runtimeErr = r.buildRuntimeContext(envOverride, profileOverride, verbose, stderr)
		return runtimeCtx, runtimeErr
	}

	command := newCommand(commandMetadata{
		Use:   "clinic",
		Short: "Wrap the Clinic API for TiDB Cloud operations.",
		Long:  rootLongDescription,
	}, stdout)
	command.RunE = func(c *cobra.Command, _ []string) error {
		return c.Help()
	}
	command.PersistentFlags().StringVar(&envOverride, "env", "", "Select the runtime environment for this invocation only.")
	command.PersistentFlags().StringVar(&profileOverride, "profile", "", "Select an env-local profile for this invocation only.")
	command.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Print runtime resolution diagnostics and executed API requests to stderr.")
	configureRootCompletion(command, r.loadConfig)

	authSubcommand := newAuthCommand(runtime, stdout)
	configureAuthCommandCompletion(authSubcommand, r.loadConfig)
	command.AddCommand(authSubcommand)

	metricsSubcommand := newMetricsCommand(runtime, stdout)
	command.AddCommand(metricsSubcommand)

	lokiSubcommand := newLokiCommand(runtime, stdout)
	command.AddCommand(lokiSubcommand)

	diagnoseSubcommand := newDiagnoseCommand(runtime, stdout)
	command.AddCommand(diagnoseSubcommand)

	command.AddCommand(newVersionCommand(stdout))
	command.AddCommand(newCompletionCommand(stdout))

	envSubcommand := newEnvCommand(runtime, stdout)
	configureEnvCommandCompletion(envSubcommand, r.loadConfig)
	command.AddCommand(envSubcommand)

	switchSubcommand := newSwitchCommand(runtime, stdout)
	configureSwitchCommandCompletion(switchSubcommand, r.loadConfig)
	command.AddCommand(switchSubcommand)

	profileSubcommand := newProfileCommand(runtime, stdout)
	configureProfileCommandCompletion(profileSubcommand, r.loadConfig)
	command.AddCommand(profileSubcommand)

	clusterSubcommand := newClusterCommand(runtime, stdout)
	configureClusterCommandCompletion(clusterSubcommand)
	command.AddCommand(clusterSubcommand)

	dataSubcommand := newDataCommand(runtime, stdout)
	command.AddCommand(dataSubcommand)

	eventsSubcommand := newEventsCommand(runtime, stdout)
	command.AddCommand(eventsSubcommand)

	rawSubcommand := newRawCommand(runtime, stdout)
	command.AddCommand(rawSubcommand)

	for _, name := range sortedCommandHandlerNames(r.handlers) {
		if isNativeRootCommand(name) {
			continue
		}
		handler := r.handlers[name]
		subcommand := newRootSubcommand(name, handler, runtime, stdout)
		configureRootSubcommandCompletion(subcommand, name, r.loadConfig)
		command.AddCommand(subcommand)
	}

	return command
}

func isNativeRootCommand(name string) bool {
	switch name {
	case "auth", "cluster", "completion", "data", "diagnose", "env", "events", "loki", "metrics", "profile", "raw", "switch":
		return true
	default:
		return false
	}
}

func (r rootRunner) buildRuntimeContext(envOverride, profileOverride string, verbose bool, diagnosticsOut io.Writer) (*RuntimeContext, error) {
	cfg, err := r.loadConfig()
	if err != nil {
		return nil, err
	}

	selectedEnv, env, err := resolveSelectedEnv(cfg, envOverride)
	if err != nil {
		return nil, err
	}
	selectedEnvSource := "active_env"
	if envOverride != "" {
		selectedEnvSource = "--env"
	}

	selectedProfileName, selectedProfile, err := resolveSelectedProfile(cfg, selectedEnv, profileOverride)
	if err != nil {
		return nil, err
	}
	selectedProfileSource := "none"
	switch {
	case profileOverride != "":
		selectedProfileSource = "--profile"
	case selectedProfileName != "":
		selectedProfileSource = "active_profile"
	}

	newClient := r.newClient
	if newClient == nil {
		newClient = defaultClientFactory
	}

	client, err := newClient(env)
	if err != nil {
		return nil, err
	}

	selectedProfileDefaults := config.ProfileDefaults{Env: selectedEnv}
	if selectedProfile != nil {
		selectedProfileDefaults = *selectedProfile
		selectedProfileDefaults.Env = selectedEnv
	}

	ctx := &RuntimeContext{
		SelectedEnv:             selectedEnv,
		SelectedEnvSource:       selectedEnvSource,
		SelectedProfileName:     selectedProfileName,
		SelectedProfileSource:   selectedProfileSource,
		SelectedProfile:         selectedProfile,
		SelectedProfileDefaults: selectedProfileDefaults,
		Config:                  cfg,
		Client:                  client,
		Verbose:                 verbose,
		DiagnosticsOut:          diagnosticsOut,
	}
	if ctx.Verbose {
		printRuntimeDiagnostics(ctx)
	}
	return ctx, nil
}

func newRootSubcommand(name string, handler commandHandler, runtime func() (*RuntimeContext, error), stdout io.Writer) *cobra.Command {
	command := newCommand(rootSubcommandMetadata(name), stdout)
	command.RunE = func(_ *cobra.Command, args []string) error {
		runtimeCtx, err := runtime()
		if err != nil {
			return err
		}
		return handler(runtimeCtx, args, stdout)
	}
	command.Flags().SetInterspersed(false)
	return command
}

const rootLongDescription = `clinic wraps the Clinic API for TiDB Cloud operations.

If the selected environment has no active profile, commands fall back to explicit flags and CLINIC_* environment variables.`

func rootSubcommandMetadata(name string) commandMetadata {
	shortDescriptions := map[string]string{
		"auth":       "Manage Clinic base URLs and API keys.",
		"cluster":    "Search clusters, fetch cluster details, and inspect topology.",
		"completion": "Generate shell completion scripts.",
		"data":       "Query cluster data-proxy SQL and table schema endpoints.",
		"diagnose":   "Correlate evidence from a free-text seed.",
		"env":        "Show and switch the active environment.",
		"events":     "List activity events for a cluster.",
		"loki":       "Run Loki log queries and list labels.",
		"metrics":    "Run Prometheus instant/range/series queries.",
		"profile":    "Manage env-scoped profiles and their defaults.",
		"raw":        "Send direct requests to /clinic/api/* or /data-proxy/*.",
		"switch":     "Persist active env and optionally active profile in one command.",
		"version":    "Show clinic CLI version information.",
	}

	return commandMetadata{
		Use:   name,
		Short: shortDescriptions[name],
	}
}

func sortedCommandHandlerNames(handlers map[string]commandHandler) []string {
	names := make([]string, 0, len(handlers))
	for name := range handlers {
		names = append(names, name)
	}
	sort.Strings(names)
	return names
}

func defaultClientFactory(env config.Environment) (Client, error) {
	return clinic.NewClient(env.URL, env.APIKey)
}

func loadProfileForEnv(cfg *config.Config, env, name string) (string, *config.ProfileRecord, error) {
	container, ok := cfg.EnvProfiles[env]
	if !ok || container.Profiles == nil {
		return "", nil, fmt.Errorf("unknown profile: %s in env %s", name, env)
	}

	profile, ok := container.Profiles[name]
	if !ok {
		return "", nil, fmt.Errorf("unknown profile: %s in env %s", name, env)
	}

	profile.Env = env
	return name, &profile, nil
}

func writeJSON(w io.Writer, value any) error {
	raw, err := json.Marshal(value)
	if err != nil {
		return err
	}
	return writeFormattedJSON(w, raw)
}
