// 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"
	"encoding/xml"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"

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

func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ,
	jserver string, filePath string) string {
	var uploadComponent string
	var maxFileSize int64

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

	// Read file
	buffer, err := readFile(filePath)
	if err != nil {
		log.Fatal(err)
	}

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

	// Get file name
	fileName := filepath.Base(filePath)
	// Just use alphanumerical characters for now to work around
	// https://github.com/mattn/go-xmpp/issues/132
	reg, err := regexp.Compile(`[^a-zA-Z0-9\+\-\_\.]+`)
	if err != nil {
		log.Fatal(err)
	}
	fileNameEscaped := reg.ReplaceAllString(fileName, "_")

	// Query server for disco#items
	iqContent, err := sendIQ(client, iqc, jserver, "get",
		"<query xmlns='http://jabber.org/protocol/disco#items'/>")
	if err != nil {
		log.Fatal(err)
	}
	iqDiscoItemsXML := etree.NewDocument()
	err = iqDiscoItemsXML.ReadFromBytes(iqContent.Query)
	if err != nil {
		log.Fatal(err)
	}
	iqDiscoItemsXMLQuery := iqDiscoItemsXML.SelectElement("query")
	iqDiscoItemsXMLItems := iqDiscoItemsXMLQuery.SelectElements("item")

	// Check the services reported by disco#items for the http upload service
	for _, r := range iqDiscoItemsXMLItems {
		jid := r.SelectAttr("jid")
		iqDiscoInfoReqXML := etree.NewDocument()
		iqDiscoInfoReqXMLQuery := iqDiscoInfoReqXML.CreateElement("query")
		iqDiscoInfoReqXMLQuery.CreateAttr("xmlns", nsDiscoInfo)
		iqdi, err := iqDiscoInfoReqXML.WriteToString()
		if err != nil {
			log.Fatal(err)
		}
		iqDiscoInfo, err := sendIQ(client, iqc, jid.Value, "get", iqdi)
		if err != nil {
			log.Fatal(err)
		}
		if iqDiscoInfo.Type != "result" {
			continue
		}
		iqDiscoInfoXML := etree.NewDocument()
		err = iqDiscoInfoXML.ReadFromBytes(iqDiscoInfo.Query)
		if err != nil {
			log.Fatal(err)
		}
		iqDiscoInfoXMLQuery := iqDiscoInfoXML.SelectElement("query")
		iqDiscoInfoXMLIdentity := iqDiscoInfoXMLQuery.SelectElement("identity")
		iqDiscoInfoXMLType := iqDiscoInfoXMLIdentity.SelectAttr("type")
		iqDiscoInfoXMLCategory := iqDiscoInfoXMLIdentity.SelectAttr("category")

		if iqDiscoInfoXMLType.Value == "file" &&
			iqDiscoInfoXMLCategory.Value == "store" {
			uploadComponent = jid.Value
		}

	}
	if uploadComponent == "" {
		log.Fatal("No http upload component found.")
	}
	iqDiscoInfoXMLX := iqDiscoItemsXMLQuery.SelectElements("x")
	for _, r := range iqDiscoInfoXMLX {
		field := r.SelectElements("field")
		for i, t := range field {
			varAttr := t.SelectAttr("var")
			prevFieldVal := field[i-1].SelectElement("value")
			curFieldVal := t.SelectElement("value")
			if varAttr.Value == "max-file-size" && prevFieldVal.Text() == nsHTTPUpload {
				maxFileSize, err = strconv.ParseInt(curFieldVal.Text(), 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).")
		}
	}

	request := etree.NewDocument()
	requestReq := request.CreateElement("request")
	requestReq.CreateAttr("xmlns", nsHTTPUpload)
	requestReq.CreateAttr("filename", fileNameEscaped)
	requestReq.CreateAttr("size", fmt.Sprint(fileSize))
	requestReq.CreateAttr("content-type", mimeType)
	r, err := request.WriteToString()
	if err != nil {
		log.Fatal(err)
	}

	// Request http upload slot
	uploadSlot, err := sendIQ(client, iqc, uploadComponent, "get", r)
	if err != nil {
		log.Fatal(err)
	}
	if uploadSlot.Type != "result" {
		log.Fatal("Error while requesting upload slot.")
	}
	iqHTTPUploadSlotXML := etree.NewDocument()
	err = iqHTTPUploadSlotXML.ReadFromBytes(uploadSlot.Query)
	if err != nil {
		log.Fatal(err)
	}
	iqHTTPUploadSlotXMLSlot := iqHTTPUploadSlotXML.SelectElement("slot")
	iqHTTPUploadSlotXMLPut := iqHTTPUploadSlotXMLSlot.SelectElement("put")
	iqHTTPUploadSlotXMLPutURL := iqHTTPUploadSlotXMLPut.SelectAttr("url")

	// Upload file
	httpClient := &http.Client{}
	req, err := http.NewRequest(http.MethodPut, iqHTTPUploadSlotXMLPutURL.Value,
		buffer)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Content-Type", mimeTypeEscaped.String())
	iqHTTPUploadSlotXMLPutHeaders := iqHTTPUploadSlotXMLPut.SelectElements("header")
	for _, h := range iqHTTPUploadSlotXMLPutHeaders {
		name := h.SelectAttr("name")
		switch name.Value {
		case "Authorization", "Cookie", "Expires":
			req.Header.Set(name.Value, h.Text())
		}
	}
	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
	iqHTTPUploadSlotXMLGet := iqHTTPUploadSlotXMLSlot.SelectElement("get")
	iqHTTPUploadSlotXMLGetURL := iqHTTPUploadSlotXMLGet.SelectAttr("url")
	return iqHTTPUploadSlotXMLGetURL.Value
}
