// Package webhook provides means to interact with webhooks directly and not
// through the bot API.
package webhook

import (
	"mime/multipart"
	"net/url"
	"strconv"

	"github.com/pkg/errors"

	"github.com/diamondburned/arikawa/api"
	"github.com/diamondburned/arikawa/discord"
	"github.com/diamondburned/arikawa/utils/httputil"
	"github.com/diamondburned/arikawa/utils/json"
)

// DefaultHTTPClient is the httputil.Client used in the helper methods.
var DefaultHTTPClient = httputil.NewClient()

// Client is the client used to interact with a webhook.
type Client struct {
	// Client is the httputil.Client used to call Discord's API.
	*httputil.Client
	// ID is the id of the webhook.
	ID discord.WebhookID
	// Token is the token of the webhook.
	Token string
}

// NewClient creates a new Client using the passed token and id.
func NewClient(id discord.WebhookID, token string) *Client {
	return NewCustomClient(id, token, httputil.NewClient())
}

// NewCustomClient creates a new Client creates a new Client using the passed
// token and id and makes API calls using the passed httputil.Client
func NewCustomClient(id discord.WebhookID, token string, c *httputil.Client) *Client {
	return &Client{
		Client: c,
		ID:     id,
		Token:  token,
	}
}

// Get gets the webhook.
func (c *Client) Get() (*discord.Webhook, error) {
	var w *discord.Webhook
	return w, c.RequestJSON(&w, "GET", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
}

// Modify modifies the webhook.
func (c *Client) Modify(data api.ModifyWebhookData) (*discord.Webhook, error) {
	var w *discord.Webhook
	return w, c.RequestJSON(
		&w, "PATCH",
		api.EndpointWebhooks+c.ID.String()+"/"+c.Token,
		httputil.WithJSONBody(data),
	)
}

// Delete deletes a webhook permanently.
func (c *Client) Delete() error {
	return c.FastRequest("DELETE", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
}

// Execute sends a message to the webhook, but doesn't wait for the message to
// get created. This is generally faster, but only applicable if no further
// interaction is required.
func (c *Client) Execute(data api.ExecuteWebhookData) (err error) {
	_, err = c.execute(data, false)
	return
}

// ExecuteAndWait executes the webhook, and waits for the generated
// discord.Message to be returned.
func (c *Client) ExecuteAndWait(data api.ExecuteWebhookData) (*discord.Message, error) {
	return c.execute(data, true)
}

func (c *Client) execute(data api.ExecuteWebhookData, wait bool) (*discord.Message, error) {
	if data.Content == "" && len(data.Embeds) == 0 && len(data.Files) == 0 {
		return nil, api.ErrEmptyMessage
	}

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

	for i, embed := range data.Embeds {
		if err := embed.Validate(); err != nil {
			return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
		}
	}

	var param = url.Values{}
	if wait {
		param.Set("wait", "true")
	}

	var URL = api.EndpointWebhooks + c.ID.String() + "/" + c.Token + "?" + param.Encode()
	var msg *discord.Message

	if len(data.Files) == 0 {
		// No files, so no need for streaming.
		return msg, c.RequestJSON(&msg, "POST", URL,
			httputil.WithJSONBody(data))
	}

	writer := func(mw *multipart.Writer) error {
		return data.WriteMultipart(mw)
	}

	resp, err := c.MeanwhileMultipart(writer, "POST", URL)
	if err != nil {
		return nil, err
	}

	var body = resp.GetBody()
	defer body.Close()

	if !wait {
		// Since we didn't tell Discord to wait, we have nothing to parse.
		return nil, nil
	}

	return msg, json.DecodeStream(body, &msg)
}

// Get is a shortcut for NewCustomClient(token, id, DefaultHTTPClient).Get().
func Get(id discord.WebhookID, token string) (*discord.Webhook, error) {
	return NewCustomClient(id, token, DefaultHTTPClient).Get()
}

// Modify is a shortcut for
// NewCustomClient(token, id, DefaultHTTPClient).Modify(data).
func Modify(
	id discord.WebhookID, token string, data api.ModifyWebhookData) (*discord.Webhook, error) {

	return NewCustomClient(id, token, DefaultHTTPClient).Modify(data)
}

// Delete is a shortcut for
// NewCustomClient(token, id, DefaultHTTPClient).Delete().
func Delete(id discord.WebhookID, token string) error {
	return NewCustomClient(id, token, DefaultHTTPClient).Delete()
}

// Execute is a shortcut for
// NewCustomClient(token, id, DefaultHTTPClient).Execute(data).
func Execute(id discord.WebhookID, token string, data api.ExecuteWebhookData) error {
	return NewCustomClient(id, token, DefaultHTTPClient).Execute(data)
}

// ExecuteAndWait is a shortcut for
// NewCustomClient(token, id, DefaultHTTPClient).ExecuteAndWait(data).
func ExecuteAndWait(
	id discord.WebhookID, token string, data api.ExecuteWebhookData) (*discord.Message, error) {

	return NewCustomClient(id, token, DefaultHTTPClient).ExecuteAndWait(data)
}