package lifecycle

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	awsarn "github.com/aws/aws-sdk-go-v2/aws/arn"
	"gopkg.in/yaml.v3"

	"github.com/tidbcloud/controlplane9/internal/host"
)

// StarterReadinessCheck is the shared named-check contract consumed by
// cp9 up, starter activate, and starter status.
type StarterReadinessCheck struct {
	Name       string `json:"name"`
	Status     string `json:"status"`
	ReasonCode string `json:"reason_code,omitempty"`
	Service    string `json:"service,omitempty"`
	Pin        string `json:"pin,omitempty"`
	Provenance string `json:"provenance,omitempty"`
	ValidUntil string `json:"valid_until,omitempty"`
	Message    string `json:"message,omitempty"`
}

const (
	StarterReasonMissingImagePin    = "missing_image_pin"
	StarterReasonInvalidImagePin    = "invalid_image_pin"
	StarterReasonImagePinUnreadable = "image_pin_unreadable"
	StarterReasonExpiredImagePin    = "expired_image_pin"
)

// StarterPrerequisiteKubectl allows the readiness contract to be tested at the
// command boundary while production uses the selected session host adapter.
type StarterPrerequisiteKubectl func(context.Context, *host.Session, ...string) ([]byte, error)

// CheckStarterActivationPrerequisites observes the local-global PCA config
// inputs consumed by Starter activation without waiting.
func CheckStarterActivationPrerequisites(
	ctx context.Context,
	selected *host.Session,
	run StarterPrerequisiteKubectl,
	configPaths ...string,
) []StarterReadinessCheck {
	if run == nil {
		run = func(ctx context.Context, selected *host.Session, args ...string) ([]byte, error) {
			return host.RunKubectl(ctx, selected, nil, args...)
		}
	}
	targets := []struct {
		name       string
		deployment string
		configMaps []string
	}{
		{name: "serverless-service", deployment: "serverless-service", configMaps: []string{"serverless-service"}},
		{
			name:       "serverless-global-service",
			deployment: "serverless-global-service",
			configMaps: []string{"serverless-global-service-config", "serverless-global-service"},
		},
	}
	checks := make([]StarterReadinessCheck, 0, len(targets)*2)
	configPath := "cp9.yaml"
	if len(configPaths) > 0 && strings.TrimSpace(configPaths[0]) != "" {
		configPath = strings.TrimSpace(configPaths[0])
	}
	for _, target := range targets {
		checks = append(checks, starterControlPlaneDeploymentCheck(ctx, selected, target.name, target.deployment, run, configPath))
		check := StarterReadinessCheck{
			Name:   "starter-pca-config-" + target.name,
			Status: "not_ready",
		}
		var lastErr error
		var configYAML string
		for _, configMap := range target.configMaps {
			out, err := run(
				ctx,
				selected,
				"-n", "cp9-global",
				"get", "configmap/"+configMap,
				"-o", `jsonpath={.data.config\.yaml}`,
			)
			if err == nil && strings.TrimSpace(string(out)) != "" {
				configYAML = string(out)
				break
			}
			lastErr = err
		}
		pcaARN, parseErr := starterPCAARN(configYAML)
		switch {
		case strings.TrimSpace(configYAML) == "":
			check.Message = "config.yaml is unavailable"
			if lastErr != nil {
				check.Message += ": " + lastErr.Error()
			}
		case parseErr != nil:
			check.Message = "config.yaml cannot be parsed: " + parseErr.Error()
		case !validStarterPCAARN(pcaARN):
			check.Message = "pcaArn is missing or still a placeholder"
		default:
			check.Status = "passed"
			check.Message = "pcaArn is activation-ready"
		}
		checks = append(checks, check)
	}
	return checks
}

type starterDeploymentSnapshot struct {
	Metadata starterDeploymentMetadata `json:"metadata"`
	Spec     starterDeploymentSpec     `json:"spec"`
	Status   starterDeploymentStatus   `json:"status"`
}

type starterDeploymentMetadata struct {
	Generation int64 `json:"generation"`
}

type starterDeploymentSpec struct {
	Replicas int32 `json:"replicas"`
}

type starterDeploymentStatus struct {
	ObservedGeneration int64                        `json:"observedGeneration"`
	ReadyReplicas      int32                        `json:"readyReplicas"`
	UpdatedReplicas    int32                        `json:"updatedReplicas"`
	Conditions         []starterDeploymentCondition `json:"conditions"`
}

type starterDeploymentCondition struct {
	Type               string `json:"type"`
	Status             string `json:"status"`
	Reason             string `json:"reason"`
	Message            string `json:"message"`
	LastTransitionTime string `json:"lastTransitionTime"`
}

func starterControlPlaneDeploymentCheck(
	ctx context.Context,
	selected *host.Session,
	service string,
	deployment string,
	run StarterPrerequisiteKubectl,
	configPath string,
) StarterReadinessCheck {
	check := StarterReadinessCheck{
		Name:   "starter-control-plane-" + service,
		Status: "not_ready",
	}
	out, err := run(ctx, selected, "-n", "cp9-global", "get", "deployment/"+deployment, "-o", "json")
	if err != nil {
		check.Message = fmt.Sprintf("deployment/%s is unavailable: %v; run %s", deployment, err, starterStatusRetryCommand(configPath))
		return check
	}
	var snapshot starterDeploymentSnapshot
	if err := json.Unmarshal(out, &snapshot); err != nil {
		check.Message = fmt.Sprintf("deployment/%s status cannot be parsed: %v; run %s", deployment, err, starterStatusRetryCommand(configPath))
		return check
	}
	state := fmt.Sprintf(
		"observedGeneration=%d/%d ready=%d/%d updated=%d",
		snapshot.Status.ObservedGeneration,
		snapshot.Metadata.Generation,
		snapshot.Status.ReadyReplicas,
		snapshot.Spec.Replicas,
		snapshot.Status.UpdatedReplicas,
	)
	var transition string
	available := false
	for _, condition := range snapshot.Status.Conditions {
		if condition.Type != "Available" {
			continue
		}
		available = strings.EqualFold(condition.Status, "True")
		transition = fmt.Sprintf(
			"last transition=%s reason=%s message=%s",
			firstNonEmpty(condition.LastTransitionTime, "unknown"),
			firstNonEmpty(condition.Reason, "unknown"),
			firstNonEmpty(condition.Message, "none"),
		)
		break
	}
	if snapshot.Spec.Replicas > 0 &&
		snapshot.Status.ObservedGeneration >= snapshot.Metadata.Generation &&
		snapshot.Status.ReadyReplicas >= snapshot.Spec.Replicas &&
		snapshot.Status.UpdatedReplicas >= snapshot.Spec.Replicas &&
		available {
		check.Status = "passed"
		check.Message = "deployment/" + deployment + " is activation-ready (" + state + ")"
		return check
	}
	if transition == "" {
		transition = "last transition=unknown reason=Available condition missing message=none"
	}
	check.Message = fmt.Sprintf(
		"deployment/%s is not activation-ready (%s; %s); run %s",
		deployment,
		state,
		transition,
		starterStatusRetryCommand(configPath),
	)
	return check
}

// WaitStarterActivationPrerequisites waits under one absolute deadline and
// returns the same named checks that starter status exposes.
func WaitStarterActivationPrerequisites(
	ctx context.Context,
	selected *host.Session,
	timeout time.Duration,
	poll time.Duration,
	run StarterPrerequisiteKubectl,
	configPaths ...string,
) ([]StarterReadinessCheck, error) {
	if timeout <= 0 {
		timeout = 15 * time.Minute
	}
	if poll <= 0 {
		poll = 2 * time.Second
	}
	waitCtx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
	var checks []StarterReadinessCheck
	for {
		checks = CheckStarterActivationPrerequisites(waitCtx, selected, run, configPaths...)
		var pending []string
		for i := range checks {
			check := &checks[i]
			if check.Status != "passed" {
				pending = append(pending, check.Name+": "+check.Message)
			}
		}
		if len(pending) == 0 {
			return checks, nil
		}
		select {
		case <-waitCtx.Done():
			return checks, fmt.Errorf(
				"starter activation prerequisites not ready before %s: %s; run %s",
				timeout,
				strings.Join(pending, "; "),
				starterStatusRetryCommand(firstConfigPath(configPaths)),
			)
		case <-time.After(poll):
		}
	}
}

func firstConfigPath(paths []string) string {
	if len(paths) == 0 {
		return "cp9.yaml"
	}
	return paths[0]
}

func starterStatusRetryCommand(configPath string) string {
	configPath = strings.TrimSpace(configPath)
	if configPath == "" {
		configPath = "cp9.yaml"
	}
	return "cp9 starter status config=" + quoteShellArgument(configPath)
}

func quoteShellArgument(value string) string {
	return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'"
}

func starterPCAARN(configYAML string) (string, error) {
	var config starterPCAConfig
	if err := yaml.Unmarshal([]byte(configYAML), &config); err != nil {
		return "", err
	}
	return firstNonEmpty(config.PCAARN, config.ServiceAddresses.PCAARN, config.AWS.PCAARN), nil
}

type starterPCAConfig struct {
	PCAARN           string            `yaml:"pcaArn"`
	ServiceAddresses starterPCASection `yaml:"svcaddr"`
	AWS              starterPCASection `yaml:"aws"`
}

type starterPCASection struct {
	PCAARN string `yaml:"pcaArn"`
}

func validStarterPCAARN(value string) bool {
	parsed, err := awsarn.Parse(strings.TrimSpace(value))
	if err != nil || parsed.Partition != "aws" || parsed.Service != "acm-pca" {
		return false
	}
	resource, found := strings.CutPrefix(parsed.Resource, "certificate-authority/")
	if !found || strings.TrimSpace(resource) == "" {
		return false
	}
	return resource != "cp9-local" && !strings.HasPrefix(resource, "cp9-")
}
