// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package user

import (
	"context"

	"code.gitea.io/gitea/models"
	"code.gitea.io/gitea/models/db"
	issues_model "code.gitea.io/gitea/models/issues"
	org_model "code.gitea.io/gitea/models/organization"
	repo_model "code.gitea.io/gitea/models/repo"
	user_model "code.gitea.io/gitea/models/user"
	repo_service "code.gitea.io/gitea/services/repository"
)

func CanBlockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
	if blocker.ID == blockee.ID {
		return false
	}
	if doer.ID == blockee.ID {
		return false
	}

	if blockee.IsOrganization() {
		return false
	}

	if user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
		return false
	}

	if blocker.IsOrganization() {
		org := org_model.OrgFromUser(blocker)
		if isMember, _ := org.IsOrgMember(ctx, blockee.ID); isMember {
			return false
		}
		if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
			return false
		}
	} else if !doer.IsAdmin && doer.ID != blocker.ID {
		return false
	}

	return true
}

func CanUnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) bool {
	if doer.ID == blockee.ID {
		return false
	}

	if !user_model.IsUserBlockedBy(ctx, blockee, blocker.ID) {
		return false
	}

	if blocker.IsOrganization() {
		org := org_model.OrgFromUser(blocker)
		if isAdmin, _ := org.IsOwnedBy(ctx, doer.ID); !isAdmin && !doer.IsAdmin {
			return false
		}
	} else if !doer.IsAdmin && doer.ID != blocker.ID {
		return false
	}

	return true
}

func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, note string) error {
	if blockee.IsOrganization() {
		return user_model.ErrBlockOrganization
	}

	if !CanBlockUser(ctx, doer, blocker, blockee) {
		return user_model.ErrCanNotBlock
	}

	return db.WithTx(ctx, func(ctx context.Context) error {
		// unfollow each other
		if err := user_model.UnfollowUser(ctx, blocker.ID, blockee.ID); err != nil {
			return err
		}
		if err := user_model.UnfollowUser(ctx, blockee.ID, blocker.ID); err != nil {
			return err
		}

		// unstar each other
		if err := unstarRepos(ctx, blocker, blockee); err != nil {
			return err
		}
		if err := unstarRepos(ctx, blockee, blocker); err != nil {
			return err
		}

		// unwatch each others repositories
		if err := unwatchRepos(ctx, blocker, blockee); err != nil {
			return err
		}
		if err := unwatchRepos(ctx, blockee, blocker); err != nil {
			return err
		}

		// unassign each other from issues
		if err := unassignIssues(ctx, blocker, blockee); err != nil {
			return err
		}
		if err := unassignIssues(ctx, blockee, blocker); err != nil {
			return err
		}

		// remove each other from repository collaborations
		if err := removeCollaborations(ctx, blocker, blockee); err != nil {
			return err
		}
		if err := removeCollaborations(ctx, blockee, blocker); err != nil {
			return err
		}

		// cancel each other repository transfers
		if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil {
			return err
		}
		if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil {
			return err
		}

		return db.Insert(ctx, &user_model.Blocking{
			BlockerID: blocker.ID,
			BlockeeID: blockee.ID,
			Note:      note,
		})
	})
}

func unstarRepos(ctx context.Context, starrer, repoOwner *user_model.User) error {
	opts := &repo_model.StarredReposOptions{
		ListOptions: db.ListOptions{
			Page:     1,
			PageSize: 25,
		},
		StarrerID:   starrer.ID,
		RepoOwnerID: repoOwner.ID,
	}

	for {
		repos, err := repo_model.GetStarredRepos(ctx, opts)
		if err != nil {
			return err
		}

		if len(repos) == 0 {
			return nil
		}

		for _, repo := range repos {
			if err := repo_model.StarRepo(ctx, starrer, repo, false); err != nil {
				return err
			}
		}

		opts.Page++
	}
}

func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) error {
	opts := &repo_model.WatchedReposOptions{
		ListOptions: db.ListOptions{
			Page:     1,
			PageSize: 25,
		},
		WatcherID:   watcher.ID,
		RepoOwnerID: repoOwner.ID,
	}

	for {
		repos, _, err := repo_model.GetWatchedRepos(ctx, opts)
		if err != nil {
			return err
		}

		if len(repos) == 0 {
			return nil
		}

		for _, repo := range repos {
			if err := repo_model.WatchRepo(ctx, watcher, repo, false); err != nil {
				return err
			}
		}

		opts.Page++
	}
}

func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error {
	transfers, err := models.GetPendingRepositoryTransfers(ctx, &models.PendingRepositoryTransferOptions{
		SenderID:    sender.ID,
		RecipientID: recipient.ID,
	})
	if err != nil {
		return err
	}

	for _, transfer := range transfers {
		repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID)
		if err != nil {
			return err
		}

		if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil {
			return err
		}
	}

	return nil
}

func unassignIssues(ctx context.Context, assignee, repoOwner *user_model.User) error {
	opts := &issues_model.AssignedIssuesOptions{
		ListOptions: db.ListOptions{
			Page:     1,
			PageSize: 25,
		},
		AssigneeID:  assignee.ID,
		RepoOwnerID: repoOwner.ID,
	}

	for {
		issues, _, err := issues_model.GetAssignedIssues(ctx, opts)
		if err != nil {
			return err
		}

		if len(issues) == 0 {
			return nil
		}

		for _, issue := range issues {
			if err := issue.LoadAssignees(ctx); err != nil {
				return err
			}

			if _, _, err := issues_model.ToggleIssueAssignee(ctx, issue, assignee, assignee.ID); err != nil {
				return err
			}
		}

		opts.Page++
	}
}

func removeCollaborations(ctx context.Context, repoOwner, collaborator *user_model.User) error {
	opts := &repo_model.FindCollaborationOptions{
		ListOptions: db.ListOptions{
			Page:     1,
			PageSize: 25,
		},
		CollaboratorID: collaborator.ID,
		RepoOwnerID:    repoOwner.ID,
	}

	for {
		collaborations, _, err := repo_model.GetCollaborators(ctx, opts)
		if err != nil {
			return err
		}

		if len(collaborations) == 0 {
			return nil
		}

		for _, collaboration := range collaborations {
			repo, err := repo_model.GetRepositoryByID(ctx, collaboration.Collaboration.RepoID)
			if err != nil {
				return err
			}

			if err := repo_service.DeleteCollaboration(ctx, repo, collaborator); err != nil {
				return err
			}
		}

		opts.Page++
	}
}

func UnblockUser(ctx context.Context, doer, blocker, blockee *user_model.User) error {
	if blockee.IsOrganization() {
		return user_model.ErrBlockOrganization
	}

	if !CanUnblockUser(ctx, doer, blocker, blockee) {
		return user_model.ErrCanNotUnblock
	}

	return db.WithTx(ctx, func(ctx context.Context) error {
		block, err := user_model.GetBlocking(ctx, blocker.ID, blockee.ID)
		if err != nil {
			return err
		}
		if block != nil {
			_, err = db.DeleteByID[user_model.Blocking](ctx, block.ID)
			return err
		}
		return nil
	})
}
