package cmd

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strconv"
	"strings"

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

const dashboardClustersPath = "/clinic/api/v1/dashboard/clusters"

// knownDeployTypeV2Values lists the dashboard deploy_type_v2 values the CLI
// knows about. Used by cluster search --deploy-type shell completion and help.
// The dashboard API itself has no enumeration endpoint, so this list is the
// built-in hint; values are matched verbatim (case- and space-sensitive).
var knownDeployTypeV2Values = []string{
	"starter",
	"dedicated",
	"premium",
	"premium reserved",
	"premium resource pool",
	"premium reserved resource pool",
	"essential",
	"essential v2",
	"essential v2 resource pool",
	"byoc",
	"byoc resource pool",
}

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

func newClusterCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	command := newCommand(commandMetadata{
		Use:   "cluster",
		Short: "Search clusters, fetch cluster details, and inspect topology.",
		Long:  "Query cluster metadata and topology endpoints.",
		Example: `  clinic cluster search --query tidb --limit 20
  clinic cluster search --deploy-type "premium resource pool" --limit 100
  clinic cluster get --org-id org-1 --cluster-id cluster-1
  clinic cluster topology --cluster-id cluster-1`,
	}, stdout)
	command.RunE = func(c *cobra.Command, _ []string) error {
		return c.Help()
	}
	command.Args = cobra.NoArgs
	command.AddCommand(
		newClusterSearchCommand(runtime, stdout),
		newClusterSearchIDCommand(runtime, stdout),
		newClusterGetCommand(runtime, stdout),
		newClusterGetByIDCommand(runtime, stdout),
		newClusterTopologyCommand(runtime, stdout),
	)
	return command
}

type clusterSearchOptions struct {
	query         string
	showDeleted   bool
	limit         int
	page          int
	clusterStatus string
	deployType    string
}

func newClusterSearchCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts clusterSearchOptions
	command := newCommand(commandMetadata{
		Use:   "search",
		Short: "Search dashboard cluster list by text query and/or filters.",
		Long: `Search clusters through dashboard list API.

Behavior:
  Provide at least one of --query or --deploy-type.
  Empty --query is omitted from the API request so other filters are not AND-ed with text search.
  Deleted clusters are excluded by default; pass --show-deleted to include them (always sends show_deleted=true|false).
  --limit and --page must be greater than 0 when provided.
  --deploy-type maps to dashboard deploy_type_v2 and supports shell completion.

Known deploy_type_v2 values:
  starter, dedicated, premium, premium reserved, premium resource pool,
  premium reserved resource pool, essential, essential v2, essential v2 resource pool,
  byoc, byoc resource pool
Values are matched verbatim (case- and space-sensitive).`,
		Example: `  clinic cluster search --query tidb
  clinic cluster search --deploy-type "premium resource pool" --limit 100
  clinic cluster search --query prod --show-deleted --limit 50 --page 1`,
	}, stdout)
	command.Args = cobra.NoArgs
	command.Flags().StringVar(&opts.query, "query", "", "Search query text.")
	command.Flags().BoolVar(&opts.showDeleted, "show-deleted", false, "Include deleted clusters.")
	command.Flags().IntVar(&opts.limit, "limit", 0, "Results per page (> 0).")
	command.Flags().IntVar(&opts.page, "page", 0, "Page number (> 0).")
	command.Flags().StringVar(&opts.clusterStatus, "cluster-status", "", "Cluster status filter.")
	command.Flags().StringVar(&opts.deployType, "deploy-type", "", "Deploy type filter.")
	command.RunE = func(_ *cobra.Command, _ []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runClusterSearch(ctx, opts, stdout)
	}
	return command
}

func runClusterSearch(ctx *RuntimeContext, opts clusterSearchOptions, stdout io.Writer) error {
	if opts.query == "" && opts.deployType == "" {
		return errors.New("missing required filter: provide --query or --deploy-type")
	}

	params := url.Values{}
	addOptionalString(params, "query", opts.query)
	params.Set("show_deleted", strconv.FormatBool(opts.showDeleted))
	if err := addOptionalPositiveInt(params, "limit", opts.limit); err != nil {
		return err
	}
	if err := addOptionalPositiveInt(params, "page", opts.page); err != nil {
		return err
	}
	addOptionalString(params, "cluster_status", opts.clusterStatus)
	addOptionalString(params, "deploy_type_v2", opts.deployType)

	return executeClinicJSON(ctx, clinic.Request{
		Method: http.MethodGet,
		Path:   dashboardClustersPath,
		Query:  params,
	}, stdout)
}

type clusterSearchIDOptions struct {
	clusterID   string
	showDeleted bool
	limit       int
	page        int
}

func newClusterSearchIDCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts clusterSearchIDOptions
	command := newCommand(commandMetadata{
		Use:   "search-id",
		Short: "Search dashboard cluster list by exact cluster ID.",
		Long: `Search clusters by exact cluster ID via dashboard list API.

Behavior:
  --cluster-id can be provided by flag, CLINIC_CLUSTER_ID, or selected profile defaults.
  Deleted clusters are excluded by default; pass --show-deleted to include them (always sends show_deleted=true|false).
  If the exact cluster is found and no profile in the selected environment already has the same
  cluster_id, clinic auto-creates one profile in that environment.
  The command does not switch the active profile, and the search output remains unchanged.`,
	}, stdout)
	command.Args = cobra.NoArgs
	command.Flags().StringVar(&opts.clusterID, "cluster-id", "", "Cluster ID.")
	command.Flags().BoolVar(&opts.showDeleted, "show-deleted", false, "Include deleted clusters.")
	command.Flags().IntVar(&opts.limit, "limit", 0, "Results per page (> 0).")
	command.Flags().IntVar(&opts.page, "page", 0, "Page number (> 0).")
	command.RunE = func(_ *cobra.Command, _ []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runClusterSearchID(ctx, opts, stdout)
	}
	return command
}

func runClusterSearchID(ctx *RuntimeContext, opts clusterSearchIDOptions, stdout io.Writer) error {
	resolvedClusterID, err := requiredFlagOrEnvWithContext(ctx, "cluster-id", opts.clusterID, envClinicClusterID)
	if err != nil {
		return err
	}

	params := url.Values{}
	params.Set("cluster_id", resolvedClusterID)
	params.Set("show_deleted", strconv.FormatBool(opts.showDeleted))
	if err := addOptionalPositiveInt(params, "limit", opts.limit); err != nil {
		return err
	}
	if err := addOptionalPositiveInt(params, "page", opts.page); err != nil {
		return err
	}

	req := clinic.Request{
		Method: http.MethodGet,
		Path:   dashboardClustersPath,
		Query:  params,
	}

	raw, err := executeClinicRaw(ctx, req)
	if err != nil {
		return err
	}
	if err := autoCreateProfileFromClusterSearchID(ctx, resolvedClusterID, raw); err != nil {
		return err
	}

	return writeFormattedJSONOrRaw(stdout, raw)
}

type clusterGetOptions struct {
	orgID     string
	clusterID string
}

func newClusterGetCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts clusterGetOptions
	command := newCommand(commandMetadata{
		Use:   "get",
		Short: "Fetch cluster detail by org ID and cluster ID.",
		Long: `Fetch cluster detail by org ID and cluster ID.

Behavior:
  --org-id is required.
  --cluster-id can be provided by flag, CLINIC_CLUSTER_ID, or selected profile defaults.`,
		Example: "  clinic cluster get --org-id org-42 --cluster-id 123456789",
	}, stdout)
	command.Args = cobra.NoArgs
	command.Flags().StringVar(&opts.orgID, "org-id", "", "Internal org ID.")
	command.Flags().StringVar(&opts.clusterID, "cluster-id", "", "Cluster ID.")
	command.RunE = func(_ *cobra.Command, _ []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runClusterGet(ctx, opts, stdout)
	}
	return command
}

func runClusterGet(ctx *RuntimeContext, opts clusterGetOptions, stdout io.Writer) error {
	if opts.orgID == "" {
		return errors.New("missing required --org-id")
	}
	resolvedClusterID, err := requiredFlagOrEnvWithContext(ctx, "cluster-id", opts.clusterID, envClinicClusterID)
	if err != nil {
		return err
	}

	return executeClinicJSON(ctx, clinic.Request{
		Method: http.MethodGet,
		Path:   fmt.Sprintf("/clinic/api/v1/orgs/%s/clusters/%s", opts.orgID, resolvedClusterID),
	}, stdout)
}

type clusterGetByIDOptions struct {
	clusterID string
}

func newClusterGetByIDCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts clusterGetByIDOptions
	command := newCommand(commandMetadata{
		Use:   "get-by-id",
		Short: "Resolve org ID by cluster ID, then fetch detail.",
		Long: `Fetch cluster detail by cluster ID only.

Behavior:
  First queries /clinic/api/v1/dashboard/clusters with cluster_id and show_deleted=true,
  then matches exact clusterID to find orgID before requesting cluster detail.
  --cluster-id can be provided by flag, CLINIC_CLUSTER_ID, or selected profile defaults.`,
	}, stdout)
	command.Args = cobra.NoArgs
	command.Flags().StringVar(&opts.clusterID, "cluster-id", "", "Cluster ID.")
	command.RunE = func(_ *cobra.Command, _ []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runClusterGetByID(ctx, opts, stdout)
	}
	return command
}

func runClusterGetByID(ctx *RuntimeContext, opts clusterGetByIDOptions, stdout io.Writer) error {
	resolvedClusterID, err := requiredFlagOrEnvWithContext(ctx, "cluster-id", opts.clusterID, envClinicClusterID)
	if err != nil {
		return err
	}

	clusterRef, err := resolveClusterReferenceByID(ctx, resolvedClusterID)
	if err != nil {
		return err
	}

	return executeClinicJSON(ctx, clinic.Request{
		Method: http.MethodGet,
		Path:   fmt.Sprintf("/clinic/api/v1/orgs/%s/clusters/%s", clusterRef.OrgID, clusterRef.ClusterID),
	}, stdout)
}

type clusterTopologyOptions struct {
	clusterID string
}

func newClusterTopologyCommand(runtime runtimeResolver, stdout io.Writer) *cobra.Command {
	var opts clusterTopologyOptions
	command := newCommand(commandMetadata{
		Use:   "topology",
		Short: "Resolve cluster type and fetch topology/components.",
		Long: `Fetch cluster topology/components by cluster ID.

Behavior:
  dedicated clusters return cluster components/topology payload.
  premium/byoc tenant clusters fetch parent resource-pool/components.
  resource-pool deploy types (for example "premium resource pool") fetch
  resource-pool/components using the cluster ID itself.
  --cluster-id can be provided by flag, CLINIC_CLUSTER_ID, or selected profile defaults.`,
		Example: "  clinic cluster topology --cluster-id 123456789",
	}, stdout)
	command.Args = cobra.NoArgs
	command.Flags().StringVar(&opts.clusterID, "cluster-id", "", "Cluster ID.")
	command.RunE = func(_ *cobra.Command, _ []string) error {
		ctx, err := runtime()
		if err != nil {
			return err
		}
		return runClusterTopology(ctx, opts, stdout)
	}
	return command
}

func runClusterTopology(ctx *RuntimeContext, opts clusterTopologyOptions, stdout io.Writer) error {
	resolvedClusterID, err := requiredFlagOrEnvWithContext(ctx, "cluster-id", opts.clusterID, envClinicClusterID)
	if err != nil {
		return err
	}

	clusterRef, err := resolveClusterReferenceByIDWithMetadata(ctx, resolvedClusterID)
	if err != nil {
		return err
	}

	deployType := clusterRef.DeployType()
	switch normalizeDeployType(deployType) {
	case "dedicated":
		return writeDedicatedClusterTopology(ctx, clusterRef, stdout)
	case "premium", "byoc":
		if clusterRef.ParentID == "" || clusterRef.ParentID == "null" {
			return fmt.Errorf("%s cluster has no parentID: %s", deployType, clusterRef.ClusterID)
		}
		return fetchResourcePoolComponents(ctx, clusterRef.OrgID, clusterRef.ParentID, stdout)
	case "premium_resource_pool", "essential_v2_resource_pool", "resource_pool", "byoc_resource_pool":
		return fetchResourcePoolComponents(ctx, clusterRef.OrgID, clusterRef.ClusterID, stdout)
	default:
		if deployType == "" {
			return fmt.Errorf("cluster %s has no deploy type", clusterRef.ClusterID)
		}
		return fmt.Errorf("cluster type %q has no topology", deployType)
	}
}

func fetchResourcePoolComponents(ctx *RuntimeContext, orgID, poolClusterID string, stdout io.Writer) error {
	return executeClinicJSON(ctx, clinic.Request{
		Method: http.MethodGet,
		Path:   fmt.Sprintf("/clinic/api/v1/orgs/%s/clusters/%s/resource-pool/components", orgID, poolClusterID),
	}, stdout)
}

func normalizeDeployType(value string) string {
	value = strings.ToLower(strings.TrimSpace(value))
	if value == "" {
		return ""
	}

	var builder strings.Builder
	lastUnderscore := false
	for _, r := range value {
		switch {
		case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'):
			builder.WriteRune(r)
			lastUnderscore = false
		case r == ' ' || r == '-' || r == '_':
			if builder.Len() == 0 || lastUnderscore {
				continue
			}
			builder.WriteByte('_')
			lastUnderscore = true
		}
	}
	return strings.Trim(builder.String(), "_")
}

func executeClinicJSON(ctx *RuntimeContext, req clinic.Request, stdout io.Writer) error {
	body, err := executeClinicRaw(ctx, req)
	if err != nil {
		return err
	}

	return writeFormattedJSONOrRaw(stdout, body)
}

func executeClinicRaw(ctx *RuntimeContext, req clinic.Request) ([]byte, error) {
	var body []byte
	if err := doClinicRequest(ctx, req, &body); err != nil {
		return nil, err
	}

	return body, nil
}

type clusterSearchResponse struct {
	Items []clusterReference `json:"items"`
}

type clusterReference struct {
	ClusterID           string `json:"clusterID"`
	ClusterName         string `json:"name"`
	OrgID               string `json:"orgID"`
	ParentID            string `json:"parentID"`
	ClusterDeployTypeV2 string `json:"clusterDeployTypeV2"`
	ClusterProviderName string `json:"clusterProviderName"`
	ClusterRegionName   string `json:"clusterRegionName"`
}

func (r *clusterReference) UnmarshalJSON(data []byte) error {
	type alias clusterReference
	var decoded struct {
		alias
		ClusterName string `json:"clusterName"`
	}
	if err := json.Unmarshal(data, &decoded); err != nil {
		return err
	}

	*r = clusterReference(decoded.alias)
	if r.ClusterName == "" {
		r.ClusterName = decoded.ClusterName
	}
	return nil
}

func (r clusterReference) DeployType() string {
	return r.ClusterDeployTypeV2
}

func autoCreateProfileFromClusterSearchID(ctx *RuntimeContext, clusterID string, raw []byte) error {
	if ctx == nil || ctx.Config == nil {
		return nil
	}

	var response clusterSearchResponse
	if err := json.Unmarshal(raw, &response); err != nil {
		return nil
	}

	match, ok := exactClusterReferenceByID(response.Items, clusterID)
	if !ok {
		return nil
	}

	selectedEnv, container, err := selectedEnvProfileContainer(ctx)
	if err != nil {
		return err
	}

	for _, profile := range container.Profiles {
		if profile.ClusterID == clusterID {
			return nil
		}
	}

	name := uniqueAutoCreatedProfileName(container.Profiles, match)
	container.Profiles[name] = config.ProfileRecord{
		Env:                 selectedEnv,
		ClusterID:           match.ClusterID,
		ClusterName:         match.ClusterName,
		OrgID:               match.OrgID,
		ParentID:            match.ParentID,
		ClusterDeployTypeV2: match.ClusterDeployTypeV2,
	}

	saveEnvProfileContainer(ctx.Config, selectedEnv, container)
	return ctx.Config.Save()
}

func exactClusterReferenceByID(items []clusterReference, clusterID string) (clusterReference, bool) {
	for _, item := range items {
		if item.ClusterID == clusterID {
			return item, true
		}
	}
	return clusterReference{}, false
}

func uniqueAutoCreatedProfileName(profiles map[string]config.ProfileRecord, match clusterReference) string {
	base := fmt.Sprintf("%s__%s__%s",
		match.ClusterID,
		slugOrUnknown(match.DeployType()),
		slugOrUnknown(match.ClusterName),
	)
	if profiles == nil {
		return base
	}
	if _, exists := profiles[base]; !exists {
		return base
	}

	for suffix := 2; ; suffix++ {
		name := fmt.Sprintf("%s__%d", base, suffix)
		if _, exists := profiles[name]; !exists {
			return name
		}
	}
}

func slugOrUnknown(value string) string {
	slug := slugifyProfileNamePart(value)
	if slug == "" {
		return "unknown"
	}
	return slug
}

func slugifyProfileNamePart(value string) string {
	value = strings.ToLower(value)

	var builder strings.Builder
	lastDash := false
	for _, r := range value {
		if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
			builder.WriteRune(r)
			lastDash = false
			continue
		}
		if builder.Len() == 0 || lastDash {
			continue
		}
		builder.WriteByte('-')
		lastDash = true
	}

	return strings.Trim(builder.String(), "-")
}

func resolveClusterReferenceByID(ctx *RuntimeContext, clusterID string) (clusterReference, error) {
	if profile, ok := selectedProfileClusterReferenceByID(ctx, clusterID); ok {
		return profile, nil
	}

	return resolveClusterReferenceByIDFromAPI(ctx, clusterID)
}

func resolveClusterReferenceByIDWithMetadata(ctx *RuntimeContext, clusterID string) (clusterReference, error) {
	if profile, ok := selectedProfileClusterReferenceWithMetadataByID(ctx, clusterID); ok {
		return profile, nil
	}

	return resolveClusterReferenceByIDFromAPI(ctx, clusterID)
}

func resolveClusterReferenceByIDFromAPI(ctx *RuntimeContext, clusterID string) (clusterReference, error) {
	if ctx == nil || ctx.Client == nil {
		return clusterReference{}, errors.New("clinic client not configured")
	}

	params := url.Values{}
	params.Set("cluster_id", clusterID)
	params.Set("show_deleted", "true")

	var response clusterSearchResponse
	if err := doClinicRequest(ctx, clinic.Request{
		Method: http.MethodGet,
		Path:   dashboardClustersPath,
		Query:  params,
	}, &response); err != nil {
		return clusterReference{}, err
	}

	if item, ok := exactClusterReferenceByID(response.Items, clusterID); ok {
		return item, nil
	}

	return clusterReference{}, fmt.Errorf("cluster not found: %s", clusterID)
}

type clusterDetailTopology struct {
	Components json.RawMessage `json:"components"`
	Topology   json.RawMessage `json:"topology"`
}

func writeDedicatedClusterTopology(ctx *RuntimeContext, clusterRef clusterReference, stdout io.Writer) error {
	var detail clusterDetailTopology
	if err := doClinicRequest(ctx, clinic.Request{
		Method: http.MethodGet,
		Path:   fmt.Sprintf("/clinic/api/v1/orgs/%s/clusters/%s", clusterRef.OrgID, clusterRef.ClusterID),
	}, &detail); err != nil {
		return err
	}

	switch {
	case hasJSONPayload(detail.Components):
		return writeFormattedJSONOrRaw(stdout, detail.Components)
	case hasJSONPayload(detail.Topology):
		return writeFormattedJSONOrRaw(stdout, detail.Topology)
	default:
		return fmt.Errorf("cluster %s has no components topology", clusterRef.ClusterID)
	}
}

func hasJSONPayload(raw json.RawMessage) bool {
	if len(raw) == 0 {
		return false
	}
	return string(raw) != "null"
}

func addOptionalString(values url.Values, key, value string) {
	if value != "" {
		values.Set(key, value)
	}
}

func addOptionalPositiveInt(values url.Values, key string, value int) error {
	if value == 0 {
		return nil
	}
	if value < 0 {
		return fmt.Errorf("%s must be greater than 0", key)
	}
	values.Set(key, strconv.Itoa(value))
	return nil
}

func selectedProfileClusterReferenceByID(ctx *RuntimeContext, clusterID string) (clusterReference, bool) {
	profile := runtimeSelectedProfile(ctx)
	if profile == nil || profile.ClusterID != clusterID || profile.OrgID == "" {
		return clusterReference{}, false
	}

	return clusterReferenceFromProfile(*profile), true
}

func selectedProfileClusterReferenceWithMetadataByID(ctx *RuntimeContext, clusterID string) (clusterReference, bool) {
	profile := runtimeSelectedProfile(ctx)
	if profile == nil || profile.ClusterID != clusterID || !profileRecordHasMetadata(*profile) {
		return clusterReference{}, false
	}

	return clusterReferenceFromProfile(*profile), true
}

func runtimeSelectedProfile(ctx *RuntimeContext) *config.ProfileRecord {
	if ctx == nil {
		return nil
	}
	return ctx.SelectedProfile
}

func clusterReferenceFromProfile(record config.ProfileRecord) clusterReference {
	return clusterReference{
		ClusterID:           record.ClusterID,
		ClusterName:         record.ClusterName,
		OrgID:               record.OrgID,
		ParentID:            record.ParentID,
		ClusterDeployTypeV2: record.ClusterDeployTypeV2,
	}
}

func profileRecordHasMetadata(record config.ProfileRecord) bool {
	if record.ClusterID == "" || record.OrgID == "" {
		return false
	}

	deployType := normalizeDeployType(record.ClusterDeployTypeV2)
	if deployType == "" {
		return false
	}
	if deployType == "premium" || deployType == "byoc" {
		return record.ParentID != "" && record.ParentID != "null"
	}
	return true
}
