package infra

import (
	"context"
	"errors"
	"fmt"
	"net/http"

	"github.com/mitchellh/mapstructure"
	"github.com/porter-dev/porter/api/server/handlers"
	"github.com/porter-dev/porter/api/server/shared"
	"github.com/porter-dev/porter/api/server/shared/apierrors"
	"github.com/porter-dev/porter/api/server/shared/config"
	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/encryption"
	"github.com/porter-dev/porter/internal/models"
	"gorm.io/gorm"

	ptypes "github.com/porter-dev/porter/provisioner/types"
)

type InfraCreateHandler struct {
	handlers.PorterHandlerReadWriter
}

func NewInfraCreateHandler(
	config *config.Config,
	decoderValidator shared.RequestDecoderValidator,
	writer shared.ResultWriter,
) *InfraCreateHandler {
	return &InfraCreateHandler{
		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
	}
}

func (c *InfraCreateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	user, _ := r.Context().Value(types.UserScope).(*models.User)
	proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)

	req := &types.CreateInfraRequest{}

	if ok := c.DecodeAndValidate(w, r, req); !ok {
		return
	}

	var cluster *models.Cluster
	var err error

	if req.ClusterID != 0 {
		cluster, err = c.Repo().Cluster().ReadCluster(proj.ID, req.ClusterID)

		if err != nil {
			if err == gorm.ErrRecordNotFound {
				c.HandleAPIError(w, r, apierrors.NewErrForbidden(
					fmt.Errorf("cluster with id %d not found in project %d", req.ClusterID, proj.ID),
				))
			} else {
				c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
			}

			return
		}
	}

	suffix, err := encryption.GenerateRandomBytes(6)
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	sourceLink, sourceVersion := getSourceLinkAndVersion(types.InfraKind(req.Kind))

	// create the infra object
	infra := &models.Infra{
		Kind:            types.InfraKind(req.Kind),
		APIVersion:      "v2",
		ProjectID:       proj.ID,
		Suffix:          suffix,
		Status:          types.StatusCreating,
		CreatedByUserID: user.ID,
		SourceLink:      sourceLink,
		SourceVersion:   sourceVersion,
		// If the cluster ID was passed in, we store the parent cluster ID in the infra
		// so it can be referenced later
		ParentClusterID: req.ClusterID,
	}

	// verify the credentials
	err = checkInfraCredentials(c.Config(), proj, infra, req.InfraCredentials)

	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
		return
	}

	// call apply on the provisioner service
	vals := req.Values

	// if this is cluster-scoped and the kind is RDS, run the postrenderer
	if req.ClusterID != 0 && req.Kind == "rds" {
		var ok bool

		pr := &InfraRDSPostrenderer{
			config: c.Config(),
		}

		if vals, ok = pr.Run(w, r, &Opts{
			Cluster: cluster,
			Values:  req.Values,
		}); !ok {
			return
		}
	}

	// handle write to the database
	infra, err = c.Repo().Infra().CreateInfra(infra)

	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	resp, err := c.Config().ProvisionerClient.Apply(context.Background(), proj.ID, infra.ID, &ptypes.ApplyBaseRequest{
		Kind:          req.Kind,
		Values:        vals,
		OperationKind: "create",
	})
	if err != nil {
		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
		return
	}

	c.WriteResult(w, r, resp)
}

func checkInfraCredentials(config *config.Config, proj *models.Project, infra *models.Infra, req *types.InfraCredentials) error {
	if req == nil {
		return nil
	}

	if req.DOIntegrationID != 0 {
		_, err := config.Repo.OAuthIntegration().ReadOAuthIntegration(proj.ID, req.DOIntegrationID)
		if err != nil {
			return fmt.Errorf("do integration id %d not found in project %d", req.DOIntegrationID, proj.ID)
		}

		infra.DOIntegrationID = req.DOIntegrationID
		infra.AWSIntegrationID = 0
		infra.GCPIntegrationID = 0
		infra.AzureIntegrationID = 0
	} else if req.AWSIntegrationID != 0 {
		_, err := config.Repo.AWSIntegration().ReadAWSIntegration(proj.ID, req.AWSIntegrationID)
		if err != nil {
			return fmt.Errorf("aws integration id %d not found in project %d", req.AWSIntegrationID, proj.ID)
		}

		infra.DOIntegrationID = 0
		infra.AWSIntegrationID = req.AWSIntegrationID
		infra.GCPIntegrationID = 0
		infra.AzureIntegrationID = 0
	} else if req.GCPIntegrationID != 0 {
		_, err := config.Repo.GCPIntegration().ReadGCPIntegration(proj.ID, req.GCPIntegrationID)
		if err != nil {
			return fmt.Errorf("gcp integration id %d not found in project %d", req.GCPIntegrationID, proj.ID)
		}

		infra.DOIntegrationID = 0
		infra.AWSIntegrationID = 0
		infra.GCPIntegrationID = req.GCPIntegrationID
		infra.AzureIntegrationID = 0
	} else if req.AzureIntegrationID != 0 {
		_, err := config.Repo.AzureIntegration().ReadAzureIntegration(proj.ID, req.AzureIntegrationID)
		if err != nil {
			return fmt.Errorf("azure integration id %d not found in project %d", req.AzureIntegrationID, proj.ID)
		}

		infra.DOIntegrationID = 0
		infra.AWSIntegrationID = 0
		infra.GCPIntegrationID = 0
		infra.AzureIntegrationID = req.AzureIntegrationID
	}

	if infra.DOIntegrationID == 0 && infra.AWSIntegrationID == 0 && infra.GCPIntegrationID == 0 && infra.AzureIntegrationID == 0 {
		return fmt.Errorf("at least one integration id must be set")
	}

	return nil
}

// getSourceLinkAndVersion returns the source link and version for the infrastructure. For now,
// this is hardcoded
func getSourceLinkAndVersion(kind types.InfraKind) (string, string) {
	switch kind {
	case types.InfraECR:
		return "porter/aws/ecr", "v0.1.0"
	case types.InfraEKS:
		return "porter/aws/eks", "v0.1.0"
	case types.InfraRDS:
		return "porter/aws/rds", "v0.1.0"
	case types.InfraS3:
		return "porter/aws/s3", "v0.1.0"
	case types.InfraGCR:
		return "porter/gcp/gcr", "v0.1.0"
	case types.InfraGAR:
		return "porter/gcp/gar", "v0.1.0"
	case types.InfraGKE:
		return "porter/gcp/gke", "v0.1.0"
	case types.InfraDOCR:
		return "porter/do/docr", "v0.1.0"
	case types.InfraDOKS:
		return "porter/do/doks", "v0.1.0"
	case types.InfraAKS:
		return "porter/azure/aks", "v0.1.0"
	case types.InfraACR:
		return "porter/azure/acr", "v0.1.0"
	}

	return "porter/test", "v0.1.0"
}

type InfraRDSPostrenderer struct {
	config *config.Config
}

type Opts struct {
	Cluster *models.Cluster
	Values  map[string]interface{}
}

func (i *InfraRDSPostrenderer) Run(w http.ResponseWriter, r *http.Request, opts *Opts) (map[string]interface{}, bool) {
	if opts.Cluster != nil {
		proj, _ := r.Context().Value(types.ProjectScope).(*models.Project)
		values := opts.Values

		// find the corresponding infra id
		clusterInfra, err := i.config.Repo.Infra().ReadInfra(proj.ID, opts.Cluster.InfraID)
		if err != nil {
			apierrors.HandleAPIError(i.config.Logger, i.config.Alerter, w, r, apierrors.NewErrForbidden(fmt.Errorf("could not get cluster infra: %v", err)), true)
			return nil, false
		}

		clusterInfraOperation, err := i.config.Repo.Infra().GetLatestOperation(clusterInfra)

		// get the raw state for the cluster
		rawState, err := i.config.ProvisionerClient.GetRawState(context.Background(), models.GetWorkspaceID(clusterInfra, clusterInfraOperation))
		if err != nil {
			apierrors.HandleAPIError(i.config.Logger, i.config.Alerter, w, r, apierrors.NewErrInternal(err), true)
			return nil, false
		}

		vpcID, subnetIDs, err := getVPCFromEKSTFState(rawState)
		if err != nil {
			return values, false
		}

		// if the length of the subnets is not 3, return an error
		if len(subnetIDs) < 3 {
			apierrors.HandleAPIError(i.config.Logger, i.config.Alerter, w, r, apierrors.NewErrInternal(fmt.Errorf("invalid number of subnet IDs in VPC configuration")), true)
			return nil, false
		}

		values["porter_cluster_vpc"] = vpcID
		values["porter_cluster_subnet_1"] = subnetIDs[0]
		values["porter_cluster_subnet_2"] = subnetIDs[1]
		values["porter_cluster_subnet_3"] = subnetIDs[2]

		return values, true
	}

	return opts.Values, true
}

type AWSVPCConfig struct {
	SubNetIDs []string `json:"subnet_ids" mapstructure:"subnet_ids"`
	VPCID     string   `json:"vpc_id" mapstructure:"vpc_id"`
}

func getVPCFromEKSTFState(tfState *ptypes.ParseableRawTFState) (string, []string, error) {
	for _, resource := range tfState.Resources {
		if "aws_eks_cluster.cluster" == resource.Type+"."+resource.Name {
			for _, instance := range resource.Instances {
				vpcConfig, ok := instance.Attributes["vpc_config"]
				if !ok {
					return "", []string{}, errors.New("name not found for the requested resource name-type")
				}

				awsVPCConfigIface, ok := vpcConfig.([]interface{})
				if !ok {
					fmt.Printf("%#v\n", vpcConfig)
					return "", []string{}, errors.New("cannot cast returned value to vpc config")
				}

				if len(awsVPCConfigIface) == 0 {
					return "", []string{}, errors.New("empty vpc config")
				}

				awsVPCConfigMap, ok := awsVPCConfigIface[0].(map[string]interface{})
				if !ok {
					return "", []string{}, errors.New("cannot cast returned value to vpc config map")
				}

				var awsVPCConfig AWSVPCConfig

				err := mapstructure.Decode(awsVPCConfigMap, &awsVPCConfig)
				if err != nil {
					return "", []string{}, errors.New("cannot cast returned value to vpc config")
				}

				return awsVPCConfig.VPCID, awsVPCConfig.SubNetIDs, nil
			}

			return "", []string{}, errors.New("name not found for the requested resource name-type")
		}
	}

	return "", []string{}, errors.New("name not found for the requested resource name-type")
}
