package environment

import (
	"fmt"
	"os"
	"strconv"

	"code.justin.tv/eventbus/controlplane/internal/clients/sts"
	"code.justin.tv/systems/sandstorm/manager"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/pkg/errors"
)

type SandstormFetcher interface {
	Get(key string) (*manager.Secret, error)
}

type Config struct {
	Environment string

	AutoprofBucketName string
	AutoprofEnabled    bool

	CloudwatchEnabled bool

	DisableGrants          bool
	AuthorizedFieldKeyARN  string
	EncryptionAtRestKeyARN string

	PagerDutyEnabled        bool
	PagerDutyHighUrgencyKey string
	PagerDutyLowUrgencyKey  string

	SandstormRoleARN string

	S2SDisabled    bool
	S2SServiceName string

	CloudformationCheckDisabled bool

	PostgresDBName         string
	PostgresDisableSSL     bool
	PostgresWriterUsername string
	PostgresWriterPassword string
	PostgresWriterHostname string
	PostgresReaderUsername string
	PostgresReaderPassword string
	PostgresReaderHostname string

	SlackToken   string
	SlackChannel string

	AWSEndpoint string // Overriding AWS endpoint supported for local development

	LDAPURL          string
	LDAPUsersBaseDN  string
	LDAPGroupsBaseDN string
}

func Read() (Config, error) {
	environment := os.Getenv("ENVIRONMENT")
	if !IsValid(environment) {
		return Config{}, fmt.Errorf("invalid environment '%s'", environment)
	}

	sandstormRoleARN := os.Getenv("SANDSTORM_ROLE_ARN")

	encryptionAtRestKeyARN := os.Getenv("ENCRYPTION_AT_REST_KEY_ARN")
	if encryptionAtRestKeyARN == "" {
		return Config{}, errors.New("must provide encryption at rest kms key arn")
	}

	authorizedFieldKeyARN := os.Getenv("AUTHORIZED_FIELD_KEY_ARN")
	if encryptionAtRestKeyARN == "" {
		return Config{}, errors.New("must provide encryption at rest kms key arn")
	}

	s2sDisabled, err := parseOptionalBool("S2S_DISABLED")
	if err != nil {
		return Config{}, err
	}

	var s2sServiceName string
	if !s2sDisabled {
		if IsProductionEnv(environment) {
			s2sServiceName = "eventbus-controlplane-production"
		} else {
			s2sServiceName = fmt.Sprintf("eventbus-controlplane-%s", environment)
		}

	}

	cloudformationChecksDisabled, err := parseOptionalBool("CLOUDFORMATION_CHECKS_DISABLED")
	if err != nil {
		return Config{}, err
	}

	cloudwatchEnabled, err := parseOptionalBool("CW_ENABLED")
	if err != nil {
		return Config{}, err
	}

	autoprofEnabled, err := parseOptionalBool("AUTOPROF_ENABLED")
	if err != nil {
		return Config{}, err
	}

	var autoprofBucketName string
	if autoprofEnabled {
		autoprofBucketName = os.Getenv("AUTOPROF_BUCKET_NAME")
		if autoprofBucketName == "" {
			return Config{}, errors.New("autoprof enabled but AUTOPROF_BUCKET_NAME is not set")
		}
	}

	pagerDutyEnabled, err := parseOptionalBool("PAGERDUTY_ENABLED")
	if err != nil {
		return Config{}, err
	}

	var pagerDutyHighUrgencyKey string
	var pagerDutyLowUrgencyKey string
	if pagerDutyEnabled {
		pagerDutyHighUrgencyKey = os.Getenv("PAGERDUTY_HIGH_URGENCY_KEY")
		if pagerDutyHighUrgencyKey == "" {
			return Config{}, errors.New("pagerduty enabled but PAGERDUTY_HIGH_URGENCY_KEY is not set")
		}

		pagerDutyHighUrgencyKey = os.Getenv("PAGERDUTY_LOW_URGENCY_KEY")
		if pagerDutyHighUrgencyKey == "" {
			return Config{}, errors.New("pagerduty enabled but PAGERDUTY_LOW_URGENCY_KEY is not set")
		}
	}

	awsEndpoint := os.Getenv("AWS_ENDPOINT")

	disableGrants, err := parseOptionalBool("DISABLE_GRANTS")
	if err != nil {
		return Config{}, err
	}

	postgresWriterHostname := os.Getenv("POSTGRES_WRITER_HOST")
	if postgresWriterHostname == "" {
		return Config{}, errors.New("must provide postgres writer hostname")
	}

	postgresReaderHostname := os.Getenv("POSTGRES_READER_HOST")
	if postgresReaderHostname == "" {
		return Config{}, errors.New("must provide postgres reader hostname")
	}

	postgresDBName := os.Getenv("POSTGRES_DB")
	if postgresDBName == "" {
		postgresDBName = postgresDefaultDatabaseName
	}

	postgresDisableSSL, err := parseOptionalBool("POSTGRES_DISABLE_SSL")
	if err != nil {
		return Config{}, err
	}

	slackChannel := os.Getenv("SLACK_CHANNEL")

	ldapURL := os.Getenv("LDAP_URL")
	if ldapURL == "" {
		ldapURL = ldapDefaultURL
	}

	ldapUsersBaseDN := os.Getenv("LDAP_USERS_BASE_DN")
	if ldapUsersBaseDN == "" {
		ldapUsersBaseDN = ldapDefaultUsersBaseDN
	}

	ldapGroupsBaseDN := os.Getenv("LDAP_GROUPS_BASE_DN")
	if ldapGroupsBaseDN == "" {
		ldapGroupsBaseDN = ldapDefaultGroupsBaseDN
	}

	c := Config{
		Environment: environment,

		SandstormRoleARN: sandstormRoleARN,

		DisableGrants:          disableGrants,
		EncryptionAtRestKeyARN: encryptionAtRestKeyARN,
		AuthorizedFieldKeyARN:  authorizedFieldKeyARN,

		SlackChannel: slackChannel,

		CloudwatchEnabled: cloudwatchEnabled,

		AutoprofEnabled:    autoprofEnabled,
		AutoprofBucketName: autoprofBucketName,

		S2SDisabled:    s2sDisabled,
		S2SServiceName: s2sServiceName,

		PagerDutyEnabled:        pagerDutyEnabled,
		PagerDutyHighUrgencyKey: pagerDutyHighUrgencyKey,
		PagerDutyLowUrgencyKey:  pagerDutyLowUrgencyKey,

		CloudformationCheckDisabled: cloudformationChecksDisabled,

		PostgresWriterHostname: postgresWriterHostname,
		PostgresReaderHostname: postgresReaderHostname,
		PostgresDBName:         postgresDBName,
		PostgresDisableSSL:     postgresDisableSSL,

		AWSEndpoint: awsEndpoint,

		LDAPURL:          ldapURL,
		LDAPUsersBaseDN:  ldapUsersBaseDN,
		LDAPGroupsBaseDN: ldapGroupsBaseDN,
	}

	// temporary aws config/session and sandstorm client used to app-wide bootstrap configuration
	awsConfig := c.AWSConfig()
	sess := session.Must(session.NewSession(awsConfig))
	stsManager := sts.NewManager(sess, c.Environment)
	secretClient := manager.New(manager.Config{
		AWSConfig: awsConfig.Copy(&aws.Config{Credentials: stsManager.AssumeRoleCredentialsByARN(c.SandstormRoleARN)}),
		TableName: "sandstorm-production",
		KeyID:     "alias/sandstorm-production",
	})

	err = c.readSecrets(secretClient)
	if err != nil {
		return Config{}, errors.Wrap(err, "could not load secret configuration")
	}

	return c, nil

}

// readSecrets handles loading configuration that can be specified via environment variable or sandstorm
func (c *Config) readSecrets(secretClient SandstormFetcher) error {
	var err error

	c.PostgresWriterUsername, err = c.getFromEnvOrSecrets(secretClient, "POSTGRES_WRITER_USER", postgresWriterUsernameSecretFormat)
	if err != nil {
		return errors.Wrap(err, "could not get postgres writer username")
	}

	c.PostgresWriterPassword, err = c.getFromEnvOrSecrets(secretClient, "POSTGRES_WRITER_PASSWORD", postgresWriterPasswordSecretFormat)
	if err != nil {
		return errors.Wrap(err, "could not get postgres writer password")
	}

	c.PostgresReaderUsername, err = c.getFromEnvOrSecrets(secretClient, "POSTGRES_READER_USER", postgresReaderUsernameSecretFormat)
	if err != nil {
		return errors.Wrap(err, "could not get postgres reader username")
	}

	c.PostgresReaderPassword, err = c.getFromEnvOrSecrets(secretClient, "POSTGRES_READER_PASSWORD", postgresReaderPasswordSecretFormat)
	if err != nil {
		return errors.Wrap(err, "could not get postgres reader password")
	}

	// only attempt to fetch a slack token if a channel is also specified
	if c.SlackChannel != "" {
		c.SlackToken, err = c.getFromEnvOrSecrets(secretClient, "SLACK_TOKEN", slackTokenSecretFormat)
		if err != nil {
			return errors.Wrap(err, "could not get slack token")
		}
	}

	return nil
}

func (c Config) getFromEnvOrSecrets(secretClient SandstormFetcher, envVar, secretKey string) (string, error) {
	value := os.Getenv(envVar)
	if value != "" {
		return value, nil
	}

	if c.SandstormRoleARN == "" {
		return "", fmt.Errorf("cannot fetch sandstorm secret with blank SANDSTORM_ROLE_ARN")
	}

	secretName := fmt.Sprintf(secretKey, c.Environment)

	secret, err := secretClient.Get(secretName)
	if err != nil {
		return "", errors.Wrapf(err, "could not get secret with key '%s'", secretName)
	}

	return string(secret.Plaintext), nil
}

// reads an environment variable meant to be a boolean; defaults to false
func parseOptionalBool(envVar string) (bool, error) {
	s := os.Getenv(envVar)
	if s != "" {
		b, err := strconv.ParseBool(s)
		if err != nil {
			return false, fmt.Errorf("invalid value for %s environment variable: %s", envVar, s)
		}
		return b, nil
	}
	return false, nil
}
