// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package repo

import (
	"net/http"

	"code.gitea.io/gitea/models/db"
	issues_model "code.gitea.io/gitea/models/issues"
	access_model "code.gitea.io/gitea/models/perm/access"
	repo_model "code.gitea.io/gitea/models/repo"
	"code.gitea.io/gitea/modules/setting"
	api "code.gitea.io/gitea/modules/structs"
	"code.gitea.io/gitea/modules/web"
	"code.gitea.io/gitea/services/context"
	"code.gitea.io/gitea/services/convert"
)

// GetIssueDependencies list an issue's dependencies
func GetIssueDependencies(ctx *context.APIContext) {
	// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies
	// ---
	// summary: List an issue's dependencies, i.e all issues that block this issue.
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: index
	//   in: path
	//   description: index of the issue
	//   type: string
	//   required: true
	// - name: page
	//   in: query
	//   description: page number of results to return (1-based)
	//   type: integer
	// - name: limit
	//   in: query
	//   description: page size of results
	//   type: integer
	// responses:
	//   "200":
	//     "$ref": "#/responses/IssueList"
	//   "404":
	//     "$ref": "#/responses/notFound"

	// If this issue's repository does not enable dependencies then there can be no dependencies by default
	if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) {
		ctx.NotFound()
		return
	}

	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
	if err != nil {
		if issues_model.IsErrIssueNotExist(err) {
			ctx.NotFound("IsErrIssueNotExist", err)
		} else {
			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
		}
		return
	}

	// 1. We must be able to read this issue
	if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
		ctx.NotFound()
		return
	}

	page := ctx.FormInt("page")
	if page <= 1 {
		page = 1
	}
	limit := ctx.FormInt("limit")
	if limit == 0 {
		limit = setting.API.DefaultPagingNum
	} else if limit > setting.API.MaxResponseItems {
		limit = setting.API.MaxResponseItems
	}

	canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)

	blockerIssues := make([]*issues_model.Issue, 0, limit)

	// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
	blockersInfo, err := issue.BlockedByDependencies(ctx, db.ListOptions{
		Page:     page,
		PageSize: limit,
	})
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "BlockedByDependencies", err)
		return
	}

	repoPerms := make(map[int64]access_model.Permission)
	repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
	for _, blocker := range blockersInfo {
		// Get the permissions for this repository
		// If the repo ID exists in the map, return the exist permissions
		// else get the permission and add it to the map
		var perm access_model.Permission
		existPerm, ok := repoPerms[blocker.RepoID]
		if ok {
			perm = existPerm
		} else {
			var err error
			perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
			if err != nil {
				ctx.ServerError("GetUserRepoPermission", err)
				return
			}
			repoPerms[blocker.RepoID] = perm
		}

		// check permission
		if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
			if !canWrite {
				hiddenBlocker := &issues_model.DependencyInfo{
					Issue: issues_model.Issue{
						Title: "HIDDEN",
					},
				}
				blocker = hiddenBlocker
			} else {
				confidentialBlocker := &issues_model.DependencyInfo{
					Issue: issues_model.Issue{
						RepoID:   blocker.Issue.RepoID,
						Index:    blocker.Index,
						Title:    blocker.Title,
						IsClosed: blocker.IsClosed,
						IsPull:   blocker.IsPull,
					},
					Repository: repo_model.Repository{
						ID:        blocker.Issue.Repo.ID,
						Name:      blocker.Issue.Repo.Name,
						OwnerName: blocker.Issue.Repo.OwnerName,
					},
				}
				confidentialBlocker.Issue.Repo = &confidentialBlocker.Repository
				blocker = confidentialBlocker
			}
		}
		blockerIssues = append(blockerIssues, &blocker.Issue)
	}

	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, blockerIssues))
}

// CreateIssueDependency create a new issue dependencies
func CreateIssueDependency(ctx *context.APIContext) {
	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies
	// ---
	// summary: Make the issue in the url depend on the issue in the form.
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: index
	//   in: path
	//   description: index of the issue
	//   type: string
	//   required: true
	// - name: body
	//   in: body
	//   schema:
	//     "$ref": "#/definitions/IssueMeta"
	// responses:
	//   "201":
	//     "$ref": "#/responses/Issue"
	//   "404":
	//     description: the issue does not exist
	//   "423":
	//     "$ref": "#/responses/repoArchivedError"

	// We want to make <:index> depend on <Form>, i.e. <:index> is the target
	target := getParamsIssue(ctx)
	if ctx.Written() {
		return
	}

	// and <Form> represents the dependency
	form := web.GetForm(ctx).(*api.IssueMeta)
	dependency := getFormIssue(ctx, form)
	if ctx.Written() {
		return
	}

	dependencyPerm := getPermissionForRepo(ctx, target.Repo)
	if ctx.Written() {
		return
	}

	createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
	if ctx.Written() {
		return
	}

	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
}

// RemoveIssueDependency remove an issue dependency
func RemoveIssueDependency(ctx *context.APIContext) {
	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
	// ---
	// summary: Remove an issue dependency
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: index
	//   in: path
	//   description: index of the issue
	//   type: string
	//   required: true
	// - name: body
	//   in: body
	//   schema:
	//     "$ref": "#/definitions/IssueMeta"
	// responses:
	//   "200":
	//     "$ref": "#/responses/Issue"
	//   "404":
	//     "$ref": "#/responses/notFound"
	//   "423":
	//     "$ref": "#/responses/repoArchivedError"

	// We want to make <:index> depend on <Form>, i.e. <:index> is the target
	target := getParamsIssue(ctx)
	if ctx.Written() {
		return
	}

	// and <Form> represents the dependency
	form := web.GetForm(ctx).(*api.IssueMeta)
	dependency := getFormIssue(ctx, form)
	if ctx.Written() {
		return
	}

	dependencyPerm := getPermissionForRepo(ctx, target.Repo)
	if ctx.Written() {
		return
	}

	removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
	if ctx.Written() {
		return
	}

	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
}

// GetIssueBlocks list issues that are blocked by this issue
func GetIssueBlocks(ctx *context.APIContext) {
	// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
	// ---
	// summary: List issues that are blocked by this issue
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: index
	//   in: path
	//   description: index of the issue
	//   type: string
	//   required: true
	// - name: page
	//   in: query
	//   description: page number of results to return (1-based)
	//   type: integer
	// - name: limit
	//   in: query
	//   description: page size of results
	//   type: integer
	// responses:
	//   "200":
	//     "$ref": "#/responses/IssueList"
	//   "404":
	//     "$ref": "#/responses/notFound"

	// We need to list the issues that DEPEND on this issue not the other way round
	// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.

	issue := getParamsIssue(ctx)
	if ctx.Written() {
		return
	}

	if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
		ctx.NotFound()
		return
	}

	page := ctx.FormInt("page")
	if page <= 1 {
		page = 1
	}
	limit := ctx.FormInt("limit")
	if limit <= 1 {
		limit = setting.API.DefaultPagingNum
	}

	skip := (page - 1) * limit
	max := page * limit

	deps, err := issue.BlockingDependencies(ctx)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err)
		return
	}

	var issues []*issues_model.Issue

	repoPerms := make(map[int64]access_model.Permission)
	repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission

	for i, depMeta := range deps {
		if i < skip || i >= max {
			continue
		}

		// Get the permissions for this repository
		// If the repo ID exists in the map, return the exist permissions
		// else get the permission and add it to the map
		var perm access_model.Permission
		existPerm, ok := repoPerms[depMeta.RepoID]
		if ok {
			perm = existPerm
		} else {
			var err error
			perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
			if err != nil {
				ctx.ServerError("GetUserRepoPermission", err)
				return
			}
			repoPerms[depMeta.RepoID] = perm
		}

		if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
			continue
		}

		depMeta.Issue.Repo = &depMeta.Repository
		issues = append(issues, &depMeta.Issue)
	}

	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
}

// CreateIssueBlocking block the issue given in the body by the issue in path
func CreateIssueBlocking(ctx *context.APIContext) {
	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
	// ---
	// summary: Block the issue given in the body by the issue in path
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: index
	//   in: path
	//   description: index of the issue
	//   type: string
	//   required: true
	// - name: body
	//   in: body
	//   schema:
	//     "$ref": "#/definitions/IssueMeta"
	// responses:
	//   "201":
	//     "$ref": "#/responses/Issue"
	//   "404":
	//     description: the issue does not exist

	dependency := getParamsIssue(ctx)
	if ctx.Written() {
		return
	}

	form := web.GetForm(ctx).(*api.IssueMeta)
	target := getFormIssue(ctx, form)
	if ctx.Written() {
		return
	}

	targetPerm := getPermissionForRepo(ctx, target.Repo)
	if ctx.Written() {
		return
	}

	createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
	if ctx.Written() {
		return
	}

	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
}

// RemoveIssueBlocking unblock the issue given in the body by the issue in path
func RemoveIssueBlocking(ctx *context.APIContext) {
	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
	// ---
	// summary: Unblock the issue given in the body by the issue in path
	// produces:
	// - application/json
	// parameters:
	// - name: owner
	//   in: path
	//   description: owner of the repo
	//   type: string
	//   required: true
	// - name: repo
	//   in: path
	//   description: name of the repo
	//   type: string
	//   required: true
	// - name: index
	//   in: path
	//   description: index of the issue
	//   type: string
	//   required: true
	// - name: body
	//   in: body
	//   schema:
	//     "$ref": "#/definitions/IssueMeta"
	// responses:
	//   "200":
	//     "$ref": "#/responses/Issue"
	//   "404":
	//     "$ref": "#/responses/notFound"

	dependency := getParamsIssue(ctx)
	if ctx.Written() {
		return
	}

	form := web.GetForm(ctx).(*api.IssueMeta)
	target := getFormIssue(ctx, form)
	if ctx.Written() {
		return
	}

	targetPerm := getPermissionForRepo(ctx, target.Repo)
	if ctx.Written() {
		return
	}

	removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
	if ctx.Written() {
		return
	}

	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
}

func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64(":index"))
	if err != nil {
		if issues_model.IsErrIssueNotExist(err) {
			ctx.NotFound("IsErrIssueNotExist", err)
		} else {
			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
		}
		return nil
	}
	issue.Repo = ctx.Repo.Repository
	return issue
}

func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
	var repo *repo_model.Repository
	if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
		if !setting.Service.AllowCrossRepositoryDependencies {
			ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled")
			return nil
		}
		var err error
		repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
		if err != nil {
			if repo_model.IsErrRepoNotExist(err) {
				ctx.NotFound("IsErrRepoNotExist", err)
			} else {
				ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err)
			}
			return nil
		}
	} else {
		repo = ctx.Repo.Repository
	}

	issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index)
	if err != nil {
		if issues_model.IsErrIssueNotExist(err) {
			ctx.NotFound("IsErrIssueNotExist", err)
		} else {
			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
		}
		return nil
	}
	issue.Repo = repo
	return issue
}

func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission {
	if repo.ID == ctx.Repo.Repository.ID {
		return &ctx.Repo.Permission
	}

	perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
		return nil
	}

	return &perm
}

func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
	if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
		// The target's repository doesn't have dependencies enabled
		ctx.NotFound()
		return
	}

	if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
		// We can't write to the target
		ctx.NotFound()
		return
	}

	if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
		// We can't read the dependency
		ctx.NotFound()
		return
	}

	err := issues_model.CreateIssueDependency(ctx, ctx.Doer, target, dependency)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
		return
	}
}

func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
	if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
		// The target's repository doesn't have dependencies enabled
		ctx.NotFound()
		return
	}

	if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
		// We can't write to the target
		ctx.NotFound()
		return
	}

	if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
		// We can't read the dependency
		ctx.NotFound()
		return
	}

	err := issues_model.RemoveIssueDependency(ctx, ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
	if err != nil {
		ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
		return
	}
}
