package gorm

import (
	"strings"
	"time"

	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/internal/repository"
	"gorm.io/gorm"
)

// BuildEventRepository holds both EventContainer and SubEvent models
type BuildEventRepository struct {
	db *gorm.DB
}

// NewBuildEventRepository returns a BuildEventRepository which uses
// gorm.DB for querying the database
func NewBuildEventRepository(db *gorm.DB) repository.BuildEventRepository {
	return &BuildEventRepository{db}
}

func (repo BuildEventRepository) CreateEventContainer(am *models.EventContainer) (*models.EventContainer, error) {
	if err := repo.db.Create(am).Error; err != nil {
		return nil, err
	}
	return am, nil
}

func (repo BuildEventRepository) CreateSubEvent(am *models.SubEvent) (*models.SubEvent, error) {
	if err := repo.db.Create(am).Error; err != nil {
		return nil, err
	}
	return am, nil
}

func (repo BuildEventRepository) ReadEventsByContainerID(id uint) ([]*models.SubEvent, error) {
	var events []*models.SubEvent
	if err := repo.db.Where("event_container_id = ?", id).Find(&events).Error; err != nil {
		return nil, err
	}
	return events, nil
}

func (repo BuildEventRepository) ReadEventContainer(id uint) (*models.EventContainer, error) {
	container := &models.EventContainer{}
	if err := repo.db.Where("id = ?", id).First(&container).Error; err != nil {
		return nil, err
	}
	return container, nil
}

func (repo BuildEventRepository) ReadSubEvent(id uint) (*models.SubEvent, error) {
	event := &models.SubEvent{}
	if err := repo.db.Where("id = ?", id).First(&event).Error; err != nil {
		return nil, err
	}
	return event, nil
}

// AppendEvent will check if subevent with same (id, index) already exists
// if yes, overrite it, otherwise make a new subevent
func (repo BuildEventRepository) AppendEvent(container *models.EventContainer, event *models.SubEvent) error {
	event.EventContainerID = container.ID
	return repo.db.Create(event).Error
}

// KubeEventRepository uses gorm.DB for querying the database
type KubeEventRepository struct {
	db  *gorm.DB
	key *[32]byte
}

// NewKubeEventRepository returns an KubeEventRepository which uses
// gorm.DB for querying the database. It accepts an encryption key to encrypt
// sensitive data
func NewKubeEventRepository(db *gorm.DB, key *[32]byte) repository.KubeEventRepository {
	return &KubeEventRepository{db, key}
}

// CreateEvent creates a new kube auth mechanism
func (repo *KubeEventRepository) CreateEvent(
	event *models.KubeEvent,
) (*models.KubeEvent, error) {
	// read the count of the events in the DB
	query := repo.db.Where("project_id = ? AND cluster_id = ?", event.ProjectID, event.ClusterID)

	var count int64

	if err := query.Model([]*models.KubeEvent{}).Count(&count).Error; err != nil {
		return nil, err
	}

	// if the count is greater than 500, remove the lowest-order event to implement a
	// basic fixed-length buffer
	if count >= 500 {
		// first, delete the matching sub events
		err := repo.db.Exec(`
		  DELETE FROM kube_sub_events 
		  WHERE kube_event_id IN (
			SELECT id FROM kube_events k2 WHERE (k2.project_id = ? AND k2.cluster_id = ?) AND k2.id NOT IN (
			  SELECT id FROM kube_events k3 WHERE (k3.project_id = ? AND k3.cluster_id = ?) ORDER BY k3.updated_at desc, k3.id desc LIMIT 499
			)
		  )
		`, event.ProjectID, event.ClusterID, event.ProjectID, event.ClusterID).Error
		if err != nil {
			return nil, err
		}

		// then, delete the matching events
		err = repo.db.Exec(`
		  DELETE FROM kube_events 
		  WHERE (project_id = ? AND cluster_id = ?) AND id NOT IN (
			SELECT id FROM kube_events k2 WHERE (k2.project_id = ? AND k2.cluster_id = ?) ORDER BY k2.updated_at desc, k2.id desc LIMIT 499
		  )
		`, event.ProjectID, event.ClusterID, event.ProjectID, event.ClusterID).Error

		if err != nil {
			return nil, err
		}
	}

	if err := repo.db.Create(event).Error; err != nil {
		return nil, err
	}

	return event, nil
}

// ReadEvent finds an event by id
func (repo *KubeEventRepository) ReadEvent(
	id, projID, clusterID uint,
) (*models.KubeEvent, error) {
	event := &models.KubeEvent{}

	if err := repo.db.Preload("SubEvents").Where(
		"id = ? AND project_id = ? AND cluster_id = ?",
		id,
		projID,
		clusterID,
	).First(&event).Error; err != nil {
		return nil, err
	}

	return event, nil
}

// ReadEventByGroup finds an event by a set of options which group events together
func (repo *KubeEventRepository) ReadEventByGroup(
	projID uint,
	clusterID uint,
	opts *types.GroupOptions,
) (*models.KubeEvent, error) {
	event := &models.KubeEvent{}

	query := repo.db.Preload("SubEvents").
		Where("project_id = ? AND cluster_id = ? AND name = ? AND LOWER(resource_type) = LOWER(?)", projID, clusterID, opts.Name, opts.ResourceType)

	// construct query for timestamp
	query = query.Where(
		"updated_at >= ?", opts.ThresholdTime,
	)

	if opts.Namespace != "" {
		query = query.Where(
			"namespace = ?",
			strings.ToLower(opts.Namespace),
		)
	}

	if err := query.First(&event).Error; err != nil {
		return nil, err
	}

	return event, nil
}

// ListEventsByProjectID finds all events for a given project id
// with the given options
func (repo *KubeEventRepository) ListEventsByProjectID(
	projectID uint,
	clusterID uint,
	opts *types.ListKubeEventRequest,
) ([]*models.KubeEvent, int64, error) {
	listOpts := opts

	if listOpts.Limit == 0 {
		listOpts.Limit = 50
	}

	events := []*models.KubeEvent{}

	// preload the subevents
	query := repo.db.Preload("SubEvents").Where("project_id = ? AND cluster_id = ?", projectID, clusterID)

	if listOpts.OwnerName != "" && listOpts.OwnerType != "" {
		query = query.Where(
			"LOWER(owner_name) = LOWER(?) AND LOWER(owner_type) = LOWER(?)",
			listOpts.OwnerName,
			listOpts.OwnerType,
		)
	}

	if listOpts.ResourceType != "" {
		query = query.Where(
			"LOWER(resource_type) = LOWER(?)",
			listOpts.ResourceType,
		)
	}

	if listOpts.Namespace != "" && listOpts.Namespace != "ALL" {
		query = query.Where(
			"LOWER(namespace) = LOWER(?)",
			listOpts.Namespace,
		)
	}

	// get the count before limit and offset
	var count int64

	if err := query.Model([]*models.KubeEvent{}).Count(&count).Error; err != nil {
		return nil, 0, err
	}

	query = query.Order("updated_at desc").Order("id desc").Limit(listOpts.Limit).Offset(listOpts.Skip)

	if err := query.Find(&events).Error; err != nil {
		return nil, 0, err
	}

	return events, count, nil
}

// AppendSubEvent will add a subevent to an existing event
func (repo *KubeEventRepository) AppendSubEvent(event *models.KubeEvent, subEvent *models.KubeSubEvent) error {
	subEvent.KubeEventID = event.ID

	var count int64

	query := repo.db.Where("kube_event_id = ?", event.ID)

	if err := query.Model([]*models.KubeSubEvent{}).Count(&count).Error; err != nil {
		return err
	}

	// if the count is greater than 20, remove the lowest-order events to implement a
	// basic fixed-length buffer
	if count >= 20 {
		err := repo.db.Exec(`
			  DELETE FROM kube_sub_events 
			  WHERE kube_event_id = ? AND 
			  id NOT IN (
				SELECT id FROM kube_sub_events k2 WHERE k2.kube_event_id = ? ORDER BY k2.updated_at desc, k2.id desc LIMIT 19
			  )
			`, event.ID, event.ID).Error
		if err != nil {
			return err
		}
	}

	// we construct a shallow copy here that just populates the primary key, because otherwise gorm
	// attempts to write subevents that have already been written via the association.
	shallowCopy := &models.KubeEvent{
		Model: gorm.Model{
			ID: event.ID,
		},
	}

	if err := repo.db.Model(shallowCopy).Association("SubEvents").Append(subEvent); err != nil {
		return err
	}

	// only update the updated_at field for the event
	if err := repo.db.Model(shallowCopy).Update("updated_at", time.Now()).Error; err != nil {
		return err
	}

	event.SubEvents = append(event.SubEvents, shallowCopy.SubEvents...)
	event.UpdatedAt = shallowCopy.UpdatedAt

	return nil
}

// DeleteEvent deletes an event by ID
func (repo *KubeEventRepository) DeleteEvent(
	id uint,
) error {
	return deleteEventPermanently(id, repo.db)
}

func deleteEventPermanently(id uint, db *gorm.DB) error {
	// delete all subevents first
	if err := db.Unscoped().Where("kube_event_id = ?", id).Delete(&models.KubeSubEvent{}).Error; err != nil {
		return err
	}

	// delete event
	return db.Preload("SubEvents").Unscoped().Where("id = ?", id).Delete(&models.KubeEvent{}).Error
}
