// Copyright Martin Dosch.
// Use of this source code is governed by the BSD-2-clause
// license that can be found in the LICENSE file.

// TODO: Add logging.
package main

import (
	"context"
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"log/slog"
	"strings"
	"time"

	"github.com/beevik/etree"  // BSD-2-clause
	"github.com/xmppo/go-xmpp" // BSD-3-Clause
)

func getDiscoInfo(client *xmpp.Client, drc chan xmpp.DiscoResult, target string) (dr xmpp.DiscoResult, err error) {
	id := getID()
	c := make(chan xmpp.DiscoResult, defaultBufferSize)
	go getDr(c, drc)
	slog.Info("stanzahandling: requesting disco info")
	_, err = client.RawInformation(client.JID(), target, id, "get",
		fmt.Sprintf("<query xmlns='%s'/>", nsDiscoInfo))
	if err != nil {
		return dr, err
	}
	select {
	case dr = <-c:
		slog.Info("stanzahandling: requesting disco info: received reply")
	case <-time.After(60 * time.Second):
		slog.Info("stanzahandling: requesting disco info: timeout")
		return dr, fmt.Errorf("get disco info: %s didn't reply to disco info request", target)
	}
	return dr, err
}

func getDr(c chan xmpp.DiscoResult, drc chan xmpp.DiscoResult) {
	for {
		dr := <-drc
		c <- dr
		return
	}
}

func getSlot(client *xmpp.Client, slc chan xmpp.Slot, target, request string) (sl xmpp.Slot, err error) {
	id := getID()
	c := make(chan xmpp.Slot, defaultBufferSize)
	go getSl(c, slc)
	slog.Info("stanzahandling: requesting upload slot")
	_, err = client.RawInformation(client.JID(), target, id, "get", request)
	if err != nil {
		return sl, fmt.Errorf("get slot: failed to send iq: %w", err)
	}
	select {
	case sl = <-c:
		slog.Info("stanzahandling: requesting upload slot: received reply")
	case <-time.After(60 * time.Second):
		slog.Info("stanzahandling: requesting upload slot: timeout")
		return sl, fmt.Errorf("get upload slot: %s didn't reply to upload slot request", target)
	}
	return sl, err
}

func getSl(c chan xmpp.Slot, slc chan xmpp.Slot) {
	for {
		sl := <-slc
		c <- sl
		return
	}
}

func getDiscoItems(client *xmpp.Client, disc chan xmpp.DiscoItems, target string) (dis xmpp.DiscoItems, err error) {
	id := getID()
	c := make(chan xmpp.DiscoItems, defaultBufferSize)
	go getDis(target, c, disc)
	slog.Info("stanzahandling: requesting disco items")
	_, err = client.RawInformation(client.JID(), target, id, "get",
		fmt.Sprintf("<query xmlns='%s'/>", nsDiscoItems))
	if err != nil {
		return dis, err
	}
	select {
	case dis = <-c:
		slog.Info("stanzahandling: requesting disco items: received reply")
	case <-time.After(60 * time.Second):
		slog.Info("stanzahandling: requesting disco items: timeout")
		return dis, fmt.Errorf("get disco items: %s didn't reply to disco items request", target)
	}
	return dis, err
}

func getDis(jid string, c chan xmpp.DiscoItems, disc chan xmpp.DiscoItems) {
	for {
		dis := <-disc
		if dis.Jid == jid {
			c <- dis
			return
		}
	}
}

func sendIQ(client *xmpp.Client, iqc chan xmpp.IQ, target string, iQtype string, content string) (xmpp.IQ, error) {
	var iq xmpp.IQ
	id := getID()
	c := make(chan xmpp.IQ, defaultBufferSize)
	go getIQ(id, c, iqc)
	_, err := client.RawInformation(client.JID(), target, id,
		iQtype, content)
	if err != nil {
		return iq, fmt.Errorf("send iq: failed to send iq: %w", err)
	}
	select {
	case iq = <-c:
	case <-time.After(60 * time.Second):
		return iq, fmt.Errorf("send iq: server didn't reply to IQ: %s", content)
	}
	return iq, nil
}

func getIQ(id string, c chan xmpp.IQ, iqc chan xmpp.IQ) {
	for {
		iq := <-iqc
		if iq.ID == id {
			c <- iq
			return
		}
	}
}

func getPresence(from string, c chan xmpp.Presence, prsc chan xmpp.Presence) {
	for {
		p := <-prsc
		if strings.HasPrefix(p.From, from) {
			c <- p
			return
		}
	}
}

func rcvStanzas(client *xmpp.Client, ctx context.Context, iqc chan xmpp.IQ, msgc chan xmpp.Chat, prsc chan xmpp.Presence, disc chan xmpp.DiscoItems, drc chan xmpp.DiscoResult, slc chan xmpp.Slot) {
	type messageError struct {
		XMLName xml.Name `xml:"message-error"`
		Text    string   `xml:"text"`
	}
	var me messageError
	var received interface{}
	r := make(chan interface{}, defaultBufferSize)
	e := make(chan error, defaultBufferSize)
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			default:
			}
			rcv, err := client.Recv()
			if err != nil {
				e <- err
			} else {
				r <- rcv
			}
		}
	}()
	for {
		select {
		case <-ctx.Done():
			return
		case err := <-e:
			switch err {
			case io.EOF, io.ErrUnexpectedEOF:
				return
			case nil:
				continue
			default:
				closeAndExit(client, fmt.Errorf("receive stanzas: %w", err))
				return
			}
		case received = <-r:
		}
		switch v := received.(type) {
		case xmpp.Chat:
			if v.Type == "error" {
				for _, oe := range v.OtherElem {
					err := xml.Unmarshal([]byte("<message-error>"+
						oe.InnerXML+"</message-error>"), &me)
					if err == nil {
						fmt.Printf("%s: %s\n", v.Remote, me.Text)
					}
				}
			}
			msgc <- v
		case xmpp.Presence:
			prsc <- v
		case xmpp.DiscoItems:
			disc <- v
		case xmpp.DiscoResult:
			drc <- v
		case xmpp.Slot:
			slc <- v
		case xmpp.IQ:
			switch v.Type {
			case "get":
				var xmlns *etree.Attr
				iq := etree.NewDocument()
				err := iq.ReadFromBytes(v.Query)
				if err != nil {
					log.Println("Couldn't parse IQ:",
						string(v.Query), err)
				}
				query := iq.SelectElement("query")
				if query != nil {
					xmlns = query.SelectAttr("xmlns")
				}
				if xmlns == nil {
					break
				}
				switch xmlns.Value {
				case nsDiscoInfo:
					root := etree.NewDocument()
					root.WriteSettings.AttrSingleQuote = true
					reply := root.CreateElement("iq")
					reply.CreateAttr("type", "result")
					reply.CreateAttr("from", client.JID())
					reply.CreateAttr("to", v.From)
					reply.CreateAttr("id", v.ID)
					replyQuery := reply.CreateElement("query")
					replyQuery.CreateAttr("xmlns", nsDiscoInfo)
					identity := replyQuery.CreateElement("identity")
					identity.CreateAttr("category", "client")
					identity.CreateAttr("type", "bot")
					identity.CreateAttr("name", "go-sendxmpp")
					feat := replyQuery.CreateElement("feature")
					feat.CreateAttr("var", nsDiscoInfo)
					feat2 := replyQuery.CreateElement("feature")
					feat2.CreateAttr("var", nsVersion)
					xmlString, err := root.WriteToString()
					if err == nil {
						_, err = client.SendOrg(xmlString)
						if err != nil {
							log.Println(err)
						}
					}
				default:
					root := etree.NewDocument()
					root.WriteSettings.AttrSingleQuote = true
					reply := root.CreateElement("iq")
					reply.CreateAttr("type", strError)
					reply.CreateAttr("from", client.JID())
					reply.CreateAttr("to", v.From)
					reply.CreateAttr("id", v.ID)
					errorReply := reply.CreateElement(strError)
					errorReply.CreateAttr("type", "cancel")
					su := errorReply.CreateElement("service-unavailable")
					su.CreateAttr("xmlns", nsXMPPStanzas)
					xmlString, err := root.WriteToString()
					if err == nil {
						_, err = client.SendOrg(xmlString)
						if err != nil {
							log.Println(err)
						}
					}
				}
			case "set":
				root := etree.NewDocument()
				root.WriteSettings.AttrSingleQuote = true
				reply := root.CreateElement("iq")
				reply.CreateAttr("type", strError)
				reply.CreateAttr("from", client.JID())
				reply.CreateAttr("to", v.From)
				reply.CreateAttr("id", v.ID)
				errorReply := reply.CreateElement(strError)
				errorReply.CreateAttr("type", "cancel")
				su := errorReply.CreateElement("service-unavailable")
				su.CreateAttr("xmlns", nsXMPPStanzas)
				xmlString, err := root.WriteToString()
				if err == nil {
					_, err = client.SendOrg(xmlString)
					if err != nil {
						log.Println(err)
					}
				}
			case "reply", strError:
				iqc <- v
			default:
				iqc <- v
			}
		}
	}
}
