//go:build ee

package jobs

import (
	"context"
	"log"
	"sync"
	"time"

	"github.com/porter-dev/porter/api/server/shared/config/env"
	"github.com/porter-dev/porter/ee/integrations/vault"
	"github.com/porter-dev/porter/internal/kubernetes"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/internal/oauth"
	"github.com/porter-dev/porter/internal/repository"
	rcreds "github.com/porter-dev/porter/internal/repository/credentials"
	rgorm "github.com/porter-dev/porter/internal/repository/gorm"
	"golang.org/x/oauth2"
	"gorm.io/gorm"
	"k8s.io/apimachinery/pkg/api/errors"
)

/*

                         === Preview Deployments TTL Deleter Job ===

   This job goes through every active preview environment in all connected clusters and deletes the
   deployments that have exceeded their TTL, corresponding to their respective preview environment.

*/

const (
	stepSize = 20
)

type previewDeploymentsTTLDeleter struct {
	enqueueTime           time.Time
	db                    *gorm.DB
	doConf                *oauth2.Config
	repo                  repository.Repository
	previewDeploymentsTTL string
}

// PreviewDeploymentsTTLDeleterOpts holds the options required to run this job
type PreviewDeploymentsTTLDeleterOpts struct {
	DBConf                *env.DBConf
	ServerURL             string
	DOClientID            string
	DOClientSecret        string
	DOScopes              []string
	PreviewDeploymentsTTL string
}

func NewPreviewDeploymentsTTLDeleter(
	db *gorm.DB,
	enqueueTime time.Time,
	opts *PreviewDeploymentsTTLDeleterOpts,
) (*previewDeploymentsTTLDeleter, error) {
	var credBackend rcreds.CredentialStorage

	if opts.DBConf.VaultAPIKey != "" && opts.DBConf.VaultServerURL != "" && opts.DBConf.VaultPrefix != "" {
		credBackend = vault.NewClient(
			opts.DBConf.VaultServerURL,
			opts.DBConf.VaultAPIKey,
			opts.DBConf.VaultPrefix,
		)
	}

	doConf := oauth.NewDigitalOceanClient(&oauth.Config{
		ClientID:     opts.DOClientID,
		ClientSecret: opts.DOClientSecret,
		Scopes:       opts.DOScopes,
		BaseURL:      opts.ServerURL,
	})

	var key [32]byte

	for i, b := range []byte(opts.DBConf.EncryptionKey) {
		key[i] = b
	}

	repo := rgorm.NewRepository(db, &key, credBackend)

	return &previewDeploymentsTTLDeleter{enqueueTime, db, doConf, repo, opts.PreviewDeploymentsTTL}, nil
}

func (n *previewDeploymentsTTLDeleter) ID() string {
	return "preview-deployments-ttl-deleter"
}

func (n *previewDeploymentsTTLDeleter) EnqueueTime() time.Time {
	return n.enqueueTime
}

func (n *previewDeploymentsTTLDeleter) Run(ctx context.Context) error {
	if n.previewDeploymentsTTL == "" {
		log.Println("no TTL set for preview deployments, skipping job altogether")
		return nil
	}

	ttlDuration, err := time.ParseDuration(n.previewDeploymentsTTL)
	if err != nil {
		log.Printf("error parsing preview deployments TTL: %v. skipping job altogether", err)
		return nil
	}

	if ttlDuration.Hours() < 24 || ttlDuration.Hours() > 720 {
		log.Printf("preview deployments TTL must be between 24 (1 day) and 720 hours (30 days). skipping job altogether")
		return nil
	}

	var count int64

	if err := n.db.Model(&models.Cluster{}).Count(&count).Error; err != nil {
		return err
	}

	var wg sync.WaitGroup

	log.Println("starting deletion of preview deployments based on TTL")

	for i := 0; i < (int(count)/stepSize)+1; i++ {
		var clusters []*models.Cluster

		if err := n.db.Order("id asc").Offset(i * stepSize).Limit(stepSize).Find(&clusters).
			Error; err != nil {
			return err
		}

		for _, cluster := range clusters {
			if !cluster.PreviewEnvsEnabled {
				continue
			}

			envs, err := n.repo.Environment().ListEnvironments(cluster.ProjectID, cluster.ID)
			if err != nil {
				log.Printf("error listing environments for cluster %s: %v", cluster.Name, err)
				continue
			}

			log.Printf("found %d environments for cluster %s", len(envs), cluster.Name)

			for _, env := range envs {
				wg.Add(1)

				go func(env *models.Environment, cluster *models.Cluster) {
					defer wg.Done()

					depls, err := n.repo.Environment().ListDeployments(env.ID)
					if err != nil {
						log.Printf("error listing deployments for %s/%s: %v", env.GitRepoOwner, env.GitRepoName, err)
						return
					}

					log.Printf("found %d deployments for %s/%s", len(depls), env.GitRepoOwner, env.GitRepoName)

					log.Printf("deleting preview deployments based on TTL %s for %s/%s",
						n.previewDeploymentsTTL, env.GitRepoOwner, env.GitRepoName)

					k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(ctx, &kubernetes.OutOfClusterConfig{
						Cluster:                   cluster,
						Repo:                      n.repo,
						DigitalOceanOAuth:         n.doConf,
						AllowInClusterConnections: false,
						Timeout:                   10 * time.Second,
					})
					if err != nil {
						log.Printf("error getting k8s agent for cluster %s: %v", cluster.Name, err)
						return
					}

					for _, depl := range depls {
						// delete the deployment if it has been inactive for longer than the set TTL
						if depl.UpdatedAt.Add(ttlDuration).Before(time.Now()) {
							if depl.Namespace != "" {
								log.Printf("deleting namespace for deployment '%s'", depl.PRName)

								_, err := k8sAgent.GetNamespace(depl.Namespace)

								if err == nil {
									err := k8sAgent.DeleteNamespace(depl.Namespace)
									if err != nil {
										log.Printf("error deleting namespace for deployment '%s': %v. skipping ...",
											depl.PRName, err)
										continue
									}
								} else if !errors.IsNotFound(err) {
									log.Printf("error getting k8s namespace for deployment '%s': %v. skipping ...",
										depl.PRName, err)
									continue
								}
							}

							log.Printf("deleting deployment '%s'", depl.PRName)

							_, err := n.repo.Environment().DeleteDeployment(depl)
							if err != nil {
								log.Printf("error deleting deployment '%s': %v", depl.PRName, err)
							}
						}
					}
				}(env, cluster)
			}

			wg.Wait()
		}
	}

	log.Println("finished deletion of preview deployments based on TTL")

	return nil
}

func (n *previewDeploymentsTTLDeleter) SetData([]byte) {}
