package issue

import (
	"encoding/csv"
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)

// Priority constants
const (
	priorityLow    = 1
	priorityNormal = 2
	priorityHigh   = 3
	priorityUrgent = 4
)

// Status constants
const (
	statusNew        = 1
	statusInProgress = 2
	statusResolved   = 3
	statusClosed     = 5
	statusRejected   = 6
)

// Tracker constants
const (
	trackerBug       = 1
	trackerFeature   = 2
	trackerSupport   = 3
	trackerDoc       = 4
	trackerTest      = 5
	trackerDuplicate = 6
	trackerQuestion  = 7
)

var priorityNames = map[int]string{
	priorityLow:    "low",
	priorityNormal: "normal",
	priorityHigh:   "high",
	priorityUrgent: "urgent",
}

var statusNames = map[int]string{
	statusNew:        "new",
	statusInProgress: "in-progress",
	statusResolved:   "resolved",
	statusClosed:     "closed",
	statusRejected:   "rejected",
}

var trackerNames = map[int]string{
	trackerBug:       "bug",
	trackerFeature:   "feature",
	trackerSupport:   "support",
	trackerDoc:       "doc",
	trackerTest:      "test",
	trackerDuplicate: "duplicate",
	trackerQuestion:  "question",
}

// Tag name → GitLink tag ID mapping
// Collect IDs from web UI DevTools: change tag → capture PATCH payload → get issue_tag_ids value
var tagIDs = map[string]int{
	"缺陷": 315526,
	"功能": 315527,
	"文档": 315533,
	"重复": 315525,
	"疑问": 315528,
	"支持": 315529,
	"任务": 315530,
	"测试": 315534,
	"协助": 315531,
	"搁置": 315532,
}

// labelNames returns all known tag names from the given mapping.
func labelNames(tags map[string]int) string {
	var names []string
	for name := range tags {
		names = append(names, name)
	}
	return strings.Join(names, ", ")
}

// BatchResult is a single item result in a batch operation.
type BatchResult struct {
	Number string `json:"number" yaml:"number"`
	Action string `json:"action" yaml:"action"`
	Status string `json:"status" yaml:"status"`
	Error  string `json:"error,omitempty" yaml:"error,omitempty"`
}

// BatchSummary is the aggregate result of a batch operation.
type BatchSummary struct {
	Repository string        `json:"repository" yaml:"repository"`
	Action     string        `json:"action" yaml:"action"`
	Value      string        `json:"value,omitempty" yaml:"value,omitempty"`
	DryRun     bool          `json:"dry_run" yaml:"dry_run"`
	Total      int           `json:"total" yaml:"total"`
	Succeeded  int           `json:"succeeded" yaml:"succeeded"`
	Failed     int           `json:"failed" yaml:"failed"`
	Results    []BatchResult `json:"results" yaml:"results"`
}

// ---- batch-close ----

func newBatchCloseShortcut() *common.Shortcut {
	return &common.Shortcut{
		Name:        "batch-close",
		Description: "Close multiple issues by issue numbers or a CSV file",
		Flags: []common.Flag{
			{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers, e.g. 1,2,3"},
			{Name: "from", Usage: "Read issue numbers from a CSV file"},
			{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
		},
		Run: runBatchClose,
	}
}

func runBatchClose(ctx *common.RuntimeContext) error {
	if err := ctx.ResolveOwnerRepo(); err != nil {
		return err
	}
	numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
	if err != nil {
		return err
	}
	if len(numbers) == 0 {
		return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
	}

	dryRun := parseBool(ctx.Arg("dry-run"))
	summary := BatchSummary{
		Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
		Action:     "close",
		DryRun:     dryRun,
		Total:      len(numbers),
		Results:    make([]BatchResult, 0, len(numbers)),
	}

	for _, number := range numbers {
		result := BatchResult{Number: number, Action: "close"}
		if dryRun {
			result.Status = "planned"
			summary.Succeeded++
			summary.Results = append(summary.Results, result)
			continue
		}
		if err := updateIssueField(ctx, number, map[string]interface{}{"status_id": statusClosed}); err != nil {
			result.Status = "failed"
			result.Error = err.Error()
			summary.Failed++
		} else {
			result.Status = "closed"
			summary.Succeeded++
		}
		summary.Results = append(summary.Results, result)
	}

	if err := ctx.OutputData(summary); err != nil {
		return err
	}
	if summary.Failed > 0 {
		return fmt.Errorf("%d of %d issue(s) failed", summary.Failed, summary.Total)
	}
	return nil
}

// ---- batch-status ----

func newBatchStatusShortcut() *common.Shortcut {
	return &common.Shortcut{
		Name:        "batch-status",
		Description: "Change status for multiple issues",
		Flags: []common.Flag{
			{Name: "state", Short: "s", Usage: "Target state: new, in-progress, resolved, closed, rejected", Required: true},
			{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers, e.g. 1,2,3"},
			{Name: "from", Usage: "Read issue numbers from a CSV file"},
			{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
		},
		Run: runBatchStatus,
	}
}

func runBatchStatus(ctx *common.RuntimeContext) error {
	if err := ctx.ResolveOwnerRepo(); err != nil {
		return err
	}
	state := ctx.Arg("state")
	statusID, err := parseStatus(state)
	if err != nil {
		return err
	}

	numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
	if err != nil {
		return err
	}
	if len(numbers) == 0 {
		return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
	}

	dryRun := parseBool(ctx.Arg("dry-run"))
	summary := BatchSummary{
		Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
		Action:     "set-status",
		Value:      state,
		DryRun:     dryRun,
		Total:      len(numbers),
		Results:    make([]BatchResult, 0, len(numbers)),
	}

	for _, number := range numbers {
		result := BatchResult{Number: number, Action: "set-status"}
		if dryRun {
			result.Status = "planned"
			summary.Succeeded++
			summary.Results = append(summary.Results, result)
			continue
		}
		if err := updateIssueField(ctx, number, map[string]interface{}{"status_id": statusID}); err != nil {
			result.Status = "failed"
			result.Error = err.Error()
			summary.Failed++
		} else {
			result.Status = state
			summary.Succeeded++
		}
		summary.Results = append(summary.Results, result)
	}

	if err := ctx.OutputData(summary); err != nil {
		return err
	}
	if summary.Failed > 0 {
		return fmt.Errorf("%d of %d issue(s) failed", summary.Failed, summary.Total)
	}
	return nil
}

// ---- batch-priority ----

func newBatchPriorityShortcut() *common.Shortcut {
	return &common.Shortcut{
		Name:        "batch-priority",
		Description: "Change priority for multiple issues",
		Flags: []common.Flag{
			{Name: "priority", Short: "p", Usage: "Target priority: low, normal, high, urgent", Required: true},
			{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers, e.g. 1,2,3"},
			{Name: "from", Usage: "Read issue numbers from a CSV file"},
			{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
		},
		Run: runBatchPriority,
	}
}

func runBatchPriority(ctx *common.RuntimeContext) error {
	if err := ctx.ResolveOwnerRepo(); err != nil {
		return err
	}
	priority := ctx.Arg("priority")
	priorityID, err := parsePriority(priority)
	if err != nil {
		return err
	}

	numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
	if err != nil {
		return err
	}
	if len(numbers) == 0 {
		return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
	}

	dryRun := parseBool(ctx.Arg("dry-run"))
	summary := BatchSummary{
		Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
		Action:     "set-priority",
		Value:      priority,
		DryRun:     dryRun,
		Total:      len(numbers),
		Results:    make([]BatchResult, 0, len(numbers)),
	}

	for _, number := range numbers {
		result := BatchResult{Number: number, Action: "set-priority"}
		if dryRun {
			result.Status = "planned"
			summary.Succeeded++
			summary.Results = append(summary.Results, result)
			continue
		}
		if err := updateIssueField(ctx, number, map[string]interface{}{"priority_id": priorityID}); err != nil {
			result.Status = "failed"
			result.Error = err.Error()
			summary.Failed++
		} else {
			result.Status = priority
			summary.Succeeded++
		}
		summary.Results = append(summary.Results, result)
	}

	if err := ctx.OutputData(summary); err != nil {
		return err
	}
	if summary.Failed > 0 {
		return fmt.Errorf("%d of %d issue(s) failed", summary.Failed, summary.Total)
	}
	return nil
}

// ---- batch-assign ----

func newBatchAssignShortcut() *common.Shortcut {
	return &common.Shortcut{
		Name:        "batch-assign",
		Description: "Change assignee for multiple issues",
		Flags: []common.Flag{
			{Name: "assignee", Short: "a", Usage: "Assignee login name or user ID", Required: true},
			{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers, e.g. 1,2,3"},
			{Name: "from", Usage: "Read issue numbers from a CSV file"},
			{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
		},
		Run: runBatchAssign,
	}
}

func runBatchAssign(ctx *common.RuntimeContext) error {
	if err := ctx.ResolveOwnerRepo(); err != nil {
		return err
	}
	assignee := ctx.Arg("assignee")

	numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
	if err != nil {
		return err
	}
	if len(numbers) == 0 {
		return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
	}

	dryRun := parseBool(ctx.Arg("dry-run"))
	summary := BatchSummary{
		Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
		Action:     "set-assignee",
		Value:      assignee,
		DryRun:     dryRun,
		Total:      len(numbers),
		Results:    make([]BatchResult, 0, len(numbers)),
	}

	var assigneeID interface{}
	if !dryRun {
		id, err := resolveUserID(ctx, assignee)
		if err != nil {
			return fmt.Errorf("cannot resolve assignee %q: %w", assignee, err)
		}
		assigneeID = id
	}

	for _, number := range numbers {
		result := BatchResult{Number: number, Action: "set-assignee"}
		if dryRun {
			result.Status = "planned"
			summary.Succeeded++
			summary.Results = append(summary.Results, result)
			continue
		}
		if err := updateIssueField(ctx, number, map[string]interface{}{"assigned_to_id": assigneeID}); err != nil {
			result.Status = "failed"
			result.Error = err.Error()
			summary.Failed++
		} else {
			result.Status = "assigned"
			summary.Succeeded++
		}
		summary.Results = append(summary.Results, result)
	}

	if err := ctx.OutputData(summary); err != nil {
		return err
	}
	if summary.Failed > 0 {
		return fmt.Errorf("%d of %d issue(s) failed", summary.Failed, summary.Total)
	}
	return nil
}

// ---- batch-label ----

func newBatchLabelShortcut() *common.Shortcut {
	return &common.Shortcut{
		Name:        "batch-label",
		Description: "Change tracker label for multiple issues",
		Flags: []common.Flag{
			{Name: "label", Short: "l", Usage: "Target label: bug, feature, support, doc, test, duplicate, question", Required: true},
			{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers, e.g. 1,2,3"},
			{Name: "from", Usage: "Read issue numbers from a CSV file"},
			{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
		},
		Run: runBatchLabel,
	}
}

func runBatchLabel(ctx *common.RuntimeContext) error {
	if err := ctx.ResolveOwnerRepo(); err != nil {
		return err
	}
	label := ctx.Arg("label")
	trackerID, err := parseTracker(label)
	if err != nil {
		return err
	}

	numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
	if err != nil {
		return err
	}
	if len(numbers) == 0 {
		return fmt.Errorf("no issue numbers provided; use --numbers 1,2,3 or --from issues.csv")
	}

	dryRun := parseBool(ctx.Arg("dry-run"))
	summary := BatchSummary{
		Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
		Action:     "set-label",
		Value:      label,
		DryRun:     dryRun,
		Total:      len(numbers),
		Results:    make([]BatchResult, 0, len(numbers)),
	}

	for _, number := range numbers {
		result := BatchResult{Number: number, Action: "set-label"}
		if dryRun {
			result.Status = "planned"
			summary.Succeeded++
			summary.Results = append(summary.Results, result)
			continue
		}
		if err := updateIssueField(ctx, number, map[string]interface{}{"tracker_id": trackerID}); err != nil {
			result.Status = "failed"
			result.Error = err.Error()
			summary.Failed++
		} else {
			result.Status = label
			summary.Succeeded++
		}
		summary.Results = append(summary.Results, result)
	}

	if err := ctx.OutputData(summary); err != nil {
		return err
	}
	if summary.Failed > 0 {
		return fmt.Errorf("%d of %d issue(s) failed", summary.Failed, summary.Total)
	}
	return nil
}

// ---- shared helpers ----

// updateIssueField fetches the current issue to preserve subject/description,
// then PATCHes with the given fields merged in.
func updateIssueField(ctx *common.RuntimeContext, number string, fields map[string]interface{}) error {
	current, err := fetchExistingIssue(ctx, number)
	if err != nil {
		return fmt.Errorf("fetch issue #%s: %w", number, err)
	}

	body := map[string]interface{}{
		"subject":     current.Subject,
		"description": current.Description,
	}
	for k, v := range fields {
		body[k] = v
	}

	if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body); err != nil {
		return fmt.Errorf("update issue #%s: %w", number, err)
	}
	return nil
}

// resolveUserID converts a login name to a numeric user ID via the users API.
func resolveUserID(ctx *common.RuntimeContext, login string) (interface{}, error) {
	if id, err := strconv.Atoi(login); err == nil {
		return id, nil
	}

	env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s", login), nil)
	if err != nil {
		return nil, fmt.Errorf("lookup user %q: %w", login, err)
	}
	data, ok := env.Data.(map[string]interface{})
	if !ok {
		return nil, fmt.Errorf("unexpected response for user %q", login)
	}
	idFloat, ok := data["id"].(float64)
	if ok {
		return int(idFloat), nil
	}
	userIDFloat, ok := data["user_id"].(float64)
	if ok {
		return int(userIDFloat), nil
	}
	return nil, fmt.Errorf("cannot determine user ID for %q", login)
}

func parseStatus(state string) (int, error) {
	switch strings.ToLower(strings.TrimSpace(state)) {
	case "new":
		return statusNew, nil
	case "in-progress", "in_progress", "inprogress":
		return statusInProgress, nil
	case "resolved":
		return statusResolved, nil
	case "closed":
		return statusClosed, nil
	case "rejected":
		return statusRejected, nil
	default:
		if id, err := strconv.Atoi(state); err == nil {
			return id, nil
		}
		return 0, fmt.Errorf("invalid state %q: use new, in-progress, resolved, closed, or rejected", state)
	}
}

func parsePriority(p string) (int, error) {
	switch strings.ToLower(strings.TrimSpace(p)) {
	case "low":
		return priorityLow, nil
	case "normal":
		return priorityNormal, nil
	case "high":
		return priorityHigh, nil
	case "urgent":
		return priorityUrgent, nil
	default:
		if id, err := strconv.Atoi(p); err == nil {
			return id, nil
		}
		return 0, fmt.Errorf("invalid priority %q: use low, normal, high, or urgent", p)
	}
}

func parseTracker(label string) (int, error) {
	switch strings.ToLower(strings.TrimSpace(label)) {
	case "bug":
		return trackerBug, nil
	case "feature":
		return trackerFeature, nil
	case "support":
		return trackerSupport, nil
	case "doc":
		return trackerDoc, nil
	case "test":
		return trackerTest, nil
	case "duplicate":
		return trackerDuplicate, nil
	case "question":
		return trackerQuestion, nil
	default:
		if id, err := strconv.Atoi(label); err == nil {
			return id, nil
		}
		return 0, fmt.Errorf("invalid label %q: use bug, feature, support, doc, test, duplicate, or question", label)
	}
}

// parseLabel converts a label name to its GitLink tag ID.
// tags is the project's name→id mapping from resolveIssueTags.
func parseLabel(name string, tags map[string]int) (int, error) {
	if id, ok := tags[name]; ok && id != 0 {
		return id, nil
	}
	if id, err := strconv.Atoi(name); err == nil {
		return id, nil
	}
	return 0, fmt.Errorf("invalid label %q: not found in project issue tags", name)
}

func collectIssueNumbers(numbersValue, csvPath string) ([]string, error) {
	numbers, err := parseIssueNumbers(numbersValue)
	if err != nil {
		return nil, err
	}
	if csvPath == "" {
		return numbers, nil
	}

	csvNumbers, err := readIssueNumbersFromCSV(csvPath)
	if err != nil {
		return nil, err
	}
	return mergeIssueNumbers(numbers, csvNumbers), nil
}

func parseIssueNumbers(value string) ([]string, error) {
	if strings.TrimSpace(value) == "" {
		return nil, nil
	}
	return normalizeIssueNumbers(strings.Split(value, ","))
}

func readIssueNumbersFromCSV(path string) ([]string, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("read CSV: %w", err)
	}
	defer file.Close()

	reader := csv.NewReader(file)
	reader.TrimLeadingSpace = true
	records, err := reader.ReadAll()
	if err != nil {
		return nil, fmt.Errorf("parse CSV: %w", err)
	}
	if len(records) == 0 {
		return nil, nil
	}

	numberColumn := -1
	startRow := 0
	for i, cell := range records[0] {
		switch strings.ToLower(strings.TrimSpace(cell)) {
		case "number", "issue_number", "project_issues_index":
			numberColumn = i
			startRow = 1
		}
	}
	if numberColumn == -1 {
		numberColumn = 0
	}

	values := make([]string, 0, len(records)-startRow)
	for _, record := range records[startRow:] {
		if numberColumn >= len(record) {
			continue
		}
		values = append(values, record[numberColumn])
	}
	return normalizeIssueNumbers(values)
}

func normalizeIssueNumbers(values []string) ([]string, error) {
	numbers := make([]string, 0, len(values))
	seen := map[string]bool{}
	for _, value := range values {
		number := strings.TrimSpace(value)
		if number == "" {
			continue
		}
		if _, err := strconv.ParseInt(number, 10, 64); err != nil {
			return nil, fmt.Errorf("invalid issue number %q: must be an integer", number)
		}
		if seen[number] {
			continue
		}
		seen[number] = true
		numbers = append(numbers, number)
	}
	return numbers, nil
}

func mergeIssueNumbers(values ...[]string) []string {
	merged := []string{}
	seen := map[string]bool{}
	for _, numbers := range values {
		for _, number := range numbers {
			if seen[number] {
				continue
			}
			seen[number] = true
			merged = append(merged, number)
		}
	}
	return merged
}

func parseBool(value string) bool {
	parsed, err := strconv.ParseBool(strings.TrimSpace(value))
	return err == nil && parsed
}
