//go:generate ${GOBIN}/mockgen -destination jira_mock.go -package=jira ${GO_MODULE}/pkg/clients/jira Interface
package jira

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/tidbcloud/jiraport/pkg/clients/engineer"
	"github.com/trivago/tgo/tcontainer"

	jira "github.com/andygrunwald/go-jira"
	"github.com/spf13/viper"
)

type Comment struct {
	Body       string       `json:"body"`
	Properties []Properties `json:"properties"`
}

type Properties struct {
	Key   string `json:"key"`
	Value Value  `json:"value"`
}

type Value struct {
	Internal bool `json:"internal"`
}

type Interface interface {
	AddComment(issueKey string, body string) error

	Assign(issueKey string, assignee string) error
	AssignFBOwner(issueKey string, fbOwner string) error
	AddWatchers(issueID string, userName string) error
	BackfillTenantID(issueKey string, tenantID string) error
	BackfillFBOwnerByEmail(issueKey string, fbOwnerEmail string) error
	BackfillVersion(issueKey string, version string) error
	BackfillClusterIDForTicket(issueKey string, clusterID string) error

	GetJiraUserKeyFromJira(email string) (string, error)

	// UpdateCustomSEE(issueKey string, email string, customField string) error
	UpdateCustomSEE2(issueKey string, email string, customField string) error

	Transition(issueKey string, transitionID string) error
}

type Client struct {
	client           *jira.Client
	engineerClient   *engineer.Client
	logger           *slog.Logger
	fbOwnerFieldName string
	httpClient       *http.Client
}

func NewClient(
	logger *slog.Logger,
	engineerClient *engineer.Client,
) (*Client, error) {
	baseURL := viper.GetString("jira-base-url")
	token := viper.GetString("jira-token")

	var client *jira.Client
	var err error
	if viper.GetString("jira-type") == "jsm" {
		tp := jira.BasicAuthTransport{
			Username: viper.GetString("jira-username"),
			Password: viper.GetString("jira-password"),
		}
		client, err = jira.NewClient(&http.Client{
			Timeout:   15 * time.Second,
			Transport: &tp,
		}, baseURL)
		if err != nil {
			return nil, err
		}
	} else {
		tp := jira.PATAuthTransport{
			Token: token,
		}
		client, err = jira.NewClient(&http.Client{
			Timeout:   15 * time.Second,
			Transport: &tp,
		}, baseURL)
		if err != nil {
			return nil, err
		}
	}

	fbOwnerFieldName := viper.GetString("fbowner-field-name")

	httpCli := &http.Client{Timeout: 30 * time.Second}
	j := &Client{
		logger:           logger,
		client:           client,
		fbOwnerFieldName: fbOwnerFieldName,
		engineerClient:   engineerClient,
		httpClient:       httpCli,
	}

	return j, nil
}

func handleJiraResponse(resp *jira.Response) error {
	if resp != nil && resp.StatusCode/100 != 2 {
		// retry := resp.StatusCode == 500 || resp.StatusCode == 502 || resp.StatusCode == 503 || resp.StatusCode == 504
		body, err := io.ReadAll(resp.Body)
		defer resp.Body.Close()
		if err != nil {
			return fmt.Errorf("JIRA request %s returned status %s, read body failed: %w", resp.Request.URL, resp.Status, err)
		}
		return fmt.Errorf("JIRA request %s returned status %s, body %q", resp.Request.URL, resp.Status, string(body))
	}
	return nil
}

func (j *Client) AddComment(issueKey string, body string) error {
	j.logger.Info("comment added", "issue", issueKey, "comment", body)
	comment := Comment{Body: body}
	if viper.IsSet("internal-comment") {
		comment.Properties = append(comment.Properties, Properties{Key: "sd.public.comment", Value: Value{Internal: false}})
		if viper.GetBool("internal-comment") {
			comment.Properties[0].Value.Internal = true
		}
	}

	commentJSON, err := json.Marshal(comment)
	if err != nil {
		return err
	}
	// _, resp, err := j.client.Issue.AddComment(issueKey, &comment)
	// req, _ := http.NewRequest("POST", viper.GetString("jira-base-url")+fmt.Sprintf("rest/api/2/issue/%s/comment", issueKey), bytes.NewBuffer(commentJson))
	req, err := j.client.NewRawRequest("POST", fmt.Sprintf("rest/api/2/issue/%s/comment", issueKey), bytes.NewBuffer(commentJSON))
	if err != nil {
		return err
	}
	var res interface{}
	resp, err := j.client.Do(req, res)
	if err != nil {
		return fmt.Errorf("JIRA request %s returned status %s, read body failed: %w", resp.Request.URL, resp.Status, err)
	}
	j.logger.Debug("comment added", "key", issueKey)
	return nil
}

func (j *Client) Assign(issueKey string, assignee string) error {
	j.logger.Info("assign issue", "issue", issueKey, "assignee", assignee)
	user, err := j.getUserByName(assignee)
	j.logger.Info("user is selected", "user id", user.AccountID)
	if err != nil {
		return err
	}
	return j.assignUser(issueKey, user)
}

func (j *Client) Transition(issueKey string, transitionID string) error {
	j.logger.Info("transition issue", "issue", issueKey, "transitionID", transitionID)
	return j.transitionIssue(issueKey, transitionID)
}

func (j *Client) AssignFBOwner(issueKey string, fbOwner string) error {
	j.logger.Info("assign fb owner for issue", "issue", issueKey, "fbOwner", fbOwner)
	user, err := j.getUserByName(fbOwner)
	if err != nil {
		return err
	}
	return j.assignFBOwnerByUser(issueKey, user)
}

func (j *Client) AddWatchers(issueID string, userName string) error {
	resp, err := j.client.Issue.AddWatcher(issueID, userName)

	if err != nil {
		return fmt.Errorf("add watcher failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}
func (j *Client) BackfillTenantID(issueKey string, tenantID string) error {
	j.logger.Info("backfill tenant id for issue", "issue", issueKey, "tenantID", tenantID)
	relURL, err := url.Parse("/rest/api/2/issue/" + issueKey)
	if err != nil {
		return fmt.Errorf("parse issue url failed: %w", err)
	}
	updateInfo := strings.NewReader("{\"fields\": {\"customfield_10157\": \"" + tenantID + "\"}" + "}")
	req, err := j.client.NewRawRequest("PUT", relURL.String(), updateInfo)
	if err != nil {
		return fmt.Errorf("create raw request BackfillTenantID failed: %w", err)
	}
	resp, err := j.client.Do(req, nil)
	if err != nil {
		return fmt.Errorf("backfill tenant id failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}

func (j *Client) BackfillFBOwnerByEmail(issueKey string, fbOwnerEmail string) error {
	j.logger.Info("backfill fb owner for issue", "issue", issueKey, "fbOwner", fbOwnerEmail)
	relURL, err := url.Parse("/rest/api/2/issue/" + issueKey)
	if err != nil {
		return fmt.Errorf("parse issue url failed: %w", err)
	}
	jiraUserKey, err := j.searchJiraUserKey(fbOwnerEmail)
	if err != nil {
		return err
	}
	updateJSON, err := j.generateFBOwnerUpdateJSON(fbOwnerEmail, jiraUserKey)
	if err != nil {
		return err
	}
	updateInfo := strings.NewReader(updateJSON)
	req, err := j.client.NewRawRequest("PUT", relURL.String(), updateInfo)
	if err != nil {
		return fmt.Errorf("create raw request BackfillFBOwnerByEmail failed: %w", err)
	}
	resp, err := j.client.Do(req, nil)
	if err != nil {
		return fmt.Errorf("backfill fb owner failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}

func (j *Client) GetJiraUserKeyFromJira(email string) (string, error) {
	return "", nil
}

func (j *Client) BackfillVersion(issueKey string, version string) error {
	relURL, err := url.Parse("/rest/api/2/issue/" + issueKey)
	if err != nil {
		return err
	}
	// MIGRATE RELATED, DONE
	updateInfo := strings.NewReader("{\"fields\": {\"customfield_10147\": \"" + version + "\"}" + "}")
	req, err := j.client.NewRawRequest("PUT", relURL.String(), updateInfo)
	if err != nil {
		return fmt.Errorf("create raw request BackfillVersion failed: %w", err)
	}
	resp, err := j.client.Do(req, nil)
	if err != nil {
		return fmt.Errorf("backfill version failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}

func (j *Client) BackfillClusterIDForTicket(issueKey string, clusterID string) error {
	relURL, err := url.Parse("/rest/api/2/issue/" + issueKey)
	if err != nil {
		return err
	}
	updateInfo := strings.NewReader("{\"fields\": {\"customfield_10088\": \"" + clusterID + "\"}" + "}")
	req, err := j.client.NewRawRequest("PUT", relURL.String(), updateInfo)
	if err != nil {
		return fmt.Errorf("create raw request BackfillClusterID failed: %w", err)
	}
	resp, err := j.client.Do(req, nil)
	if err != nil {
		return fmt.Errorf("backfill cluster id failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}

func (j *Client) assignUser(issueKey string, assignee *jira.User) error {
	resp, err := j.client.Issue.UpdateAssignee(issueKey, assignee)
	if err != nil {
		return fmt.Errorf("assign user failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}

func (j *Client) transitionIssue(issueKey string, transitionID string) error {
	resp, err := j.client.Issue.DoTransition(issueKey, transitionID)
	if err != nil {
		return fmt.Errorf("transition failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}

func (j *Client) assignFBOwnerByUser(issueKey string, fbOwner *jira.User) error {
	relURL, err := url.Parse("/rest/api/2/issue/" + issueKey)
	if err != nil {
		return err
	}
	updateJSON, err := j.generateFBOwnerUpdateJSON(fbOwner.Name, fbOwner.Key)
	if err != nil {
		return err
	}
	updateInfo := strings.NewReader(updateJSON)
	req, err := j.client.NewRawRequest("PUT", relURL.String(), updateInfo)
	if err != nil {
		return fmt.Errorf("create raw request assignFBOwnerByUser failed: %w", err)
	}
	resp, err := j.client.Do(req, nil)
	if err != nil {
		return fmt.Errorf("assign fb owner failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}

// search user by email
func (j *Client) getUserByName(username string) (*jira.User, error) {
	relURL, err := url.Parse("/rest/api/2/user/search")
	if err != nil {
		return nil, err
	}
	param := url.Values{}
	param.Add("query", username)
	relURL.RawQuery = param.Encode()
	req, err := j.client.NewRequest("GET", relURL.String(), nil)
	if err != nil {
		return nil, fmt.Errorf("create raw request failed: %w", err)
	}
	users := make([]jira.User, 0)
	user := &jira.User{}
	resp, err := j.client.Do(req, &users)
	if err != nil {
		return nil, fmt.Errorf("get user failed: %w", err)
	}
	err = handleJiraResponse(resp)
	for _, u := range users {
		if u.AccountType == "atlassian" {
			cp := u
			user = &cp
		}
	}
	if err != nil {
		return nil, err
	}
	return user, nil
}

func (j *Client) searchJiraUserKey(email string) (string, error) {
	fbOwnerInfo, err := j.engineerClient.GetEngineerByEmail(email)
	if err != nil {
		return "", err
	}
	if fbOwnerInfo == nil {
		return "", fmt.Errorf("cannot find user info for the specified email %s", email)
	}

	if fbOwnerInfo.JiraUserKey == "" {
		key, err := j.GetJiraUserKeyFromJira(email)
		if err != nil {
			return "", err
		} else if key == "" {
			return "", fmt.Errorf("cannot find jira user key for email %s", email)
		} else {
			return key, nil
		}
	}
	return fbOwnerInfo.JiraUserKey, nil
}

func (j *Client) generateFBOwnerUpdateJSON(name string, key string) (string, error) {
	userKv := make(map[string]string)
	userKv["name"] = name
	userKv["key"] = key
	jsonBytes, err := json.Marshal(userKv)
	if err != nil {
		return "", err
	}
	jsonStr := string(jsonBytes)
	return "{\"fields\": {\"" + j.fbOwnerFieldName + "\": " + jsonStr + "}" + "}", nil
}

// func (j *Client) UpdateCustomSEE(issueKey string, email string, customField string) error {
// 	relURL, err := url.Parse("/rest/api/2/issue/" + issueKey)
// 	if err != nil {
// 		return err
// 	}

// 	type SetRoleInner struct {
// 		Name string `json:"name"`
// 	}

// 	type SetBody struct {
// 		Set SetRoleInner `json:"set"`
// 	}

// 	type UpdateBody struct {
// 		Update map[string]([]SetBody) `json:"update"`
// 	}

// 	// 更新自定义字段
// 	issueUpdate := &UpdateBody{
// 		Update: make(map[string]([]SetBody)),
// 	}
// 	issueUpdate.Update[customField] = make([]SetBody, 0)
// 	issueUpdate.Update[customField] = append(issueUpdate.Update[customField], SetBody{Set: SetRoleInner{Name: email}})
// 	jsonstring, _ := json.Marshal(issueUpdate)

// 	updateInfo := strings.NewReader(string(jsonstring))
// 	req, err := j.client.NewRawRequest("PUT", relURL.String(), updateInfo)
// 	if err != nil {
// 		return fmt.Errorf("create raw request UpdatecustomSEE failed: %w", err)
// 	}
// 	resp, err := j.client.Do(req, nil)
// 	if err != nil {
// 		return fmt.Errorf("update custom field failed: %w", err)
// 	}
// 	err = handleJiraResponse(resp)
// 	if err != nil {
// 		return err
// 	}
// 	return nil
// }

func (j *Client) UpdateCustomSEE2(issueKey string, email string, customField string) error {
	fieldValue := make(map[string]string)
	if viper.GetString("jira-type") == "jsm" {
		users, resp, err := j.client.User.Find(email)
		if err != nil {
			return fmt.Errorf("find user failed: %w", err)
		}
		err = handleJiraResponse(resp)
		if err != nil {
			return err
		}
		if len(users) == 0 {
			return fmt.Errorf("no user is found for %s", email)
		}

		accountID := users[0].AccountID
		fieldValue["accountId"] = accountID
	} else {
		fieldValue["name"] = email
	}

	unknowns := make(tcontainer.MarshalMap)
	unknowns[customField] = fieldValue
	issueUpdate := &jira.Issue{
		Key: issueKey,
		Fields: &jira.IssueFields{
			Unknowns: unknowns,
		},
	}
	_, resp, err := j.client.Issue.UpdateWithOptions(issueUpdate, nil)
	if err != nil {
		return fmt.Errorf("update custom field failed: %w", err)
	}
	err = handleJiraResponse(resp)
	if err != nil {
		return err
	}
	return nil
}
