package config

import (
	"context"
	"fmt"
	"log"
	"time"

	redis "github.com/go-redis/redis/v8"

	"github.com/joeshaw/envdecode"
	"github.com/porter-dev/porter/api/server/shared/apierrors/alerter"
	"github.com/porter-dev/porter/api/server/shared/config/env"
	"github.com/porter-dev/porter/internal/adapter"
	"github.com/porter-dev/porter/internal/analytics"
	"github.com/porter-dev/porter/internal/kubernetes"
	klocal "github.com/porter-dev/porter/internal/kubernetes/local"
	"github.com/porter-dev/porter/internal/oauth"

	"github.com/porter-dev/porter/internal/repository"
	"github.com/porter-dev/porter/internal/repository/credentials"
	"github.com/porter-dev/porter/internal/repository/gorm"
	"github.com/porter-dev/porter/pkg/logger"
	"github.com/porter-dev/porter/provisioner/integrations/provisioner"
	"github.com/porter-dev/porter/provisioner/integrations/provisioner/k8s"
	"github.com/porter-dev/porter/provisioner/integrations/provisioner/local"
	"github.com/porter-dev/porter/provisioner/integrations/storage"
	"github.com/porter-dev/porter/provisioner/integrations/storage/s3"
	"golang.org/x/oauth2"

	_gorm "gorm.io/gorm"
)

var (
	InstanceCredentialBackend credentials.CredentialStorage
	InstanceEnvConf           *EnvConf
)

func sharedInit() {
	var envDecoderConf EnvDecoderConf = EnvDecoderConf{}

	if err := envdecode.StrictDecode(&envDecoderConf); err != nil {
		log.Fatalf("Failed to decode server conf: %s", err)
	}

	InstanceEnvConf = &EnvConf{
		ProvisionerConf: &envDecoderConf.ProvisionerConf,
		DBConf:          &envDecoderConf.DBConf,
		RedisConf:       envDecoderConf.RedisConf,
	}
}

type Config struct {
	ProvisionerConf *ProvisionerConf
	DBConf          *env.DBConf
	RedisConf       *env.RedisConf

	StorageManager storage.StorageManager
	Repo           repository.Repository

	// Logger for logging
	Logger *logger.Logger

	// Alerter to send alerts to a third-party aggregator
	Alerter alerter.Alerter

	DB *_gorm.DB

	// DOConf is the configuration for a DigitalOcean OAuth client
	DOConf *oauth2.Config

	RedisClient *redis.Client

	Provisioner provisioner.Provisioner

	// AnalyticsClient if Segment analytics reporting is enabled on the API instance
	AnalyticsClient analytics.AnalyticsSegmentClient
}

// ProvisionerConf is the env var configuration for the provisioner server
type ProvisionerConf struct {
	Debug bool `env:"DEBUG,default=false"`
	Port  int  `env:"PROV_PORT,default=8082"`

	TimeoutRead  time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
	TimeoutWrite time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
	TimeoutIdle  time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`

	StaticAuthToken string `env:"STATIC_AUTH_TOKEN"`

	SentryDSN string `env:"SENTRY_DSN"`
	SentryEnv string `env:"SENTRY_ENV,default=dev"`

	// Configuration for the S3 storage backend
	S3AWSAccessKeyID string `env:"S3_AWS_ACCESS_KEY_ID"`
	S3AWSSecretKey   string `env:"S3_AWS_SECRET_KEY"`
	S3AWSRegion      string `env:"S3_AWS_REGION"`
	S3BucketName     string `env:"S3_BUCKET_NAME"`
	S3EncryptionKey  string `env:"S3_ENCRYPTION_KEY,default=__random_strong_encryption_key__"`

	// Configuration for the digitalocean client
	DOClientID        string `env:"DO_CLIENT_ID"`
	DOClientSecret    string `env:"DO_CLIENT_SECRET"`
	DOClientServerURL string `env:"DO_CLIENT_SERVER_URL"`

	// ProvisionerMethod defines the method to use for provisioner: options are "local" or "kubernetes"
	ProvisionerMethod          string `env:"PROVISIONER_METHOD,default=local"`
	ProvisionerBackendURL      string `env:"PROV_BACKEND_URL,default=http://localhost:8082"`
	ProvisionerCredExchangeURL string `env:"PROV_CRED_EXCHANGE_URL,default=http://localhost:8082"`

	// Options to configure for the "kubernetes" provisioner method
	ProvisionerCluster         string `env:"PROVISIONER_CLUSTER"`
	SelfKubeconfig             string `env:"SELF_KUBECONFIG"`
	ProvisionerImageRepo       string `env:"PROV_IMAGE_REPO,default=gcr.io/porter-dev-273614/provisioner"`
	ProvisionerImageTag        string `env:"PROV_IMAGE_TAG,default=latest"`
	ProvisionerImagePullSecret string `env:"PROV_IMAGE_PULL_SECRET"`
	ProvisionerJobNamespace    string `env:"PROV_JOB_NAMESPACE,default=default"`

	// Options to configure for the "local" provisioner method
	LocalTerraformDirectory string `env:"LOCAL_TERRAFORM_DIRECTORY"`

	// Client key for segment to report provisioning events
	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
}

type EnvConf struct {
	*ProvisionerConf
	*env.DBConf
	env.RedisConf
}

type EnvDecoderConf struct {
	ProvisionerConf ProvisionerConf
	DBConf          env.DBConf
	RedisConf       env.RedisConf
}

// FromEnv generates a configuration from environment variables
func FromEnv() (*EnvConf, error) {
	return InstanceEnvConf, nil
}

func GetConfig(envConf *EnvConf) (*Config, error) {
	ctx := context.Background()

	res := &Config{
		ProvisionerConf: envConf.ProvisionerConf,
		DBConf:          envConf.DBConf,
		RedisConf:       &envConf.RedisConf,
		Logger:          logger.NewConsole(envConf.ProvisionerConf.Debug),
	}

	db, err := adapter.New(envConf.DBConf)
	if err != nil {
		return nil, err
	}

	res.DB = db

	var key [32]byte

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

	res.Repo = gorm.NewRepository(db, &key, InstanceCredentialBackend)

	if envConf.ProvisionerConf.SentryDSN != "" {
		res.Alerter, err = alerter.NewSentryAlerter(envConf.ProvisionerConf.SentryDSN, envConf.ProvisionerConf.SentryEnv)
	}

	// load a storage backend; if correct env vars are not set, throw an error
	if envConf.ProvisionerConf.S3AWSAccessKeyID != "" && envConf.ProvisionerConf.S3AWSSecretKey != "" && envConf.ProvisionerConf.S3EncryptionKey != "" {
		var s3Key [32]byte

		for i, b := range []byte(envConf.ProvisionerConf.S3EncryptionKey) {
			s3Key[i] = b
		}

		res.StorageManager, err = s3.NewS3StorageClient(&s3.S3Options{
			AWSRegion:      envConf.ProvisionerConf.S3AWSRegion,
			AWSAccessKeyID: envConf.ProvisionerConf.S3AWSAccessKeyID,
			AWSSecretKey:   envConf.ProvisionerConf.S3AWSSecretKey,
			AWSBucketName:  envConf.ProvisionerConf.S3BucketName,
			EncryptionKey:  &s3Key,
		})

		if err != nil {
			return nil, err
		}
	} else {
		return nil, fmt.Errorf("no storage backend is available")
	}

	if envConf.RedisConf.Enabled {
		redis, err := adapter.NewRedisClient(&envConf.RedisConf)
		if err != nil {
			return nil, fmt.Errorf("redis connection failed: %v", err)
		}

		res.RedisClient = redis
	} else {
		return nil, fmt.Errorf("no redis client is available")
	}

	if envConf.ProvisionerMethod == "local" {
		res.Provisioner = local.NewLocalProvisioner(&local.LocalProvisionerConfig{
			ProvisionerBackendURL:   envConf.ProvisionerBackendURL,
			LocalTerraformDirectory: envConf.LocalTerraformDirectory,
		})
	} else if envConf.ProvisionerMethod == "kubernetes" {
		provAgent, err := getProvisionerAgent(ctx, envConf.ProvisionerConf)
		if err != nil {
			return nil, err
		}

		res.Provisioner = k8s.NewKubernetesProvisioner(provAgent.Clientset, &k8s.KubernetesProvisionerConfig{
			ProvisionerImageRepo:       envConf.ProvisionerImageRepo,
			ProvisionerImageTag:        envConf.ProvisionerImageTag,
			ProvisionerImagePullSecret: envConf.ProvisionerImagePullSecret,
			ProvisionerJobNamespace:    envConf.ProvisionerJobNamespace,
			ProvisionerBackendURL:      envConf.ProvisionerBackendURL,
		})
	}

	if envConf.ProvisionerConf.DOClientID != "" && envConf.ProvisionerConf.DOClientSecret != "" && envConf.ProvisionerConf.DOClientServerURL != "" {
		res.DOConf = oauth.NewDigitalOceanClient(&oauth.Config{
			ClientID:     envConf.ProvisionerConf.DOClientID,
			ClientSecret: envConf.ProvisionerConf.DOClientSecret,
			Scopes:       []string{"read", "write"},
			BaseURL:      envConf.ProvisionerConf.DOClientServerURL,
		})
	}

	res.AnalyticsClient = analytics.InitializeAnalyticsSegmentClient(envConf.ProvisionerConf.SegmentClientKey, res.Logger)

	return res, nil
}

func getProvisionerAgent(ctx context.Context, conf *ProvisionerConf) (*kubernetes.Agent, error) {
	if conf.ProvisionerCluster == "kubeconfig" && conf.SelfKubeconfig != "" {
		agent, err := klocal.GetSelfAgentFromFileConfig(conf.SelfKubeconfig)
		if err != nil {
			return nil, fmt.Errorf("could not get in-cluster agent: %v", err)
		}

		return agent, nil
	} else if conf.ProvisionerCluster == "kubeconfig" {
		return nil, fmt.Errorf(`"kubeconfig" cluster option requires path to kubeconfig`)
	}

	agent, _ := kubernetes.GetAgentInClusterConfig(ctx, conf.ProvisionerJobNamespace)

	return agent, nil
}
