package pretix

import (
	"encoding/json"
	"fmt"
	"math/rand"
	"net/http"
	"strconv"
	"strings"
	"time"

	"maunium.net/go/mautrix/event"
)

const (
	eventOrderPlaced	= "pretix.event.order.placed"

	urlIndividualOrder	= "%s/api/v1/organizers/%s/events/%s/orders/%s/"
	urlItem			= "%s/api/v1/organizers/%s/events/%s/items/%d/"
	urlItemCategory		= "%s/api/v1/organizers/%s/events/%s/categories/%d/"
)

var adverbs = []string{" Außerdem ", " Zusätzlich ", " Weiterhin ", " Des Weiteren "}

type categoryBin struct {
	NumFree	uint
	NumPaid	uint
}

func (s Server) orderPlaced(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get("Content-Type") != "application/json" {
		writeStatus(w, http.StatusBadRequest)
		return
	}

	data := struct{
		Action		string
		Code		string
		Event		string
		Organizer	string
	}{}
	decoder := json.NewDecoder(r.Body)

	err := decoder.Decode(&data)
	if err != nil {
		writeStatus(w, http.StatusBadRequest)
		return
	}

	if data.Action != eventOrderPlaced {
		writeStatus(w, http.StatusBadRequest)
		return
	}

	resp, err := s.doGetRequest(s.buildURL(urlIndividualOrder, data.Organizer, data.Event, data.Code))
	if err != nil {
		writeStatus(w, http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		writeStatus(w, http.StatusInternalServerError)
		return
	}

	if resp.Header.Get("Content-Type") != "application/json" {
		writeStatus(w, http.StatusInternalServerError)
		return
	}

	decoder = json.NewDecoder(resp.Body)
	order := struct{
		Code		string
		Datetime	time.Time
		Positions	[]struct{
			Item	uint
			Price	string
		}
		Total		string
	}{}

	err = decoder.Decode(&order)
	if err != nil {
		writeStatus(w, http.StatusInternalServerError)
		return
	}

	result, err := s.db.Exec("INSERT INTO orders VALUES(?);", order.Code)
	if err != nil {
		writeStatus(w, http.StatusInternalServerError)
		return
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		writeStatus(w, http.StatusInternalServerError)
		return
	}

	if rowsAffected > 0 && order.Datetime.Add(72 * time.Hour).After(time.Now()) { // pretix retries failed webhooks for up to three days
		categories := map[string]categoryBin{}

		for _, position := range order.Positions {
			euros, cents, err := parsePrice(position.Price)
			if err != nil {
				continue
			}

			name := s.fetchCategory(data.Organizer, data.Event, position.Item)

			category := categories[name]
			if euros > 0 || cents > 0 {
				category.NumPaid++
			} else {
				category.NumFree++
			}

			categories[name] = category
		}

		var body strings.Builder
		body.WriteString("\U0001f389")

		rng := rand.New(rand.NewSource(time.Now().Unix()))

		adverb := " Es "
		for category, bin := range categories {
			var verb, free, conjunction, paid, noun, preposition, quote, total string
			if bin.NumFree+bin.NumPaid == 1 {
				verb = "wurde "
			} else {
				verb = "wurden "
			}
			if bin.NumFree == 1 {
				free = "ein kostenloses "
			} else if bin.NumFree > 1 {
				free = fmt.Sprint(bin.NumFree, " kostenlose ")
			}
			if bin.NumPaid == 1 {
				paid = "ein bezahltes "
			} else if bin.NumPaid > 1 {
				paid = fmt.Sprint(bin.NumPaid, " bezahlte ")
			}
			if bin.NumFree > 0 && bin.NumPaid > 0 {
				conjunction = "und "
			}
			if bin.NumPaid == 1 || bin.NumPaid == 0 && bin.NumFree == 1 { // match number of adjective immediately before noun
				noun = "Produkt "
			} else {
				noun = "Produkte "
			}
			if category != "" {
				preposition = `aus der Kategorie "`
				quote = `" `
			}
			if totalEuros, totalCents, err := parsePrice(order.Total); err == nil && (totalEuros > 0 || totalCents > 0) {
				var adverb, fraction string
				if bin.NumPaid > 1 {
					adverb = "insgesamt "
				}
				if totalCents > 0 {
					fraction = fmt.Sprintf(",%02d", totalCents)
				}
				total = fmt.Sprint("für ", adverb, totalEuros, fraction, " Euro ")
			}

			body.WriteString(fmt.Sprint(adverb, verb, free, conjunction, paid, noun, preposition, category, quote, total, "bestellt."))

			adverb = adverbs[rng.Intn(len(adverbs))]
		}

		message := event.MessageEventContent{
			MsgType:	event.MsgText,
			Body:		body.String(),
		}

		success := s.matrix.Broadcast(&message)
		if !success {
			writeStatus(w, http.StatusInternalServerError)
		}
	}
}

func (s Server) fetchCategory(organizer, event string, item uint) string {
	resp, err := s.doGetRequest(s.buildURL(urlItem, organizer, event, item))
	if err != nil {
		return ""
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 || resp.Header.Get("Content-Type") != "application/json" {
		return ""
	}

	decoder := json.NewDecoder(resp.Body)
	itemData := struct{
		Category	uint
	}{}

	err = decoder.Decode(&itemData)
	if err != nil || itemData.Category == 0 {
		return ""
	}

	resp, err = s.doGetRequest(s.buildURL(urlItemCategory, organizer, event, itemData.Category))
	if err != nil {
		return ""
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 || resp.Header.Get("Content-Type") != "application/json" {
		return ""
	}

	decoder = json.NewDecoder(resp.Body)
	itemCategory := struct{
		Name		string
	}{}

	err = decoder.Decode(&itemCategory)
	if err != nil {
		return ""
	}

	return itemCategory.Name
}

func parsePrice(price string) (euros uint64, cents uint64, err error) {
	parts := strings.Split(price, ".")

	euros, err = strconv.ParseUint(parts[0], 10, 64)
	if err != nil {
		return 0, 0, err
	}

	if len(parts) > 1 {
		for len(parts[1]) < 2 {
			parts[1] += "0"
		}
		parts[1] = parts[1][:2]

		parts[1] = strings.TrimLeft(parts[1], "0")
		if parts[1] == "" {
			parts[1] = "0"
		}

		cents, err = strconv.ParseUint(parts[1], 10, 64)
		if err != nil {
			return 0, 0, err
		}
	}

	return euros, cents, nil
}
