package cmd

import (
	"errors"
	"fmt"
	"io"
	"os/exec"
	"runtime"
	"sort"
	"strings"

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

const profileSetCurrentAlias = "current"

var openProfileURLInBrowser = openURLInBrowser

type profileListResponse struct {
	Env           string          `json:"env"`
	ActiveProfile string          `json:"active_profile"`
	Profiles      []profileOutput `json:"profiles"`
}

type profileCurrentResponse struct {
	Env           string         `json:"env"`
	ActiveProfile string         `json:"active_profile"`
	Profile       *profileOutput `json:"profile"`
}

type profileDeleteResponse struct {
	Env            string `json:"env"`
	ActiveProfile  string `json:"active_profile"`
	DeletedProfile string `json:"deleted_profile"`
}

type profileOutput struct {
	Name                string `json:"name"`
	Env                 string `json:"env"`
	ClusterID           string `json:"cluster_id,omitempty"`
	ClusterName         string `json:"cluster_name,omitempty"`
	OrgID               string `json:"org_id,omitempty"`
	ClinicURL           string `json:"clinic_url,omitempty"`
	ParentID            string `json:"parent_id,omitempty"`
	ClusterDeployTypeV2 string `json:"cluster_deploy_type_v2,omitempty"`
	RangeStart          string `json:"range_start,omitempty"`
	RangeEnd            string `json:"range_end,omitempty"`
	Active              bool   `json:"active"`
}

func profileCommandHandler(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	command := newProfileCommand(staticRuntime(ctx), stdout)
	if err := executeCobraCommand(command, args); err != nil {
		return normalizeFamilyCommandError(err, "profile")
	}
	return nil
}

func newProfileCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "profile",
		Short: "Manage env-scoped profiles and their defaults.",
		Long:  "Manage env-scoped runtime profiles and defaults.",
		Example: `  clinic --env staging profile list
  clinic --env staging profile current
  clinic --env staging profile open
  clinic --env staging profile open blue
  clinic --env staging profile use blue
  clinic --env staging profile set current --start 10 --end 20
  clinic --env staging profile set blue --cluster-id 123 --start 10 --end 20
  clinic --env staging profile delete blue`,
	}, stdout)
	command.RunE = func(c *cobra.Command, _ []string) error {
		return c.Help()
	}
	command.Args = cobra.NoArgs
	command.AddCommand(
		newProfileListCommand(runtime, stdout),
		newProfileCurrentCommand(runtime, stdout),
		newProfileOpenCommand(runtime, stdout),
		newProfileUseCommand(runtime, stdout),
		newProfileSetCommand(runtime, stdout),
		newProfileDeleteCommand(runtime, stdout),
	)
	return command
}

func newProfileListCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "list",
		Short: "List profiles in the selected environment.",
		Long: `List profiles for the selected environment.

Output:
  JSON with env, active_profile, and all env-local profile objects.
  Profile objects include optional clinic_url when env URL + org_id + cluster_id are available.`,
	}, stdout)
	command.Args = cobra.NoArgs
	command.RunE = func(_ *cobra.Command, args []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runProfileList(ctx, args, stdout)
	}
	return command
}

func newProfileCurrentCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "current",
		Short: "Show the active profile in the selected environment.",
		Long: `Show the active profile for the selected environment.

Output:
  JSON with env, active_profile, and the active profile object.
  The profile object includes optional clinic_url when env URL + org_id + cluster_id are available.
  When no active profile is selected in that env, the command succeeds with "profile": null.`,
	}, stdout)
	command.Args = cobra.NoArgs
	command.RunE = func(_ *cobra.Command, args []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runProfileCurrent(ctx, args, stdout)
	}
	return command
}

func newProfileOpenCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "open [<name>]",
		Short: "Open a profile Clinic URL in the default browser.",
		Long: `Open profile clinic_url in the default browser.

Arguments:
  <name>  Optional profile name in the selected environment. When omitted, uses active_profile.

Behavior:
  The command uses the selected env.
  Opens the selected profile clinic_url in the default browser.
  clinic_url requires env URL + org_id + cluster_id on the selected profile.
  The command does not switch active_profile.`,
	}, stdout)
	command.Args = cobra.MaximumNArgs(1)
	command.RunE = func(_ *cobra.Command, args []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runProfileOpen(ctx, args, stdout)
	}
	return command
}

func newProfileUseCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "use <name>",
		Short: "Switch the active profile in the selected environment.",
		Long: `Switch the active profile in the selected environment.

Arguments:
  <name>  Profile name to activate.

Output:
  JSON with env, active_profile, and the selected profile object.`,
	}, stdout)
	command.Args = cobra.ExactArgs(1)
	command.RunE = func(_ *cobra.Command, args []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runProfileUse(ctx, args, stdout)
	}
	return command
}

type profileSetOptions struct {
	clusterID     string
	rangeStart    string
	rangeEnd      string
	rangeStartSet bool
	rangeEndSet   bool
}

func newProfileSetCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts profileSetOptions
	command := newCommand(commandMetadata{
		Use:   "set <name>",
		Short: "Create or update a profile in the selected environment.",
		Long: `Create or update a profile in the selected environment.

Behavior:
  The command uses the selected env.
  Use reserved name "current" to target the selected env's active profile without repeating its name.
  "current" cannot be used as a literal profile record name in this command.
  When --cluster-id is provided, it fetches fixed cluster metadata from the API lookup and writes
  cluster_id, cluster_name, org_id, parent_id, and cluster_deploy_type_v2
  into the profile record.
  When --cluster-id is omitted and the target profile already has cluster_id, only provided
  range flags are updated and fixed cluster metadata is preserved.
  The command does not switch the active profile automatically.

Output:
  JSON with env, active_profile, and the updated profile object.`,
	}, stdout)
	command.Args = cobra.ExactArgs(1)
	command.Flags().StringVar(&opts.clusterID, "cluster-id", "", "Exact cluster ID to bind to the profile.")
	command.Flags().StringVar(&opts.rangeStart, "start", "", "Default range start for metrics/loki/events.")
	command.Flags().StringVar(&opts.rangeEnd, "end", "", "Default range end for metrics/loki/events.")
	command.RunE = func(c *cobra.Command, args []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		opts.rangeStartSet = c.Flags().Changed("start")
		opts.rangeEndSet = c.Flags().Changed("end")
		return runProfileSetWithOptions(ctx, args[0], opts, stdout)
	}
	return command
}

func newProfileDeleteCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "delete <name>",
		Short: "Remove a profile from the selected environment.",
		Long: `Remove a profile from the selected environment.

Behavior:
  Deleting the active profile is allowed. The selected env's active_profile is cleared.

Output:
  JSON with env, active_profile, and deleted_profile.`,
	}, stdout)
	command.Args = cobra.ExactArgs(1)
	command.RunE = func(_ *cobra.Command, args []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runProfileDelete(ctx, args, stdout)
	}
	return command
}

func runProfileList(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	selectedEnv, container, err := selectedEnvProfileContainer(ctx)
	if err != nil {
		return err
	}

	return writeJSON(stdout, profileListResponse{
		Env:           selectedEnv,
		ActiveProfile: container.ActiveProfile,
		Profiles:      profileOutputsForEnv(ctx.Config, container, selectedEnv),
	})
}

func runProfileCurrent(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	selectedEnv, container, err := selectedEnvProfileContainer(ctx)
	if err != nil {
		return err
	}

	var current *profileOutput
	if container.ActiveProfile != "" {
		output, err := profileOutputForEnvName(ctx.Config, selectedEnv, container.ActiveProfile, container.ActiveProfile)
		if err != nil {
			return err
		}
		current = &output
	}

	return writeJSON(stdout, profileCurrentResponse{
		Env:           selectedEnv,
		ActiveProfile: container.ActiveProfile,
		Profile:       current,
	})
}

func runProfileUse(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	selectedEnv, container, err := selectedEnvProfileContainer(ctx)
	if err != nil {
		return err
	}

	output, err := profileOutputForEnvName(ctx.Config, selectedEnv, args[0], args[0])
	if err != nil {
		return err
	}

	container.ActiveProfile = args[0]
	saveEnvProfileContainer(ctx.Config, selectedEnv, container)
	if err := ctx.Config.Save(); err != nil {
		return err
	}

	output.Active = true
	return writeJSON(stdout, profileCurrentResponse{
		Env:           selectedEnv,
		ActiveProfile: container.ActiveProfile,
		Profile:       &output,
	})
}

func runProfileOpen(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	selectedEnv, container, err := selectedEnvProfileContainer(ctx)
	if err != nil {
		return err
	}

	targetName := ""
	if len(args) == 1 {
		targetName = args[0]
	} else {
		if container.ActiveProfile == "" {
			return fmt.Errorf("profile open requires an active profile in env %s when no profile name is provided", selectedEnv)
		}
		targetName = container.ActiveProfile
	}

	output, err := profileOutputForEnvName(ctx.Config, selectedEnv, targetName, container.ActiveProfile)
	if err != nil {
		return err
	}
	if output.ClinicURL == "" {
		return fmt.Errorf("profile open requires clinic_url for profile %s in env %s", targetName, selectedEnv)
	}

	if err := openProfileURLInBrowser(output.ClinicURL); err != nil {
		return fmt.Errorf("open profile clinic_url in browser: %w", err)
	}

	return nil
}

func runProfileSetWithOptions(ctx *RuntimeContext, name string, opts profileSetOptions, stdout io.Writer) error {
	selectedEnv, container, err := selectedEnvProfileContainer(ctx)
	if err != nil {
		return err
	}
	name, err = resolveProfileSetTargetName(name, selectedEnv, container)
	if err != nil {
		return err
	}

	record := container.Profiles[name]
	if opts.clusterID != "" {
		resolved, err := resolveClusterReferenceByIDFromAPI(ctx, opts.clusterID)
		if err != nil {
			return err
		}

		record.ClusterID = resolved.ClusterID
		record.ClusterName = resolved.ClusterName
		record.OrgID = resolved.OrgID
		record.ParentID = resolved.ParentID
		record.ClusterDeployTypeV2 = resolved.ClusterDeployTypeV2
	} else if record.ClusterID == "" {
		return errors.New("missing required --cluster-id")
	}

	record.Env = selectedEnv
	if opts.rangeStartSet {
		record.RangeStart = opts.rangeStart
	}
	if opts.rangeEndSet {
		record.RangeEnd = opts.rangeEnd
	}

	container.Profiles[name] = record
	saveEnvProfileContainer(ctx.Config, selectedEnv, container)
	if err := ctx.Config.Save(); err != nil {
		return err
	}

	output := profileOutputForConfig(
		name,
		record,
		name == container.ActiveProfile,
		envURLForProfileOutput(ctx.Config, selectedEnv),
	)
	return writeJSON(stdout, profileCurrentResponse{
		Env:           selectedEnv,
		ActiveProfile: container.ActiveProfile,
		Profile:       &output,
	})
}

func resolveProfileSetTargetName(name, selectedEnv string, container config.EnvProfileContainer) (string, error) {
	if name != profileSetCurrentAlias {
		return name, nil
	}

	if container.ActiveProfile == "" {
		return "", fmt.Errorf("profile set current requires an active profile in env %s", selectedEnv)
	}

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

	return container.ActiveProfile, nil
}

func runProfileDelete(ctx *RuntimeContext, args []string, stdout io.Writer) error {
	selectedEnv, container, err := selectedEnvProfileContainer(ctx)
	if err != nil {
		return err
	}
	if _, _, err := loadProfileForEnv(ctx.Config, selectedEnv, args[0]); err != nil {
		return err
	}

	delete(container.Profiles, args[0])
	if container.ActiveProfile == args[0] {
		container.ActiveProfile = ""
	}
	saveEnvProfileContainer(ctx.Config, selectedEnv, container)
	if err := ctx.Config.Save(); err != nil {
		return err
	}

	return writeJSON(stdout, profileDeleteResponse{
		Env:            selectedEnv,
		ActiveProfile:  container.ActiveProfile,
		DeletedProfile: args[0],
	})
}

func selectedEnvProfileContainer(ctx *RuntimeContext) (string, config.EnvProfileContainer, error) {
	if ctx == nil || ctx.Config == nil {
		return "", config.EnvProfileContainer{}, errors.New("profile config not loaded")
	}

	selectedEnv := ctx.SelectedEnv
	if selectedEnv == "" {
		selectedEnv = ctx.Config.ActiveEnv
	}
	if selectedEnv == "" {
		return "", config.EnvProfileContainer{}, errors.New("profile environment not selected")
	}
	if _, err := ctx.Config.ResolveEnv(selectedEnv); err != nil {
		return "", config.EnvProfileContainer{}, err
	}

	container, ok := ctx.Config.EnvProfiles[selectedEnv]
	if !ok {
		container = config.EnvProfileContainer{}
	}
	if container.Profiles == nil {
		container.Profiles = map[string]config.ProfileRecord{}
	}
	return selectedEnv, container, nil
}

func saveEnvProfileContainer(cfg *config.Config, env string, container config.EnvProfileContainer) {
	if cfg.EnvProfiles == nil {
		cfg.EnvProfiles = map[string]config.EnvProfileContainer{}
	}
	if container.Profiles == nil {
		container.Profiles = map[string]config.ProfileRecord{}
	}
	cfg.EnvProfiles[env] = container
}

func profileOutputsForEnv(cfg *config.Config, container config.EnvProfileContainer, env string) []profileOutput {
	envURL := envURLForProfileOutput(cfg, env)
	names := make([]string, 0, len(container.Profiles))
	for name := range container.Profiles {
		names = append(names, name)
	}
	sort.Strings(names)

	items := make([]profileOutput, 0, len(names))
	for _, name := range names {
		record := container.Profiles[name]
		record.Env = env
		items = append(items, profileOutputForConfig(name, record, name == container.ActiveProfile, envURL))
	}
	return items
}

func profileOutputForEnvName(cfg *config.Config, env, name, activeProfile string) (profileOutput, error) {
	_, profile, err := loadProfileForEnv(cfg, env, name)
	if err != nil {
		return profileOutput{}, err
	}
	return profileOutputForConfig(name, *profile, name == activeProfile, envURLForProfileOutput(cfg, env)), nil
}

func profileOutputForConfig(name string, profile config.ProfileDefaults, active bool, envURL string) profileOutput {
	return profileOutput{
		Name:                name,
		Env:                 profile.Env,
		ClusterID:           profile.ClusterID,
		ClusterName:         profile.ClusterName,
		OrgID:               profile.OrgID,
		ClinicURL:           buildClinicClusterURL(envURL, profile.OrgID, profile.ClusterID),
		ParentID:            profile.ParentID,
		ClusterDeployTypeV2: profile.ClusterDeployTypeV2,
		RangeStart:          profile.RangeStart,
		RangeEnd:            profile.RangeEnd,
		Active:              active,
	}
}

func envURLForProfileOutput(cfg *config.Config, env string) string {
	if cfg == nil {
		return ""
	}

	environment, ok := cfg.Environments[env]
	if !ok {
		return ""
	}
	return environment.URL
}

func buildClinicClusterURL(endpoint, orgID, clusterID string) string {
	if endpoint == "" || orgID == "" || clusterID == "" {
		return ""
	}
	endpoint = strings.TrimRight(endpoint, "/")
	return fmt.Sprintf("%s/portal/#/orgs/%s/clusters/%s", endpoint, orgID, clusterID)
}

func openURLInBrowser(url string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "darwin":
		cmd = exec.Command("open", url)
	case "linux":
		cmd = exec.Command("xdg-open", url)
	case "windows":
		cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
	default:
		return fmt.Errorf("unsupported OS for browser auto-open: %s", runtime.GOOS)
	}

	if err := cmd.Start(); err != nil {
		return err
	}
	return nil
}
