// Copyright 2018 Unknwon
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.

package clog

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"time"
)

type (
	discordEmbed struct {
		Title       string `json:"title"`
		Description string `json:"description"`
		Timestamp   string `json:"timestamp"`
		Color       int    `json:"color"`
	}

	discordPayload struct {
		Username string          `json:"username,omitempty"`
		Embeds   []*discordEmbed `json:"embeds"`
	}
)

var (
	discordTitles = []string{
		"Tracing",
		"Information",
		"Warning",
		"Error",
		"Fatal",
	}

	discordColors = []int{
		0,        // Trace
		3843043,  // Info
		16761600, // Warn
		13041721, // Error
		9440319,  // Fatal
	}
)

type DiscordConfig struct {
	// Minimum level of messages to be processed.
	Level LEVEL
	// Buffer size defines how many messages can be queued before hangs.
	BufferSize int64
	// Discord webhook URL.
	URL string
	// Username to be shown for the message.
	// Leave empty to use default as set in the Discord.
	Username string
}

type discord struct {
	Adapter

	url      string
	username string
}

func newDiscord() Logger {
	return &discord{
		Adapter: Adapter{
			quitChan: make(chan struct{}),
		},
	}
}

func (d *discord) Level() LEVEL { return d.level }

func (d *discord) Init(v interface{}) error {
	cfg, ok := v.(DiscordConfig)
	if !ok {
		return ErrConfigObject{"DiscordConfig", v}
	}

	if !isValidLevel(cfg.Level) {
		return ErrInvalidLevel{}
	}
	d.level = cfg.Level

	if len(cfg.URL) == 0 {
		return errors.New("URL cannot be empty")
	}
	d.url = cfg.URL
	d.username = cfg.Username

	d.msgChan = make(chan *Message, cfg.BufferSize)
	return nil
}

func (d *discord) ExchangeChans(errorChan chan<- error) chan *Message {
	d.errorChan = errorChan
	return d.msgChan
}

func buildDiscordPayload(username string, msg *Message) (string, error) {
	payload := discordPayload{
		Username: username,
		Embeds: []*discordEmbed{
			{
				Title:       discordTitles[msg.Level],
				Description: msg.Body[8:],
				Timestamp:   time.Now().Format(time.RFC3339),
				Color:       discordColors[msg.Level],
			},
		},
	}
	p, err := json.Marshal(&payload)
	if err != nil {
		return "", err
	}
	return string(p), nil
}

type rateLimitMsg struct {
	RetryAfter int64 `json:"retry_after"`
}

func (d *discord) postMessage(r io.Reader) (int64, error) {
	resp, err := http.Post(d.url, "application/json", r)
	if err != nil {
		return -1, fmt.Errorf("HTTP Post: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode == 429 {
		rlMsg := &rateLimitMsg{}
		if err = json.NewDecoder(resp.Body).Decode(&rlMsg); err != nil {
			return -1, fmt.Errorf("decode rate limit message: %v", err)
		}

		return rlMsg.RetryAfter, nil
	} else if resp.StatusCode/100 != 2 {
		data, _ := ioutil.ReadAll(resp.Body)
		return -1, fmt.Errorf("%s", data)
	}

	return -1, nil
}

func (d *discord) write(msg *Message) {
	payload, err := buildDiscordPayload(d.username, msg)
	if err != nil {
		d.errorChan <- fmt.Errorf("discord: builddiscordPayload: %v", err)
		return
	}

	const RETRY_TIMES = 3
	// Due to discord limit, try at most x times with respect to "retry_after" parameter.
	for i := 1; i <= 3; i++ {
		retryAfter, err := d.postMessage(bytes.NewReader([]byte(payload)))
		if err != nil {
			d.errorChan <- fmt.Errorf("discord: postMessage: %v", err)
			return
		}

		if retryAfter > 0 {
			time.Sleep(time.Duration(retryAfter) * time.Millisecond)
			continue
		}

		return
	}

	d.errorChan <- fmt.Errorf("discord: failed to send message after %d retries", RETRY_TIMES)
}

func (d *discord) Start() {
LOOP:
	for {
		select {
		case msg := <-d.msgChan:
			d.write(msg)
		case <-d.quitChan:
			break LOOP
		}
	}

	for {
		if len(d.msgChan) == 0 {
			break
		}

		d.write(<-d.msgChan)
	}
	d.quitChan <- struct{}{} // Notify the cleanup is done.
}

func (d *discord) Destroy() {
	d.quitChan <- struct{}{}
	<-d.quitChan

	close(d.msgChan)
	close(d.quitChan)
}

func init() {
	Register(DISCORD, newDiscord)
}