package job

import (
	"context"
	"fmt"
	"io"
	"regexp"
	"sort"
	"strconv"
	"strings"

	"github.com/spf13/cobra"
	"github.com/tidbcloud/metrics-integration/global/internal/domain/entity"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/config"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/dataplane"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/di"
	"github.com/tidbcloud/metrics-integration/global/internal/infrastructure/helm"
	helmpackage "github.com/tidbcloud/metrics-integration/global/pkg/helm/pipeline"
	"github.com/tidbcloud/pkgv2/secret/encrypt"
	"gorm.io/gorm"
)

type redeployRegionOptions struct {
	Cloud               string
	Regions             []string
	PipelineIDAllowlist string
	PipelineIDBlocklist string
	Apply               bool
}

type redeployRegionRequest struct {
	Cloud               entity.IntegrationCloudProviderType
	Regions             []string
	PipelineIDAllowlist []uint64
	PipelineIDBlocklist []uint64
	Apply               bool
}

type regionalIntegrationFinder func(redeployRegionRequest) ([]entity.Integration, error)
type pipelineRedeployer func(context.Context, *entity.Integration) error

var regionPattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)

func writeRedeployOutput(out io.Writer, format string, args ...interface{}) error {
	if _, err := fmt.Fprintf(out, format, args...); err != nil {
		return fmt.Errorf("write redeploy output: %w", err)
	}
	return nil
}

func parseIntegrationCloudProvider(cloud string) (entity.IntegrationCloudProviderType, error) {
	switch strings.ToLower(strings.TrimSpace(cloud)) {
	case "aws":
		return entity.IntegrationCloudProviderTypeAWS, nil
	case "gcp":
		return entity.IntegrationCloudProviderTypeGCP, nil
	case "azure":
		return entity.IntegrationCloudProviderTypeAzure, nil
	case "alicloud", "alibaba", "alibabacloud":
		return entity.IntegrationCloudProviderTypeAlibabaCloud, nil
	default:
		return entity.IntegrationCloudProviderTypeUnspecified,
			fmt.Errorf("unsupported cloud %q: supported clouds are aws, gcp, azure, and alicloud", cloud)
	}
}

func normalizeRegions(regions []string) ([]string, error) {
	unique := make(map[string]struct{}, len(regions))
	for _, rawRegion := range regions {
		region := strings.ToLower(strings.TrimSpace(rawRegion))
		if region == "" {
			continue
		}
		if region == "all" {
			return nil, fmt.Errorf("explicit region names are required; region %q is not supported", rawRegion)
		}
		if !regionPattern.MatchString(region) {
			return nil, fmt.Errorf("invalid region %q", rawRegion)
		}
		unique[region] = struct{}{}
	}

	if len(unique) == 0 {
		return nil, fmt.Errorf("at least one region is required")
	}

	normalized := make([]string, 0, len(unique))
	for region := range unique {
		normalized = append(normalized, region)
	}
	sort.Strings(normalized)
	return normalized, nil
}

func parsePipelineIDList(raw string) ([]uint64, error) {
	if strings.TrimSpace(raw) == "" {
		return nil, nil
	}

	unique := make(map[uint64]struct{})
	for _, value := range strings.Split(raw, ",") {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		id, err := strconv.ParseUint(value, 10, 64)
		if err != nil || id == 0 {
			return nil, fmt.Errorf("invalid pipeline ID %q", value)
		}
		unique[id] = struct{}{}
	}

	ids := make([]uint64, 0, len(unique))
	for id := range unique {
		ids = append(ids, id)
	}
	sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
	return ids, nil
}

func buildRedeployRegionRequest(opts redeployRegionOptions) (redeployRegionRequest, error) {
	cloud, err := parseIntegrationCloudProvider(opts.Cloud)
	if err != nil {
		return redeployRegionRequest{}, err
	}
	regions, err := normalizeRegions(opts.Regions)
	if err != nil {
		return redeployRegionRequest{}, err
	}
	allowlist, err := parsePipelineIDList(opts.PipelineIDAllowlist)
	if err != nil {
		return redeployRegionRequest{}, fmt.Errorf("invalid pipeline ID allowlist: %w", err)
	}
	blocklist, err := parsePipelineIDList(opts.PipelineIDBlocklist)
	if err != nil {
		return redeployRegionRequest{}, fmt.Errorf("invalid pipeline ID blocklist: %w", err)
	}

	return redeployRegionRequest{
		Cloud:               cloud,
		Regions:             regions,
		PipelineIDAllowlist: allowlist,
		PipelineIDBlocklist: blocklist,
		Apply:               opts.Apply,
	}, nil
}

func findIntegrationByID(id uint64) (*entity.Integration, error) {
	pipeline := &entity.Integration{}
	if err := di.DiCall(func(db *gorm.DB) error {
		return db.Where("id = ?", id).First(pipeline).Error
	}); err != nil {
		return nil, fmt.Errorf("get pipeline %d failed: %w", id, err)
	}
	return pipeline, nil
}

func findRegionalIntegrations(req redeployRegionRequest) ([]entity.Integration, error) {
	var pipelines []entity.Integration
	err := di.DiCall(func(db *gorm.DB) error {
		query := db.Model(&entity.Integration{}).
			Where("cloud_provider_type = ?", req.Cloud).
			Where("region IN ?", req.Regions).
			Where("provider_type IN ?", []entity.IntegrationType{
				entity.IntegrationTypeDatadog,
				entity.IntegrationTypeNewrelic,
			}).
			Where("status != ?", entity.IntegrationStatusNone).
			Order("region ASC, id ASC")
		if len(req.PipelineIDAllowlist) > 0 {
			query = query.Where("id IN ?", req.PipelineIDAllowlist)
		}
		if len(req.PipelineIDBlocklist) > 0 {
			query = query.Where("id NOT IN ?", req.PipelineIDBlocklist)
		}
		return query.Find(&pipelines).Error
	})
	if err != nil {
		return nil, fmt.Errorf("query regional integrations failed: %w", err)
	}
	return pipelines, nil
}

func redeployPipeline(ctx context.Context, pipeline *entity.Integration) error {
	if pipeline == nil || pipeline.ID == 0 {
		return fmt.Errorf("pipeline ID is required")
	}

	targetCluster, err := helmpackage.GetTargetClusterByClusterID(uint64(pipeline.ClusterID))
	if err != nil {
		return fmt.Errorf("failed to get clusterinfo: %w", err)
	}

	decryptedAPIKey := ""
	if err := di.DiCall(func(encryptor encrypt.Encryptor) error {
		switch pipeline.ProviderType {
		case entity.IntegrationTypeDatadog:
			if len(pipeline.EncryptedDatadogApiKey) > 0 {
				secret, err := encrypt.DecryptBase64(encryptor, pipeline.EncryptedDatadogApiKey)
				if err != nil {
					return fmt.Errorf("%s, please check the key or region", err.Error())
				}
				decryptedAPIKey = secret.String()
			}
		case entity.IntegrationTypeNewrelic:
			if len(pipeline.EncryptedNewrelicApiKey) > 0 {
				secret, err := encrypt.DecryptBase64(encryptor, pipeline.EncryptedNewrelicApiKey)
				if err != nil {
					return fmt.Errorf("%s, please check the key or region", err.Error())
				}
				decryptedAPIKey = secret.String()
			}
		default:
			return fmt.Errorf("unsupported integration type %q", pipeline.ProviderType)
		}
		return nil
	}); err != nil {
		return fmt.Errorf("failed to decrypt API key: %w", err)
	}

	packageConfig := helmpackage.NewPipelineConfig(
		fmt.Sprintf("%d", pipeline.ID),
		targetCluster,
		pipeline,
		decryptedAPIKey,
		false,
	)

	serverAddr := config.Global().RegionalExecutorConfig.GetPipelineDataplaneServerAddr(
		uint64(pipeline.ID),
		string(pipeline.CloudProviderType),
		pipeline.Region,
	)
	if serverAddr != "" {
		if err := helm.RemoteApply(ctx, packageConfig, serverAddr); err != nil {
			return fmt.Errorf("failed to apply helm chart: %w", err)
		}
		return nil
	}

	restConfig, err := dataplane.GetRestConfig(pipeline.Region)
	if err != nil {
		return fmt.Errorf("failed to get rest config: %w", err)
	}
	if err := helm.Apply(ctx, restConfig, packageConfig); err != nil {
		return fmt.Errorf("failed to apply helm chart: %w", err)
	}
	return nil
}

func executeRegionalRedeploy(
	ctx context.Context,
	out io.Writer,
	req redeployRegionRequest,
	finder regionalIntegrationFinder,
	redeployer pipelineRedeployer,
) error {
	pipelines, err := finder(req)
	if err != nil {
		return err
	}

	if err := writeRedeployOutput(out, "Found %d pipeline(s) for cloud=%s regions=%s\n", len(pipelines), req.Cloud, strings.Join(req.Regions, ",")); err != nil {
		return err
	}
	for i := range pipelines {
		pipeline := &pipelines[i]
		if err := writeRedeployOutput(out, "- pipeline=%d region=%s type=%s status=%s\n", pipeline.ID, pipeline.Region, pipeline.ProviderType, pipeline.Status); err != nil {
			return err
		}
	}

	if len(pipelines) == 0 {
		return nil
	}
	if !req.Apply {
		return writeRedeployOutput(out, "Plan only: rerun with --apply to redeploy these pipelines.\n")
	}

	succeeded := 0
	failed := make([]uint64, 0)
	for i := range pipelines {
		if err := ctx.Err(); err != nil {
			return fmt.Errorf("regional redeploy canceled: %w", err)
		}
		pipeline := &pipelines[i]
		if err := writeRedeployOutput(out, "Redeploying pipeline %d (%s/%s)...\n", pipeline.ID, pipeline.CloudProviderType, pipeline.Region); err != nil {
			return err
		}
		if err := redeployer(ctx, pipeline); err != nil {
			failed = append(failed, uint64(pipeline.ID))
			if writeErr := writeRedeployOutput(out, "FAILED pipeline %d: %v\n", pipeline.ID, err); writeErr != nil {
				return writeErr
			}
			continue
		}
		succeeded++
		if err := writeRedeployOutput(out, "SUCCEEDED pipeline %d\n", pipeline.ID); err != nil {
			return err
		}
	}

	if err := writeRedeployOutput(out, "Summary: total=%d succeeded=%d failed=%d\n", len(pipelines), succeeded, len(failed)); err != nil {
		return err
	}
	if len(failed) > 0 {
		failedIDs := make([]string, 0, len(failed))
		for _, id := range failed {
			failedIDs = append(failedIDs, strconv.FormatUint(id, 10))
		}
		return fmt.Errorf("failed to redeploy pipeline IDs: %s", strings.Join(failedIDs, ","))
	}
	return nil
}

// redeployIntegrationCommand redeploys a single integration pipeline.
func redeployIntegrationCommand() *cobra.Command {
	var req IntegrationReq

	cmd := &cobra.Command{
		Use:   "redeploy",
		Short: "Redeploy an integration pipeline",
		Example: `  # Redeploy one integration pipeline
  job integration redeploy --pipeline-id 1`,
		RunE: func(cmd *cobra.Command, args []string) error {
			if req.PipelineID == 0 {
				return fmt.Errorf("pipeline-id is required")
			}
			if err := writeRedeployOutput(cmd.OutOrStdout(), "Pipeline ID: %d\n", req.PipelineID); err != nil {
				return err
			}

			pipeline, err := findIntegrationByID(req.PipelineID)
			if err != nil {
				return err
			}
			if err := redeployPipeline(cmd.Context(), pipeline); err != nil {
				return err
			}

			return writeRedeployOutput(cmd.OutOrStdout(), "Integration redeployed successfully.\n")
		},
	}

	cmd.Flags().Uint64Var(&req.PipelineID, "pipeline-id", 0, "Pipeline ID (required)")
	mustMarkFlagRequired(cmd, "pipeline-id")
	return cmd
}

// redeployRegionCommand plans or applies integration pipeline redeployments by cloud and region.
func redeployRegionCommand() *cobra.Command {
	var opts redeployRegionOptions

	cmd := &cobra.Command{
		Use:   "redeploy-region",
		Short: "Redeploy Datadog and New Relic pipelines by cloud and region",
		Long: `Find non-deleted Datadog and New Relic integration pipelines in the selected
cloud and regions. The command only prints the plan unless --apply is provided.`,
		Example: `  # Preview pipelines in two AWS regions
  job integration redeploy-region --cloud aws --regions us-west-2,sa-east-1

  # Redeploy an explicit allowlist
  job integration redeploy-region --cloud aws --regions us-west-2 \
    --pipeline-id-allowlist 12,34 --apply`,
		RunE: func(cmd *cobra.Command, args []string) error {
			req, err := buildRedeployRegionRequest(opts)
			if err != nil {
				return err
			}
			return executeRegionalRedeploy(
				cmd.Context(),
				cmd.OutOrStdout(),
				req,
				findRegionalIntegrations,
				redeployPipeline,
			)
		},
	}

	cmd.Flags().StringVar(&opts.Cloud, "cloud", "", "Cloud provider: aws, gcp, azure, or alicloud (required)")
	cmd.Flags().StringSliceVar(&opts.Regions, "regions", nil, "Comma-separated region names (required)")
	cmd.Flags().StringVar(&opts.PipelineIDAllowlist, "pipeline-id-allowlist", "", "Optional comma-separated pipeline IDs to include")
	cmd.Flags().StringVar(&opts.PipelineIDBlocklist, "pipeline-id-blocklist", "", "Optional comma-separated pipeline IDs to exclude")
	cmd.Flags().BoolVar(&opts.Apply, "apply", false, "Apply the redeployments; defaults to plan only")
	mustMarkFlagRequired(cmd, "cloud", "regions")
	return cmd
}
