/*
 * Copyright 2021 PingCAP, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package main

import (
	"context"
	"encoding/base64"
	"fmt"
	"net"
	"net/http"
	"os"
	"path"

	"github.com/fatih/color"
	"github.com/gorilla/mux"
	"github.com/pingcap/fn"
	"github.com/skratchdot/open-golang/open"
	"github.com/spf13/cobra"

	"github.com/pingcap/mimir/client"
	"github.com/pingcap/mimir/pkg/constant"
	"github.com/pingcap/mimir/pkg/protos"
)

func newLoginCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   `login <endpoint>`,
		Short: `login mimir server`,
		RunE: func(cmd *cobra.Command, args []string) error {
			if len(args) != 1 {
				return cmd.Help()
			}

			ctx := context.Background()

			client := client.New(args[0], nil)
			codeAddr, _ := getFreeAddr()
			data, err := client.Login(ctx, &protos.LoginParam{
				State: codeAddr,
			})
			if err != nil {
				return fmt.Errorf("login failed: %s", err)
			}

			if err := open.Run(data.URL); err != nil || codeAddr == "" {
				codeAddr = ""
				fmt.Println("Please open", color.GreenString(data.URL), "in your browser and copy the code here.")
				fmt.Printf("Your code: ")
			}
			code, err := serveCode(ctx, codeAddr)
			if err != nil {
				return err
			}

			cert, err := client.GetCertificate(ctx, &protos.CertificateParam{Code: code})
			if err != nil {
				return err
			}

			if err := writeCert(cert); err != nil {
				return err
			}

			fmt.Println("login success")
			return nil
		},
	}

	return cmd
}

func writeCert(data *protos.ClientAuthData) error {
	if err := os.MkdirAll(path.Join(constant.HomeDir(), "tls"), os.ModeDir|os.ModePerm); err != nil {
		return err
	}

	if data.Server != "" {
		if err := os.WriteFile(path.Join(constant.HomeDir(), "tls", "host"), []byte(data.Server), os.ModePerm); err != nil {
			return err
		}
	}

	certCA, err := base64.StdEncoding.DecodeString(data.CA)
	if err != nil {
		return err
	}
	if err := os.WriteFile(path.Join(constant.HomeDir(), "tls", "ca.pem"), certCA, os.ModePerm); err != nil {
		return err
	}

	certClient, err := base64.StdEncoding.DecodeString(data.Client)
	if err != nil {
		return err
	}
	if err := os.WriteFile(path.Join(constant.HomeDir(), "tls", "client.pem"), certClient, os.ModePerm); err != nil {
		return err
	}

	clientKey, err := base64.StdEncoding.DecodeString(data.ClientKey)
	if err != nil {
		return err
	}
	if err := os.WriteFile(path.Join(constant.HomeDir(), "tls", "client-key.pem"), clientKey, os.ModePerm); err != nil {
		return err
	}

	return nil
}

// serveCode starts a temp server for receiving auth code
func serveCode(ctx context.Context, addr string) (string, error) {
	r := mux.NewRouter()

	codeCh := make(chan string)
	r.Handle("/{code}", fn.Wrap(func(r *http.Request) (map[string]string, error) {
		codeCh <- mux.Vars(r)["code"]
		return nil, nil
	})).Methods("POST")

	srv := &http.Server{Addr: addr, Handler: r}
	defer func() {
		srv.Shutdown(ctx) // nolint:errcheck
	}()

	if addr != "" {
		go func() {
			srv.ListenAndServe() // nolint:errcheck
		}()
	} else {
		go func() {
			var code string
			fmt.Scan(&code)
			codeCh <- code
		}()
	}

	select {
	case code := <-codeCh:
		return code, nil
	case <-ctx.Done():
		return "", nil
	}
}

func getFreeAddr() (string, error) {
	host := "127.0.0.1"
	port, err := getFreePort(host)
	if err != nil {
		return "", err
	}
	return fmt.Sprintf("%s:%d", host, port), nil
}

func getFreePort(host string) (int, error) {
	addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", host))
	if err != nil {
		return 0, err
	}

	l, err := net.ListenTCP("tcp", addr)
	if err != nil {
		return 0, err
	}
	defer l.Close()

	return l.Addr().(*net.TCPAddr).Port, nil
}
