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

	"github.com/ProtonMail/gopenpgp/v3/crypto" // MIT License
	"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, disc chan xmpp.DiscoItems, drc chan xmpp.DiscoResult, slc chan xmpp.Slot,
	jserver string, filePaths []string, timeout time.Duration, oxPrivKey *crypto.Key, keyRing *crypto.KeyRing,
) (urls []string, err error) {
	var (
		uploadComponent string
		maxFileSize     int64
		req             *http.Request
	)

	buffer := new(bytes.Buffer)

	// Query server for disco#items
	slog.Info("http-upload: querying disco items", "server", jserver)
	discoItems, err := getDiscoItems(client, disc, jserver)
	if err != nil {
		return urls, err
	}

	// Check the services reported by disco#items for the http upload service
	for _, r := range discoItems.Items {
		slog.Info("http-upload: querying disco info", "disco-item", r.Jid)
		// Don't check error as it seems some components in the wild do not reply.
		discoResult, err := getDiscoInfo(client, drc, r.Jid)
		if err != nil {
			continue
		}
		slog.Info("http-upload: checking identities for type = \"file\" and category = \"store\"")
		for _, identity := range discoResult.Identities {
			if identity.Type == "file" &&
				identity.Category == "store" {
				uploadComponent = r.Jid
				slog.Info("http-upload:", "upload component", uploadComponent)
				for _, x := range discoResult.X {
					for i, field := range x.Field {
						if field.Var == "max-file-size" {
							if i > 0 {
								if x.Field[i-1].Value[0] == nsHTTPUpload {
									maxFileSize, err = strconv.ParseInt(field.Value[0], 10, 64)
									if err != nil {
										return urls, fmt.Errorf("http-upload: error while checking server maximum http upload file size")
									}
									slog.Info("http-upload:", "max file size", maxFileSize)
									break
								}
							}
						}
					}
				}
				break
			}
			if uploadComponent != "" {
				break
			}
		}
		if uploadComponent != "" {
			break
		}
	}
	if uploadComponent == "" {
		return urls, fmt.Errorf("http-upload: no http upload component found")
	}
	for _, filePath := range filePaths {
		// Read file
		// #nosec G304 -- file path is supplied by the user, it's no untrusted input from the network
		file, err := os.Open(filePath)
		if err != nil {
			return urls, fmt.Errorf("http-upload: read file: %w", err)
		}

		fileStat, err := file.Stat()
		if err != nil {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, fmt.Errorf("http-upload: stat file: %w", err)
		}
		fileSize := fileStat.Size()
		slog.Info("http-upload: checking file size:", "file", filePath, "size", fileSize)
		// Get mime type
		mtype, err := mimetype.DetectFile(filePath)
		if err != nil {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, fmt.Errorf("http-upload: detect mimetype: %w", err)
		}
		mimeType := mtype.String()
		slog.Info("http-upload:", "mime type", mimeType)
		var mimeTypeEscaped bytes.Buffer
		xml.Escape(&mimeTypeEscaped, []byte(mimeType))
		if oxPrivKey != nil {
			buf, err := readFile(filePath)
			if err != nil {
				err2 := file.Close()
				if err2 != nil {
					slog.Warn("closing file failed", "error", err2)
				}
				return nil, fmt.Errorf("http-upload: %w", err)
			}
			buffer, err = legacyPGPEncryptFile(oxPrivKey, keyRing, buf)
			if err != nil {
				err2 := file.Close()
				if err2 != nil {
					slog.Warn("closing file failed", "error", err2)
				}
				return urls, err
			}
			fileSize = int64(buffer.Len())
			slog.Info("http-upload: updated file size after encrypting:", "file", filePath, "size", fileSize)
		}
		// Get file name
		fileName := filepath.Base(filePath)
		if oxPrivKey != nil {
			fileName = getID() + ".pgp"
		}
		// 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, "_")
		slog.Info("http-upload: escape file", "name", fileNameEscaped)

		// 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 int64(fileSize) > maxFileSize {
				err2 := file.Close()
				if err2 != nil {
					slog.Warn("closing file failed", "error", err2)
				}
				return urls, fmt.Errorf("http-upload: file size %s MiB is larger than the maximum file size allowed (%s MiB)",
					strconv.FormatInt(int64(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 {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, err
		}

		// Request http upload slot
		slog.Info("http-upload: requesting upload slot")
		uploadSlot, err := getSlot(client, slc, uploadComponent, r)
		if err != nil {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, err
		}
		if !strings.HasPrefix(uploadSlot.Put.Url, "https://") {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, fmt.Errorf("http-upload: upload slot does not provide https")
		}
		// Upload file
		slog.Info("http-upload: uploading file to", "slot", uploadSlot.Put.Url)
		httpTransport := &http.Transport{
			IdleConnTimeout:     timeout,
			TLSHandshakeTimeout: timeout,
		}
		proxyEnv := os.Getenv("HTTP_PROXY")
		slog.Info("http-upload: getting environment variable:", "HTTP_PROXY", proxyEnv)
		if proxyEnv != "" {
			proxyURL, err := url.Parse(proxyEnv)
			if err != nil {
				err2 := file.Close()
				if err2 != nil {
					slog.Warn("closing file failed", "error", err2)
				}
				return urls, err
			}
			slog.Info("http-upload: using http transport", "proxy", proxyURL)
			httpTransport.Proxy = http.ProxyURL(proxyURL)
		}
		httpClient := &http.Client{Transport: httpTransport}
		if oxPrivKey != nil {
			req, err = http.NewRequest(http.MethodPut, uploadSlot.Put.Url, buffer)
		} else {
			req, err = http.NewRequest(http.MethodPut, uploadSlot.Put.Url, file)
		}
		if err != nil {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, err
		}
		req.ContentLength = fileSize
		req.Header.Set("Content-Type", mimeTypeEscaped.String())
		for _, h := range uploadSlot.Put.Headers {
			if h.Name == strEmpty {
				continue
			}
			switch h.Name {
			case "Authorization", "Cookie", "Expires":
				req.Header.Set(h.Name, h.Value)
			}
		}
		resp, err := httpClient.Do(req)
		if err != nil {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, err
		}
		slog.Info("http-upload: received http", "status", resp.StatusCode)
		// Test for http status code "200 OK" or "201 Created"
		if resp.StatusCode != 200 && resp.StatusCode != 201 {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, fmt.Errorf("http-upload: upload failed")
		}

		// Return http link
		if uploadSlot.Get.Url == strEmpty {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, fmt.Errorf("http-upload: no url attribute")
		}
		if !strings.HasPrefix(uploadSlot.Get.Url, "https://") {
			err2 := file.Close()
			if err2 != nil {
				slog.Warn("closing file failed", "error", err2)
			}
			return urls, fmt.Errorf("http-upload: get url does not provide https")
		}
		slog.Info("http-upload: received get", "URL", uploadSlot.Get.Url)
		err = resp.Body.Close()
		if err != nil {
			fmt.Println("http-upload: error while closing http request body:", err)
		}
		urls = append(urls, uploadSlot.Get.Url)
		err = file.Close()
		if err != nil {
			slog.Warn("closing file failed", "error", err)
		}
	}
	return urls, nil
}
