package models

import (
	"errors"
	"fmt"
	"sort"

	"code.gitea.io/gitea/modules/timeutil"
	"xorm.io/builder"
)

const (
	DatasetStatusPrivate int32 = iota
	DatasetStatusPublic
	DatasetStatusDeleted
)

type Dataset struct {
	ID            int64  `xorm:"pk autoincr"`
	Title         string `xorm:"INDEX NOT NULL"`
	Status        int32  `xorm:"INDEX"` // normal_private: 0, pulbic: 1, is_delete: 2
	Category      string
	Description   string `xorm:"TEXT"`
	DownloadTimes int64
	License       string
	Task          string
	ReleaseID     int64              `xorm:"INDEX"`
	UserID        int64              `xorm:"INDEX"`
	RepoID        int64              `xorm:"INDEX"`
	Repo          *Repository        `xorm:"-"`
	CreatedUnix   timeutil.TimeStamp `xorm:"INDEX created"`
	UpdatedUnix   timeutil.TimeStamp `xorm:"INDEX updated"`

	User        *User         `xorm:"-"`
	Attachments []*Attachment `xorm:"-"`
}

func (d *Dataset) IsPrivate() bool {
	switch d.Status {
	case DatasetStatusPrivate:
		return true
	case DatasetStatusPublic:
		return false
	case DatasetStatusDeleted:
		return false
	default:
		return false
	}
}

type DatasetList []*Dataset

func (datasets DatasetList) loadAttributes(e Engine) error {
	if len(datasets) == 0 {
		return nil
	}

	set := make(map[int64]struct{})
	datasetIDs := make([]int64, len(datasets))
	for i := range datasets {
		set[datasets[i].UserID] = struct{}{}
		set[datasets[i].RepoID] = struct{}{}
		datasetIDs[i] = datasets[i].ID
	}

	// Load owners.
	users := make(map[int64]*User, len(set))
	repos := make(map[int64]*Repository, len(set))
	if err := e.
		Where("id > 0").
		In("id", keysInt64(set)).
		Find(&users); err != nil {
		return fmt.Errorf("find users: %v", err)
	}
	if err := e.
		Where("id > 0").
		In("id", keysInt64(set)).
		Find(&repos); err != nil {
		return fmt.Errorf("find repos: %v", err)
	}
	for i := range datasets {
		datasets[i].User = users[datasets[i].UserID]
		datasets[i].Repo = repos[datasets[i].RepoID]
	}

	return nil
}

type SearchDatasetOptions struct {
	Keyword       string
	OwnerID       int64
	RepoID        int64
	IncludePublic bool
	ListOptions
	SearchOrderBy
	IsOwner bool
}

func CreateDataset(dataset *Dataset) (err error) {
	if _, err = x.Insert(dataset); err != nil {
		return err
	}

	return nil
}

func CreateDefaultDatasetToRepo(repo *Repository) (err error) {
	dataset := &Dataset{RepoID: repo.ID}
	has, err := x.Get(dataset)
	if err != nil {
		return err
	}
	if !has {
		dataset.Status = DatasetStatusPrivate
		dataset.Title = repo.Name
		if err = CreateDataset(dataset); err != nil {
			return err
		}
	}
	return nil
}

func SearchDataset(opts *SearchDatasetOptions) (DatasetList, int64, error) {
	cond := SearchDatasetCondition(opts)
	return SearchDatasetByCondition(opts, cond)
}

func SearchDatasetCondition(opts *SearchDatasetOptions) builder.Cond {
	var cond = builder.NewCond()
	cond = cond.And(builder.Neq{"dataset.status": DatasetStatusDeleted})

	if len(opts.Keyword) > 0 {
		cond = cond.And(builder.Like{"dataset.title", opts.Keyword})
	}

	if opts.RepoID > 0 {
		cond = cond.And(builder.Eq{"dataset.repo_id": opts.RepoID})
	}

	if opts.IncludePublic {
		cond = cond.And(builder.Eq{"dataset.status": DatasetStatusPublic})
		if opts.OwnerID > 0 {
			if len(opts.Keyword) == 0 {
				cond = cond.Or(builder.Eq{"repository.owner_id": opts.OwnerID})
			} else {
				subCon := builder.NewCond()
				subCon = subCon.And(builder.Eq{"repository.owner_id": opts.OwnerID}, builder.Like{"dataset.title", opts.Keyword})
				cond = cond.Or(subCon)

			}
		}
	} else if opts.OwnerID > 0 {
		cond = cond.And(builder.Eq{"repository.owner_id": opts.OwnerID})
		if !opts.IsOwner {
			cond = cond.And(builder.Eq{"dataset.status": DatasetStatusPublic})
		}
	}

	return cond
}

func SearchDatasetByCondition(opts *SearchDatasetOptions, cond builder.Cond) (DatasetList, int64, error) {
	if opts.Page <= 0 {
		opts.Page = 1
	}

	var err error
	sess := x.NewSession()
	defer sess.Close()

	datasets := make(DatasetList, 0, opts.PageSize)

	count, err := sess.Join("INNER", "repository", "repository.id = dataset.repo_id").Where(cond).Count(new(Dataset))

	if err != nil {
		return nil, 0, fmt.Errorf("Count: %v", err)
	}

	sess.Select("dataset.*").Join("INNER", "repository", "repository.id = dataset.repo_id").Where(cond).OrderBy(opts.SearchOrderBy.String())
	if opts.PageSize > 0 {
		sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
	}
	if err = sess.Find(&datasets); err != nil {
		return nil, 0, fmt.Errorf("Dataset: %v", err)
	}

	if err = datasets.loadAttributes(sess); err != nil {
		return nil, 0, fmt.Errorf("LoadAttributes: %v", err)
	}

	return datasets, count, nil
}

type datasetMetaSearch struct {
	ID  []int64
	Rel []*Dataset
}

func (s datasetMetaSearch) Len() int {
	return len(s.ID)
}
func (s datasetMetaSearch) Swap(i, j int) {
	s.ID[i], s.ID[j] = s.ID[j], s.ID[i]
	s.Rel[i], s.Rel[j] = s.Rel[j], s.Rel[i]
}
func (s datasetMetaSearch) Less(i, j int) bool {
	return s.ID[i] < s.ID[j]
}

func GetDatasetAttachments(typeCloudBrain int, isSigned bool, user *User, rels ...*Dataset) (err error) {
	return getDatasetAttachments(x, typeCloudBrain, isSigned, user, rels...)
}

func getDatasetAttachments(e Engine, typeCloudBrain int, isSigned bool, user *User, rels ...*Dataset) (err error) {
	if len(rels) == 0 {
		return
	}

	// To keep this efficient as possible sort all datasets by id,
	//    select attachments by dataset id,
	//    then merge join them

	// Sort
	var sortedRels = datasetMetaSearch{ID: make([]int64, len(rels)), Rel: make([]*Dataset, len(rels))}
	var attachments []*Attachment
	for index, element := range rels {
		element.Attachments = []*Attachment{}
		sortedRels.ID[index] = element.ID
		sortedRels.Rel[index] = element
	}
	sort.Sort(sortedRels)

	// Select attachments
	err = e.
		Asc("dataset_id").
		In("dataset_id", sortedRels.ID).
		And("type = ?", typeCloudBrain).
		Find(&attachments, Attachment{})
	if err != nil {
		return err
	}

	// merge join
	var currentIndex = 0
	for _, attachment := range attachments {
		for sortedRels.ID[currentIndex] < attachment.DatasetID {
			currentIndex++
		}
		fileChunks := make([]*FileChunk, 0, 10)
		err = e.
			Where("uuid = ?", attachment.UUID).
			Find(&fileChunks)
		if err != nil {
			return err
		}
		attachment.FileChunk = fileChunks[0]
		attachment.CanDel = CanDelAttachment(isSigned, user, attachment)
		sortedRels.Rel[currentIndex].Attachments = append(sortedRels.Rel[currentIndex].Attachments, attachment)
	}

	return
}

// AddDatasetAttachments adds a Dataset attachments
func AddDatasetAttachments(DatasetID int64, attachmentUUIDs []string) (err error) {
	// Check attachments
	attachments, err := GetAttachmentsByUUIDs(attachmentUUIDs)
	if err != nil {
		return fmt.Errorf("GetAttachmentsByUUIDs [uuids: %v]: %v", attachmentUUIDs, err)
	}

	for i := range attachments {
		attachments[i].DatasetID = DatasetID
		// No assign value could be 0, so ignore AllCols().
		if _, err = x.ID(attachments[i].ID).Update(attachments[i]); err != nil {
			return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
		}
	}

	return
}

func UpdateDataset(ctx DBContext, rel *Dataset) error {
	_, err := ctx.e.ID(rel.ID).AllCols().Update(rel)
	return err
}

// GetDatasetByID returns Dataset with given ID.
func GetDatasetByID(id int64) (*Dataset, error) {
	rel := new(Dataset)
	has, err := x.
		ID(id).
		Get(rel)
	if err != nil {
		return nil, err
	} else if !has {
		return nil, ErrDatasetNotExist{id}
	}

	return rel, nil
}

func GetDatasetByRepo(repo *Repository) (*Dataset, error) {
	if err := CreateDefaultDatasetToRepo(repo); err != nil {
		return nil, err
	}
	dataset := &Dataset{RepoID: repo.ID}
	has, err := x.Get(dataset)
	if err != nil {
		return nil, err
	}
	if has {
		return dataset, nil
	} else {
		return nil, errors.New("Not Found")
	}
}

func DeleteDataset(datasetID int64, uid int64) error {
	var err error
	sess := x.NewSession()
	defer sess.Close()
	if err = sess.Begin(); err != nil {
		return err
	}

	dataset := &Dataset{ID: datasetID, UserID: uid}
	has, err := sess.Get(dataset)
	if err != nil {
		return err
	} else if !has {
		return errors.New("not found")
	}

	if cnt, err := sess.ID(datasetID).Delete(new(Dataset)); err != nil {
		return err
	} else if cnt != 1 {
		return errors.New("not found")
	}
	if err = sess.Commit(); err != nil {
		sess.Close()
		return fmt.Errorf("Commit: %v", err)
	}
	return nil
}

func GetOwnerDatasetByID(id int64, user *User) (*Dataset, error) {
	dataset, err := GetDatasetByID(id)
	if err != nil {
		return nil, err
	}

	if !dataset.IsPrivate() {
		return dataset, nil
	}
	if dataset.IsPrivate() && user != nil && user.ID == dataset.UserID {
		return dataset, nil
	}
	return nil, errors.New("dataset not fount")
}

func IncreaseDownloadCount(datasetID int64) error {
	// Update download count.
	if _, err := x.Exec("UPDATE `dataset` SET download_times=download_times+1 WHERE id=?", datasetID); err != nil {
		return fmt.Errorf("increase dataset count: %v", err)
	}

	return nil
}
