package jira

import (
	"context"
	"encoding/json"
	"io/ioutil"

	"github.com/andygrunwald/go-jira"
	"github.com/juju/errors"
	"github.com/ngaut/log"
)

// Jira defines the client of jira.
type Jira struct {
	conf *Config
	cli  *jira.Client

	fields map[FieldKey]jira.Field
}

// Config defines the config for jira client
type Config struct {
	UserName  string `json:"username" toml:"username"`
	Password  string `json:"password" toml:"password"`
	BaseURL   string `json:"base_url" toml:"base-url"`
	Project   string `json:"project" toml:"project"`
	IssueType string `json:"issue_type" toml:"issue-type"`

	// FieldKeys defines for custom fields
	FieldKeys []FieldKey `json:"field_keys"`
}

// Validate checks the config of jira
func (j *Config) Validate() error {
	if j.UserName == "" {
		return errors.New("username of jira can not be empty")
	}

	if j.Password == "" {
		return errors.New("password of jira can not be empty")
	}

	if j.BaseURL == "" {
		return errors.New("base url of jira can not be empty")
	}

	return nil
}

// FieldKey is string type to represent the customfield ID keys
type FieldKey string

// CreateJiraClient creates a new jira client
func CreateJiraClient(conf *Config) (*Jira, error) {
	log.Infof("Jira Config: %v", conf)

	if err := conf.Validate(); err != nil {
		return nil, errors.Trace(err)
	}

	tp := jira.BasicAuthTransport{
		Username: conf.UserName,
		Password: conf.Password,
	}
	client, err := jira.NewClient(tp.Client(), conf.BaseURL)
	if err != nil {
		log.Errorf("create jira client failed, error: %v", err)
		return nil, errors.Trace(err)
	}

	j := &Jira{
		conf:   conf,
		cli:    client,
		fields: make(map[FieldKey]jira.Field),
	}

	log.Info("start to init jira fields")
	if err := j.initFileds(); err != nil {
		log.Errorf("init jira fields failed, error: %v", err)
		return nil, errors.Trace(err)
	}

	return j, nil
}

// CreateIssue creates a new jira issue
// will replace the issue with the response issue
func (j *Jira) CreateIssue(issue *jira.Issue) error {
	_, resp, err := j.cli.Issue.Create(issue)
	if err != nil {
		data, err1 := ioutil.ReadAll(resp.Body)
		if err1 != nil {
			log.Errorf("could not read the returned data, error: %v", err1)
			return errors.Trace(err)
		}
		defer resp.Body.Close()

		var errMsg interface{}
		if err1 := json.Unmarshal(data, &errMsg); err1 != nil {
			log.Errorf("could not unmarshall the data into struct, error: %v", err1)
			return errors.Trace(err)
		}

		log.Errorf("create issue [%+v] failed, resp: [%+v], error: %v", issue, errMsg, err)

		return errors.Trace(err)
	}

	return nil
}

// UpdateIssue updates the jira issue
// will replace the issue with the response issue
func (j *Jira) UpdateIssue(issue *jira.Issue) error {
	_, _, err := j.cli.Issue.Update(issue)
	if err != nil {
		log.Errorf("update issue [%+v] failed, error: %v", issue, err)
	}
	return nil
}

// GetIssue returns the issue according the issue ID
func (j *Jira) GetIssue(issueID string) (*jira.Issue, error) {
	respIssue, _, err := j.cli.Issue.Get(issueID, &jira.GetQueryOptions{})
	if err != nil {
		log.Errorf("get jira issue %s failed, error: %v", issueID, err)
		return nil, errors.Trace(err)
	}

	return respIssue, nil
}

// SearchIssue finds the jira issue according the query string
func (j *Jira) SearchIssue(jql string) ([]jira.Issue, error) {
	jiraIssue, resp, err := j.cli.Issue.Search(jql, nil)
	if err != nil {
		log.Errorf("search jira issue %s failed, error: %v", jql, err)
		return jiraIssue, errors.Trace(err)
	}
	resp.Body.Close()

	if len(jiraIssue) == 0 {
		log.Errorf("search jira jssue %s failed, error: issue not exists", jql)
		return jiraIssue, errors.New("Issue not exists")
	}

	return jiraIssue, nil
}

func (j *Jira) initFileds() error {
	fields, _, err := j.cli.Field.GetList()
	if err != nil {
		log.Errorf("list jira fields failed, error: %v", err)
		return errors.Trace(err)
	}

	for _, field := range fields {
		if MatchInFields(j.conf.FieldKeys, field.Name) {
			j.fields[FieldKey(field.Name)] = field
		}
	}

	return nil
}

// GetFieldID return the id of the field,
// 'customfield_' prefix, e.g. "customfield_10109"
func (j *Jira) GetFieldID(key FieldKey) (string, error) {
	field, ok := j.fields[key]
	if !ok {
		log.Errorf("field %s not found", key)
		return "", errors.NotFoundf("field %s", key)
	}

	return field.ID, nil
}

// DoAllTransitions does all transitions
func (j *Jira) DoAllTransitions(ctx context.Context, issueID string) error {
	for {
		select {
		case <-ctx.Done():
			return nil
		default:
		}
		trans, _, err := j.cli.Issue.GetTransitions(issueID)
		if err != nil {
			log.Errorf("get %s jira transitions failed, error: %v", issueID, err)
			return errors.Trace(err)
		}
		if len(trans) == 0 {
			break
		}
		if _, err := j.cli.Issue.DoTransition(issueID, trans[0].ID); err != nil {
			log.Errorf("do jira %s transition %s failed,  error: %v",
				issueID, trans[0].ID, err)
			return errors.Trace(err)
		}
	}

	return nil
}

// Config returns the config of the jira
func (j *Jira) Config() *Config {
	return j.conf
}
