//go:generate ${GOBIN}/mockgen -destination jiracli_mock.go -package=client ${GO_MODULE}/infra/jira/client JiraClient
package client

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/config"

	"github.com/andygrunwald/go-jira"
	"github.com/go-resty/resty/v2"
)

type jiraClient struct {
	conf           config.JiraClient
	cli            *jira.Client
	jiraResty      *resty.Client
	customFieldSet CustomFieldSet
}

func NewJiraClient(conf config.JiraClient) (JiraClient, error) {
	if conf.Dummy {
		return dummyJiraClient{}, nil
	}

	tp := jira.BasicAuthTransport{
		Username:  conf.Username,
		Password:  conf.Password,
		Transport: http.DefaultTransport,
	}
	client, err := jira.NewClient(tp.Client(), conf.URL)
	if err != nil {
		return nil, fmt.Errorf("create jira client failed: %s", err)
	}

	jiraResty := resty.New().
		SetBaseURL(conf.URL).
		SetBasicAuth(conf.Username, conf.Password).
		OnAfterResponse(func(c *resty.Client, r *resty.Response) error {
			if !r.IsSuccess() {
				return fmt.Errorf("status: %s, body: %s", r.Status(), string(r.Body()))
			}
			return nil
		})

	customFieldSet := TIDB_JIRA_CUSTOM_FIELD_SET
	if strings.Contains(conf.URL, "tidb-sandbox.atlassian.net") {
		customFieldSet = SANDBOX_JIRA_CUSTOM_FIELD_SET
	}

	return &jiraClient{
		conf:           conf,
		cli:            client,
		jiraResty:      jiraResty,
		customFieldSet: customFieldSet,
	}, nil
}

func (j *jiraClient) Raw() *jira.Client {
	return j.cli
}

func (j *jiraClient) CustomFieldSet() CustomFieldSet {
	return j.customFieldSet
}

func (c *jiraClient) ListVersions(ctx context.Context, projectName string) ([]jira.Version, error) {
	project, _, err := c.cli.Project.GetWithContext(ctx, projectName)
	if err != nil {
		return nil, fmt.Errorf("get project info failed: %s", err)
	}

	return project.Versions, nil
}

func (c *jiraClient) ListProjectComponents(ctx context.Context, projectKey string) ([]ProjectComponent, error) {
	path := "/rest/api/2/project/{projectIdOrKey}/components"
	result := make([]ProjectComponent, 0)
	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetPathParam("projectIdOrKey", projectKey).
		SetResult(&result).
		Get(path)
	if err != nil {
		return nil, fmt.Errorf("list project %s components failed: %s", projectKey, err)
	}
	return result, nil
}

func (c *jiraClient) GetIssue(ctx context.Context, name string, options *jira.GetQueryOptions) (*jira.Issue, error) {
	issue, _, err := c.cli.Issue.GetWithContext(ctx, name, options)
	if err != nil {
		return nil, fmt.Errorf("get ticket %s failed: %s", name, err)
	}
	return issue, nil
}

func (c *jiraClient) ListIssueComments(ctx context.Context, issueKey string, options *ListIssueCommentsOptions) (*ListIssueCommentsResult, error) {
	path := "/rest/api/2/issue/{issueIdOrKey}/comment"
	result := &ListIssueCommentsResult{}
	query := map[string]string{}
	if options != nil {
		if options.StartAt > 0 {
			query["startAt"] = strconv.Itoa(options.StartAt)
		}
		if options.MaxResults > 0 {
			query["maxResults"] = strconv.Itoa(options.MaxResults)
		}
		if options.OrderBy != "" {
			query["orderBy"] = options.OrderBy
		}
	}

	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetPathParam("issueIdOrKey", issueKey).
		SetQueryParams(query).
		SetResult(result).
		Get(path)
	if err != nil {
		return nil, fmt.Errorf("list issue %s comments failed: %s", issueKey, err)
	}
	result.IsLast = result.StartAt+len(result.Comments) >= result.Total
	return result, nil
}

func (c *jiraClient) ListIssueChangelogs(ctx context.Context, issueKey string, options *ListIssueChangelogsOptions) (*ListIssueChangelogsResult, error) {
	result := &ListIssueChangelogsResult{}
	query := map[string]string{}
	if options != nil {
		if options.StartAt > 0 {
			query["startAt"] = strconv.Itoa(options.StartAt)
		}
		if options.MaxResults > 0 {
			query["maxResults"] = strconv.Itoa(options.MaxResults)
		}
	}

	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetPathParam("issueIdOrKey", issueKey).
		SetQueryParams(query).
		SetResult(result).
		Get("/rest/api/3/issue/{issueIdOrKey}/changelog")
	if err != nil {
		return nil, fmt.Errorf("list issue %s changelogs failed: %s", issueKey, err)
	}
	result.IsLast = result.StartAt+len(result.Values) >= result.Total
	return result, nil
}

// FindAtlassianUserByEmail finds the Atlassian account that exactly matches the email.
func FindAtlassianUserByEmail(ctx context.Context, jiraClient *jira.Client, email string) (*jira.User, error) {
	users, response, err := jiraClient.User.FindWithContext(ctx, email)
	if response != nil {
		_ = response.Body.Close()
	}
	if err != nil {
		return nil, fmt.Errorf("find Jira user %s failed: %s", email, err)
	}
	for i := range users {
		if users[i].AccountType == "atlassian" && strings.EqualFold(users[i].EmailAddress, email) {
			return &users[i], nil
		}
	}
	return nil, fmt.Errorf("Jira user %s not found", email)
}

func (c *jiraClient) ResolveAccountIDByEmail(ctx context.Context, email string) (string, error) {
	user, err := FindAtlassianUserByEmail(ctx, c.cli, email)
	if err != nil {
		return "", err
	}
	return user.AccountID, nil
}

func (c *jiraClient) UpdateAssignee(ctx context.Context, name string, assignee string) error {
	user, err := FindAtlassianUserByEmail(ctx, c.cli, assignee)
	if err != nil {
		return err
	}

	response, err := c.cli.Issue.UpdateAssigneeWithContext(ctx, name, &jira.User{AccountID: user.AccountID})
	if response != nil {
		_ = response.Body.Close()
	}
	if err != nil {
		return fmt.Errorf("update issue %s assignee to %s failed: %s", name, assignee, err)
	}
	return nil
}

func (c *jiraClient) UpdateL2Assignee(ctx context.Context, name string, assignee string) error {
	user, err := FindAtlassianUserByEmail(ctx, c.cli, assignee)
	if err != nil {
		return err
	}
	response, err := c.cli.Issue.UpdateIssueWithContext(ctx, name, map[string]interface{}{
		"fields": map[string]interface{}{
			c.customFieldSet.L2Assignee: map[string]string{"accountId": user.AccountID},
		},
	})
	if response != nil {
		_ = response.Body.Close()
	}
	if err != nil {
		return fmt.Errorf("update issue %s L2 assignee to %s failed: %w", name, assignee, err)
	}
	return nil
}

func (c *jiraClient) UpdateIssueStatus(ctx context.Context, name string, status string) error {
	transitions, _, err := c.cli.Issue.GetTransitionsWithContext(ctx, name)
	if err != nil {
		return fmt.Errorf("get issue %s transitions failed: %s", name, err)
	}

	tid := ""
	for _, tran := range transitions {
		if tran.To.Name == status {
			tid = tran.ID
			break
		}
	}
	if tid == "" {
		return fmt.Errorf("can't transition issue %s status to %s", name, status)
	}

	_, err = c.cli.Issue.DoTransitionWithContext(ctx, name, tid)
	if err != nil {
		return fmt.Errorf("transition issue %s status to %s failed: %s", name, status, err)
	}

	return nil
}

func (c *jiraClient) CreateIssue(ctx context.Context, issue *jira.Issue) (*jira.Issue, error) {
	createdIssue, _, err := c.cli.Issue.CreateWithContext(ctx, issue)
	if err != nil {
		return nil, fmt.Errorf("create issue failed: %s", err)
	}
	return createdIssue, nil
}

func (c *jiraClient) AddIssueLabel(ctx context.Context, issueKey, label string) error {
	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetPathParam("issueIdOrKey", issueKey).
		SetBody(map[string]interface{}{
			"update": map[string]interface{}{
				"labels": []map[string]string{{"add": label}},
			},
		}).
		Put("/rest/api/2/issue/{issueIdOrKey}")
	if err != nil {
		return fmt.Errorf("add label to issue %s failed: %s", issueKey, err)
	}
	return nil
}

func (c *jiraClient) RemoveIssueLabel(ctx context.Context, issueKey, label string) error {
	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetPathParam("issueIdOrKey", issueKey).
		SetBody(map[string]interface{}{
			"update": map[string]interface{}{
				"labels": []map[string]string{{"remove": label}},
			},
		}).
		Put("/rest/api/2/issue/{issueIdOrKey}")
	if err != nil {
		return fmt.Errorf("remove label from issue %s failed: %s", issueKey, err)
	}
	return nil
}

func (c *jiraClient) AddInternalComment(ctx context.Context, issueKey, body string) error {
	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetPathParam("issueIdOrKey", issueKey).
		SetBody(map[string]interface{}{"body": body, "public": false}).
		Post("/rest/servicedeskapi/request/{issueIdOrKey}/comment")
	if err != nil {
		return fmt.Errorf("add internal comment to issue %s failed: %s", issueKey, err)
	}
	return nil
}

func (c *jiraClient) CreateIssueLink(ctx context.Context, linkTypeID, outwardIssueKey, inwardIssueKey string) error {
	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetBody(map[string]interface{}{
			"type":         map[string]string{"id": linkTypeID},
			"outwardIssue": map[string]string{"key": outwardIssueKey},
			"inwardIssue":  map[string]string{"key": inwardIssueKey},
		}).
		Post("/rest/api/2/issueLink")
	if err != nil {
		return fmt.Errorf("create issue link from %s to %s failed: %s", outwardIssueKey, inwardIssueKey, err)
	}
	return nil
}

func (c *jiraClient) DownloadAttachment(ctx context.Context, attachmentID string) (io.ReadCloser, error) {
	response, err := c.jiraResty.R().
		SetContext(ctx).
		SetPathParam("attachmentId", attachmentID).
		Get("/rest/api/3/attachment/content/{attachmentId}")
	if err != nil {
		return nil, fmt.Errorf("download attachment %s failed: %s", attachmentID, err)
	}
	return io.NopCloser(bytes.NewReader(response.Body())), nil
}

func (c *jiraClient) UploadAttachment(ctx context.Context, issueKey, filename string, content io.Reader) error {
	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetHeader("X-Atlassian-Token", "no-check").
		SetPathParam("issueIdOrKey", issueKey).
		SetFileReader("file", filename, content).
		Post("/rest/api/2/issue/{issueIdOrKey}/attachments")
	if err != nil {
		return fmt.Errorf("upload attachment %s to issue %s failed: %s", filename, issueKey, err)
	}
	return nil
}

func (c *jiraClient) SearchIssues(ctx context.Context, jql string, options *SearchOptions) (*SearchResults, error) {
	path := "/rest/api/2/search/jql"

	result := &SearchResults{}

	query := map[string]string{
		"jql": jql,
	}
	if options != nil {
		if options.NextPageToken != "" {
			query["nextPageToken"] = options.NextPageToken
		}
		if options.MaxResults > 0 {
			query["maxResults"] = strconv.Itoa(options.MaxResults)
		}
		if len(options.Fields) > 0 {
			query["fields"] = strings.Join(options.Fields, ",")
		}
		if options.Expand != "" {
			query["expand"] = options.Expand
		}
		if options.FailFast {
			query["failFast"] = "true"
		}
	}

	_, err := c.jiraResty.R().
		SetContext(ctx).
		SetQueryParams(query).
		SetResult(result).
		Get(path)
	if err != nil {
		return nil, fmt.Errorf("search issues failed: %s", err)
	}

	return result, nil
}

type dummyJiraClient struct{}

func (dummyJiraClient) Raw() *jira.Client {
	return nil
}

func (dummyJiraClient) CustomFieldSet() CustomFieldSet {
	return TIDB_JIRA_CUSTOM_FIELD_SET
}

func (dummyJiraClient) ListVersions(ctx context.Context, projectName string) ([]jira.Version, error) {
	return nil, fmt.Errorf("dummy jira client does not support list versions for project %s", projectName)
}

func (dummyJiraClient) ListProjectComponents(ctx context.Context, projectKey string) ([]ProjectComponent, error) {
	return nil, fmt.Errorf("dummy jira client does not support list project %s components", projectKey)
}

func (dummyJiraClient) SearchIssues(ctx context.Context, jql string, options *SearchOptions) (*SearchResults, error) {
	return nil, fmt.Errorf("dummy jira client does not support search issues")
}

func (dummyJiraClient) GetIssue(ctx context.Context, name string, options *jira.GetQueryOptions) (*jira.Issue, error) {
	return nil, fmt.Errorf("dummy jira client does not support get issue %s", name)
}

func (dummyJiraClient) ListIssueComments(ctx context.Context, issueKey string, options *ListIssueCommentsOptions) (*ListIssueCommentsResult, error) {
	return nil, fmt.Errorf("dummy jira client does not support list issue comments %s", issueKey)
}

func (dummyJiraClient) ListIssueChangelogs(ctx context.Context, issueKey string, options *ListIssueChangelogsOptions) (*ListIssueChangelogsResult, error) {
	return nil, fmt.Errorf("dummy jira client does not support list issue changelogs %s", issueKey)
}

func (dummyJiraClient) ResolveAccountIDByEmail(ctx context.Context, email string) (string, error) {
	return "", fmt.Errorf("dummy jira client does not support resolving user %s", email)
}

func (dummyJiraClient) UpdateAssignee(ctx context.Context, name string, assignee string) error {
	return nil
}

func (dummyJiraClient) UpdateL2Assignee(ctx context.Context, name string, assignee string) error {
	return nil
}

func (dummyJiraClient) UpdateIssueStatus(ctx context.Context, name string, status string) error {
	return nil
}

func (dummyJiraClient) CreateIssue(ctx context.Context, issue *jira.Issue) (*jira.Issue, error) {
	return nil, fmt.Errorf("dummy jira client does not support create issue")
}

func (dummyJiraClient) AddIssueLabel(ctx context.Context, issueKey, label string) error {
	return nil
}

func (dummyJiraClient) RemoveIssueLabel(ctx context.Context, issueKey, label string) error {
	return nil
}

func (dummyJiraClient) AddInternalComment(ctx context.Context, issueKey, body string) error {
	return nil
}

func (dummyJiraClient) CreateIssueLink(ctx context.Context, linkTypeID, outwardIssueKey, inwardIssueKey string) error {
	return nil
}

func (dummyJiraClient) DownloadAttachment(ctx context.Context, attachmentID string) (io.ReadCloser, error) {
	return nil, fmt.Errorf("dummy jira client does not support download attachment %s", attachmentID)
}

func (dummyJiraClient) UploadAttachment(ctx context.Context, issueKey, filename string, content io.Reader) error {
	return nil
}
