package api

import (
	"mime/multipart"
	"strconv"

	"github.com/pkg/errors"

	"github.com/diamondburned/arikawa/v3/discord"
	"github.com/diamondburned/arikawa/v3/utils/json/option"
	"github.com/diamondburned/arikawa/v3/utils/sendpart"
)

var EndpointInteractions = Endpoint + "interactions/"

type InteractionResponseType uint

// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-object-interaction-callback-type
const (
	PongInteraction InteractionResponseType = iota + 1
	_
	_
	MessageInteractionWithSource
	DeferredMessageInteractionWithSource
	DeferredMessageUpdate
	UpdateMessage
	AutocompleteResult
	ModalResponse
)

// InteractionResponseFlags implements flags for an
// InteractionApplicationCommandCallbackData.
//
// Deprecated: use discord.MessageFlags instead.
type InteractionResponseFlags = discord.MessageFlags

// EphemeralMessage specifies whether the message is only visible to the user
// who invoked the Interaction.
//
// Deprecated: use discord.EphemeralMessage instead.
const EphemeralResponse InteractionResponseFlags = discord.EphemeralMessage

type InteractionResponse struct {
	Type InteractionResponseType  `json:"type"`
	Data *InteractionResponseData `json:"data,omitempty"`
}

// NeedsMultipart returns true if the InteractionResponse has files.
func (resp InteractionResponse) NeedsMultipart() bool {
	return resp.Data != nil && resp.Data.NeedsMultipart()
}

func (resp InteractionResponse) WriteMultipart(body *multipart.Writer) error {
	return sendpart.Write(body, resp, resp.Data.Files)
}

// InteractionResponseData is InteractionApplicationCommandCallbackData in the
// official documentation.
type InteractionResponseData struct {
	// Content are the message contents (up to 2000 characters).
	//
	// Required: one of content, file, embeds
	Content option.NullableString `json:"content,omitempty"`
	// TTS is true if this is a TTS message.
	TTS bool `json:"tts,omitempty"`
	// Embeds contains embedded rich content.
	//
	// Required: one of content, file, embeds
	Embeds *[]discord.Embed `json:"embeds,omitempty"`
	// Components is the list of components (such as buttons) to be attached to
	// the message.
	Components *discord.ContainerComponents `json:"components,omitempty"`
	// AllowedMentions are the allowed mentions for the message.
	AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
	// Flags are the interaction application command callback data flags.
	// Only SuppressEmbeds and EphemeralMessage may be set.
	Flags discord.MessageFlags `json:"flags,omitempty"`

	// Files represents a list of files to upload. This will not be
	// JSON-encoded and will only be available through WriteMultipart.
	Files []sendpart.File `json:"-"`

	// Choices are the results to display on autocomplete interaction events.
	//
	// During all other events, this should not be provided.
	Choices AutocompleteChoices `json:"choices,omitempty"`

	// CustomID used with the modal
	CustomID option.NullableString `json:"custom_id,omitempty"`
	// Title is the heading of the modal window
	Title option.NullableString `json:"title,omitempty"`
}

// NeedsMultipart returns true if the InteractionResponseData has files.
func (d InteractionResponseData) NeedsMultipart() bool {
	return len(d.Files) > 0
}

func (d InteractionResponseData) WriteMultipart(body *multipart.Writer) error {
	return sendpart.Write(body, d, d.Files)
}

// AutocompleteChoices are the choices to send back to Discord when sending a
// ApplicationCommandAutocompleteResult interaction response.
//
// The following types implement this interface:
//
//    - AutocompleteStringChoices
//    - AutocompleteIntegerChoices
//    - AutocompleteNumberChoices
//
type AutocompleteChoices interface {
	choices()
}

// AutocompleteStringChoices are string choices to send back to Discord as
// autocomplete results.
type AutocompleteStringChoices []discord.StringChoice

func (c AutocompleteStringChoices) choices() {}

// AutocompleteIntegerChoices are integer choices to send back to Discord as
// autocomplete results.
type AutocompleteIntegerChoices []discord.IntegerChoice

func (c AutocompleteIntegerChoices) choices() {}

// AutocompleteNumberChoices are number choices to send back to Discord as
// autocomplete results.
type AutocompleteNumberChoices []discord.NumberChoice

func (c AutocompleteNumberChoices) choices() {}

// RespondInteraction responds to an incoming interaction. It is also known as
// an "interaction callback".
func (c *Client) RespondInteraction(
	id discord.InteractionID, token string, resp InteractionResponse) error {

	if resp.Data != nil {
		switch resp.Type {
		case MessageInteractionWithSource:
			// A new message is being created, make sure none of the fields
			// are null or empty.
			if (resp.Data.Content == nil || resp.Data.Content.Val == "") &&
				(resp.Data.Embeds == nil || len(*resp.Data.Embeds) == 0) &&
				len(resp.Data.Files) == 0 {
				return ErrEmptyMessage
			}
		case UpdateMessage:
			// A component is being updated. We therefore don't know what
			// fields are filled. The only thing we can check is if content,
			// embeds and files are null.
			if (resp.Data.Content != nil && !resp.Data.Content.Init) &&
				(resp.Data.Embeds != nil && *resp.Data.Embeds == nil) && len(resp.Data.Files) == 0 {
				return ErrEmptyMessage
			}
		}

		if resp.Data.AllowedMentions != nil {
			if err := resp.Data.AllowedMentions.Verify(); err != nil {
				return errors.Wrap(err, "allowedMentions error")
			}
		}

		if resp.Data.Embeds != nil {
			sum := 0
			for i, embed := range *resp.Data.Embeds {
				if err := embed.Validate(); err != nil {
					return errors.Wrap(err, "embed error at "+strconv.Itoa(i))
				}
				sum += embed.Length()
				if sum > 6000 {
					return &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
				}

				(*resp.Data.Embeds)[i] = embed // embed.Validate changes fields
			}
		}
	}

	URL := EndpointInteractions + id.String() + "/" + token + "/callback"
	return sendpart.POST(c.Client, resp, nil, URL)
}

// InteractionResponse returns the initial interaction response.
func (c *Client) InteractionResponse(
	appID discord.AppID, token string) (*discord.Message, error) {

	var m *discord.Message
	return m, c.RequestJSON(
		&m, "GET",
		EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
}

type EditInteractionResponseData struct {
	// Content are the new message contents (up to 2000 characters).
	Content option.NullableString `json:"content,omitempty"`
	// Embeds contains embedded rich content.
	Embeds *[]discord.Embed `json:"embeds,omitempty"`
	// Components contains the new components to attach.
	Components *discord.ContainerComponents `json:"components,omitempty"`
	// AllowedMentions are the allowed mentions for the message.
	AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
	// Attachments are the attached files to keep.
	Attachments *[]discord.Attachment `json:"attachments,omitempty"`

	// Files represents a list of files to upload. This will not be
	// JSON-encoded and will only be available through WriteMultipart.
	Files []sendpart.File `json:"-"`
}

// NeedsMultipart returns true if the SendMessageData has files.
func (data EditInteractionResponseData) NeedsMultipart() bool {
	return len(data.Files) > 0
}

func (data EditInteractionResponseData) WriteMultipart(body *multipart.Writer) error {
	return sendpart.Write(body, data, data.Files)
}

// EditInteractionResponse edits the initial Interaction response.
func (c *Client) EditInteractionResponse(
	appID discord.AppID,
	token string, data EditInteractionResponseData) (*discord.Message, error) {

	if data.AllowedMentions != nil {
		if err := data.AllowedMentions.Verify(); err != nil {
			return nil, errors.Wrap(err, "allowedMentions error")
		}
	}

	if data.Embeds != nil {
		sum := 0
		for i, e := range *data.Embeds {
			if err := e.Validate(); err != nil {
				return nil, errors.Wrap(err, "embed error")
			}
			sum += e.Length()
			if sum > 6000 {
				return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of text in embeds"}
			}

			(*data.Embeds)[i] = e // e.Validate changes fields
		}
	}

	var msg *discord.Message
	return msg, sendpart.PATCH(c.Client, data, &msg,
		EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
}

// DeleteInteractionResponse deletes the initial interaction response.
func (c *Client) DeleteInteractionResponse(appID discord.AppID, token string) error {
	return c.FastRequest("DELETE",
		EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
}

// CreateInteractionFollowup creates a followup message for an interaction.
//
// Deprecated: use FollowUpInteraction instead.
func (c *Client) CreateInteractionFollowup(
	appID discord.AppID, token string, data InteractionResponseData) (*discord.Message, error) {

	return c.FollowUpInteraction(appID, token, data)
}

// FollowUpInteraction creates a followup message for an interaction.
func (c *Client) FollowUpInteraction(
	appID discord.AppID, token string, data InteractionResponseData) (*discord.Message, error) {

	if (data.Content == nil || data.Content.Val == "") &&
		(data.Embeds == nil || len(*data.Embeds) == 0) && len(data.Files) == 0 {
		return nil, ErrEmptyMessage
	}

	if data.AllowedMentions != nil {
		if err := data.AllowedMentions.Verify(); err != nil {
			return nil, errors.Wrap(err, "allowedMentions error")
		}
	}

	if data.Embeds != nil {
		sum := 0
		for i, embed := range *data.Embeds {
			if err := embed.Validate(); err != nil {
				return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
			}
			sum += embed.Length()
			if sum > 6000 {
				return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
			}

			(*data.Embeds)[i] = embed // embed.Validate changes fields
		}
	}

	var msg *discord.Message
	return msg, sendpart.POST(
		c.Client, data, &msg, EndpointWebhooks+appID.String()+"/"+token+"?")
}

func (c *Client) EditInteractionFollowup(
	appID discord.AppID, messageID discord.MessageID,
	token string, data EditInteractionResponseData) (*discord.Message, error) {

	if data.AllowedMentions != nil {
		if err := data.AllowedMentions.Verify(); err != nil {
			return nil, errors.Wrap(err, "allowedMentions error")
		}
	}

	if data.Embeds != nil {
		sum := 0
		for i, e := range *data.Embeds {
			if err := e.Validate(); err != nil {
				return nil, errors.Wrap(err, "embed error")
			}
			sum += e.Length()
			if sum > 6000 {
				return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of text in embeds"}
			}

			(*data.Embeds)[i] = e // e.Validate changes fields
		}
	}

	var msg *discord.Message
	return msg, sendpart.PATCH(c.Client, data, &msg,
		EndpointWebhooks+appID.String()+"/"+token+"/messages/"+messageID.String())
}

// DeleteInteractionFollowup deletes a followup message for an interaction.
func (c *Client) DeleteInteractionFollowup(
	appID discord.AppID, messageID discord.MessageID, token string) error {

	return c.FastRequest("DELETE",
		EndpointWebhooks+appID.String()+"/"+token+"/messages/"+messageID.String())
}