package commands

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/github/hub/git"
	"github.com/github/hub/github"
	"github.com/github/hub/ui"
	"github.com/github/hub/utils"
)

var (
	cmdIssue = &Command{
		Run: listIssues,
		Usage: `
issue [-a <ASSIGNEE>] [-c <CREATOR>] [-@ <USER>] [-s <STATE>] [-f <FORMAT>] [-M <MILESTONE>] [-l <LABELS>] [-d <DATE>] [-o <SORT_KEY> [-^]] [-L <LIMIT>]
issue show [-f <FORMAT>] <NUMBER>
issue create [-oc] [-m <MESSAGE>|-F <FILE>] [--edit] [-a <USERS>] [-M <MILESTONE>] [-l <LABELS>]
issue labels [--color]
`,
		Long: `Manage GitHub issues for the current project.

## Commands:

With no arguments, show a list of open issues.

	* _show_:
		Show an existing issue specified by <NUMBER>.

	* _create_:
		Open an issue in the current project.

	* _labels_:
		List the labels available in this repository.

## Options:
	-a, --assignee=<ASSIGNEE>
		Display only issues assigned to <ASSIGNEE>.

		When opening an issue, this can be a comma-separated list of people to
		assign to the new issue.

	-c, --creator=<CREATOR>
		Display only issues created by <CREATOR>.

	-@, --mentioned=<USER>
		Display only issues mentioning <USER>.

	-s, --state=<STATE>
		Display issues with state <STATE> (default: "open").

	-f, --format=<FORMAT>
		Pretty print the contents of the issues using format <FORMAT> (default:
		"%sC%>(8)%i%Creset  %t%  l%n"). See the "PRETTY FORMATS" section of
		git-log(1) for some additional details on how placeholders are used in
		format. The available placeholders for issues are:

		%I: issue number

		%i: issue number prefixed with "#"

		%U: the URL of this issue

		%S: state (i.e. "open", "closed")

		%sC: set color to red or green, depending on issue state.

		%t: title

		%l: colored labels

		%L: raw, comma-separated labels

		%b: body

		%au: login name of author

		%as: comma-separated list of assignees

		%Mn: milestone number

		%Mt: milestone title

		%NC: number of comments

		%Nc: number of comments wrapped in parentheses, or blank string if zero.

		%cD: created date-only (no time of day)

		%cr: created date, relative

		%ct: created date, UNIX timestamp

		%cI: created date, ISO 8601 format

		%uD: updated date-only (no time of day)

		%ur: updated date, relative

		%ut: updated date, UNIX timestamp

		%uI: updated date, ISO 8601 format

		%n: newline

		%%: a literal %

	-m, --message=<MESSAGE>
		The text up to the first blank line in <MESSAGE> is treated as the issue
		title, and the rest is used as issue description in Markdown format.

		If multiple <MESSAGE> options are given, their values are concatenated as
		separate paragraphs.

	-F, --file=<FILE>
		Read the issue title and description from <FILE>.

	-e, --edit
		Further edit the contents of <FILE> in a text editor before submitting.

	-o, --browse
		Open the new issue in a web browser.

	-c, --copy
		Put the URL of the new issue to clipboard instead of printing it.

	-M, --milestone=<ID>
		Display only issues for a GitHub milestone with id <ID>.

		When opening an issue, add this issue to a GitHub milestone with id <ID>.

	-l, --labels=<LABELS>
		Display only issues with certain labels.

		When opening an issue, add a comma-separated list of labels to this issue.

	-d, --since=<DATE>
		Display only issues updated on or after <DATE> in ISO 8601 format.

	-o, --sort=<SORT_KEY>
		Sort displayed issues by "created" (default), "updated" or "comments".

	-^ --sort-ascending
		Sort by ascending dates instead of descending.

	-L, --limit=<LIMIT>
		Display only the first <LIMIT> issues.

	--include-pulls
		Include pull requests as well as issues.

	--color
		Enable colored output for labels list.
`,
	}

	cmdCreateIssue = &Command{
		Key:   "create",
		Run:   createIssue,
		Usage: "issue create [-o] [-m <MESSAGE>|-F <FILE>] [-a <USERS>] [-M <MILESTONE>] [-l <LABELS>]",
		Long:  "Open an issue in the current project.",
	}

	cmdShowIssue = &Command{
		Key:   "show",
		Run:   showIssue,
		Usage: "issue show <NUMBER>",
		Long:  "Show an issue in the current project.",
	}

	cmdLabel = &Command{
		Key:   "labels",
		Run:   listLabels,
		Usage: "issue labels [--color]",
		Long:  "List the labels available in this repository.",
	}

	flagIssueAssignee,
	flagIssueState,
	flagIssueFormat,
	flagShowIssueFormat,
	flagIssueMilestoneFilter,
	flagIssueCreator,
	flagIssueMentioned,
	flagIssueLabelsFilter,
	flagIssueSince,
	flagIssueSort,
	flagIssueFile string

	flagIssueMessage messageBlocks

	flagIssueEdit,
	flagIssueCopy,
	flagIssueBrowse,
	flagIssueSortAscending bool
	flagIssueIncludePulls bool

	flagIssueMilestone uint64

	flagIssueAssignees,
	flagIssueLabels listFlag

	flagIssueLimit int

	flagLabelsColorize bool
)

func init() {
	cmdShowIssue.Flag.StringVarP(&flagShowIssueFormat, "format", "f", "", "FORMAT")

	cmdCreateIssue.Flag.VarP(&flagIssueMessage, "message", "m", "MESSAGE")
	cmdCreateIssue.Flag.StringVarP(&flagIssueFile, "file", "F", "", "FILE")
	cmdCreateIssue.Flag.Uint64VarP(&flagIssueMilestone, "milestone", "M", 0, "MILESTONE")
	cmdCreateIssue.Flag.VarP(&flagIssueLabels, "label", "l", "LABEL")
	cmdCreateIssue.Flag.VarP(&flagIssueAssignees, "assign", "a", "ASSIGNEE")
	cmdCreateIssue.Flag.BoolVarP(&flagIssueBrowse, "browse", "o", false, "BROWSE")
	cmdCreateIssue.Flag.BoolVarP(&flagIssueCopy, "copy", "c", false, "COPY")
	cmdCreateIssue.Flag.BoolVarP(&flagIssueEdit, "edit", "e", false, "EDIT")

	cmdIssue.Flag.StringVarP(&flagIssueAssignee, "assignee", "a", "", "ASSIGNEE")
	cmdIssue.Flag.StringVarP(&flagIssueState, "state", "s", "", "STATE")
	cmdIssue.Flag.StringVarP(&flagIssueFormat, "format", "f", "%sC%>(8)%i%Creset  %t%  l%n", "FORMAT")
	cmdIssue.Flag.StringVarP(&flagIssueMilestoneFilter, "milestone", "M", "", "MILESTONE")
	cmdIssue.Flag.StringVarP(&flagIssueCreator, "creator", "c", "", "CREATOR")
	cmdIssue.Flag.StringVarP(&flagIssueMentioned, "mentioned", "@", "", "USER")
	cmdIssue.Flag.StringVarP(&flagIssueLabelsFilter, "labels", "l", "", "LABELS")
	cmdIssue.Flag.StringVarP(&flagIssueSince, "since", "d", "", "DATE")
	cmdIssue.Flag.StringVarP(&flagIssueSort, "sort", "o", "created", "SORT_KEY")
	cmdIssue.Flag.BoolVarP(&flagIssueSortAscending, "sort-ascending", "^", false, "SORT_KEY")
	cmdIssue.Flag.BoolVarP(&flagIssueIncludePulls, "include-pulls", "", false, "INCLUDE_PULLS")
	cmdIssue.Flag.IntVarP(&flagIssueLimit, "limit", "L", -1, "LIMIT")

	cmdLabel.Flag.BoolVarP(&flagLabelsColorize, "color", "", false, "COLORIZE")

	cmdIssue.Use(cmdShowIssue)
	cmdIssue.Use(cmdCreateIssue)
	cmdIssue.Use(cmdLabel)
	CmdRunner.Use(cmdIssue)
}

func listIssues(cmd *Command, args *Args) {
	localRepo, err := github.LocalRepo()
	utils.Check(err)

	project, err := localRepo.MainProject()
	utils.Check(err)

	gh := github.NewClient(project.Host)

	if args.Noop {
		ui.Printf("Would request list of issues for %s\n", project)
	} else {
		flagFilters := map[string]string{
			"state":     flagIssueState,
			"assignee":  flagIssueAssignee,
			"milestone": flagIssueMilestoneFilter,
			"creator":   flagIssueCreator,
			"mentioned": flagIssueMentioned,
			"labels":    flagIssueLabelsFilter,
			"sort":      flagIssueSort,
		}
		filters := map[string]interface{}{}
		for flag, filter := range flagFilters {
			if cmd.FlagPassed(flag) {
				filters[flag] = filter
			}
		}

		if flagIssueSortAscending {
			filters["direction"] = "asc"
		} else {
			filters["direction"] = "desc"
		}

		if cmd.FlagPassed("since") {
			if sinceTime, err := time.ParseInLocation("2006-01-02", flagIssueSince, time.Local); err == nil {
				filters["since"] = sinceTime.Format(time.RFC3339)
			} else {
				filters["since"] = flagIssueSince
			}
		}

		issues, err := gh.FetchIssues(project, filters, flagIssueLimit, func(issue *github.Issue) bool {
			return issue.PullRequest == nil || flagIssueIncludePulls
		})
		utils.Check(err)

		maxNumWidth := 0
		for _, issue := range issues {
			if numWidth := len(strconv.Itoa(issue.Number)); numWidth > maxNumWidth {
				maxNumWidth = numWidth
			}
		}

		colorize := ui.IsTerminal(os.Stdout)
		for _, issue := range issues {
			ui.Print(formatIssue(issue, flagIssueFormat, colorize))
		}
	}

	args.NoForward()
}

func formatIssuePlaceholders(issue github.Issue, colorize bool) map[string]string {
	var stateColorSwitch string
	if colorize {
		issueColor := 32
		if issue.State == "closed" {
			issueColor = 31
		}
		stateColorSwitch = fmt.Sprintf("\033[%dm", issueColor)
	}

	var labelStrings []string
	var rawLabels []string
	for _, label := range issue.Labels {
		if !colorize {
			labelStrings = append(labelStrings, fmt.Sprintf(" %s ", label.Name))
			continue
		}
		color, err := utils.NewColor(label.Color)
		if err != nil {
			utils.Check(err)
		}

		labelStrings = append(labelStrings, colorizeLabel(label, color))
		rawLabels = append(rawLabels, label.Name)
	}

	var assignees []string
	for _, assignee := range issue.Assignees {
		assignees = append(assignees, assignee.Login)
	}

	var milestoneNumber, milestoneTitle string
	if issue.Milestone != nil {
		milestoneNumber = fmt.Sprintf("%d", issue.Milestone.Number)
		milestoneTitle = issue.Milestone.Title
	}

	var numCommentsWrapped string
	numComments := fmt.Sprintf("%d", issue.Comments)
	if issue.Comments > 0 {
		numCommentsWrapped = fmt.Sprintf("(%d)", issue.Comments)
	}

	var createdDate, createdAtISO8601, createdAtUnix, createdAtRelative,
		updatedDate, updatedAtISO8601, updatedAtUnix, updatedAtRelative string
	if !issue.CreatedAt.IsZero() {
		createdDate = issue.CreatedAt.Format("02 Jan 2006")
		createdAtISO8601 = issue.CreatedAt.Format(time.RFC3339)
		createdAtUnix = fmt.Sprintf("%d", issue.CreatedAt.Unix())
		createdAtRelative = utils.TimeAgo(issue.CreatedAt)
	}
	if !issue.UpdatedAt.IsZero() {
		updatedDate = issue.UpdatedAt.Format("02 Jan 2006")
		updatedAtISO8601 = issue.UpdatedAt.Format(time.RFC3339)
		updatedAtUnix = fmt.Sprintf("%d", issue.UpdatedAt.Unix())
		updatedAtRelative = utils.TimeAgo(issue.UpdatedAt)
	}

	return map[string]string{
		"I":  fmt.Sprintf("%d", issue.Number),
		"i":  fmt.Sprintf("#%d", issue.Number),
		"U":  issue.HtmlUrl,
		"S":  issue.State,
		"sC": stateColorSwitch,
		"t":  issue.Title,
		"l":  strings.Join(labelStrings, " "),
		"L":  strings.Join(rawLabels, ", "),
		"b":  issue.Body,
		"au": issue.User.Login,
		"as": strings.Join(assignees, ", "),
		"Mn": milestoneNumber,
		"Mt": milestoneTitle,
		"NC": numComments,
		"Nc": numCommentsWrapped,
		"cD": createdDate,
		"cI": createdAtISO8601,
		"ct": createdAtUnix,
		"cr": createdAtRelative,
		"uD": updatedDate,
		"uI": updatedAtISO8601,
		"ut": updatedAtUnix,
		"ur": updatedAtRelative,
	}
}

func formatPullRequestPlaceholders(pr github.PullRequest) map[string]string {
	base := pr.Base.Ref
	head := pr.Head.Label
	if pr.IsSameRepo() {
		head = pr.Head.Ref
	}

	var requestedReviewers []string
	for _, requestedReviewer := range pr.RequestedReviewers {
		requestedReviewers = append(requestedReviewers, requestedReviewer.Login)
	}
	for _, requestedTeam := range pr.RequestedTeams {
		teamSlug := fmt.Sprintf("%s/%s", pr.Base.Repo.Owner.Login, requestedTeam.Slug)
		requestedReviewers = append(requestedReviewers, teamSlug)
	}

	var mergedDate, mergedAtISO8601, mergedAtUnix, mergedAtRelative string
	if !pr.MergedAt.IsZero() {
		mergedDate = pr.MergedAt.Format("02 Jan 2006")
		mergedAtISO8601 = pr.MergedAt.Format(time.RFC3339)
		mergedAtUnix = fmt.Sprintf("%d", pr.MergedAt.Unix())
		mergedAtRelative = utils.TimeAgo(pr.MergedAt)
	}

	return map[string]string{
		"B":  base,
		"H":  head,
		"sB": pr.Base.Sha,
		"sH": pr.Head.Sha,
		"sm": pr.MergeCommitSha,
		"rs": strings.Join(requestedReviewers, ", "),
		"mD": mergedDate,
		"mI": mergedAtISO8601,
		"mt": mergedAtUnix,
		"mr": mergedAtRelative,
	}
}

func formatIssue(issue github.Issue, format string, colorize bool) string {
	placeholders := formatIssuePlaceholders(issue, colorize)
	return ui.Expand(format, placeholders, colorize)
}

func showIssue(cmd *Command, args *Args) {
	issueNumber := cmd.Arg(0)
	if issueNumber == "" {
		utils.Check(fmt.Errorf(cmd.Synopsis()))
	}

	localRepo, err := github.LocalRepo()
	utils.Check(err)

	project, err := localRepo.MainProject()
	utils.Check(err)

	gh := github.NewClient(project.Host)

	var issue = &github.Issue{}
	issue, err = gh.FetchIssue(project, issueNumber)
	utils.Check(err)

	args.NoForward()

	colorize := ui.IsTerminal(os.Stdout)
	if flagShowIssueFormat != "" {
		ui.Print(formatIssue(*issue, flagShowIssueFormat, colorize))
		return
	}

	var closed = ""
	if issue.State != "open" {
		closed = "[CLOSED] "
	}
	commentsList, err := gh.FetchComments(project, issueNumber)
	utils.Check(err)

	ui.Printf("# %s%s\n\n", closed, issue.Title)
	ui.Printf("* created by @%s on %s\n", issue.User.Login, issue.CreatedAt.String())

	if len(issue.Assignees) > 0 {
		var assignees []string
		for _, user := range issue.Assignees {
			assignees = append(assignees, user.Login)
		}
		ui.Printf("* assignees: %s\n", strings.Join(assignees, ", "))
	}

	ui.Printf("\n%s\n", issue.Body)

	if issue.Comments > 0 {
		ui.Printf("\n## Comments:\n")
		for _, comment := range commentsList {
			ui.Printf("\n### comment by @%s on %s\n\n%s\n", comment.User.Login, comment.CreatedAt.String(), comment.Body)
		}
	}

	return
}

func createIssue(cmd *Command, args *Args) {
	localRepo, err := github.LocalRepo()
	utils.Check(err)

	project, err := localRepo.MainProject()
	utils.Check(err)

	gh := github.NewClient(project.Host)

	messageBuilder := &github.MessageBuilder{
		Filename: "ISSUE_EDITMSG",
		Title:    "issue",
	}

	messageBuilder.AddCommentedSection(fmt.Sprintf(`Creating an issue for %s

Write a message for this issue. The first block of
text is the title and the rest is the description.`, project))

	if len(flagIssueMessage) > 0 {
		messageBuilder.Message = flagIssueMessage.String()
		messageBuilder.Edit = flagIssueEdit
	} else if cmd.FlagPassed("file") {
		messageBuilder.Message, err = msgFromFile(flagIssueFile)
		utils.Check(err)
		messageBuilder.Edit = flagIssueEdit
	} else {
		messageBuilder.Edit = true

		workdir, _ := git.WorkdirName()
		if workdir != "" {
			template, err := github.ReadTemplate(github.IssueTemplate, workdir)
			utils.Check(err)
			if template != "" {
				messageBuilder.Message = template
			}
		}

	}

	title, body, err := messageBuilder.Extract()
	utils.Check(err)

	if title == "" {
		utils.Check(fmt.Errorf("Aborting creation due to empty issue title"))
	}

	params := map[string]interface{}{
		"title": title,
		"body":  body,
	}

	if len(flagIssueLabels) > 0 {
		params["labels"] = flagIssueLabels
	}

	if len(flagIssueAssignees) > 0 {
		params["assignees"] = flagIssueAssignees
	}

	if flagIssueMilestone > 0 {
		params["milestone"] = flagIssueMilestone
	}

	args.NoForward()
	if args.Noop {
		ui.Printf("Would create issue `%s' for %s\n", params["title"], project)
	} else {
		issue, err := gh.CreateIssue(project, params)
		utils.Check(err)

		printBrowseOrCopy(args, issue.HtmlUrl, flagIssueBrowse, flagIssueCopy)
	}

	messageBuilder.Cleanup()
}

func listLabels(cmd *Command, args *Args) {
	localRepo, err := github.LocalRepo()
	utils.Check(err)

	project, err := localRepo.MainProject()
	utils.Check(err)

	gh := github.NewClient(project.Host)

	args.NoForward()
	if args.Noop {
		ui.Printf("Would request list of labels for %s\n", project)
		return
	}

	labels, err := gh.FetchLabels(project)
	utils.Check(err)

	for _, label := range labels {
		ui.Print(formatLabel(label, flagLabelsColorize))
	}
}

func formatLabel(label github.IssueLabel, colorize bool) string {
	if colorize {
		if color, err := utils.NewColor(label.Color); err == nil {
			return fmt.Sprintf("%s\n", colorizeLabel(label, color))
		}
	}
	return fmt.Sprintf("%s\n", label.Name)
}

func colorizeLabel(label github.IssueLabel, color *utils.Color) string {
	bgColorCode := utils.RgbToTermColorCode(color)
	return fmt.Sprintf("\033[38;5;%d;48;%sm %s \033[m",
		getSuitableLabelTextColor(color), bgColorCode, label.Name)
}

func getSuitableLabelTextColor(color *utils.Color) int {
	if color.Brightness() < 0.65 {
		return 15 // white text
	}
	return 16 // black text
}
