package router

import (
	"bufio"
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"github.com/buger/jsonparser"
	"github.com/gin-gonic/gin"
	"github.com/mholt/archiver/v3"
	"github.com/pterodactyl/wings/api"
	"github.com/pterodactyl/wings/config"
	"github.com/pterodactyl/wings/installer"
	"github.com/pterodactyl/wings/router/tokens"
	"github.com/pterodactyl/wings/server"
	"go.uber.org/zap"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

func getServerArchive(c *gin.Context) {
	auth := strings.SplitN(c.GetHeader("Authorization"), " ", 2)

	if len(auth) != 2 || auth[0] != "Bearer" {
		c.Header("WWW-Authenticate", "Bearer")
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
			"error": "The required authorization heads were not present in the request.",
		})
		return
	}

	token := tokens.TransferPayload{}
	if err := tokens.ParseToken([]byte(auth[1]), &token); err != nil {
		TrackedError(err).AbortWithServerError(c)
		return
	}

	if token.Subject != c.Param("server") {
		c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
			"error": "( .. •˘___˘• .. )",
		})
		return
	}

	s := GetServer(c.Param("server"))

	st, err := s.Archiver.Stat()
	if err != nil {
		if !os.IsNotExist(err) {
			TrackedServerError(err, s).SetMessage("failed to stat archive").AbortWithServerError(c)
			return
		}

		c.AbortWithStatus(http.StatusNotFound)
		return
	}

	checksum, err := s.Archiver.Checksum()
	if err != nil {
		TrackedServerError(err, s).SetMessage("failed to calculate checksum").AbortWithServerError(c)
		return
	}

	file, err := os.Open(s.Archiver.ArchivePath())
	if err != nil {
		tserr := TrackedServerError(err, s)
		if !os.IsNotExist(err) {
			tserr.SetMessage("failed to open archive for reading")
		} else {
			tserr.SetMessage("failed to open archive")
		}

		tserr.AbortWithServerError(c)
		return
	}
	defer file.Close()

	c.Header("X-Checksum", checksum)
	c.Header("X-Mime-Type", st.Mimetype)
	c.Header("Content-Length", strconv.Itoa(int(st.Info.Size())))
	c.Header("Content-Disposition", "attachment; filename="+s.Archiver.ArchiveName())
	c.Header("Content-Type", "application/octet-stream")

	bufio.NewReader(file).WriteTo(c.Writer)
}

func postServerArchive(c *gin.Context) {
	s := GetServer(c.Param("server"))

	go func(server *server.Server) {
		start := time.Now()

		if err := server.Archiver.Archive(); err != nil {
			zap.S().Errorw("failed to get archive for server", zap.String("server", s.Uuid), zap.Error(err))
			return
		}

		zap.S().Debugw(
			"successfully created archive for server",
			zap.String("server", server.Uuid),
			zap.Duration("time", time.Now().Sub(start).Round(time.Microsecond)),
		)

		r := api.NewRequester()
		rerr, err := r.SendArchiveStatus(server.Uuid, true)
		if rerr != nil || err != nil {
			if err != nil {
				zap.S().Errorw("failed to notify panel with archive status", zap.String("server", server.Uuid), zap.Error(err))
				return
			}

			zap.S().Errorw(
				"panel returned an error when sending the archive status",
				zap.String("server", server.Uuid),
				zap.Error(errors.New(rerr.String())),
			)
			return
		}

		zap.S().Debugw("successfully notified panel about archive status", zap.String("server", server.Uuid))
	}(s)

	c.Status(http.StatusAccepted)
}

func postTransfer(c *gin.Context) {
	zap.S().Debug("incoming transfer from panel")

	buf := bytes.Buffer{}
	buf.ReadFrom(c.Request.Body)

	go func(data []byte) {
		serverID, _ := jsonparser.GetString(data, "server_id")
		url, _ := jsonparser.GetString(data, "url")
		token, _ := jsonparser.GetString(data, "token")

		// Create an http client with no timeout.
		client := &http.Client{Timeout: 0}

		hasError := true
		defer func() {
			if !hasError {
				return
			}

			zap.S().Errorw("server transfer has failed", zap.String("server", serverID))
			rerr, err := api.NewRequester().SendTransferFailure(serverID)
			if rerr != nil || err != nil {
				if err != nil {
					zap.S().Errorw("failed to notify panel with transfer failure", zap.String("server", serverID), zap.Error(err))
					return
				}

				zap.S().Errorw("panel returned an error when notifying of a transfer failure", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
				return
			}

			zap.S().Debugw("successfully notified panel about transfer failure", zap.String("server", serverID))
		}()

		// Make a new GET request to the URL the panel gave us.
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			zap.S().Errorw("failed to create http request", zap.Error(err))
			return
		}

		// Add the authorization header.
		req.Header.Set("Authorization", token)

		// Execute the http request.
		res, err := client.Do(req)
		if err != nil {
			zap.S().Errorw("failed to send http request", zap.Error(err))
			return
		}
		defer res.Body.Close()

		// Handle non-200 status codes.
		if res.StatusCode != 200 {
			body, err := ioutil.ReadAll(res.Body)
			if err != nil {
				zap.S().Errorw("failed to read response body", zap.Int("status", res.StatusCode), zap.Error(err))
				return
			}

			zap.S().Errorw("failed to request server archive", zap.Int("status", res.StatusCode), zap.String("body", string(body)))
			return
		}

		// Get the path to the archive.
		archivePath := filepath.Join(config.Get().System.ArchiveDirectory, serverID + ".tar.gz")

		// Check if the archive already exists and delete it if it does.
		_, err = os.Stat(archivePath)
		if err != nil {
			if !os.IsNotExist(err) {
				zap.S().Errorw("failed to stat file", zap.Error(err))
				return
			}
		} else {
			if err := os.Remove(archivePath); err != nil {
				zap.S().Errorw("failed to delete old file", zap.Error(err))
				return
			}
		}

		// Create the file.
		file, err := os.Create(archivePath)
		if err != nil {
			zap.S().Errorw("failed to open file on disk", zap.Error(err))
			return
		}

		// Copy the file.
		_, err = io.Copy(file, res.Body)
		if err != nil {
			zap.S().Errorw("failed to copy file to disk", zap.Error(err))
			return
		}

		// Close the file so it can be opened to verify the checksum.
		if err := file.Close(); err != nil {
			zap.S().Errorw("failed to close archive file", zap.Error(err))
			return
		}
		zap.S().Debug("server archive has been downloaded, computing checksum..", zap.String("server", serverID))

		// Open the archive file for computing a checksum.
		file, err = os.Open(archivePath)
		if err != nil {
			zap.S().Errorw("failed to open file on disk", zap.Error(err))
			return
		}

		// Compute the sha256 checksum of the file.
		hash := sha256.New()
		if _, err := io.Copy(hash, file); err != nil {
			zap.S().Errorw("failed to copy file for checksum verification", zap.Error(err))
			return
		}

		// Verify the two checksums.
		if hex.EncodeToString(hash.Sum(nil)) != res.Header.Get("X-Checksum") {
			zap.S().Errorw("checksum failed verification")
			return
		}

		// Close the file.
		if err := file.Close(); err != nil {
			zap.S().Errorw("failed to close archive file", zap.Error(err))
			return
		}

		zap.S().Infow("server archive transfer was successful", zap.String("server", serverID))

		// Get the server data from the request.
		serverData, t, _, _ := jsonparser.Get(data, "server")
		if t != jsonparser.Object {
			zap.S().Errorw("invalid server data passed in request")
			return
		}

		// Create a new server installer (note this does not execute the install script)
		i, err := installer.New(serverData)
		if err != nil {
			zap.S().Warnw("failed to validate the received server data", zap.Error(err))
			return
		}

		// Add the server to the collection.
		server.GetServers().Add(i.Server())

		// Create the server's environment (note this does not execute the install script)
		i.Execute()

		// Un-archive the archive. That sounds weird..
		if err := archiver.NewTarGz().Unarchive(archivePath, i.Server().Filesystem.Path()); err != nil {
			zap.S().Errorw("failed to extract archive", zap.String("server", serverID), zap.Error(err))
			return
		}

		// We mark the process as being successful here as if we fail to send a transfer success,
		// then a transfer failure won't probably be successful either.
		//
		// It may be useful to retry sending the transfer success every so often just in case of a small
		// hiccup or the fix of whatever error causing the success request to fail.
		hasError = false

		// Notify the panel that the transfer succeeded.
		rerr, err := api.NewRequester().SendTransferSuccess(serverID)
		if rerr != nil || err != nil {
			if err != nil {
				zap.S().Errorw("failed to notify panel with transfer success", zap.String("server", serverID), zap.Error(err))
				return
			}

			zap.S().Errorw("panel returned an error when notifying of a transfer success", zap.String("server", serverID), zap.Error(errors.New(rerr.String())))
			return
		}

		zap.S().Debugw("successfully notified panel about transfer success", zap.String("server", serverID))
	}(buf.Bytes())

	c.Status(http.StatusAccepted)
}