// Copyright 2022 Cloudbase Solutions SRL
//
//    Licensed under the Apache License, Version 2.0 (the "License"); you may
//    not use this file except in compliance with the License. You may obtain
//    a copy of the License at
//
//         http://www.apache.org/licenses/LICENSE-2.0
//
//    Unless required by applicable law or agreed to in writing, software
//    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
//    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
//    License for the specific language governing permissions and limitations
//    under the License.

package sql

import (
	"time"

	"github.com/google/uuid"
	"github.com/pkg/errors"
	"gorm.io/datatypes"
	"gorm.io/gorm"

	commonParams "github.com/cloudbase/garm-provider-common/params"
	"github.com/cloudbase/garm/params"
)

type Base struct {
	ID        uuid.UUID `gorm:"type:uuid;primary_key;"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

func (b *Base) BeforeCreate(_ *gorm.DB) error {
	emptyID := uuid.UUID{}
	if b.ID != emptyID {
		return nil
	}
	newID, err := uuid.NewRandom()
	if err != nil {
		return errors.Wrap(err, "generating id")
	}
	b.ID = newID
	return nil
}

type Tag struct {
	Base

	Name  string  `gorm:"type:varchar(64);uniqueIndex"`
	Pools []*Pool `gorm:"many2many:pool_tags;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"`
}

type Pool struct {
	Base

	ProviderName           string `gorm:"index:idx_pool_type"`
	RunnerPrefix           string
	MaxRunners             uint
	MinIdleRunners         uint
	RunnerBootstrapTimeout uint
	Image                  string `gorm:"index:idx_pool_type"`
	Flavor                 string `gorm:"index:idx_pool_type"`
	OSType                 commonParams.OSType
	OSArch                 commonParams.OSArch
	Tags                   []*Tag `gorm:"many2many:pool_tags;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"`
	Enabled                bool
	// ExtraSpecs is an opaque json that gets sent to the provider
	// as part of the bootstrap params for instances. It can contain
	// any kind of data needed by providers.
	ExtraSpecs        datatypes.JSON
	GitHubRunnerGroup string

	RepoID     *uuid.UUID `gorm:"index"`
	Repository Repository `gorm:"foreignKey:RepoID;"`

	OrgID        *uuid.UUID   `gorm:"index"`
	Organization Organization `gorm:"foreignKey:OrgID"`

	EnterpriseID *uuid.UUID `gorm:"index"`
	Enterprise   Enterprise `gorm:"foreignKey:EnterpriseID"`

	Instances []Instance `gorm:"foreignKey:PoolID"`
	Priority  uint       `gorm:"index:idx_pool_priority"`
}

type Repository struct {
	Base

	CredentialsName string

	CredentialsID *uint             `gorm:"index"`
	Credentials   GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"`

	Owner            string `gorm:"index:idx_owner_nocase,unique,collate:nocase"`
	Name             string `gorm:"index:idx_owner_nocase,unique,collate:nocase"`
	WebhookSecret    []byte
	Pools            []Pool                  `gorm:"foreignKey:RepoID"`
	Jobs             []WorkflowJob           `gorm:"foreignKey:RepoID;constraint:OnDelete:SET NULL"`
	PoolBalancerType params.PoolBalancerType `gorm:"type:varchar(64)"`

	EndpointName *string        `gorm:"index:idx_owner_nocase,unique,collate:nocase"`
	Endpoint     GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"`
}

type Organization struct {
	Base

	CredentialsName string

	CredentialsID *uint             `gorm:"index"`
	Credentials   GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"`

	Name             string `gorm:"index:idx_org_name_nocase,collate:nocase"`
	WebhookSecret    []byte
	Pools            []Pool                  `gorm:"foreignKey:OrgID"`
	Jobs             []WorkflowJob           `gorm:"foreignKey:OrgID;constraint:OnDelete:SET NULL"`
	PoolBalancerType params.PoolBalancerType `gorm:"type:varchar(64)"`

	EndpointName *string        `gorm:"index:idx_org_name_nocase,collate:nocase"`
	Endpoint     GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"`
}

type Enterprise struct {
	Base

	CredentialsName string

	CredentialsID *uint             `gorm:"index"`
	Credentials   GithubCredentials `gorm:"foreignKey:CredentialsID;constraint:OnDelete:SET NULL"`

	Name             string `gorm:"index:idx_ent_name_nocase,collate:nocase"`
	WebhookSecret    []byte
	Pools            []Pool                  `gorm:"foreignKey:EnterpriseID"`
	Jobs             []WorkflowJob           `gorm:"foreignKey:EnterpriseID;constraint:OnDelete:SET NULL"`
	PoolBalancerType params.PoolBalancerType `gorm:"type:varchar(64)"`

	EndpointName *string        `gorm:"index:idx_ent_name_nocase,collate:nocase"`
	Endpoint     GithubEndpoint `gorm:"foreignKey:EndpointName;constraint:OnDelete:SET NULL"`
}

type Address struct {
	Base

	Address string
	Type    string

	InstanceID uuid.UUID
	Instance   Instance `gorm:"foreignKey:InstanceID"`
}

type InstanceStatusUpdate struct {
	Base

	EventType  params.EventType `gorm:"index:eventType"`
	EventLevel params.EventLevel
	Message    string `gorm:"type:text"`

	InstanceID uuid.UUID `gorm:"index:idx_instance_status_updates_instance_id"`
	Instance   Instance  `gorm:"foreignKey:InstanceID"`
}

type Instance struct {
	Base

	ProviderID        *string `gorm:"uniqueIndex"`
	Name              string  `gorm:"uniqueIndex"`
	AgentID           int64
	OSType            commonParams.OSType
	OSArch            commonParams.OSArch
	OSName            string
	OSVersion         string
	Addresses         []Address `gorm:"foreignKey:InstanceID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"`
	Status            commonParams.InstanceStatus
	RunnerStatus      params.RunnerStatus
	CallbackURL       string
	MetadataURL       string
	ProviderFault     []byte `gorm:"type:longblob"`
	CreateAttempt     int
	TokenFetched      bool
	JitConfiguration  []byte `gorm:"type:longblob"`
	GitHubRunnerGroup string
	AditionalLabels   datatypes.JSON

	PoolID uuid.UUID
	Pool   Pool `gorm:"foreignKey:PoolID"`

	StatusMessages []InstanceStatusUpdate `gorm:"foreignKey:InstanceID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"`

	Job *WorkflowJob `gorm:"foreignKey:InstanceID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"`
}

type User struct {
	Base

	Username   string `gorm:"uniqueIndex;varchar(64)"`
	FullName   string `gorm:"type:varchar(254)"`
	Email      string `gorm:"type:varchar(254);unique;index:idx_email"`
	Password   string `gorm:"type:varchar(60)"`
	Generation uint
	IsAdmin    bool
	Enabled    bool
}

type ControllerInfo struct {
	Base

	ControllerID uuid.UUID

	CallbackURL    string
	MetadataURL    string
	WebhookBaseURL string
	// MinimumJobAgeBackoff is the minimum time that a job must be in the queue
	// before GARM will attempt to allocate a runner to service it. This backoff
	// is useful if you have idle runners in various pools that could potentially
	// pick up the job. GARM would allow this amount of time for runners to react
	// before spinning up a new one and potentially having to scale down later.
	MinimumJobAgeBackoff uint
}

type WorkflowJob struct {
	// ID is the ID of the job.
	ID int64 `gorm:"index"`
	// RunID is the ID of the workflow run. A run may have multiple jobs.
	RunID int64
	// Action is the specific activity that triggered the event.
	Action string `gorm:"type:varchar(254);index"`
	// Conclusion is the outcome of the job.
	// Possible values: "success", "failure", "neutral", "cancelled", "skipped",
	// "timed_out", "action_required"
	Conclusion string
	// Status is the phase of the lifecycle that the job is currently in.
	// "queued", "in_progress" and "completed".
	Status string
	// Name is the name if the job that was triggered.
	Name string

	StartedAt   time.Time
	CompletedAt time.Time

	GithubRunnerID int64

	InstanceID *uuid.UUID `gorm:"index:idx_instance_job"`
	Instance   Instance   `gorm:"foreignKey:InstanceID"`

	RunnerGroupID   int64
	RunnerGroupName string

	// repository in which the job was triggered.
	RepositoryName  string
	RepositoryOwner string

	Labels datatypes.JSON

	// The entity that received the hook.
	//
	// Webhooks may be configured on the repo, the org and/or the enterprise.
	// If we only configure a repo to use garm, we'll only ever receive a
	// webhook from the repo. But if we configure the parent org of the repo and
	// the parent enterprise of the org to use garm, a webhook will be sent for each
	// entity type, in response to one workflow event. Thus, we will get 3 webhooks
	// with the same run_id and job id. Record all involved entities in the same job
	// if we have them configured in garm.
	RepoID     *uuid.UUID `gorm:"index"`
	Repository Repository `gorm:"foreignKey:RepoID"`

	OrgID        *uuid.UUID   `gorm:"index"`
	Organization Organization `gorm:"foreignKey:OrgID"`

	EnterpriseID *uuid.UUID `gorm:"index"`
	Enterprise   Enterprise `gorm:"foreignKey:EnterpriseID"`

	LockedBy uuid.UUID

	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

type GithubEndpoint struct {
	Name      string `gorm:"type:varchar(64) collate nocase;primary_key;"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`

	Description   string `gorm:"type:text"`
	APIBaseURL    string `gorm:"type:text collate nocase"`
	UploadBaseURL string `gorm:"type:text collate nocase"`
	BaseURL       string `gorm:"type:text collate nocase"`
	CACertBundle  []byte `gorm:"type:longblob"`
}

type GithubCredentials struct {
	gorm.Model

	Name   string     `gorm:"index:idx_github_credentials,unique;type:varchar(64) collate nocase"`
	UserID *uuid.UUID `gorm:"index:idx_github_credentials,unique"`
	User   User       `gorm:"foreignKey:UserID"`

	Description string                `gorm:"type:text"`
	AuthType    params.GithubAuthType `gorm:"index"`
	Payload     []byte                `gorm:"type:longblob"`

	Endpoint     GithubEndpoint `gorm:"foreignKey:EndpointName"`
	EndpointName *string        `gorm:"index"`

	Repositories  []Repository   `gorm:"foreignKey:CredentialsID"`
	Organizations []Organization `gorm:"foreignKey:CredentialsID"`
	Enterprises   []Enterprise   `gorm:"foreignKey:CredentialsID"`
}
