package cicd

import (
	"context"
	"encoding/json"
	"log"
	"path/filepath"

	"geeks-accelerator/oss/saas-starter-kit/internal/platform/tests"
	"geeks-accelerator/oss/saas-starter-kit/internal/schema"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/secretsmanager"
	"github.com/lib/pq"
	_ "github.com/lib/pq"
	"github.com/pkg/errors"
	sqltrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/database/sql"
	sqlxtrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/jmoiron/sqlx"
	"gopkg.in/go-playground/validator.v9"
)

// MigrateFlags defines the flags used for executing schema migration.
type MigrateFlags struct {
	// Required flags.
	Env string `validate:"oneof=dev stage prod" example:"dev"`

	// Optional flags.
	ProjectRoot string `validate:"omitempty" example:"."`
	ProjectName string ` validate:"omitempty" example:"example-project"`
}

// migrateRequest defines the details needed to execute a service build.
type migrateRequest struct {
	Env         string `validate:"oneof=dev stage prod"`
	ProjectRoot string `validate:"required"`
	ProjectName string `validate:"required"`
	GoModFile   string `validate:"required"`
	GoModName   string `validate:"required"`

	AwsCreds    awsCredentials `validate:"required,dive,required"`
	_awsSession *session.Session

	flags MigrateFlags
}

// awsSession returns the current AWS session for the serviceDeployRequest.
func (r *migrateRequest) awsSession() *session.Session {
	if r._awsSession == nil {
		r._awsSession = r.AwsCreds.Session()
	}

	return r._awsSession
}

// NewMigrateRequest generates a new request for executing schema migration for a given set of CLI flags.
func NewMigrateRequest(log *log.Logger, flags MigrateFlags) (*migrateRequest, error) {

	// Validates specified CLI flags map to struct successfully.
	log.Println("Validate flags.")
	{
		errs := validator.New().Struct(flags)
		if errs != nil {
			return nil, errs
		}
		log.Printf("\t%s\tFlags ok.", tests.Success)
	}

	// Generate a migrate request using CLI flags and AWS credentials.
	log.Println("Generate migrate request.")
	var req migrateRequest
	{

		// Define new migrate request.
		req = migrateRequest{
			Env:         flags.Env,
			ProjectRoot: flags.ProjectRoot,
			ProjectName: flags.ProjectName,

			flags: flags,
		}

		// When project root directory is empty or set to current working path, then search for the project root by locating
		// the go.mod file.
		log.Println("\tDetermining the project root directory.")
		{
			if req.ProjectRoot == "" || req.ProjectRoot == "." {
				log.Println("\tAttempting to location project root directory from current working directory.")

				var err error
				req.GoModFile, err = findProjectGoModFile()
				if err != nil {
					return nil, err
				}
				req.ProjectRoot = filepath.Dir(req.GoModFile)
			} else {
				log.Printf("\t\tUsing supplied project root directory '%s'.\n", req.ProjectRoot)
				req.GoModFile = filepath.Join(req.ProjectRoot, "go.mod")
			}
			log.Printf("\t\t\tproject root: %s", req.ProjectRoot)
			log.Printf("\t\t\tgo.mod: %s", req.GoModFile)
		}

		log.Println("\tExtracting go module name from go.mod.")
		{
			var err error
			req.GoModName, err = loadGoModName(req.GoModFile)
			if err != nil {
				return nil, err
			}
			log.Printf("\t\t\tmodule name: %s", req.GoModName)
		}

		log.Println("\tDetermining the project name.")
		{
			if req.ProjectName != "" {
				log.Printf("\t\tUse provided value.")
			} else {
				req.ProjectName = filepath.Base(req.GoModName)
				log.Printf("\t\tSet from go module.")
			}
			log.Printf("\t\t\tproject name: %s", req.ProjectName)
		}

		// Verifies AWS credentials specified as environment variables.
		log.Println("\tVerify AWS credentials.")
		{
			var err error
			req.AwsCreds, err = GetAwsCredentials(req.Env)
			if err != nil {
				return nil, err
			}
			if req.AwsCreds.UseRole {
				log.Printf("\t\t\tUsing role")
			} else {
				log.Printf("\t\t\tAccessKeyID: '%s'", req.AwsCreds.AccessKeyID)
			}

			log.Printf("\t\t\tRegion: '%s'", req.AwsCreds.Region)
			log.Printf("\t%s\tAWS credentials valid.", tests.Success)
		}
	}

	return &req, nil
}

// Run is the main entrypoint for migration of database schema for a given target environment.
func Migrate(log *log.Logger, ctx context.Context, req *migrateRequest) error {

	// Load the database details.
	var db DB
	{
		log.Println("Get Database Details from AWS Secret Manager")

		dbId := dBInstanceIdentifier(req.ProjectName, req.Env)

		// Secret ID used to store the DB username and password across deploys.
		dbSecretId := secretID(req.ProjectName, req.Env, dbId)

		// Retrieve the current secret value if something is stored.
		{
			sm := secretsmanager.New(req.awsSession())
			res, err := sm.GetSecretValue(&secretsmanager.GetSecretValueInput{
				SecretId: aws.String(dbSecretId),
			})
			if err != nil {
				if aerr, ok := err.(awserr.Error); !ok || aerr.Code() != secretsmanager.ErrCodeResourceNotFoundException {
					return errors.Wrapf(err, "Failed to get value for secret id %s", dbSecretId)
				} else {
					// This should only happen when the deploy script first runs and no resources exist in the
					// AWS account. To create a database, need the VPC and need to come up with a better strategy for
					// defining resources that can be shared between deployment steps.
					log.Printf("\t%s\tDatabase credentials not found.", tests.Failed)
					return nil
				}
			} else {
				err = json.Unmarshal([]byte(*res.SecretString), &db)
				if err != nil {
					return errors.Wrap(err, "Failed to json decode db credentials")
				}
			}

			log.Printf("\t%s\tDatabase credentials found.", tests.Success)
		}
	}

	// Start Database and run the migration.
	{
		log.Println("Proceed with schema migration")

		log.Printf("\t\tOpen database connection")
		// Register informs the sqlxtrace package of the driver that we will be using in our program.
		// It uses a default service name, in the below case "postgres.db". To use a custom service
		// name use RegisterWithServiceName.
		sqltrace.Register(db.Driver, &pq.Driver{}, sqltrace.WithServiceName("devops:migrate"))
		masterDb, err := sqlxtrace.Open(db.Driver, db.URL())
		if err != nil {
			return errors.WithStack(err)
		}
		defer masterDb.Close()

		// Start Migrations
		log.Printf("\t\tStart migrations.")
		if err = schema.Migrate(ctx, masterDb, log, false); err != nil {
			return errors.WithStack(err)
		}

		log.Printf("\t%s\tMigrate complete.", tests.Success)
	}

	return nil
}
