package activepromotion

import (
	"context"
	"fmt"
	"math/rand"

	"github.com/pkg/errors"
	corev1 "k8s.io/api/core/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"

	s2hv1 "github.com/agoda-com/samsahai/api/v1"
	"github.com/agoda-com/samsahai/internal"
	s2herrors "github.com/agoda-com/samsahai/internal/errors"
)

func (c *controller) createPreActiveEnvAndDeployStableCompObjects(ctx context.Context, atpComp *s2hv1.ActivePromotion) error {
	teamName := atpComp.Name
	suffix := c.randomToken(tokenLength, atpComp.CreationTimestamp.UnixNano())
	targetNs := fmt.Sprintf("%s%s-%s", internal.AppPrefix, teamName, suffix)

	logger.Debug("start creating pre-active environment", "team", teamName, "namespace", targetNs)
	if err := c.createPreActiveEnvironment(ctx, teamName, targetNs); err != nil {
		return err
	}

	teamComp, err := c.getTeam(ctx, teamName)
	if err != nil {
		return err
	}

	logger.Debug("start copying stable component objects into target namespace",
		"team", teamName, "namespace", targetNs)
	stagingNs := teamComp.Status.Namespace.Staging
	if err = c.copyStableComponentObjectsToTargetNamespace(ctx, atpComp, stagingNs, targetNs); err != nil {
		return err
	}

	logger.Debug("start deploying stable components into target namespace",
		"team", teamName, "namespace", targetNs)
	atpComp.Status.SetNamespace(targetNs, teamComp.Status.Namespace.Active)
	atpComp.Status.SetCondition(s2hv1.ActivePromotionCondPreActiveCreated, corev1.ConditionTrue,
		"Pre-active environment has been created")
	atpComp.Status.SetCondition(s2hv1.ActivePromotionCondVerificationStarted, corev1.ConditionTrue,
		"Verifying pre-active environment")
	atpComp.SetState(s2hv1.ActivePromotionDeployingComponents,
		"Deploying stable components into target namespace")
	return nil
}

func (c *controller) ensureActiveEnvironmentPromoted(ctx context.Context, atpComp *s2hv1.ActivePromotion) error {
	targetNs := c.getTargetNamespace(atpComp)

	teamComp, err := c.getTeam(ctx, atpComp.Name)
	if err != nil {
		return err
	}

	err = c.s2hCtrl.PromoteActiveEnvironment(teamComp, targetNs, atpComp.Spec.PromotedBy, atpComp.Status.ActiveComponents)
	if err != nil && k8serrors.IsAlreadyExists(err) {
		return err
	}

	if err := c.ensureTeamNamespaceUpdated(teamComp.Status.Namespace.Active, targetNs); err != nil {
		logger.Warn("cannot ensure team active namespace updated", "error", err.Error())
		return s2herrors.ErrEnsureActivePromoted
	}

	return nil
}

func (c *controller) createPreActiveEnvironment(ctx context.Context, teamName, preActiveNs string) error {
	if err := c.s2hCtrl.CreatePreActiveEnvironment(teamName, preActiveNs); err != nil && !k8serrors.IsAlreadyExists(err) {
		if s2herrors.IsNamespaceStillCreating(err) {
			return s2herrors.ErrEnsurePreActiveEnvironmentCreated
		}
		return err
	}

	teamComp, err := c.getTeam(ctx, teamName)
	if err != nil {
		return err
	}

	if err := c.ensureNamespaceReady(ctx, preActiveNs); err != nil {
		logger.Warn("cannot ensure pre-active namespace created", "error", err.Error())
		return s2herrors.ErrEnsurePreActiveEnvironmentCreated
	}

	if err := c.ensureTeamNamespaceUpdated(teamComp.Status.Namespace.PreActive, preActiveNs); err != nil {
		logger.Warn("cannot ensure team pre-active namespace updated", "error", err.Error())
		return s2herrors.ErrEnsurePreActiveEnvironmentCreated
	}

	return nil
}

func (c *controller) copyStableComponentObjectsToTargetNamespace(
	ctx context.Context,
	atpComp *s2hv1.ActivePromotion,
	baseNs, targetNs string,
) error {
	stableComps, err := c.getStableComponentObjects(ctx, baseNs)
	if err != nil && !k8serrors.IsNotFound(err) {
		return errors.Wrapf(err, "cannot get stable components from staging namespace %s", baseNs)
	}

	if err = c.deployStableComponentObjects(ctx, stableComps, targetNs); err != nil {
		return err
	}

	atpComp.Status.SetActiveComponents(stableComps.Items)

	return nil
}

func (c *controller) getStableComponentObjects(ctx context.Context, ns string) (*s2hv1.StableComponentList, error) {
	stableComps := &s2hv1.StableComponentList{}
	if err := c.client.List(ctx, stableComps, &client.ListOptions{Namespace: ns}); err != nil {
		return &s2hv1.StableComponentList{}, err
	}

	return stableComps, nil
}

func (c *controller) deployStableComponentObjects(ctx context.Context, comps *s2hv1.StableComponentList, targetNs string) error {
	if targetNs == "" {
		return errors.Wrap(fmt.Errorf("target namespace is empty"), "cannot deploy stable components")
	}

	for _, comp := range comps.Items {
		newComp := s2hv1.StableComponent{
			ObjectMeta: metav1.ObjectMeta{
				Name:      comp.Name,
				Namespace: targetNs,
				Labels:    comp.Labels,
			},
			Spec: comp.Spec,
		}

		if err := c.client.Create(ctx, &newComp); err != nil {
			if k8serrors.IsAlreadyExists(err) {
				// update stable components
				ctemp := s2hv1.StableComponent{}
				err := c.client.Get(ctx, types.NamespacedName{Name: comp.Name, Namespace: targetNs}, &ctemp)
				if err != nil {
					return err
				}

				ctemp.Spec = newComp.Spec
				ctemp.Status = newComp.Status
				if err := c.client.Update(ctx, &ctemp); err != nil {
					return err
				}
			} else {
				return errors.Wrapf(err, "cannot deploy stable components into target namespace %s", targetNs)
			}
		}
	}

	return nil
}

func (c *controller) ensureTeamNamespaceUpdated(teamNs, atpNs string) error {
	if teamNs != atpNs {
		return fmt.Errorf("team pre-active namespace was not updated, expected: %s, actual: %s", atpNs, teamNs)
	}

	return nil
}

func (c *controller) ensureNamespaceReady(ctx context.Context, ns string) error {
	preActiveNs := &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: ns,
		},
	}

	if err := c.client.Get(ctx, types.NamespacedName{Name: ns}, preActiveNs); err != nil {
		return errors.Wrapf(err, "cannot get namespace %s", ns)
	}

	return nil
}

func (c *controller) randomToken(l int, seed int64) string {
	rand.Seed(seed)
	letters := []rune("0123456789abcdefghijklmnopqrstuvwxyz")
	b := make([]rune, l)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return string(b)
}
