package auth

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"time"

	"github.com/google/uuid"
	"github.com/up9inc/mizu/cli/config"
	"github.com/up9inc/mizu/cli/config/configStructs"
	"github.com/up9inc/mizu/cli/uiUtils"
	"github.com/up9inc/mizu/logger"
	"golang.org/x/oauth2"
)

const loginTimeoutInMin = 2

// Ports are configured in keycloak "cli" client as valid redirect URIs. A change here must be reflected there as well.
var listenPorts = []int{3141, 4001, 5002, 6003, 7004, 8005, 9006, 10007}

func Login() error {
	token, loginErr := loginInteractively()
	if loginErr != nil {
		return fmt.Errorf("failed login interactively, err: %v", loginErr)
	}

	authConfig := configStructs.AuthConfig{
		EnvName: config.Config.Auth.EnvName,
		Token:   token.AccessToken,
	}

	if err := config.UpdateConfig(func(configStruct *config.ConfigStruct) { configStruct.Auth = authConfig }); err != nil {
		return fmt.Errorf("failed updating config with auth, err: %v", err)
	}

	config.Config.Auth = authConfig

	logger.Log.Infof("Login successfully, token stored in config path: %s", fmt.Sprintf(uiUtils.Purple, config.Config.ConfigFilePath))
	return nil
}

func loginInteractively() (*oauth2.Token, error) {
	tokenChannel := make(chan *oauth2.Token)
	errorChannel := make(chan error)

	server := http.Server{}
	go startLoginServer(tokenChannel, errorChannel, &server)

	defer func() {
		if err := server.Shutdown(context.Background()); err != nil {
			logger.Log.Debugf("Error shutting down server, err: %v", err)
		}
	}()

	select {
	case <-time.After(loginTimeoutInMin * time.Minute):
		return nil, errors.New("auth timed out")
	case err := <-errorChannel:
		return nil, err
	case token := <-tokenChannel:
		return token, nil
	}
}

func startLoginServer(tokenChannel chan *oauth2.Token, errorChannel chan error, server *http.Server) {
	for _, port := range listenPorts {
		var authConfig = &oauth2.Config{
			ClientID:    "cli",
			RedirectURL: fmt.Sprintf("http://localhost:%v/callback", port),
			Endpoint: oauth2.Endpoint{
				AuthURL:  fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/auth", config.Config.Auth.EnvName),
				TokenURL: fmt.Sprintf("https://auth.%s/auth/realms/testr/protocol/openid-connect/token", config.Config.Auth.EnvName),
			},
		}

		state := uuid.New()

		mux := http.NewServeMux()
		server.Handler = mux
		mux.Handle("/callback", loginCallbackHandler(tokenChannel, errorChannel, authConfig, state))

		listener, listenErr := net.Listen("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", port))
		if listenErr != nil {
			logger.Log.Debugf("failed to start listening on port %v, err: %v", port, listenErr)
			continue
		}

		authorizationUrl := authConfig.AuthCodeURL(state.String())
		uiUtils.OpenBrowser(authorizationUrl)

		serveErr := server.Serve(listener)
		if serveErr == http.ErrServerClosed {
			logger.Log.Debugf("received server shutdown, server on port %v is closed", port)
			return
		} else if serveErr != nil {
			logger.Log.Debugf("failed to start serving on port %v, err: %v", port, serveErr)
			continue
		}

		logger.Log.Debugf("didn't receive server closed on port %v", port)
		return
	}

	errorChannel <- fmt.Errorf("failed to start serving on all listen ports, ports: %v", listenPorts)
}

func loginCallbackHandler(tokenChannel chan *oauth2.Token, errorChannel chan error, authConfig *oauth2.Config, state uuid.UUID) http.Handler {
	return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		if err := request.ParseForm(); err != nil {
			errorMsg := fmt.Sprintf("failed to parse form, err: %v", err)
			http.Error(writer, errorMsg, http.StatusBadRequest)
			errorChannel <- fmt.Errorf(errorMsg)
			return
		}

		requestState := request.Form.Get("state")
		if requestState != state.String() {
			errorMsg := fmt.Sprintf("state invalid, requestState: %v, authState:%v", requestState, state.String())
			http.Error(writer, errorMsg, http.StatusBadRequest)
			errorChannel <- fmt.Errorf(errorMsg)
			return
		}

		code := request.Form.Get("code")
		if code == "" {
			errorMsg := "code not found"
			http.Error(writer, errorMsg, http.StatusBadRequest)
			errorChannel <- fmt.Errorf(errorMsg)
			return
		}

		token, err := authConfig.Exchange(context.Background(), code)
		if err != nil {
			errorMsg := fmt.Sprintf("failed to create token, err: %v", err)
			http.Error(writer, errorMsg, http.StatusInternalServerError)
			errorChannel <- fmt.Errorf(errorMsg)
			return
		}

		tokenChannel <- token

		http.Redirect(writer, request, fmt.Sprintf("https://%s/CliLogin", config.Config.Auth.EnvName), http.StatusFound)
	})
}
