package repo

import (
	"errors"
	"time"
	"voltaserve/errorpkg"
	"voltaserve/helper"
	"voltaserve/infra"
	"voltaserve/model"

	"gorm.io/gorm"
)

type FileInsertOptions struct {
	Name        string
	WorkspaceID string
	ParentID    *string
	Type        string
}

type FileRepo interface {
	Insert(opts FileInsertOptions) (model.File, error)
	Find(id string) (model.File, error)
	FindChildren(id string) ([]model.File, error)
	FindPath(id string) ([]model.File, error)
	FindTree(id string) ([]model.File, error)
	GetIDsByWorkspace(workspaceID string) ([]string, error)
	AssignSnapshots(cloneID string, originalID string) error
	MoveSourceIntoTarget(targetID string, sourceID string) error
	Save(file model.File) error
	BulkInsert(values []model.File, chunkSize int) error
	BulkInsertPermissions(values []*UserPermission, chunkSize int) error
	Delete(id string) error
	GetChildrenIDs(id string) ([]string, error)
	GetItemCount(id string) (int64, error)
	IsGrandChildOf(id string, ancestorID string) (bool, error)
	GetSize(id string) (int64, error)
	GrantUserPermission(id string, userID string, permission string) error
	RevokeUserPermission(id string, userID string) error
	GrantGroupPermission(id string, groupID string, permission string) error
	RevokeGroupPermission(id string, groupID string) error
}

func NewFileRepo() FileRepo {
	return newFileRepo()
}

func NewFile() model.File {
	return &fileEntity{}
}

type fileEntity struct {
	ID               string                  `json:"id" gorm:"column:id"`
	WorkspaceID      string                  `json:"workspaceId" gorm:"column:workspace_id"`
	Name             string                  `json:"name" gorm:"column:name"`
	Type             string                  `json:"type" gorm:"column:type"`
	ParentID         *string                 `json:"parentId,omitempty" gorm:"column:parent_id"`
	Snapshots        []*snapshotEntity       `json:"snapshots,omitempty" gorm:"-"`
	UserPermissions  []*userPermissionValue  `json:"userPermissions" gorm:"-"`
	GroupPermissions []*groupPermissionValue `json:"groupPermissions" gorm:"-"`
	Text             *string                 `json:"text,omitempty" gorm:"-"`
	CreateTime       string                  `json:"createTime" gorm:"column:create_time"`
	UpdateTime       *string                 `json:"updateTime,omitempty" gorm:"column:update_time"`
}

func (*fileEntity) TableName() string {
	return "file"
}

func (f *fileEntity) BeforeCreate(*gorm.DB) (err error) {
	f.CreateTime = time.Now().UTC().Format(time.RFC3339)
	return nil
}

func (f *fileEntity) BeforeSave(*gorm.DB) (err error) {
	timeNow := time.Now().UTC().Format(time.RFC3339)
	f.UpdateTime = &timeNow
	return nil
}

func (f *fileEntity) GetID() string {
	return f.ID
}

func (f *fileEntity) GetWorkspaceID() string {
	return f.WorkspaceID
}

func (f *fileEntity) GetName() string {
	return f.Name
}

func (f *fileEntity) GetType() string {
	return f.Type
}

func (f *fileEntity) GetParentID() *string {
	return f.ParentID
}

func (f *fileEntity) GetSnapshots() []model.Snapshot {
	var res []model.Snapshot
	for _, s := range f.Snapshots {
		res = append(res, s)
	}
	return res
}

func (f *fileEntity) GetUserPermissions() []model.CoreUserPermission {
	var res []model.CoreUserPermission
	for _, p := range f.UserPermissions {
		res = append(res, p)
	}
	return res
}

func (f *fileEntity) GetGroupPermissions() []model.CoreGroupPermission {
	var res []model.CoreGroupPermission
	for _, p := range f.GroupPermissions {
		res = append(res, p)
	}
	return res
}

func (f *fileEntity) GetText() *string {
	return f.Text
}

func (f *fileEntity) GetCreateTime() string {
	return f.CreateTime
}

func (f *fileEntity) GetUpdateTime() *string {
	return f.UpdateTime
}

func (f *fileEntity) SetID(id string) {
	f.ID = id
}

func (f *fileEntity) SetParentID(parentID *string) {
	f.ParentID = parentID
}

func (f *fileEntity) SetWorkspaceID(workspaceID string) {
	f.WorkspaceID = workspaceID
}

func (f *fileEntity) SetType(fileType string) {
	f.Type = fileType
}

func (f *fileEntity) SetName(name string) {
	f.Name = name
}

func (f *fileEntity) SetText(text *string) {
	f.Text = text
}

func (f *fileEntity) SetCreateTime(createTime string) {
	f.CreateTime = createTime
}

func (f *fileEntity) SetUpdateTime(updateTime *string) {
	f.UpdateTime = updateTime
}

type fileRepo struct {
	db             *gorm.DB
	snapshotRepo   *snapshotRepo
	permissionRepo *permissionRepo
}

func newFileRepo() *fileRepo {
	return &fileRepo{
		db:             infra.GetDb(),
		snapshotRepo:   newSnapshotRepo(),
		permissionRepo: newPermissionRepo(),
	}
}

func (repo *fileRepo) Insert(opts FileInsertOptions) (model.File, error) {
	id := helper.NewID()
	file := fileEntity{
		ID:          id,
		WorkspaceID: opts.WorkspaceID,
		Name:        opts.Name,
		Type:        opts.Type,
		ParentID:    opts.ParentID,
	}
	if db := repo.db.Save(&file); db.Error != nil {
		return nil, db.Error
	}
	res, err := repo.find(id)
	if err != nil {
		return nil, err
	}
	if err := repo.populateModelFields([]*fileEntity{res}); err != nil {
		return nil, err
	}
	return res, nil
}

func (repo *fileRepo) Find(id string) (model.File, error) {
	file, err := repo.find(id)
	if err != nil {
		return nil, err
	}
	if err := repo.populateModelFields([]*fileEntity{file}); err != nil {
		return nil, err
	}
	return file, nil
}

func (repo *fileRepo) find(id string) (*fileEntity, error) {
	var res = fileEntity{}
	db := repo.db.Raw("SELECT * FROM file WHERE id = ?", id).Scan(&res)
	if db.Error != nil {
		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
			return nil, errorpkg.NewFileNotFoundError(db.Error)
		} else {
			return nil, errorpkg.NewInternalServerError(db.Error)
		}
	}
	if len(res.ID) == 0 {
		return nil, errorpkg.NewFileNotFoundError(db.Error)
	}
	return &res, nil
}

func (repo *fileRepo) FindChildren(id string) ([]model.File, error) {
	var entities []*fileEntity
	db := repo.db.Raw("SELECT * FROM file WHERE parent_id = ? ORDER BY create_time ASC", id).Scan(&entities)
	if db.Error != nil {
		return nil, db.Error
	}
	if err := repo.populateModelFields(entities); err != nil {
		return nil, err
	}
	var res []model.File
	for _, f := range entities {
		res = append(res, f)
	}
	return res, nil
}

func (repo *fileRepo) FindPath(id string) ([]model.File, error) {
	var entities []*fileEntity
	if db := repo.db.
		Raw("WITH RECURSIVE rec (id, name, type, parent_id, workspace_id, create_time, update_time) AS "+
			"(SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM file f WHERE f.id = ? "+
			"UNION SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM rec, file f WHERE f.id = rec.parent_id) "+
			"SELECT * FROM rec", id).
		Scan(&entities); db.Error != nil {
		return nil, db.Error
	}
	if err := repo.populateModelFields(entities); err != nil {
		return nil, err
	}
	var res []model.File
	for _, f := range entities {
		res = append(res, f)
	}
	return res, nil
}

func (repo *fileRepo) FindTree(id string) ([]model.File, error) {
	var entities []*fileEntity
	db := repo.db.
		Raw("WITH RECURSIVE rec (id, name, type, parent_id, workspace_id, create_time, update_time) AS "+
			"(SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM file f WHERE f.id = ? "+
			"UNION SELECT f.id, f.name, f.type, f.parent_id, f.workspace_id, f.create_time, f.update_time FROM rec, file f WHERE f.parent_id = rec.id) "+
			"SELECT rec.* FROM rec ORDER BY create_time ASC", id).
		Scan(&entities)
	if db.Error != nil {
		return nil, db.Error
	}
	if err := repo.populateModelFields(entities); err != nil {
		return nil, err
	}
	var res []model.File
	for _, f := range entities {
		res = append(res, f)
	}
	return res, nil
}

func (repo *fileRepo) GetIDsByWorkspace(workspaceID string) ([]string, error) {
	type IDResult struct {
		Result string
	}
	var ids []IDResult
	db := repo.db.Raw("SELECT id result FROM file WHERE workspace_id = ? ORDER BY create_time ASC", workspaceID).Scan(&ids)
	if db.Error != nil {
		return nil, db.Error
	}
	res := []string{}
	for _, id := range ids {
		res = append(res, id.Result)
	}
	return res, nil
}

func (repo *fileRepo) AssignSnapshots(cloneID string, originalID string) error {
	if db := repo.db.Exec("INSERT INTO snapshot_file (snapshot_id, file_id) SELECT s.id, ? "+
		"FROM snapshot s LEFT JOIN snapshot_file map ON s.id = map.snapshot_id "+
		"WHERE map.file_id = ? ORDER BY s.version DESC LIMIT 1", cloneID, originalID); db.Error != nil {
		return db.Error
	}
	return nil
}

func (repo *fileRepo) MoveSourceIntoTarget(targetID string, sourceID string) error {
	if db := repo.db.Exec("UPDATE file SET parent_id = ? WHERE id = ?", targetID, sourceID); db.Error != nil {
		return db.Error
	}
	return nil
}

func (repo *fileRepo) Save(file model.File) error {
	if db := repo.db.Save(file); db.Error != nil {
		return db.Error
	}
	return nil
}

func (repo *fileRepo) BulkInsert(values []model.File, chunkSize int) error {
	var entities []*fileEntity
	for _, f := range values {
		entities = append(entities, f.(*fileEntity))
	}
	if db := repo.db.CreateInBatches(entities, chunkSize); db.Error != nil {
		return db.Error
	}
	return nil
}

func (repo *fileRepo) BulkInsertPermissions(values []*UserPermission, chunkSize int) error {
	if db := repo.db.CreateInBatches(values, chunkSize); db.Error != nil {
		return db.Error
	}
	return nil
}

func (repo *fileRepo) Delete(id string) error {
	db := repo.db.Exec("DELETE FROM file WHERE id = ?", id)
	if db.Error != nil {
		return db.Error
	}
	db = repo.db.Exec("DELETE FROM userpermission WHERE resource_id = ?", id)
	if db.Error != nil {
		return db.Error
	}
	db = repo.db.Exec("DELETE FROM grouppermission WHERE resource_id = ?", id)
	if db.Error != nil {
		return db.Error
	}
	return nil
}

func (repo *fileRepo) GetChildrenIDs(id string) ([]string, error) {
	type Result struct {
		Result string
	}
	var results []Result
	db := repo.db.Raw("SELECT id result FROM file WHERE parent_id = ? ORDER BY create_time ASC", id).Scan(&results)
	if db.Error != nil {
		return []string{}, db.Error
	}
	res := []string{}
	for _, v := range results {
		res = append(res, v.Result)
	}
	return res, nil
}

func (repo *fileRepo) GetItemCount(id string) (int64, error) {
	type Result struct {
		Result int64
	}
	var res Result
	db := repo.db.
		Raw("WITH RECURSIVE rec (id, parent_id) AS "+
			"(SELECT f.id, f.parent_id FROM file f WHERE f.id = ? "+
			"UNION SELECT f.id, f.parent_id FROM rec, file f WHERE f.parent_id = rec.id) "+
			"SELECT count(rec.id) as result FROM rec", id).
		Scan(&res)
	if db.Error != nil {
		return 0, db.Error
	}
	return res.Result - 1, nil
}

func (repo *fileRepo) IsGrandChildOf(id string, ancestorID string) (bool, error) {
	type Result struct {
		Result bool
	}
	var res Result
	if db := repo.db.
		Raw("WITH RECURSIVE rec (id, parent_id) AS "+
			"(SELECT f.id, f.parent_id FROM file f WHERE f.id = ? "+
			"UNION SELECT f.id, f.parent_id FROM rec, file f WHERE f.parent_id = rec.id) "+
			"SELECT count(rec.id) > 0 as result FROM rec WHERE rec.id = ?", ancestorID, id).
		Scan(&res); db.Error != nil {
		return false, db.Error
	}
	return res.Result, nil
}

func (repo *fileRepo) GetSize(id string) (int64, error) {
	type Result struct {
		Result int64
	}
	var res Result
	db := repo.db.
		Raw("WITH RECURSIVE rec (id, parent_id) AS "+
			"(SELECT f.id, f.parent_id FROM file f WHERE f.id = ? "+
			"UNION SELECT f.id, f.parent_id FROM rec, file f WHERE f.parent_id = rec.id) "+
			"SELECT coalesce(sum((s.original->>'size')::int), 0) as result FROM snapshot s, rec "+
			"LEFT JOIN snapshot_file map ON rec.id = map.file_id WHERE map.snapshot_id = s.id", id).
		Scan(&res)
	if db.Error != nil {
		return res.Result, db.Error
	}
	return res.Result, nil
}

func (repo *fileRepo) GrantUserPermission(id string, userID string, permission string) error {
	/* Grant permission to workspace */
	db := repo.db.Exec("INSERT INTO userpermission (id, user_id, resource_id, permission) "+
		"(SELECT ?, ?, w.id, 'viewer' FROM file f "+
		"INNER JOIN workspace w ON w.id = f.workspace_id AND f.id = ?) "+
		"ON CONFLICT DO NOTHING",
		helper.NewID(), userID, id)
	if db.Error != nil {
		return db.Error
	}

	/* Grant 'viewer' permission to path files */
	path, err := repo.FindPath(id)
	if err != nil {
		return err
	}
	for _, f := range path {
		db := repo.db.Exec("INSERT INTO userpermission (id, user_id, resource_id, permission) "+
			"VALUES (?, ?, ?, 'viewer') ON CONFLICT DO NOTHING",
			helper.NewID(), userID, f.GetID())
		if db.Error != nil {
			return db.Error
		}
	}

	/* Grant the requested permission to tree files */
	tree, err := repo.FindTree(id)
	if err != nil {
		return err
	}
	for _, f := range tree {
		db := repo.db.Exec("INSERT INTO userpermission (id, user_id, resource_id, permission) "+
			"VALUES (?, ?, ?, ?) ON CONFLICT (user_id, resource_id) DO UPDATE SET permission = ?",
			helper.NewID(), userID, f.GetID(), permission, permission)
		if db.Error != nil {
			return db.Error
		}
	}

	return nil
}

func (repo *fileRepo) RevokeUserPermission(id string, userID string) error {
	tree, err := repo.FindTree(id)
	if err != nil {
		return err
	}
	for _, f := range tree {
		db := repo.db.Exec("DELETE FROM userpermission WHERE user_id = ? AND resource_id = ?", userID, f.GetID())
		if db.Error != nil {
			return db.Error
		}
	}
	return nil
}

func (repo *fileRepo) GrantGroupPermission(id string, groupID string, permission string) error {
	/* Grant permission to workspace */
	db := repo.db.Exec("INSERT INTO grouppermission (id, group_id, resource_id, permission) "+
		"(SELECT ?, ?, w.id, 'viewer' FROM file f "+
		"INNER JOIN workspace w ON w.id = f.workspace_id AND f.id = ?) "+
		"ON CONFLICT DO NOTHING",
		helper.NewID(), groupID, id)
	if db.Error != nil {
		return db.Error
	}

	/* Grant 'viewer' permission to path files */
	path, err := repo.FindPath(id)
	if err != nil {
		return err
	}
	for _, f := range path {
		db := repo.db.Exec("INSERT INTO grouppermission (id, group_id, resource_id, permission) "+
			"VALUES (?, ?, ?, 'viewer') ON CONFLICT DO NOTHING",
			helper.NewID(), groupID, f.GetID())
		if db.Error != nil {
			return db.Error
		}
	}

	/* Grant the requested permission to tree files */
	tree, err := repo.FindTree(id)
	if err != nil {
		return err
	}
	for _, f := range tree {
		db := repo.db.Exec("INSERT INTO grouppermission (id, group_id, resource_id, permission) "+
			"VALUES (?, ?, ?, ?) ON CONFLICT (group_id, resource_id) DO UPDATE SET permission = ?",
			helper.NewID(), groupID, f.GetID(), permission, permission)
		if db.Error != nil {
			return db.Error
		}
	}

	return nil
}

func (repo *fileRepo) RevokeGroupPermission(id string, groupID string) error {
	tree, err := repo.FindTree(id)
	if err != nil {
		return err
	}
	for _, f := range tree {
		db := repo.db.Exec("DELETE FROM grouppermission WHERE group_id = ? AND resource_id = ?", groupID, f.GetID())
		if db.Error != nil {
			return db.Error
		}
	}
	return nil
}

func (repo *fileRepo) populateModelFields(entities []*fileEntity) error {
	for _, f := range entities {
		f.UserPermissions = make([]*userPermissionValue, 0)
		userPermissions, err := repo.permissionRepo.GetUserPermissions(f.ID)
		if err != nil {
			return err
		}
		for _, p := range userPermissions {
			f.UserPermissions = append(f.UserPermissions, &userPermissionValue{
				UserID: p.UserID,
				Value:  p.Permission,
			})
		}
		f.GroupPermissions = make([]*groupPermissionValue, 0)
		groupPermissions, err := repo.permissionRepo.GetGroupPermissions(f.ID)
		if err != nil {
			return err
		}
		for _, p := range groupPermissions {
			f.GroupPermissions = append(f.GroupPermissions, &groupPermissionValue{
				GroupID: p.GroupID,
				Value:   p.Permission,
			})
		}
		snapshots, err := repo.snapshotRepo.findAllForFile(f.ID)
		if err != nil {
			return nil
		}
		f.Snapshots = snapshots
	}
	return nil
}
