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

package main

import (
	"bytes"
	"crypto/rand"
	"encoding/xml"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"

	"github.com/gabriel-vasile/mimetype" // MIT License
	"github.com/mattn/go-xmpp"           // BSD-3-Clause"
)

func getID() string {
	b := make([]byte, 12)
	_, err := rand.Read(b)
	if err != nil {
		log.Fatal(err)
	}
	id := fmt.Sprintf("%x-%x-%x", b[0:4], b[4:8], b[8:])
	return id
}

func httpUpload(client *xmpp.Client, jserver string, filePath string) string {

	// Created with https://github.com/miku/zek
	type IQDiscoItemsType struct {
		XMLName xml.Name `xml:"query"`
		Text    string   `xml:",chardata"`
		Xmlns   string   `xml:"xmlns,attr"`
		Item    []struct {
			Text string `xml:",chardata"`
			Jid  string `xml:"jid,attr"`
		} `xml:"item"`
	}

	// Created with https://github.com/miku/zek
	type IQDiscoInfoType struct {
		XMLName  xml.Name `xml:"query"`
		Text     string   `xml:",chardata"`
		Xmlns    string   `xml:"xmlns,attr"`
		Identity struct {
			Text     string `xml:",chardata"`
			Type     string `xml:"type,attr"`
			Name     string `xml:"name,attr"`
			Category string `xml:"category,attr"`
		} `xml:"identity"`
		Feature []struct {
			Text string `xml:",chardata"`
			Var  string `xml:"var,attr"`
		} `xml:"feature"`
		X []struct {
			Text  string `xml:",chardata"`
			Type  string `xml:"type,attr"`
			Xmlns string `xml:"xmlns,attr"`
			Field []struct {
				Text  string `xml:",chardata"`
				Type  string `xml:"type,attr"`
				Var   string `xml:"var,attr"`
				Value string `xml:"value"`
			} `xml:"field"`
		} `xml:"x"`
	}

	type IQHttpUploadSlotRequest struct {
		XMLName  xml.Name `xml:"request"`
		Xmlns    string   `xml:"xmlns,attr"`
		FileName string   `xml:"filename,attr"`
		FileType string   `xml:"mime-type,attr"`
		FileSize int64    `xml:"size,attr"`
	}

	// Created with https://github.com/miku/zek
	type IQHttpUploadSlot struct {
		XMLName xml.Name `xml:"slot"`
		Text    string   `xml:",chardata"`
		Xmlns   string   `xml:"xmlns,attr"`
		Get     struct {
			Text string `xml:",chardata"`
			URL  string `xml:"url,attr"`
		} `xml:"get"`
		Put struct {
			Text    string `xml:",chardata"`
			URL     string `xml:"url,attr"`
			Headers []struct {
				Name  string `xml:"name,attr"`
				Value string `xml:",chardata"`
			} `xml:"header"`
		} `xml:"put"`
	}

	var iqDiscoItemsXML IQDiscoItemsType
	var iqDiscoInfoXML IQDiscoInfoType
	var iqHttpUploadSlotXML IQHttpUploadSlot
	var uploadComponent string
	var maxFileSize int64

	// Get file size
	fileInfo, err := os.Stat(filePath)
	if err != nil {
		log.Fatal(err)
	}
	fileSize := fileInfo.Size()

	// Open File
	f, err := os.Open(filePath)
	if err != nil {
		log.Fatal(err)
	}
	defer f.Close()

	// Read file
	buffer := make([]byte, fileSize)
	_, err = f.Read(buffer)
	if err != nil {
		log.Fatal(err)
	}

	// Get mime type
	mimeType := mimetype.Detect(buffer).String()
	var mimeTypeEscaped bytes.Buffer
	xml.Escape(&mimeTypeEscaped, []byte(mimeType))

	// Get file name
	fileName := filepath.Base(filePath)
	var fileNameEscaped bytes.Buffer
	xml.Escape(&fileNameEscaped, []byte(fileName))

	// Query server for disco#items
	id := getID()
	c := make(chan xmpp.IQ)
	go getIQ(client, id, c)
	_, err = client.RawInformation(client.JID(), jserver, id,
		"get", "<query xmlns='http://jabber.org/protocol/disco#items'/>")
	if err != nil {
		log.Fatal(err)
	}
	iqContent := <-c
	close(c)
	if iqContent.Type != "result" {
		log.Fatal("Error while disco#items query.")
	}
	err = xml.Unmarshal(iqContent.Query, &iqDiscoItemsXML)
	if err != nil {
		log.Fatal(err)
	}

	// Check the services reported by disco#items for the http upload service
	for _, r := range iqDiscoItemsXML.Item {
		id = getID()
		c := make(chan xmpp.IQ)
		go getIQ(client, id, c)
		_, err = client.RawInformation(client.JID(), r.Jid, id, "get",
			"<query xmlns='http://jabber.org/protocol/disco#info'/>")
		if err != nil {
			log.Fatal(err)
		}
		iqDiscoInfo := <-c
		close(c)
		if iqDiscoInfo.Type != "result" {
			log.Fatal("Error while disco#info query.")
		}
		err = xml.Unmarshal(iqDiscoInfo.Query, &iqDiscoInfoXML)
		if err != nil {
			log.Fatal(err)
		}

		if iqDiscoInfoXML.Identity.Type == "file" && iqDiscoInfoXML.Identity.Category == "store" {
			uploadComponent = r.Jid
		}

	}
	if uploadComponent == "" {
		log.Fatal("No http upload component found.")
	}
	for _, r := range iqDiscoInfoXML.X {
		for i, t := range r.Field {
			if t.Var == "max-file-size" && r.Field[i-1].Value == "urn:xmpp:http:upload:0" {
				maxFileSize, err = strconv.ParseInt(t.Value, 10, 64)
				if err != nil {
					log.Fatal("Error while checking server maximum http upload file size.")
				}
			}
		}
	}
	// Check if the file size doesn't exceed the maximum file size of the http upload
	// component if a maximum file size is reported, if not just continue and hope for
	// the best.
	if maxFileSize != 0 {
		if fileSize > maxFileSize {
			log.Fatal("File size " + strconv.FormatInt(fileSize/1024/1024, 10) +
				" MB is larger than the maximum file size allowed (" +
				strconv.FormatInt(maxFileSize/1024/1024, 10) + " MB).")
		}
	}

	var request IQHttpUploadSlotRequest
	request.Xmlns = "urn:xmpp:http:upload:0"
	request.FileName = fileName
	request.FileSize = fileSize
	request.FileType = mimeType
	r, err := xml.Marshal(request)
	if err != nil {
		log.Fatal(err)
	}

	// Request http upload slot
	id = getID()
	c = make(chan xmpp.IQ)
	go getIQ(client, id, c)
	_, err = client.RawInformation(client.JID(), uploadComponent, id, "get", string(r))

	if err != nil {
		log.Fatal(err)
	}
	uploadSlot := <-c
	close(c)
	if uploadSlot.Type != "result" {
		log.Fatal("Error while requesting upload slot.")
	}
	err = xml.Unmarshal(uploadSlot.Query, &iqHttpUploadSlotXML)
	if err != nil {
		log.Fatal(err)
	}

	// Upload file
	httpClient := &http.Client{}
	req, err := http.NewRequest(http.MethodPut, iqHttpUploadSlotXML.Put.URL, bytes.NewBuffer(buffer))
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Content-Type", mimeTypeEscaped.String())
	for _, h := range iqHttpUploadSlotXML.Put.Headers {
		switch h.Name {
		case "Authorization", "Cookie", "Expires":
			req.Header.Set(h.Name, h.Value)
		}
	}
	resp, err := httpClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	// Test for http status code "200 OK" or "201 Created"
	if resp.StatusCode != 200 && resp.StatusCode != 201 {
		log.Fatal("Http upload failed.")
	}

	// Return http link
	return iqHttpUploadSlotXML.Get.URL
}

func getIQ(client *xmpp.Client, id string, c chan xmpp.IQ) {
	for {
		msg, err := client.Recv()
		if err != nil {
			log.Fatal(err)
		}

		switch v := msg.(type) {
		case xmpp.IQ:
			if v.ID == id {
				c <- v
				return
			}
		}
	}
}
