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

package print

import (
	"fmt"
	"strings"

	"gitea.dev/sdk"
)

var ciStatusSymbols = map[gitea.StatusState]string{
	gitea.StatusSuccess: "✓ ",
	gitea.StatusPending: "⏳ ",
	gitea.StatusWarning: "⚠ ",
	gitea.StatusError:   "✘ ",
	gitea.StatusFailure: "❌ ",
}

// PullDetails print an pull rendered to stdout
func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *gitea.CombinedStatus) {
	base := pr.Base.Name
	head := formatPRHead(pr)
	state := formatPRState(pr)

	out := fmt.Sprintf(
		"# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n\n",
		pr.Index,
		pr.Title,
		state,
		pr.Poster.UserName,
		FormatTime(*pr.Created, false),
		base,
		head,
		pr.Body,
	)

	if ciStatus != nil || len(reviews) != 0 || pr.State == gitea.StateOpen {
		out += "---\n"
	}

	out += formatReviews(pr, reviews)

	if ciStatus != nil && len(ciStatus.Statuses) != 0 {
		out += "- CI:\n"
		for _, s := range ciStatus.Statuses {
			symbol := ciStatusSymbols[s.State]
			if s.TargetURL != "" {
				out += fmt.Sprintf("  - %s[**%s**](%s)", symbol, s.Context, s.TargetURL)
			} else {
				out += fmt.Sprintf("  - %s**%s**", symbol, s.Context)
			}
			if s.Description != "" {
				out += fmt.Sprintf(": %s", s.Description)
			}
			out += "\n"
		}
	}

	if pr.State == gitea.StateOpen {
		switch {
		case pr.Mergeable:
			out += "- No Conflicts\n"
		case pr.Draft:
			out += "- Draft (not mergeable until marked ready)\n"
		default:
			out += "- **Conflicting files**\n"
		}
	}

	if pr.AllowMaintainerEdit {
		out += "- Maintainers are allowed to edit\n"
	}

	if pr.HTMLURL != "" {
		out = strings.TrimRight(out, "\n") + "\n\n" + pr.HTMLURL + "\n"
	}

	outputMarkdown(out, getRepoURL(pr.HTMLURL))
}

func formatPRHead(pr *gitea.PullRequest) string {
	head := pr.Head.Name
	if pr.Head.RepoID != pr.Base.RepoID {
		if pr.Head.Repository != nil {
			head = pr.Head.Repository.Owner.UserName + ":" + head
		} else {
			head = "delete:" + head
		}
	}
	return head
}

func formatPRState(pr *gitea.PullRequest) string {
	if pr.Merged != nil {
		return "merged"
	}
	return string(pr.State)
}

func formatCIStatus(ci *gitea.CombinedStatus, machineReadable bool) string {
	if ci == nil || len(ci.Statuses) == 0 {
		return ""
	}
	if machineReadable {
		return string(ci.State)
	}
	items := make([]string, 0, len(ci.Statuses))
	for _, s := range ci.Statuses {
		items = append(items, fmt.Sprintf("%s%s", ciStatusSymbols[s.State], s.Context))
	}
	return strings.Join(items, ", ")
}

func formatReviews(pr *gitea.PullRequest, reviews []*gitea.PullReview) string {
	result := ""
	if len(reviews) == 0 {
		return result
	}

	// deduplicate reviews by user (via review time & userID),
	reviewByUserOrTeam := make(map[string]*gitea.PullReview)
	for _, review := range reviews {
		switch review.State {
		case gitea.ReviewStateApproved,
			gitea.ReviewStateRequestChanges,
			gitea.ReviewStateRequestReview:
			if review.Reviewer != nil {
				if r, ok := reviewByUserOrTeam[fmt.Sprintf("user_%d", review.Reviewer.ID)]; !ok || review.Submitted.After(r.Submitted) {
					reviewByUserOrTeam[fmt.Sprintf("user_%d", review.Reviewer.ID)] = review
				}
			} else if review.ReviewerTeam != nil {
				if r, ok := reviewByUserOrTeam[fmt.Sprintf("team_%d", review.ReviewerTeam.ID)]; !ok || review.Submitted.After(r.Submitted) {
					reviewByUserOrTeam[fmt.Sprintf("team_%d", review.ReviewerTeam.ID)] = review
				}
			}
		}
	}

	// group reviews by type
	reviewByState := make(map[gitea.ReviewStateType][]string)
	for _, r := range reviewByUserOrTeam {
		if r.Reviewer != nil {
			reviewByState[r.State] = append(reviewByState[r.State],
				r.Reviewer.UserName,
			)
		} else if r.ReviewerTeam != nil {
			// only pulls to orgs can have team reviews
			org := pr.Base.Repository.Owner
			reviewByState[r.State] = append(reviewByState[r.State],
				fmt.Sprintf("%s/%s", org.UserName, r.ReviewerTeam.Name),
			)
		}
	}

	// stringify
	for state, user := range reviewByState {
		result += fmt.Sprintf("- %s by @%s\n", state, strings.Join(user, ", @"))
	}
	return result
}

// PullsList prints a listing of pulls
func PullsList(prs []*gitea.PullRequest, output string, fields []string, ciStatuses map[int64]*gitea.CombinedStatus) error {
	return printPulls(prs, output, fields, ciStatuses)
}

// PullFields are all available fields to print with PullsList()
var PullFields = []string{
	"index",
	"state",
	"author",
	"author-id",
	"url",

	"title",
	"body",

	"mergeable",
	"base",
	"base-commit",
	"head",
	"diff",
	"patch",

	"created",
	"updated",
	"deadline",

	"assignees",
	"milestone",
	"labels",
	"comments",
	"ci",
}

func printPulls(pulls []*gitea.PullRequest, output string, fields []string, ciStatuses map[int64]*gitea.CombinedStatus) error {
	labelMap := map[int64]string{}
	printables := make([]printable, len(pulls))
	machineReadable := isMachineReadable(output)

	for i, x := range pulls {
		// pre-serialize labels for performance
		for _, label := range x.Labels {
			if _, ok := labelMap[label.ID]; !ok {
				labelMap[label.ID] = formatLabel(label, !machineReadable, "")
			}
		}
		// store items with printable interface
		printables[i] = &printablePull{x, &labelMap, &ciStatuses}
	}

	t := tableFromItems(fields, printables, machineReadable)
	return t.print(output)
}

type printablePull struct {
	*gitea.PullRequest
	formattedLabels *map[int64]string
	ciStatuses      *map[int64]*gitea.CombinedStatus
}

func (x printablePull) FormatField(field string, machineReadable bool) string {
	switch field {
	case "index":
		return fmt.Sprintf("%d", x.Index)
	case "state":
		return formatPRState(x.PullRequest)
	case "author":
		return formatUserName(x.Poster)
	case "author-id":
		return x.Poster.UserName
	case "url":
		return x.HTMLURL
	case "title":
		return x.Title
	case "body":
		return x.Body
	case "created":
		return FormatTime(*x.Created, machineReadable)
	case "updated":
		return FormatTime(*x.Updated, machineReadable)
	case "deadline":
		if x.Deadline == nil {
			return ""
		}
		return FormatTime(*x.Deadline, machineReadable)
	case "milestone":
		if x.Milestone != nil {
			return x.Milestone.Title
		}
		return ""
	case "labels":
		labels := make([]string, len(x.Labels))
		for i, l := range x.Labels {
			labels[i] = (*x.formattedLabels)[l.ID]
		}
		return strings.Join(labels, " ")
	case "assignees":
		assignees := make([]string, len(x.Assignees))
		for i, a := range x.Assignees {
			assignees[i] = formatUserName(a)
		}
		return strings.Join(assignees, " ")
	case "comments":
		return fmt.Sprintf("%d", x.Comments)
	case "mergeable":
		isMergeable := x.Mergeable && x.State == gitea.StateOpen
		return formatBoolean(isMergeable, !machineReadable)
	case "base":
		return x.Base.Ref
	case "base-commit":
		return x.MergeBase
	case "head":
		return formatPRHead(x.PullRequest)
	case "diff":
		return x.DiffURL
	case "patch":
		return x.PatchURL
	case "ci":
		if x.ciStatuses != nil {
			if ci, ok := (*x.ciStatuses)[x.Index]; ok {
				return formatCIStatus(ci, machineReadable)
			}
		}
		return ""
	}
	return ""
}
