package internal

import (
	"fmt"
	"github.com/pkg/errors"
	"gitlab.com/pcanilho/go-jira"
	"gopkg.in/yaml.v2"
	"io"
	"strings"
)

func (j *jiraController) CreateIssue(opts *IssueCreationOptions) (*jira.Issue, error) {
	// Fetch object from key
	projectObj, _, err := j.client.Project.GetWithContext(j.ctx, opts.Project)
	if err != nil {
		return nil, errors.Wrapf(err, "unable to find a project ID matching the key [%s]", opts.Project)
	}

	// Type
	if len(strings.TrimSpace(opts.IssueType)) == 0 {
		return nil, fmt.Errorf("an issue type must be specified")
	}

	// Assignee
	var assignee *jira.User
	if len(strings.TrimSpace(opts.Assignee)) > 0 {
		assignee = &jira.User{Name: opts.Assignee}
	}

	// Components
	var components []*jira.Component
	for _, c := range opts.Components {
		components = append(components, &jira.Component{
			Name: c,
		})
	}

	// Priority
	var priority *jira.Priority
	if len(strings.TrimSpace(opts.Priority)) > 0 {
		priority = &jira.Priority{Name: opts.Priority}
	}

	// Set fields
	newIssueFields := &jira.IssueFields{
		Type:        jira.IssueType{Name: opts.IssueType},
		Project:     *projectObj,
		Description: opts.Description,
		Reporter:    &jira.User{Name: j.username},
		Summary:     opts.Summary,
		Unknowns:    opts.CustomFields,
		Labels:      opts.Labels,
		Assignee:    assignee,
		Components:  components,
		Priority:    priority,
	}

	// Create the issue
	issue, resp, err := j.client.Issue.CreateWithContext(j.ctx, &jira.Issue{Fields: newIssueFields})
	if err != nil {
		if resp != nil {
			b, _ := io.ReadAll(resp.Body)
			fmt.Println(string(b))
		}
		return nil, errors.Wrapf(err, "unable to create the Jira ticket with summary [%s]", opts.Summary)
	}

	// If comment was provided
	if len(strings.TrimSpace(opts.Comment)) > 0 {
		if _, err = j.AddCommentToIssue(issue.Key, opts.Comment); err != nil {
			return issue, errors.Wrapf(err, "unable to attach comment to the newly created ticket [%s]", issue.Key)
		}
	}

	// If attachments were provided
	if len(opts.Attachments) > 0 {
		if err = j.UploadAttachmentsToIssue(issue.Key, opts.Attachments...); err != nil {
			return issue, errors.Wrapf(err, "unable to upload attachments to the newly created ticket [%s]", issue.Key)
		}
	}

	return issue, nil
}

func (j *jiraController) SearchIssues(jql string, options *jira.SearchOptions, maxOccurrences int) ([]jira.Issue, error) {
	issues, resp, err := j.client.Issue.SearchWithContext(j.ctx, jql, options)
	if err != nil {
		if resp != nil {
			b, _ := io.ReadAll(resp.Body)
			fmt.Println(string(b))
		}
		return nil, errors.Wrapf(err, "unable to search issues with the provided JQL query [%s]", jql)
	}
	if maxOccurrences > 0 && len(issues) > maxOccurrences {
		return nil, fmt.Errorf("the provided JQL query returned [%d] instead of the desired max occurrence value [%d]",
			len(issues), maxOccurrences)
	}
	return issues, nil
}

func (j *jiraController) CloneIssue(issueID interface{}) (*jira.Issue, error) {
	issue, err := j.getIssueFromIdentifier(issueID)
	if err != nil {
		return nil, err
	}

	createOptions := &IssueCreationOptions{
		Summary:      issue.Fields.Summary,
		Description:  issue.Fields.Description,
		Project:      issue.Fields.Project.Key,
		IssueType:    issue.Fields.Type.Name,
		CustomFields: issue.Fields.Unknowns,
	}

	return j.CreateIssue(createOptions)
}

func (j *jiraController) GetIssue(identifier string, options *jira.GetQueryOptions) (*jira.Issue, error) {
	issue, _, err := j.client.Issue.Get(identifier, options)
	if err != nil {
		return nil, errors.Wrapf(err, "unable to retrieve issue with provided identifier [%s]", identifier)
	}
	return issue, nil
}

func (j *jiraController) AddCommentToIssue(issueID interface{}, comment string) (*jira.Comment, error) {
	issue, err := j.getIssueFromIdentifier(issueID)
	if err != nil {
		return nil, err
	}

	updatedComment, _, err := j.client.Issue.AddCommentWithContext(j.ctx, issue.ID, &jira.Comment{
		Author: jira.User{Name: j.username},
		Body:   comment,
	})
	if err != nil {
		return nil, errors.Wrapf(err, "unable to add comment to the issue [%s]", issueID)
	}
	return updatedComment, nil
}

func (j *jiraController) UpdateIssue(issueID interface{}, data map[string]interface{}) (*jira.Issue, error) {
	if issueID == nil {
		return nil, fmt.Errorf("cannot update a nil instance of the [jira.Issue] object")
	}

	issue, err := j.getIssueFromIdentifier(issueID)
	if err != nil {
		return nil, err
	}

	resp, err := j.client.Issue.UpdateIssueWithContext(j.ctx, issue.ID, mapToPayload(data))
	if err != nil {
		if resp != nil {
			b, _ := io.ReadAll(resp.Body)
			fmt.Println(string(b))
		}
		return nil, errors.Wrapf(err, "unable to update the issue with ID [%v]", issue.ID)
	}
	return issue, nil
}

func (j *jiraController) UploadAttachmentsToIssue(issueID interface{}, attachments ...*IssueAttachment) error {
	issue, err := j.getIssueFromIdentifier(issueID)
	if err != nil {
		return err
	}
	for _, a := range attachments {
		if _, _, err = j.client.Issue.PostAttachmentWithContext(j.ctx, issue.ID, a.Reader, a.Filename); err != nil {
			return errors.Wrapf(err, "unable to upload the attachment [%s]", a.Filename)
		}
	}
	return nil
}

func (j *jiraController) DeleteIssue(issueID interface{}) error {
	issue, err := j.getIssueFromIdentifier(issueID)
	if err != nil {
		return err
	}
	_, err = j.client.Issue.DeleteWithContext(j.ctx, issue.ID)
	if err != nil {
		return errors.Wrapf(err, "unable to delete issue with provided identifier [%s]", issueID)
	}
	return nil
}

// Helpers

func (j *jiraController) getIssueFromIdentifier(issueID interface{}) (*jira.Issue, error) {
	switch ji := issueID.(type) {
	case *jira.Issue:
		return ji, nil
	case string:
		return j.GetIssue(ji, nil)
	}
	return nil, fmt.Errorf("the issue identifier must be given in [string] or [*jira.Issue] format")
}

type uploadPayload struct {
	Upload struct {
		Data map[string]interface{} `yaml:",inline"`
	} `yaml:"update"`
}

type setEntry struct {
	Set string `json:"set"`
}

func mapToPayload(data map[string]interface{}) (out map[string]interface{}) {
	var inter uploadPayload
	inter.Upload.Data = make(map[string]interface{})
	for k, v := range data {
		inter.Upload.Data[k] = []setEntry{{fmt.Sprintf("%v", v)}}
	}
	b, _ := yaml.Marshal(inter)
	_ = yaml.Unmarshal(b, &out)

	return
}