/*
File Name:  Gateway.go
Copyright:  2022 Peernet s.r.o.
Author:     Peter Kleissner
*/

package main

import (
	"crypto/tls"
	"encoding/hex"
	"fmt"
	"io"
	"net"
	"net/http"
	"path"
	"strings"
	"time"

	"github.com/PeernetOfficial/core"
	"github.com/PeernetOfficial/core/btcec"
	"github.com/PeernetOfficial/core/webapi"
	"github.com/gorilla/mux"
)

func startWebGateway(backend *core.Backend) {
	router := mux.NewRouter()
	router.PathPrefix("/").Handler(http.HandlerFunc(webGatewayHandler(backend))).Methods("GET")

	for _, listen := range config.WebListen {
		go startWebServer(backend, listen, config.WebUseSSL, config.WebCertificateFile, config.WebCertificateKey, router, "Web Listen", parseDuration(config.WebTimeoutRead), parseDuration(config.WebTimeoutWrite))
	}

	if config.Redirect80 != "" {
		go webRedirect80(config.Redirect80)
	}

	// wait forever
	select {}
}

// startWebServer starts the web-server and may block forever
func startWebServer(backend *core.Backend, WebListen string, UseSSL bool, CertificateFile, CertificateKey string, Handler http.Handler, Info string, ReadTimeout, WriteTimeout time.Duration) {
	//func startWebServer(backend *core.Backend, webListen string, useSSL bool, certificateFile, certificateKey string, server *http.Server) {
	backend.LogError("startWebServer", "Web Gateway to listen on '%s'", WebListen)

	tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} // for security reasons disable TLS 1.0/1.1

	server := &http.Server{
		Addr:         WebListen,
		Handler:      Handler,
		ReadTimeout:  ReadTimeout,  // ReadTimeout is the maximum duration for reading the entire request, including the body.
		WriteTimeout: WriteTimeout, // WriteTimeout is the maximum duration before timing out writes of the response. This includes processing time and is therefore the max time any HTTP function may take.
		//IdleTimeout:  IdleTimeout,  // IdleTimeout is the maximum amount of time to wait for the next request when keep-alives are enabled.
		TLSConfig: tlsConfig,
	}

	if UseSSL {
		// HTTPS
		if err := server.ListenAndServeTLS(CertificateFile, CertificateKey); err != nil {
			backend.LogError("startWebServer", "Error listening on '%s': %v\n", WebListen, err)
		}
	} else {
		// HTTP
		if err := server.ListenAndServe(); err != nil {
			backend.LogError("startWebServer", "Error listening on '%s': %v\n", WebListen, err)
		}
	}
}

// parseDuration is the same as time.ParseDuration without returning an error. Valid units are ms, s, m, h. For example "10s".
func parseDuration(input string) (result time.Duration) {
	result, _ = time.ParseDuration(input)
	return
}

func redirect(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, "https://"+r.Host+r.URL.String(), http.StatusMovedPermanently)
}

func webRedirect80(listen80 string) {
	// redirect HTTP -> HTTPS
	http.ListenAndServe(net.JoinHostPort(listen80, "80"), http.HandlerFunc(redirect))
}

func webGatewayHandler(backend *core.Backend) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		// For security and simplicity reasons, below paths are hard-coded.
		// Using arbitrary user input is an avoidable security risk here.
		switch r.URL.Path {
		case "/", "/index.html":
			http.ServeFile(w, r, path.Join(config.WebFiles, "index.html"))
			return
		case "/favicon.ico":
			http.ServeFile(w, r, path.Join(config.WebFiles, "favicon.ico"))
			return
		}

		// Assume it is in the format "/[blockchain public key]/[file hash]".

		// Remove slash prefix and suffix.
		pathA := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, "/"), "/")

		pathParts := strings.Split(pathA, "/")
		if len(pathParts) != 1 && len(pathParts) != 2 {
			http.Error(w, "404 not found", http.StatusNotFound)
			return
		}

		// Default timeout for connection is 10 seconds. This will be an optional parameter in the future.
		timeout := 10 * time.Second

		// First part must be the public key as peer ID or node ID, hex encoded. Form: "/[blockchain public key]"
		nodeIDA := pathParts[0]
		nodeID, validNodeID := webapi.DecodeBlake3Hash(nodeIDA)
		publicKey, errPK := core.PublicKeyFromPeerID(nodeIDA)

		if !validNodeID && errPK != nil {
			http.Error(w, "404 not found", http.StatusNotFound)
			return
		}
		if !validNodeID {
			nodeID = []byte{}
		}

		// Check if a blockchain is requested.
		// The format must be "/[blockchain public key]". Part 1 = hex encoding, peer ID or node ID.
		if len(pathParts) == 1 {
			webGatewayShowBlockchain(backend, w, r, nodeID, publicKey, timeout)
		} else if len(pathParts) == 2 {
			// Check if a specific file on a specific blockchain is requested.
			// The format must be "/[blockchain public key]/[file hash]". Part 2 = hex encoding, blake3 hash.
			hash, valid := webapi.DecodeBlake3Hash(pathParts[1])
			if !valid {
				http.Error(w, "Invalid file hash.", http.StatusBadRequest)
				return
			}

			webGatewayShowFile(backend, w, r, nodeID, publicKey, hash, timeout)
		}

		// Check if an arbitrary directory on a specific blockchain is requested.
		// Directories are identified by name.
		// TODO

		//http.Error(w, "test handler", http.StatusOK)
	}
}

func webGatewayShowBlockchain(backend *core.Backend, w http.ResponseWriter, r *http.Request, nodeID []byte, publicKey *btcec.PublicKey, timeout time.Duration) {
	var err error
	var peer *core.PeerInfo
	if len(nodeID) != 0 {
		peer, err = webapi.PeerConnectNode(backend, nodeID, timeout)
	} else {
		peer, err = webapi.PeerConnectPublicKey(backend, publicKey, timeout)
	}
	if err != nil {
		http.Error(w, "Could not connect to remote peer. 😢", http.StatusNotFound)
		return
	}

	// connection established!
	text := fmt.Sprintf("Peer %s blockchain height %d version %d\nUser Agent: %s\n", hex.EncodeToString(peer.NodeID), peer.BlockchainHeight, peer.BlockchainVersion, peer.UserAgent)
	http.Error(w, text, http.StatusOK)
}

func webGatewayShowFile(backend *core.Backend, w http.ResponseWriter, r *http.Request, nodeID []byte, publicKey *btcec.PublicKey, fileHash []byte, timeout time.Duration) {
	var err error
	var peer *core.PeerInfo
	if len(nodeID) != 0 {
		peer, err = webapi.PeerConnectNode(backend, nodeID, timeout)
	} else {
		peer, err = webapi.PeerConnectPublicKey(backend, publicKey, timeout)
	}
	if err != nil {
		http.Error(w, "Could not connect to remote peer. 😢", http.StatusNotFound)
		return
	}

	// Todo: Try webapi.serveFileFromWarehouse

	// Todo: Cache.

	offset := 0
	limit := 0

	// start the reader
	//reader, fileSize, transferSize, err := webapi.FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
	reader, _, transferSize, err := webapi.FileStartReader(peer, fileHash, uint64(offset), uint64(limit), r.Context().Done())
	if reader != nil {
		defer reader.Close()
	}
	if err != nil || reader == nil {
		http.Error(w, "File not found.", http.StatusNotFound)
		return
	}

	// set the right headers
	//webapi.setContentLengthRangeHeader(w, uint64(offset), transferSize, fileSize, ranges)

	// Start sending the data!
	io.Copy(w, io.LimitReader(reader, int64(transferSize)))
}
