// Copyright 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"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

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

func httpUpload(client *xmpp.Client, iqc chan xmpp.IQ, jserver string, filePath string,
	timeout time.Duration,
) (string, error) {
	var uploadComponent string
	var maxFileSize int64
	var iqDiscoItemsXMLQuery, iqDiscoInfoXMLQuery *etree.Element

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

	// Read file
	buffer, err := readFile(filePath)
	if err != nil {
		return "", 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 and some special characters for now
	// to work around https://github.com/xmppo/go-xmpp/issues/132
	reg := regexp.MustCompile(`[^a-zA-Z0-9\+\-\_\.]+`)
	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 {
		return "", err
	}
	iqDiscoItemsXML := etree.NewDocument()
	err = iqDiscoItemsXML.ReadFromBytes(iqContent.Query)
	if err != nil {
		return "", err
	}
	iqDiscoItemsXMLQuery = iqDiscoItemsXML.SelectElement("query")
	if iqDiscoItemsXMLQuery == nil {
		return "", fmt.Errorf("http-upload: no query element in disco items reply")
	}
	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()
		iqDiscoInfoReqXML.WriteSettings.AttrSingleQuote = true
		iqDiscoInfoReqXMLQuery := iqDiscoInfoReqXML.CreateElement("query")
		iqDiscoInfoReqXMLQuery.CreateAttr("xmlns", nsDiscoInfo)
		iqdi, err := iqDiscoInfoReqXML.WriteToString()
		if err != nil {
			return "", err
		}
		iqDiscoInfo, err := sendIQ(client, iqc, jid.Value, "get", iqdi)
		if err != nil {
			return "", err
		}
		if iqDiscoInfo.Type != strResult {
			continue
		}
		iqDiscoInfoXML := etree.NewDocument()
		err = iqDiscoInfoXML.ReadFromBytes(iqDiscoInfo.Query)
		if err != nil {
			return "", err
		}
		iqDiscoInfoXMLQuery = iqDiscoInfoXML.SelectElement("query")
		if iqDiscoInfoXMLQuery == nil {
			continue
		}
		iqDiscoInfoXMLIdentity := iqDiscoInfoXMLQuery.SelectElement("identity")
		if iqDiscoInfoXMLIdentity == nil {
			continue
		}
		iqDiscoInfoXMLType := iqDiscoInfoXMLIdentity.SelectAttr("type")
		if iqDiscoInfoXMLType == nil {
			continue
		}
		iqDiscoInfoXMLCategory := iqDiscoInfoXMLIdentity.SelectAttr("category")
		if iqDiscoInfoXMLCategory == nil {
			continue
		}
		if iqDiscoInfoXMLType.Value == "file" &&
			iqDiscoInfoXMLCategory.Value == "store" {
			uploadComponent = jid.Value
			break
		}
	}
	if uploadComponent == "" {
		return "", fmt.Errorf("http-upload: no http upload component found")
	}
	iqDiscoInfoXMLX := iqDiscoInfoXMLQuery.SelectElements("x")
	for _, r := range iqDiscoInfoXMLX {
		field := r.SelectElements("field")
		for i, t := range field {
			varAttr := t.SelectAttr("var")
			if varAttr == nil {
				continue
			}
			curFieldVal := t.SelectElement("value")
			if curFieldVal == nil {
				continue
			}
			if varAttr.Value == "max-file-size" {
				var prevFieldVal *etree.Element
				if i > 0 {
					prevFieldVal = field[i-1].SelectElement("value")
					if prevFieldVal == nil {
						continue
					}
				}
				if prevFieldVal.Text() == nsHTTPUpload {
					maxFileSize, err = strconv.ParseInt(curFieldVal.Text(), 10, 64)
					if err != nil {
						return "", fmt.Errorf("http-upload: 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 {
			return "", fmt.Errorf("http-upload: file size %s MiB is larger than the maximum file size allowed (%s MiB)",
				strconv.FormatInt(fileSize/1024/1024, 10), strconv.FormatInt(maxFileSize/1024/1024, 10))
		}
	}

	request := etree.NewDocument()
	request.WriteSettings.AttrSingleQuote = true
	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 {
		return "", err
	}

	// Request http upload slot
	uploadSlot, err := sendIQ(client, iqc, uploadComponent, "get", r)
	if err != nil {
		return "", err
	}
	if uploadSlot.Type != strResult {
		return "", fmt.Errorf("http-upload: error while requesting upload slot")
	}
	iqHTTPUploadSlotXML := etree.NewDocument()
	err = iqHTTPUploadSlotXML.ReadFromBytes(uploadSlot.Query)
	if err != nil {
		return "", err
	}
	iqHTTPUploadSlotXMLSlot := iqHTTPUploadSlotXML.SelectElement("slot")
	if iqHTTPUploadSlotXMLSlot == nil {
		return "", fmt.Errorf("http-upload: no slot element")
	}
	iqHTTPUploadSlotXMLPut := iqHTTPUploadSlotXMLSlot.SelectElement("put")
	if iqHTTPUploadSlotXMLPut == nil {
		return "", fmt.Errorf("http-upload: no put element")
	}
	iqHTTPUploadSlotXMLPutURL := iqHTTPUploadSlotXMLPut.SelectAttr("url")
	if iqHTTPUploadSlotXMLPutURL == nil {
		return "", fmt.Errorf("http-upload: no url attribute")
	}
	if !strings.HasPrefix(iqHTTPUploadSlotXMLPutURL.Value, "https://") {
		return "", fmt.Errorf("http-upload: upload slot does not provide https")
	}
	// Upload file
	httpTransport := &http.Transport{
		IdleConnTimeout:     timeout,
		TLSHandshakeTimeout: timeout,
	}
	proxyEnv := os.Getenv("HTTP_PROXY")
	if proxyEnv != "" {
		proxyURL, err := url.Parse(proxyEnv)
		if err != nil {
			return "", err
		}
		httpTransport.Proxy = http.ProxyURL(proxyURL)
	}
	httpClient := &http.Client{Transport: httpTransport}
	req, err := http.NewRequest(http.MethodPut, iqHTTPUploadSlotXMLPutURL.Value,
		buffer)
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", mimeTypeEscaped.String())
	iqHTTPUploadSlotXMLPutHeaders := iqHTTPUploadSlotXMLPut.SelectElements("header")
	for _, h := range iqHTTPUploadSlotXMLPutHeaders {
		name := h.SelectAttr("name")
		if name == nil {
			continue
		}
		switch name.Value {
		case "Authorization", "Cookie", "Expires":
			req.Header.Set(name.Value, h.Text())
		}
	}
	resp, err := httpClient.Do(req)
	if err != nil {
		return "", err
	}
	// Test for http status code "200 OK" or "201 Created"
	if resp.StatusCode != 200 && resp.StatusCode != 201 {
		return "", fmt.Errorf("http-upload: upload failed")
	}

	// Return http link
	iqHTTPUploadSlotXMLGet := iqHTTPUploadSlotXMLSlot.SelectElement("get")
	if iqHTTPUploadSlotXMLGet == nil {
		return "", fmt.Errorf("http-upload: no get element")
	}
	iqHTTPUploadSlotXMLGetURL := iqHTTPUploadSlotXMLGet.SelectAttr("url")
	if iqHTTPUploadSlotXMLGetURL == nil {
		return "", fmt.Errorf("http-upload: no url attribute")
	}
	err = resp.Body.Close()
	if err != nil {
		fmt.Println("http-upload: error while closing http request body:", err)
	}
	return iqHTTPUploadSlotXMLGetURL.Value, nil
}
