package cli

import (
	"bytes"
	"context"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"net"
	"net/http"
	"os"
	"time"

	"github.com/pkg/errors"

	"github.com/kopia/kopia/internal/tlsutil"
)

const oneDay = 24 * time.Hour

var (
	serverStartTLSGenerateCert          = serverStartCommand.Flag("tls-generate-cert", "Generate TLS certificate").Hidden().Bool()
	serverStartTLSCertFile              = serverStartCommand.Flag("tls-cert-file", "TLS certificate PEM").String()
	serverStartTLSKeyFile               = serverStartCommand.Flag("tls-key-file", "TLS key PEM file").String()
	serverStartTLSGenerateRSAKeySize    = serverStartCommand.Flag("tls-generate-rsa-key-size", "TLS RSA Key size (bits)").Hidden().Default("4096").Int()
	serverStartTLSGenerateCertValidDays = serverStartCommand.Flag("tls-generate-cert-valid-days", "How long should the TLS certificate be valid").Default("3650").Hidden().Int()
	serverStartTLSGenerateCertNames     = serverStartCommand.Flag("tls-generate-cert-name", "Host names/IP addresses to generate TLS certificate for").Default("127.0.0.1").Hidden().Strings()
	serverStartTLSPrintFullServerCert   = serverStartCommand.Flag("tls-print-server-cert", "Print server certificate").Hidden().Bool()
)

func generateServerCertificate(ctx context.Context) (*x509.Certificate, *rsa.PrivateKey, error) {
	return tlsutil.GenerateServerCertificate(
		ctx,
		*serverStartTLSGenerateRSAKeySize,
		time.Duration(*serverStartTLSGenerateCertValidDays)*oneDay, //nolint:durationcheck
		*serverStartTLSGenerateCertNames)
}

func startServerWithOptionalTLS(ctx context.Context, httpServer *http.Server) error {
	l, err := net.Listen("tcp", httpServer.Addr)
	if err != nil {
		return errors.Wrap(err, "listen error")
	}
	defer l.Close() //nolint:errcheck

	httpServer.Addr = l.Addr().String()

	return startServerWithOptionalTLSAndListener(ctx, httpServer, l)
}

func maybeGenerateTLS(ctx context.Context) error {
	if !*serverStartTLSGenerateCert || *serverStartTLSCertFile == "" || *serverStartTLSKeyFile == "" {
		return nil
	}

	if _, err := os.Stat(*serverStartTLSCertFile); err == nil {
		return errors.Errorf("TLS cert file already exists: %q", *serverStartTLSCertFile)
	}

	if _, err := os.Stat(*serverStartTLSKeyFile); err == nil {
		return errors.Errorf("TLS key file already exists: %q", *serverStartTLSKeyFile)
	}

	cert, key, err := generateServerCertificate(ctx)
	if err != nil {
		return errors.Wrap(err, "unable to generate server cert")
	}

	fingerprint := sha256.Sum256(cert.Raw)
	fmt.Fprintf(os.Stderr, "SERVER CERT SHA256: %v\n", hex.EncodeToString(fingerprint[:]))

	log(ctx).Infof("writing TLS certificate to %v", *serverStartTLSCertFile)

	if err := tlsutil.WriteCertificateToFile(*serverStartTLSCertFile, cert); err != nil {
		return errors.Wrap(err, "unable to write private key")
	}

	log(ctx).Infof("writing TLS private key to %v", *serverStartTLSKeyFile)

	if err := tlsutil.WritePrivateKeyToFile(*serverStartTLSKeyFile, key); err != nil {
		return errors.Wrap(err, "unable to write private key")
	}

	return nil
}

func startServerWithOptionalTLSAndListener(ctx context.Context, httpServer *http.Server, listener net.Listener) error {
	if err := maybeGenerateTLS(ctx); err != nil {
		return err
	}

	switch {
	case *serverStartTLSCertFile != "" && *serverStartTLSKeyFile != "":
		// PEM files provided
		fmt.Fprintf(os.Stderr, "SERVER ADDRESS: https://%v\n", httpServer.Addr)
		showServerUIPrompt(ctx)

		return httpServer.ServeTLS(listener, *serverStartTLSCertFile, *serverStartTLSKeyFile)

	case *serverStartTLSGenerateCert:
		// PEM files not provided, generate in-memory TLS cert/key but don't persit.
		cert, key, err := generateServerCertificate(ctx)
		if err != nil {
			return errors.Wrap(err, "unable to generate server cert")
		}

		httpServer.TLSConfig = &tls.Config{
			MinVersion: tls.VersionTLS13,
			Certificates: []tls.Certificate{
				{
					Certificate: [][]byte{cert.Raw},
					PrivateKey:  key,
				},
			},
		}

		fingerprint := sha256.Sum256(cert.Raw)
		fmt.Fprintf(os.Stderr, "SERVER CERT SHA256: %v\n", hex.EncodeToString(fingerprint[:]))

		if *serverStartTLSPrintFullServerCert {
			// dump PEM-encoded server cert, only used by KopiaUI to securely connnect.
			var b bytes.Buffer

			if err := pem.Encode(&b, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
				return errors.Wrap(err, "Failed to write data")
			}

			fmt.Fprintf(os.Stderr, "SERVER CERTIFICATE: %v\n", base64.StdEncoding.EncodeToString(b.Bytes()))
		}

		fmt.Fprintf(os.Stderr, "SERVER ADDRESS: https://%v\n", httpServer.Addr)
		showServerUIPrompt(ctx)

		return httpServer.ServeTLS(listener, "", "")

	default:
		if !*serverStartInsecure {
			return errors.Errorf("TLS not configured. To start server without encryption pass --insecure.")
		}

		fmt.Fprintf(os.Stderr, "SERVER ADDRESS: http://%v\n", httpServer.Addr)
		showServerUIPrompt(ctx)

		return httpServer.Serve(listener)
	}
}

func showServerUIPrompt(ctx context.Context) {
	if *serverStartUI {
		log(ctx).Infof("Open the address above in a web browser to use the UI.")
	}
}
