// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import (
	"fmt"

	"code.gitea.io/gitea/modules/setting"

	"github.com/go-xorm/xorm"
)

func removeStaleWatches(x *xorm.Engine) error {
	type Watch struct {
		ID     int64
		UserID int64
		RepoID int64
	}

	type IssueWatch struct {
		ID         int64
		UserID     int64
		RepoID     int64
		IsWatching bool
	}

	type Repository struct {
		ID        int64
		IsPrivate bool
		OwnerID   int64
	}

	type Access struct {
		UserID int64
		RepoID int64
		Mode   int
	}

	const (
		// AccessModeNone no access
		AccessModeNone int = iota // 0
		// AccessModeRead read access
		AccessModeRead // 1
	)

	accessLevel := func(e *xorm.Session, userID int64, repo *Repository) (int, error) {
		mode := AccessModeNone
		if !repo.IsPrivate {
			mode = AccessModeRead
		}

		if userID == 0 {
			return mode, nil
		}

		if userID == repo.OwnerID {
			return 4, nil
		}

		a := &Access{UserID: userID, RepoID: repo.ID}
		if has, err := e.Get(a); !has || err != nil {
			return mode, err
		}
		return a.Mode, nil
	}

	sess := x.NewSession()
	defer sess.Close()
	if err := sess.Begin(); err != nil {
		return err
	}

	var issueWatch IssueWatch
	if exist, err := sess.IsTableExist(&issueWatch); err != nil {
		return fmt.Errorf("IsExist IssueWatch: %v", err)
	} else if !exist {
		return nil
	}

	repoCache := make(map[int64]*Repository)
	err := sess.BufferSize(setting.IterateBufferSize).Iterate(new(Watch),
		func(idx int, bean interface{}) error {
			watch := bean.(*Watch)

			repo := repoCache[watch.RepoID]
			if repo == nil {
				repo = &Repository{
					ID: watch.RepoID,
				}
				if _, err := sess.Get(repo); err != nil {
					return err
				}
				repoCache[watch.RepoID] = repo
			}

			// Remove watches from now unaccessible repositories
			mode, err := accessLevel(sess, watch.UserID, repo)
			if err != nil {
				return err
			}
			has := AccessModeRead <= mode
			if has {
				return nil
			}

			if _, err = sess.Delete(&Watch{0, watch.UserID, repo.ID}); err != nil {
				return err
			}
			_, err = sess.Exec("UPDATE `repository` SET num_watches = num_watches - 1 WHERE id = ?", repo.ID)

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

	repoCache = make(map[int64]*Repository)
	err = sess.BufferSize(setting.IterateBufferSize).
		Distinct("issue_watch.user_id", "issue.repo_id").
		Join("INNER", "issue", "issue_watch.issue_id = issue.id").
		Where("issue_watch.is_watching = ?", true).
		Iterate(new(IssueWatch),
			func(idx int, bean interface{}) error {
				watch := bean.(*IssueWatch)

				repo := repoCache[watch.RepoID]
				if repo == nil {
					repo = &Repository{
						ID: watch.RepoID,
					}
					if _, err := sess.Get(repo); err != nil {
						return err
					}
					repoCache[watch.RepoID] = repo
				}

				// Remove issue watches from now unaccssible repositories
				mode, err := accessLevel(sess, watch.UserID, repo)
				if err != nil {
					return err
				}
				has := AccessModeRead <= mode
				if has {
					return nil
				}

				iw := &IssueWatch{
					IsWatching: false,
				}

				_, err = sess.
					Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", watch.RepoID).
					Cols("is_watching", "updated_unix").
					Where("`issue_watch`.user_id = ?", watch.UserID).
					Update(iw)

				return err

			})
	if err != nil {
		return err
	}

	return sess.Commit()
}
