package jira

import (
	"context"
	"encoding/json"
	"maps"
	"net/http"
	"slices"
	"strconv"
	"strings"
)

const separatorMinus = "-"

// EditResponse struct holds response from POST /issue endpoint.
type EditResponse struct {
	ID  string `json:"id"`
	Key string `json:"key"`
}

// EditRequest struct holds request data for edit request.
// Setting an Assignee requires an account ID.
type EditRequest struct {
	IssueType       string
	ParentIssueKey  string
	Summary         string
	Body            string
	Priority        string
	Labels          []string
	Components      []string
	FixVersions     []string
	AffectsVersions []string
	// CustomFields holds all custom fields passed
	// while editing the issue.
	CustomFields map[string]string
	SkipNotify   bool

	configuredCustomFields []IssueTypeField
}

// WithCustomFields sets valid custom fields for the issue.
func (er *EditRequest) WithCustomFields(cf []IssueTypeField) {
	er.configuredCustomFields = cf
}

// Edit updates an issue using POST /issue endpoint.
func (c *Client) Edit(key string, req *EditRequest) error {
	data := getRequestDataForEdit(req)

	body, err := json.Marshal(&data)
	if err != nil {
		return err
	}

	endpoint := "/issue/" + key
	if req.SkipNotify {
		endpoint += "?notifyUsers=false"
	}

	res, err := c.PutV2(context.Background(), endpoint, body, Header{
		"Accept":       "application/json",
		"Content-Type": "application/json",
	})
	if err != nil {
		return err
	}
	if res == nil {
		return ErrEmptyResponse
	}
	defer func() { _ = res.Body.Close() }()

	if res.StatusCode != http.StatusNoContent {
		return formatUnexpectedResponse(res)
	}

	return nil
}

type editFields struct {
	Summary []struct {
		Set string `json:"set,omitempty"`
	} `json:"summary,omitempty"`
	Description []struct {
		Set string `json:"set,omitempty"`
	} `json:"description,omitempty"`
	Priority []struct {
		Set struct {
			Name string `json:"name,omitempty"`
		} `json:"set"`
	} `json:"priority,omitempty"`
	Labels []struct {
		Add    string `json:"add,omitempty"`
		Remove string `json:"remove,omitempty"`
	} `json:"labels,omitempty"`
	Components []struct {
		Add *struct {
			Name string `json:"name,omitempty"`
		} `json:"add,omitempty"`
		Remove *struct {
			Name string `json:"name,omitempty"`
		} `json:"remove,omitempty"`
	} `json:"components,omitempty"`
	FixVersions []struct {
		Add *struct {
			Name string `json:"name,omitempty"`
		} `json:"add,omitempty"`
		Remove *struct {
			Name string `json:"name,omitempty"`
		} `json:"remove,omitempty"`
	} `json:"fixVersions,omitempty"`
	AffectsVersions []struct {
		Add *struct {
			Name string `json:"name,omitempty"`
		} `json:"add,omitempty"`
		Remove *struct {
			Name string `json:"name,omitempty"`
		} `json:"remove,omitempty"`
	} `json:"versions,omitempty"`

	customFields customField
}

type editFieldsMarshaler struct {
	M editFields
}

// MarshalJSON is a custom marshaler to handle empty fields.
func (cfm *editFieldsMarshaler) MarshalJSON() ([]byte, error) {
	if len(cfm.M.Summary) == 0 || cfm.M.Summary[0].Set == "" {
		cfm.M.Summary = nil
	}
	if len(cfm.M.Description) == 0 || cfm.M.Description[0].Set == "" {
		cfm.M.Description = nil
	}
	if len(cfm.M.Priority) == 0 || cfm.M.Priority[0].Set.Name == "" {
		cfm.M.Priority = nil
	}
	if len(cfm.M.Components) == 0 || (cfm.M.Components[0].Add != nil && cfm.M.Components[0].Remove != nil) {
		cfm.M.Components = nil
	}
	if len(cfm.M.Labels) == 0 || (cfm.M.Labels[0].Add == "" && cfm.M.Labels[0].Remove == "") {
		cfm.M.Labels = nil
	}

	m, err := json.Marshal(cfm.M)
	if err != nil {
		return m, err
	}

	var temp any
	if err := json.Unmarshal(m, &temp); err != nil {
		return nil, err
	}
	dm := temp.(map[string]any)

	maps.Copy(dm, cfm.M.customFields)
	return json.Marshal(dm)
}

type editRequest struct {
	Update editFieldsMarshaler `json:"update"`
	Fields struct {
		Parent *struct {
			Key string `json:"key,omitempty"`
			Set string `json:"set,omitempty"`
		} `json:"parent,omitempty"`
	} `json:"fields"`
}

func getRequestDataForEdit(req *EditRequest) *editRequest {
	if req.Labels == nil {
		req.Labels = []string{}
	}

	update := editFieldsMarshaler{editFields{
		Summary: []struct {
			Set string `json:"set,omitempty"`
		}{{Set: req.Summary}},
		Description: []struct {
			Set string `json:"set,omitempty"`
		}{{Set: req.Body}},
		Priority: []struct {
			Set struct {
				Name string `json:"name,omitempty"`
			} `json:"set"`
		}{{Set: struct {
			Name string `json:"name,omitempty"`
		}{Name: req.Priority}}},
	}}

	if len(req.Labels) > 0 {
		add, sub := splitAddAndRemove(req.Labels)

		labels := make([]struct {
			Add    string `json:"add,omitempty"`
			Remove string `json:"remove,omitempty"`
		}, 0, len(req.Labels))

		for _, l := range sub {
			labels = append(labels, struct {
				Add    string `json:"add,omitempty"`
				Remove string `json:"remove,omitempty"`
			}{Remove: l})
		}
		for _, l := range add {
			labels = append(labels, struct {
				Add    string `json:"add,omitempty"`
				Remove string `json:"remove,omitempty"`
			}{Add: l})
		}

		update.M.Labels = labels
	}
	if len(req.Components) > 0 {
		add, sub := splitAddAndRemove(req.Components)

		cmp := make([]struct {
			Add *struct {
				Name string `json:"name,omitempty"`
			} `json:"add,omitempty"`
			Remove *struct {
				Name string `json:"name,omitempty"`
			} `json:"remove,omitempty"`
		}, 0, len(req.Components))

		for _, c := range sub {
			cmp = append(cmp, struct {
				Add *struct {
					Name string `json:"name,omitempty"`
				} `json:"add,omitempty"`
				Remove *struct {
					Name string `json:"name,omitempty"`
				} `json:"remove,omitempty"`
			}{Remove: &struct {
				Name string `json:"name,omitempty"`
			}{Name: c}})
		}
		for _, c := range add {
			cmp = append(cmp, struct {
				Add *struct {
					Name string `json:"name,omitempty"`
				} `json:"add,omitempty"`
				Remove *struct {
					Name string `json:"name,omitempty"`
				} `json:"remove,omitempty"`
			}{Add: &struct {
				Name string `json:"name,omitempty"`
			}{Name: c}})
		}

		update.M.Components = cmp
	}
	if len(req.FixVersions) > 0 {
		add, sub := splitAddAndRemove(req.FixVersions)

		versions := make([]struct {
			Add *struct {
				Name string `json:"name,omitempty"`
			} `json:"add,omitempty"`
			Remove *struct {
				Name string `json:"name,omitempty"`
			} `json:"remove,omitempty"`
		}, 0, len(req.FixVersions))

		for _, v := range sub {
			versions = append(versions, struct {
				Add *struct {
					Name string `json:"name,omitempty"`
				} `json:"add,omitempty"`
				Remove *struct {
					Name string `json:"name,omitempty"`
				} `json:"remove,omitempty"`
			}{Remove: &struct {
				Name string `json:"name,omitempty"`
			}{Name: v}})
		}
		for _, v := range add {
			versions = append(versions, struct {
				Add *struct {
					Name string `json:"name,omitempty"`
				} `json:"add,omitempty"`
				Remove *struct {
					Name string `json:"name,omitempty"`
				} `json:"remove,omitempty"`
			}{Add: &struct {
				Name string `json:"name,omitempty"`
			}{Name: v}})
		}

		update.M.FixVersions = versions
	}

	if len(req.AffectsVersions) > 0 {
		add, sub := splitAddAndRemove(req.AffectsVersions)

		versions := make([]struct {
			Add *struct {
				Name string `json:"name,omitempty"`
			} `json:"add,omitempty"`
			Remove *struct {
				Name string `json:"name,omitempty"`
			} `json:"remove,omitempty"`
		}, 0, len(req.AffectsVersions))

		for _, v := range sub {
			versions = append(versions, struct {
				Add *struct {
					Name string `json:"name,omitempty"`
				} `json:"add,omitempty"`
				Remove *struct {
					Name string `json:"name,omitempty"`
				} `json:"remove,omitempty"`
			}{Remove: &struct {
				Name string `json:"name,omitempty"`
			}{Name: v}})
		}
		for _, v := range add {
			versions = append(versions, struct {
				Add *struct {
					Name string `json:"name,omitempty"`
				} `json:"add,omitempty"`
				Remove *struct {
					Name string `json:"name,omitempty"`
				} `json:"remove,omitempty"`
			}{Add: &struct {
				Name string `json:"name,omitempty"`
			}{Name: v}})
		}

		update.M.AffectsVersions = versions
	}

	fields := struct {
		Parent *struct {
			Key string `json:"key,omitempty"`
			Set string `json:"set,omitempty"`
		} `json:"parent,omitempty"`
	}{
		Parent: &struct {
			Key string `json:"key,omitempty"`
			Set string `json:"set,omitempty"`
		}{},
	}
	if req.ParentIssueKey != "" {
		if req.ParentIssueKey == AssigneeNone {
			fields.Parent.Set = AssigneeNone
		} else {
			fields.Parent.Key = req.ParentIssueKey
		}
	}

	data := editRequest{
		Update: update,
		Fields: fields,
	}
	constructCustomFieldsForEdit(req.CustomFields, req.configuredCustomFields, &data)

	return &data
}

func constructCustomFieldsForEdit(fields map[string]string, configuredFields []IssueTypeField, data *editRequest) {
	if len(fields) == 0 || len(configuredFields) == 0 {
		return
	}

	data.Update.M.customFields = make(customField)

	for key, val := range fields {
		for _, configured := range configuredFields {
			identifier := strings.ReplaceAll(strings.ToLower(strings.TrimSpace(configured.Name)), " ", "-")
			if identifier != strings.ToLower(key) {
				continue
			}

			switch configured.Schema.DataType {
			case customFieldFormatOption:
				data.Update.M.customFields[configured.Key] = []customFieldTypeOptionSet{{Set: customFieldTypeOption{Value: val}}}
			case customFieldFormatProject:
				data.Update.M.customFields[configured.Key] = []customFieldTypeProjectSet{{Set: customFieldTypeProject{Value: val}}}
			case customFieldFormatArray:
				pieces := strings.Split(strings.TrimSpace(val), ",")
				if configured.Schema.Items == customFieldFormatOption {
					items := make([]customFieldTypeOptionAddRemove, 0)
					for _, p := range pieces {
						if strings.HasPrefix(p, separatorMinus) {
							items = append(items, customFieldTypeOptionAddRemove{Remove: &customFieldTypeOption{Value: strings.TrimPrefix(p, separatorMinus)}})
						} else {
							items = append(items, customFieldTypeOptionAddRemove{Add: &customFieldTypeOption{Value: p}})
						}
					}
					data.Update.M.customFields[configured.Key] = items
				} else {
					data.Update.M.customFields[configured.Key] = pieces
				}
			case customFieldFormatNumber:
				num, err := strconv.ParseFloat(val, 64) //nolint:gomnd
				if err != nil {
					// Let Jira API handle data type error for now.
					data.Update.M.customFields[configured.Key] = []customFieldTypeStringSet{{Set: val}}
				} else {
					data.Update.M.customFields[configured.Key] = []customFieldTypeNumberSet{{Set: customFieldTypeNumber(num)}}
				}
			default:
				data.Update.M.customFields[configured.Key] = []customFieldTypeStringSet{{Set: val}}
			}
		}
	}
}

func splitAddAndRemove(input []string) ([]string, []string) {
	add := make([]string, 0, len(input))
	sub := make([]string, 0, len(input))

	for _, inp := range input {
		if strings.HasPrefix(inp, separatorMinus) {
			sub = append(sub, strings.TrimPrefix(inp, separatorMinus))
		}
	}
	for _, inp := range input {
		if !strings.HasPrefix(inp, separatorMinus) && !slices.Contains(sub, inp) {
			add = append(add, inp)
		}
	}

	return add, sub
}
