package templateversion

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"sort"
	"strings"

	apiv1 "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/cluster-service-ng/domain/entity"
	"github.com/tidbcloud/cluster-service-ng/pkg/storage/repo"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"gorm.io/gorm"
)

const (
	TemplateNameAnnotationKey    = "tidbcloud.pingcap.com/template-name"
	TemplateVersionAnnotationKey = "tidbcloud.pingcap.com/template-version"
)

var (
	statusAvailable = apiv1.TemplateVersionStatus_TEMPLATE_VERSION_STATUS_AVAILABLE
	statusDisabled  = apiv1.TemplateVersionStatus_TEMPLATE_VERSION_STATUS_DISABLED
	statusExpired   = apiv1.TemplateVersionStatus_TEMPLATE_VERSION_STATUS_EXPIRED
)

var usageDetectableComponentTemplates = map[string]struct{}{
	"tidb_group":             {},
	"tikv_group":             {},
	"pd_group":               {},
	"tiproxy_group":          {},
	"tiflash_group":          {},
	"tikv_worker_group":      {},
	"tidb_worker_group":      {},
	"tso_group":              {},
	"scheduling_group":       {},
	"resource_manager_group": {},
}

type Service struct {
	templateRepo repo.TemplateRegistryRepo
	maxLag       int
}

func NewService(templateRepo repo.TemplateRegistryRepo, maxLag int) *Service {
	if maxLag <= 0 {
		maxLag = 3
	}
	return &Service{
		templateRepo: templateRepo,
		maxLag:       maxLag,
	}
}

func (s *Service) Bootstrap(ctx context.Context, templates map[string]string) error {
	templateNames := make([]string, 0, len(templates))
	for name, content := range templates {
		if strings.TrimSpace(name) == "" {
			continue
		}
		normalized := NormalizeTemplateContent(content)
		version := ComputeTemplateVersion(normalized)
		item := &entity.TemplateRegistry{
			TemplateVersionInfo: entity.TemplateVersionInfo{
				TemplateName: name,
				Version:      version,
				Status:       statusAvailable,
			},
			Content: normalized,
		}
		if err := s.templateRepo.Upsert(ctx, item); err != nil {
			return fmt.Errorf("failed to upsert template %s: %w", name, err)
		}
		persisted, err := s.templateRepo.Load(ctx, name, version)
		if err != nil {
			return fmt.Errorf("failed to read-back template %s/%s: %w", name, version, err)
		}
		if ComputeTemplateVersion(NormalizeTemplateContent(persisted.Content)) != persisted.Version {
			return fmt.Errorf("template hash mismatch for %s/%s", name, version)
		}
		templateNames = append(templateNames, name)
	}

	sort.Strings(templateNames)
	templateNames = uniqueStrings(templateNames)
	for _, name := range templateNames {
		items, err := s.templateRepo.ListByTemplateName(ctx, name)
		if err != nil {
			return fmt.Errorf("failed to list template versions for %s: %w", name, err)
		}
		defaultAvailableIdx := -1
		for idx, item := range items {
			if item.Status == statusAvailable {
				defaultAvailableIdx = idx
				break
			}
		}

		availableCount := 0
		for idx, item := range items {
			wantStatus := item.Status
			distanceFromDefault := idx - defaultAvailableIdx
			// Keep DISABLED untouched; expire only versions outside rollback window.
			if defaultAvailableIdx >= 0 &&
				distanceFromDefault >= s.maxLag &&
				item.Status != statusDisabled {
				wantStatus = statusExpired
			}
			if wantStatus != item.GetStatus() {
				if err := s.templateRepo.UpdateStatus(ctx, item.GetTemplateName(), item.GetVersion(), wantStatus); err != nil {
					return fmt.Errorf("failed to update template status for %s/%s: %w", item.GetTemplateName(), item.GetVersion(), err)
				}
				item.Status = wantStatus
			}
			if item.Status == statusAvailable {
				availableCount++
			}
		}
		if availableCount == 0 {
			return fmt.Errorf("no AVAILABLE template version for %s", name)
		}
	}
	return nil
}

func (s *Service) Resolve(ctx context.Context, templateName, templateVersion string) (resolvedName, resolvedVersion, content string, err error) {
	if strings.TrimSpace(templateName) == "" {
		return "", "", "", status.Error(codes.InvalidArgument, "template name is required")
	}

	var item *entity.TemplateRegistry
	if templateVersion == "" {
		items, listErr := s.templateRepo.ListByTemplateName(ctx, templateName)
		if listErr != nil {
			return "", "", "", status.Errorf(codes.Internal, "failed to list template %s: %v", templateName, listErr)
		}
		for _, candidate := range items {
			if candidate.Status == statusAvailable {
				item = candidate
				break
			}
		}
		if item == nil {
			return "", "", "", status.Errorf(codes.FailedPrecondition, "no AVAILABLE template version for %s", templateName)
		}
	} else {
		item, err = s.templateRepo.Load(ctx, templateName, templateVersion)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return "", "", "", status.Errorf(codes.NotFound, "template version not found %s/%s", templateName, templateVersion)
			}
			return "", "", "", status.Errorf(codes.Internal, "failed to load template %s/%s: %v", templateName, templateVersion, err)
		}
		if item.Status != statusAvailable {
			return "", "", "", status.Errorf(codes.FailedPrecondition, "template %s/%s is %s", templateName, templateVersion, item.Status.String())
		}
	}

	return item.TemplateName, item.Version, item.Content, nil
}

func IsUsageDetectableComponentTemplate(templateName string) bool {
	_, ok := usageDetectableComponentTemplates[templateName]
	return ok
}

func NormalizeTemplateContent(content string) string {
	return strings.ReplaceAll(content, "\r\n", "\n")
}

func ComputeTemplateVersion(normalizedContent string) string {
	sum := sha256.Sum256([]byte(normalizedContent))
	return hex.EncodeToString(sum[:])
}

func uniqueStrings(in []string) []string {
	if len(in) <= 1 {
		return in
	}
	ret := make([]string, 0, len(in))
	var prev string
	for i, v := range in {
		if i == 0 || v != prev {
			ret = append(ret, v)
			prev = v
		}
	}
	return ret
}

// ListVersions returns all versions for a template, ordered by creation time descending.
func (s *Service) ListVersions(ctx context.Context, templateName string) ([]*entity.TemplateVersionInfo, error) {
	items, err := s.templateRepo.ListByTemplateName(ctx, templateName)
	if err != nil {
		return nil, fmt.Errorf("failed to list template versions for %s: %w", templateName, err)
	}
	result := make([]*entity.TemplateVersionInfo, 0, len(items))
	for _, item := range items {
		result = append(result, toVersionInfo(item))
	}
	return result, nil
}

// LoadVersionInfo loads a single template version's metadata. Returns gorm.ErrRecordNotFound when absent.
func (s *Service) LoadVersionInfo(ctx context.Context, templateName, version string) (*entity.TemplateVersionInfo, error) {
	item, err := s.templateRepo.Load(ctx, templateName, version)
	if err != nil {
		return nil, err
	}
	return toVersionInfo(item), nil
}

// SetStatus transitions a template version to the given target status.
// Allowed transitions: AVAILABLE↔DISABLED. EXPIRED is system-managed and cannot be set externally.
// Disabling the last AVAILABLE version for a given template is prohibited.
// The caller is responsible for wrapping this call in a transaction.
func (s *Service) SetStatus(ctx context.Context, templateName, version string, targetStatus entity.TemplateVersionStatus) (*entity.TemplateVersionInfo, error) {
	current, err := s.templateRepo.Load(ctx, templateName, version)
	if err != nil {
		return nil, err // caller handles ErrRecordNotFound mapping
	}

	if current.Status == statusExpired {
		return nil, status.Error(codes.FailedPrecondition, "status transition from EXPIRED is forbidden")
	}

	if targetStatus != statusAvailable && targetStatus != statusDisabled {
		return nil, status.Errorf(codes.InvalidArgument, "unsupported target status: %s", targetStatus.String())
	}

	if targetStatus == statusDisabled && current.Status == statusAvailable {
		items, err := s.templateRepo.ListByTemplateNameForUpdate(ctx, templateName)
		if err != nil {
			return nil, fmt.Errorf("failed to lock template versions for %s: %w", templateName, err)
		}
		availableCount := 0
		for _, item := range items {
			if item.Status == statusAvailable {
				availableCount++
			}
		}
		if availableCount <= 1 {
			return nil, status.Errorf(codes.FailedPrecondition, "cannot disable last AVAILABLE version for %s", templateName)
		}
	}

	if current.GetStatus() != targetStatus {
		if err := s.templateRepo.UpdateStatus(ctx, templateName, version, targetStatus); err != nil {
			return nil, fmt.Errorf("failed to update template status: %w", err)
		}
		current.Status = targetStatus
	}

	return toVersionInfo(current), nil
}

// toVersionInfo extracts the TemplateVersionInfo from a TemplateRegistry.
func toVersionInfo(item *entity.TemplateRegistry) *entity.TemplateVersionInfo {
	return &item.TemplateVersionInfo
}

// ListAllTemplates returns all distinct template names with their default version
// by delegating to the underlying repository.
func (s *Service) ListAllTemplates(ctx context.Context) ([]*repo.TemplateSummary, error) {
	return s.templateRepo.ListTemplateSummaries(ctx)
}
