package models

import (
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"gorm.io/gorm"

	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/encryption"
)

// Infra represents the metadata for an infrastructure type provisioned on
// Porter
type Infra struct {
	gorm.Model

	// The type of infra that was provisioned
	Kind types.InfraKind

	// The infrastructure API version
	APIVersion string

	// The source link (only set on apiVersion >= v2)
	SourceLink string

	// The source version (only set on apiVersion >= v2)
	SourceVersion string

	// A random 6-byte suffix to ensure workspace/stream ids are unique
	Suffix string

	// The project that this infra belongs to
	ProjectID uint

	// The ID of the user that created this infra
	CreatedByUserID uint

	// If this infra was created under a cluster scope (for example, for RDS), the parent cluster
	// ID
	ParentClusterID uint

	// Status is the status of the infra
	Status types.InfraStatus

	Operations []Operation

	// The AWS integration that was used to create the infra
	AWSIntegrationID uint

	// The Azure integration that was used to create the infra
	AzureIntegrationID uint

	// The GCP integration that was used to create the infra
	GCPIntegrationID uint

	// The DO integration that was used to create the infra:
	// this points to an OAuthIntegrationID
	DOIntegrationID uint

	// The database id for the infra, if this infra provisioned a database
	DatabaseID uint

	Database Database

	// ------------------------------------------------------------------
	// All fields below this line are encrypted before storage
	// ------------------------------------------------------------------

	// The last-applied input variables to the provisioner
	LastApplied []byte
}

type Operation struct {
	gorm.Model

	UID             string `gorm:"unique"`
	InfraID         uint
	Type            string
	Status          string
	Errored         bool
	Error           string
	TemplateVersion string

	// ------------------------------------------------------------------
	// All fields below this line are encrypted before storage
	// ------------------------------------------------------------------

	// The last-applied input variables to the provisioner
	LastApplied []byte
}

func (o *Operation) ToOperationMetaType() *types.OperationMeta {
	return &types.OperationMeta{
		LastUpdated: o.UpdatedAt,
		UID:         o.UID,
		InfraID:     o.InfraID,
		Type:        o.Type,
		Status:      o.Status,
		Errored:     o.Errored,
		Error:       o.Error,
	}
}

func (o *Operation) ToOperationType() (*types.Operation, error) {
	// unmarshal last applied
	lastApplied := make(map[string]interface{})

	err := json.Unmarshal(o.LastApplied, &lastApplied)
	if err != nil {
		return nil, err
	}

	return &types.Operation{
		OperationMeta: o.ToOperationMetaType(),
		LastApplied:   lastApplied,
	}, nil
}

func GetOperationID() (string, error) {
	return encryption.GenerateRandomBytes(10)
}

type getInfraName struct {
	Name        string `json:"name"`
	ClusterName string `json:"cluster_name"`
	DBName      string `json:"db_name"`
	BucketName  string `json:"bucket_name"`
	DOCRName    string `json:"docr_name"`
	ECRName     string `json:"ecr_name"`
	ACRName     string `json:"acr_name"`
}

// ToInfraType generates an external Infra to be shared over REST
func (i *Infra) ToInfraType() *types.Infra {
	// perform best attempt to get infra name
	var name string
	infraName := &getInfraName{}

	if err := json.Unmarshal(i.LastApplied, infraName); err == nil {
		if infraName.DOCRName != "" {
			name = infraName.DOCRName
		}

		if infraName.ECRName != "" {
			name = infraName.ECRName
		}

		if infraName.ACRName != "" {
			name = infraName.ACRName
		}

		if infraName.DBName != "" {
			name = infraName.DBName
		}

		if infraName.BucketName != "" {
			name = infraName.BucketName
		}

		if infraName.ClusterName != "" {
			name = infraName.ClusterName
		}

		if infraName.Name != "" {
			name = infraName.Name
		}
	}

	return &types.Infra{
		ID:               i.ID,
		Name:             name,
		CreatedAt:        i.CreatedAt,
		UpdatedAt:        i.UpdatedAt,
		ProjectID:        i.ProjectID,
		APIVersion:       i.APIVersion,
		SourceLink:       i.SourceLink,
		SourceVersion:    i.SourceVersion,
		Kind:             i.Kind,
		Status:           i.Status,
		AWSIntegrationID: i.AWSIntegrationID,
		DOIntegrationID:  i.DOIntegrationID,
		GCPIntegrationID: i.GCPIntegrationID,
	}
}

// GetID returns the unique id for this infra
func (i *Infra) GetUniqueName() string {
	return fmt.Sprintf("%s-%d-%d-%s", i.Kind, i.ProjectID, i.ID, i.Suffix)
}

// ParseUniqueName returns the (kind, projectID, infraID, suffix)
func ParseUniqueName(workspaceID string) (string, uint, uint, string, error) {
	strArr := strings.Split(workspaceID, "-")

	if len(strArr) != 4 {
		return "", 0, 0, "", fmt.Errorf("workspace id improperly formatted")
	}

	projID, err := strconv.ParseUint(strArr[1], 10, 64)
	if err != nil {
		return "", 0, 0, "", err
	}

	infraID, err := strconv.ParseUint(strArr[2], 10, 64)
	if err != nil {
		return "", 0, 0, "", err
	}

	return strArr[0], uint(projID), uint(infraID), strArr[3], nil
}

type UniqueNameWithOperation struct {
	Kind         string
	ProjectID    uint
	InfraID      uint
	Suffix       string
	OperationUID string
}

func ParseWorkspaceID(workspaceID string) (*UniqueNameWithOperation, error) {
	strArr := strings.Split(workspaceID, "-")

	if len(strArr) != 5 {
		return nil, fmt.Errorf("workspace id improperly formatted: %s", workspaceID)
	}

	projID, err := strconv.ParseUint(strArr[1], 10, 64)
	if err != nil {
		return nil, err
	}

	infraID, err := strconv.ParseUint(strArr[2], 10, 64)
	if err != nil {
		return nil, err
	}

	if len(strArr[4]) != hex.EncodedLen(10) {
		return nil, fmt.Errorf("operation uid does not have hex length 10")
	}

	return &UniqueNameWithOperation{
		Kind:         strArr[0],
		ProjectID:    uint(projID),
		InfraID:      uint(infraID),
		Suffix:       strArr[3],
		OperationUID: strArr[4],
	}, nil
}

func GetWorkspaceID(infra *Infra, operation *Operation) string {
	return fmt.Sprintf("%s-%d-%d-%s-%s", infra.Kind, infra.ProjectID, infra.ID, infra.Suffix, operation.UID)
}
