package messenger

import (
	"bytes"
	"encoding/json"
	"fmt"
	"image"
	"image/jpeg"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"strings"

	"golang.org/x/xerrors"
)

// AttachmentType is attachment type.
type AttachmentType string
type MessagingType string
type TopElementStyle string
type ImageAspectRatio string

const (
	// DefaultSendAPIVersion is a default Send API version
	DefaultSendAPIVersion = "v2.11"
	// SendMessageURL is API endpoint for sending messages.
	SendMessageURL = "https://graph.facebook.com/%s/me/messages"
	// ThreadControlURL is the API endpoint for passing thread control.
	ThreadControlURL = "https://graph.facebook.com/%s/me/pass_thread_control"
	// InboxPageID is managed by facebook for secondary pass to inbox features: https://developers.facebook.com/docs/messenger-platform/handover-protocol/pass-thread-control
	InboxPageID = 263902037430900

	// ImageAttachment is image attachment type.
	ImageAttachment AttachmentType = "image"
	// AudioAttachment is audio attachment type.
	AudioAttachment AttachmentType = "audio"
	// VideoAttachment is video attachment type.
	VideoAttachment AttachmentType = "video"
	// FileAttachment is file attachment type.
	FileAttachment AttachmentType = "file"

	// ResponseType is response messaging type.
	ResponseType MessagingType = "RESPONSE"
	// UpdateType is update messaging type.
	UpdateType MessagingType = "UPDATE"
	// MessageTagType is message_tag messaging type.
	MessageTagType MessagingType = "MESSAGE_TAG"
	// NonPromotionalSubscriptionType is NON_PROMOTIONAL_SUBSCRIPTION messaging type.
	NonPromotionalSubscriptionType MessagingType = "NON_PROMOTIONAL_SUBSCRIPTION"

	// TopElementStyle is compact.
	CompactTopElementStyle TopElementStyle = "compact"
	// TopElementStyle is large.
	LargeTopElementStyle TopElementStyle = "large"

	// ImageAspectRatio is horizontal (1.91:1). Default.
	HorizontalImageAspectRatio ImageAspectRatio = "horizontal"
	// ImageAspectRatio is square.
	SquareImageAspectRatio ImageAspectRatio = "square"
)

// QueryResponse is the response sent back by Facebook when setting up things
// like greetings or call-to-actions.
type QueryResponse struct {
	Error       *QueryError `json:"error,omitempty"`
	RecipientID string      `json:"recipient_id"`
	MessageID   string      `json:"message_id"`
}

// QueryError is representing an error sent back by Facebook.
type QueryError struct {
	Message      string `json:"message"`
	Type         string `json:"type"`
	Code         int    `json:"code"`
	ErrorSubcode int    `json:"error_subcode"`
	FBTraceID    string `json:"fbtrace_id"`
}

// QueryError implements error.
func (e QueryError) Error() string {
	return e.Message
}

func checkFacebookError(r io.Reader) error {
	var err error

	qr := QueryResponse{}
	decoder := json.NewDecoder(r)
	err = decoder.Decode(&qr)
	if err != nil {
		return NewUnmarshalError(err).WithReader(decoder.Buffered())
	}
	if qr.Error != nil {
		return xerrors.Errorf("facebook error: %w", qr.Error)
	}

	return nil
}

func getFacebookQueryResponse(r io.Reader) (QueryResponse, error) {
	qr := QueryResponse{}
	decoder := json.NewDecoder(r)
	if err := decoder.Decode(&qr); err != nil {
		return qr, NewUnmarshalError(err).WithReader(decoder.Buffered())
	}
	if qr.Error != nil {
		return qr, xerrors.Errorf("facebook error: %w", qr.Error)
	}
	return qr, nil
}

// Response is used for responding to events with messages.
type Response struct {
	token          string
	to             Recipient
	sendAPIVersion string
}

// SetToken is for using DispatchMessage from outside.
func (r *Response) SetToken(token string) {
	r.token = token
}

// Text sends a textual message.
func (r *Response) Text(message string, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
	return r.TextWithReplies(message, nil, messagingType, metadata, tags...)
}

// TextWithReplies sends a textual message with some replies
// messagingType should be one of the following: "RESPONSE","UPDATE","MESSAGE_TAG","NON_PROMOTIONAL_SUBSCRIPTION"
// only supply tags when messagingType == "MESSAGE_TAG"
// (see https://developers.facebook.com/docs/messenger-platform/send-messages#messaging_types for more).
func (r *Response) TextWithReplies(message string, replies []QuickReply, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
	var tag string
	if len(tags) > 0 {
		tag = tags[0]
	}

	m := SendMessage{
		MessagingType: messagingType,
		Recipient:     r.to,
		Message: MessageData{
			Text:         message,
			Attachment:   nil,
			QuickReplies: replies,
			Metadata:     metadata,
		},
		Tag: tag,
	}
	return r.DispatchMessage(&m)
}

// AttachmentWithReplies sends a attachment message with some replies.
func (r *Response) AttachmentWithReplies(attachment *StructuredMessageAttachment, replies []QuickReply, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
	var tag string
	if len(tags) > 0 {
		tag = tags[0]
	}

	m := SendMessage{
		MessagingType: messagingType,
		Recipient:     r.to,
		Message: MessageData{
			Attachment:   attachment,
			QuickReplies: replies,
			Metadata:     metadata,
		},
		Tag: tag,
	}
	return r.DispatchMessage(&m)
}

// Image sends an image.
func (r *Response) Image(im image.Image) (QueryResponse, error) {
	var qr QueryResponse

	imageBytes := new(bytes.Buffer)
	err := jpeg.Encode(imageBytes, im, nil)
	if err != nil {
		return qr, err
	}

	return r.AttachmentData(ImageAttachment, "meme.jpg", "image/jpeg", imageBytes)
}

// Attachment sends an image, sound, video or a regular file to a chat.
func (r *Response) Attachment(dataType AttachmentType, url string, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
	var tag string
	if len(tags) > 0 {
		tag = tags[0]
	}

	m := SendStructuredMessage{
		MessagingType: messagingType,
		Recipient:     r.to,
		Message: StructuredMessageData{
			Metadata: metadata,
			Attachment: StructuredMessageAttachment{
				Type: dataType,
				Payload: StructuredMessagePayload{
					Url: url,
				},
			},
		},
		Tag: tag,
	}
	return r.DispatchMessage(&m)
}

// copied from multipart package.
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")

// copied from multipart package.
func escapeQuotes(s string) string {
	return quoteEscaper.Replace(s)
}

// copied from multipart package with slight changes due to fixed content-type there.
func createFormFile(filename string, w *multipart.Writer, contentType string) (io.Writer, error) {
	h := make(textproto.MIMEHeader)
	h.Set("Content-Disposition",
		fmt.Sprintf(`form-data; name="filedata"; filename="%s"`,
			escapeQuotes(filename)))
	h.Set("Content-Type", contentType)
	return w.CreatePart(h)
}

// AttachmentData sends an image, sound, video or a regular file to a chat via an io.Reader.
func (r *Response) AttachmentData(
	dataType AttachmentType, filename string, contentType string, filedata io.Reader) (QueryResponse, error) {
	var qr QueryResponse

	filedataBytes, err := ioutil.ReadAll(filedata)
	if err != nil {
		return qr, err
	}

	var body bytes.Buffer
	multipartWriter := multipart.NewWriter(&body)
	data, err := createFormFile(filename, multipartWriter, contentType)
	if err != nil {
		return qr, err
	}

	_, err = bytes.NewBuffer(filedataBytes).WriteTo(data)
	if err != nil {
		return qr, err
	}

	multipartWriter.WriteField("recipient", fmt.Sprintf(`{"id":"%v"}`, r.to.ID))
	multipartWriter.WriteField("message", fmt.Sprintf(`{"attachment":{"type":"%v", "payload":{}}}`, dataType))

	req, err := http.NewRequest("POST", fmt.Sprintf(SendMessageURL, r.sendAPIVersion), &body)
	if err != nil {
		return qr, err
	}

	req.URL.RawQuery = "access_token=" + r.token

	req.Header.Set("Content-Type", multipartWriter.FormDataContentType())

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return qr, err
	}
	defer resp.Body.Close()

	return getFacebookQueryResponse(resp.Body)
}

// ButtonTemplate sends a message with the main contents being button elements.
func (r *Response) ButtonTemplate(text string, buttons *[]StructuredMessageButton, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
	var tag string
	if len(tags) > 0 {
		tag = tags[0]
	}

	m := SendStructuredMessage{
		MessagingType: messagingType,
		Recipient:     r.to,
		Message: StructuredMessageData{
			Metadata: metadata,
			Attachment: StructuredMessageAttachment{
				Type: "template",
				Payload: StructuredMessagePayload{
					TemplateType: "button",
					Text:         text,
					Buttons:      buttons,
					Elements:     nil,
				},
			},
		},
		Tag: tag,
	}

	return r.DispatchMessage(&m)
}

// GenericTemplate is a message which allows for structural elements to be sent.
func (r *Response) GenericTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, metadata string, tags ...string) (QueryResponse, error) {
	var tag string
	if len(tags) > 0 {
		tag = tags[0]
	}

	m := SendStructuredMessage{
		MessagingType: messagingType,
		Recipient:     r.to,
		Message: StructuredMessageData{
			Metadata: metadata,
			Attachment: StructuredMessageAttachment{
				Type: "template",
				Payload: StructuredMessagePayload{
					TemplateType: "generic",
					Buttons:      nil,
					Elements:     elements,
				},
			},
		},
		Tag: tag,
	}
	return r.DispatchMessage(&m)
}

// ListTemplate sends a list of elements.
func (r *Response) ListTemplate(elements *[]StructuredMessageElement, messagingType MessagingType, tags ...string) (QueryResponse, error) {
	var tag string
	if len(tags) > 0 {
		tag = tags[0]
	}

	m := SendStructuredMessage{
		MessagingType: messagingType,
		Recipient:     r.to,
		Message: StructuredMessageData{
			Attachment: StructuredMessageAttachment{
				Type: "template",
				Payload: StructuredMessagePayload{
					TopElementStyle: "compact",
					TemplateType:    "list",
					Buttons:         nil,
					Elements:        elements,
				},
			},
		},
		Tag: tag,
	}
	return r.DispatchMessage(&m)
}

// SenderAction sends an info about sender action.
func (r *Response) SenderAction(action SenderAction) (QueryResponse, error) {
	m := SendSenderAction{
		Recipient:    r.to,
		SenderAction: action,
	}
	return r.DispatchMessage(&m)
}

// InstagramReaction sends an info about Instagram reaction.
func (r *Response) InstagramReaction(mid string, action ReactionAction, reaction ...string) (QueryResponse, error) {
	m := SendInstagramReaction{
		Recipient:    r.to,
		SenderAction: action,
		Payload: SenderInstagramReactionPayload{
			MessageID: mid,
		},
	}
	if len(reaction) > 0 {
		m.Payload.Reaction = reaction[0]
	}
	return r.DispatchMessage(&m)
}

// DispatchMessage posts the message to messenger, return the error if there's any.
func (r *Response) DispatchMessage(m interface{}) (QueryResponse, error) {
	var res QueryResponse
	data, err := json.Marshal(m)
	if err != nil {
		return res, err
	}

	req, err := http.NewRequest("POST", fmt.Sprintf(SendMessageURL, r.sendAPIVersion), bytes.NewBuffer(data))
	if err != nil {
		return res, err
	}

	req.Header.Set("Content-Type", "application/json")
	req.URL.RawQuery = "access_token=" + r.token

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return res, err
	}

	defer resp.Body.Close()

	return getFacebookQueryResponse(resp.Body)
}

// PassThreadToInbox Uses Messenger Handover Protocol for live inbox
// https://developers.facebook.com/docs/messenger-platform/handover-protocol/#inbox
func (r *Response) PassThreadToInbox() error {
	p := passThreadControl{
		Recipient:   r.to,
		TargetAppID: InboxPageID,
		Metadata:    "Passing to inbox secondary app",
	}

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

	req, err := http.NewRequest("POST", fmt.Sprintf(ThreadControlURL, r.sendAPIVersion), bytes.NewBuffer(data))
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", "application/json")
	req.URL.RawQuery = "access_token=" + r.token

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	return checkFacebookError(resp.Body)
}

// SendMessage is the information sent in an API request to Facebook.
type SendMessage struct {
	MessagingType MessagingType `json:"messaging_type"`
	Recipient     Recipient     `json:"recipient"`
	Message       MessageData   `json:"message"`
	Tag           string        `json:"tag,omitempty"`
}

// MessageData is a message consisting of text or an attachment, with an additional selection of optional quick replies.
type MessageData struct {
	Text         string                       `json:"text,omitempty"`
	Attachment   *StructuredMessageAttachment `json:"attachment,omitempty"`
	QuickReplies []QuickReply                 `json:"quick_replies,omitempty"`
	Metadata     string                       `json:"metadata,omitempty"`
}

// SendStructuredMessage is a structured message template.
type SendStructuredMessage struct {
	MessagingType MessagingType         `json:"messaging_type"`
	Recipient     Recipient             `json:"recipient"`
	Message       StructuredMessageData `json:"message"`
	Tag           string                `json:"tag,omitempty"`
}

// StructuredMessageData is an attachment sent with a structured message.
type StructuredMessageData struct {
	Attachment StructuredMessageAttachment `json:"attachment"`
	Metadata   string                      `json:"metadata,omitempty"`
}

// StructuredMessageAttachment is the attachment of a structured message.
type StructuredMessageAttachment struct {
	// Type must be template
	Title string         `json:"title,omitempty"`
	URL   string         `json:"url,omitempty"`
	Type  AttachmentType `json:"type"`
	// Payload is the information for the file which was sent in the attachment.
	Payload StructuredMessagePayload `json:"payload"`
}

// StructuredMessagePayload is the actual payload of an attachment.
type StructuredMessagePayload struct {
	// TemplateType must be button, generic or receipt
	TemplateType     string                      `json:"template_type,omitempty"`
	TopElementStyle  TopElementStyle             `json:"top_element_style,omitempty"`
	Text             string                      `json:"text,omitempty"`
	ImageAspectRatio ImageAspectRatio            `json:"image_aspect_ratio,omitempty"`
	Sharable         bool                        `json:"sharable,omitempty"`
	Elements         *[]StructuredMessageElement `json:"elements,omitempty"`
	Buttons          *[]StructuredMessageButton  `json:"buttons,omitempty"`
	Url              string                      `json:"url,omitempty"`
	AttachmentID     string                      `json:"attachment_id,omitempty"`
	ReceiptMessagePayload
}

type ReceiptMessagePayload struct {
	RecipientName string       `json:"recipient_name,omitempty"`
	OrderNumber   string       `json:"order_number,omitempty"`
	Currency      string       `json:"currency,omitempty"`
	PaymentMethod string       `json:"payment_method,omitempty"`
	Timestamp     int64        `json:"timestamp,omitempty"`
	Address       *Address     `json:"address,omitempty"`
	Summary       *Summary     `json:"summary,omitempty"`
	Adjustments   []Adjustment `json:"adjustments,omitempty"`
}

type Address struct {
	Street1    string `json:"street_1,omitempty"`
	Street2    string `json:"street_2,omitempty"`
	City       string `json:"city,omitempty"`
	PostalCode string `json:"postal_code,omitempty"`
	State      string `json:"state,omitempty"`
	Country    string `json:"country,omitempty"`
}

type Summary struct {
	Subtotal     float32 `json:"subtotal,omitempty"`
	ShippingCost float32 `json:"shipping_cost,omitempty"`
	TotalTax     float32 `json:"total_tax,omitempty"`
	TotalCost    float32 `json:"total_cost,omitempty"`
}

type Adjustment struct {
	Name   string  `json:"name,omitempty"`
	Amount float32 `json:"amount,omitempty"`
}

// StructuredMessageElement is a response containing structural elements.
type StructuredMessageElement struct {
	Title         string                     `json:"title"`
	ImageURL      string                     `json:"image_url"`
	ItemURL       string                     `json:"item_url,omitempty"`
	Subtitle      string                     `json:"subtitle"`
	DefaultAction *DefaultAction             `json:"default_action,omitempty"`
	Buttons       *[]StructuredMessageButton `json:"buttons,omitempty"`
	ReceiptMessageElement
}

type ReceiptMessageElement struct {
	Quantity float32 `json:"quantity,omitempty"`
	Price    float32 `json:"price,omitempty"`
	Currency string  `json:"currency,omitempty"`
}

// DefaultAction is a response containing default action properties.
type DefaultAction struct {
	Type                string `json:"type"`
	URL                 string `json:"url,omitempty"`
	WebviewHeightRatio  string `json:"webview_height_ratio,omitempty"`
	MessengerExtensions bool   `json:"messenger_extensions,omitempty"`
	FallbackURL         string `json:"fallback_url,omitempty"`
	WebviewShareButton  string `json:"webview_share_button,omitempty"`
}

// StructuredMessageButton is a response containing buttons.
type StructuredMessageButton struct {
	Type                string                 `json:"type"`
	URL                 string                 `json:"url,omitempty"`
	Title               string                 `json:"title,omitempty"`
	Payload             string                 `json:"payload,omitempty"`
	WebviewHeightRatio  string                 `json:"webview_height_ratio,omitempty"`
	MessengerExtensions bool                   `json:"messenger_extensions,omitempty"`
	FallbackURL         string                 `json:"fallback_url,omitempty"`
	WebviewShareButton  string                 `json:"webview_share_button,omitempty"`
	ShareContents       *StructuredMessageData `json:"share_contents,omitempty"`
}

// SendSenderAction is the information about sender action.
type SendSenderAction struct {
	Recipient    Recipient    `json:"recipient"`
	SenderAction SenderAction `json:"sender_action"`
}

// ReactionAction contains info about reaction action type.
type ReactionAction string

const (
	// ReactionActionReact is used when user added a reaction.
	ReactionActionReact ReactionAction = "react"
	// ReactionActionUnReact is used when user removed a reaction.
	ReactionActionUnReact ReactionAction = "unreact"
)

// SendInstagramReaction is the information about sender action.
type SendInstagramReaction struct {
	Recipient    Recipient                      `json:"recipient"`
	SenderAction ReactionAction                 `json:"sender_action"`
	Payload      SenderInstagramReactionPayload `json:"payload"`
}

// SenderInstagramReactionPayload contains target message ID and reaction name.
type SenderInstagramReactionPayload struct {
	MessageID string `json:"message_id"`
	Reaction  string `json:"reaction"`
}