/*
 * Copyright 2021-2024 JetBrains s.r.o.
 *
 * 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
 *
 * https://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,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package tokenloader

import (
	"fmt"
	"os"

	"github.com/JetBrains/qodana-cli/internal/cloud"
	"github.com/JetBrains/qodana-cli/internal/platform/git"
	"github.com/JetBrains/qodana-cli/internal/platform/msg"
	"github.com/JetBrains/qodana-cli/internal/platform/product"
	"github.com/JetBrains/qodana-cli/internal/platform/qdenv"
	"github.com/JetBrains/qodana-cli/internal/platform/utils"
	"github.com/pterm/pterm"
	log "github.com/sirupsen/logrus"
	"github.com/zalando/go-keyring"
)

const keyringDefaultService = "qodana-cli"

type CloudTokenLoader interface {
	GetQodanaToken() string

	GetId() string
	GetAnalyzer() product.Analyzer

	GetRepositoryRoot() string
	GetProjectDir() string
	GetLogDir() string
}

func IsCloudTokenRequired(tokenLoader CloudTokenLoader) bool {
	if tokenLoader.GetQodanaToken() != "" || os.Getenv(qdenv.QodanaLicenseOnlyToken) != "" {
		return true
	}

	isQodanaLicenseSet := os.Getenv(qdenv.QodanaLicense) != ""
	analyzer := tokenLoader.GetAnalyzer()
	isFreeAnalyzer := !analyzer.GetLinter().IsPaid
	isEapAnalyzer := analyzer.IsEAP()

	return !isQodanaLicenseSet && !isFreeAnalyzer && !isEapAnalyzer
}

func LoadCloudUploadToken(tokenLoader CloudTokenLoader, refresh bool, requiresToken bool, interactive bool) string {
	tokenFetchers := []func(bool) string{
		func(_ bool) string { return tokenLoader.GetQodanaToken() },
		func(refresh bool) string { return getTokenFromKeychain(refresh, tokenLoader.GetId()) },
	}
	if interactive && requiresToken {
		fetcherFromUserInput := func(_ bool) string {
			return getTokenFromUserInput(
				tokenLoader.GetProjectDir(),
				tokenLoader.GetRepositoryRoot(),
				tokenLoader.GetId(),
				tokenLoader.GetLogDir(),
			)
		}
		tokenFetchers = append(tokenFetchers, fetcherFromUserInput)
	}

	for _, fetcher := range tokenFetchers {
		if token := fetcher(refresh); token != "" {
			return token
		}
	}
	return ""
}

func ValidateCloudToken(tokenLoader CloudTokenLoader, refresh bool) string {
	token := LoadCloudUploadToken(tokenLoader, refresh, true, true)
	if token != "" {
		ValidateTokenPrintProject(token)
	}
	return token
}

// ValidateTokenPrintProject validates given token by requesting linked project name.
func ValidateTokenPrintProject(token string) {
	client := cloud.GetCloudApiEndpoints().NewCloudApiClient(token)
	if projectName, err := client.RequestProjectName(); err != nil {
		msg.ErrorMessage(cloud.InvalidTokenMessage)
		os.Exit(1)
	} else if !qdenv.IsContainer() {
		msg.SuccessMessage("Linked %s project: %s", cloud.GetCloudRootEndpoint().Url, projectName)
	}

}

// saveCloudToken saves token to the system keyring
func saveCloudToken(id string, token string) error {
	err := keyring.Set(keyringDefaultService, id, token)
	if err != nil {
		return err
	}
	log.Debugf("Saved token to the system keyring with id %s", id)
	return nil
}

// getCloudToken returns token from the system keyring
func getCloudToken(id string) (string, error) {
	secret, err := keyring.Get(keyringDefaultService, id)
	if err != nil {
		return "", err
	}
	log.Debugf("Got token from the system keyring with id %s", id)
	return secret, nil
}

func setupToken(projectDir string, repositoryRoot, id string, logdir string) string {
	openCloud := msg.AskUserConfirm("Do you want to open the team page to get the token?")
	if openCloud {
		origin, err := git.RemoteUrl(repositoryRoot, logdir)
		if err != nil {
			msg.ErrorMessage("%s", err)
			return ""
		}
		err = utils.OpenBrowser(cloud.GetCloudRootEndpoint().GetCloudTeamsPageUrl(origin, projectDir))
		if err != nil {
			msg.ErrorMessage("%s", err)
			return ""
		}
	}
	token, err := pterm.DefaultInteractiveTextInput.WithMask("*").WithTextStyle(msg.PrimaryStyle).Show(
		fmt.Sprintf(">  Enter the token (will be used for %s; enter 'q' to exit)", msg.PrimaryBold(projectDir)),
	)
	if token == "q" {
		return "q"
	}
	if err != nil {
		msg.ErrorMessage("%s", err)
		return ""
	}
	if token == "" {
		msg.ErrorMessage("Token cannot be empty")
		return ""
	}
	client := cloud.GetCloudApiEndpoints().NewCloudApiClient(token)
	_, err = client.RequestProjectName()
	if err != nil {
		msg.ErrorMessage("Invalid token, try again")
		return ""
	}
	err = saveCloudToken(id, token)
	if err != nil {
		msg.ErrorMessage("Failed to save credentials: %s", err)
		return ""
	}
	return token
}

func getTokenFromKeychain(refresh bool, id string) string {
	log.Debugf("project id: %s", id)
	if refresh || os.Getenv(qdenv.QodanaClearKeyring) != "" {
		err := keyring.Delete(keyringDefaultService, id)
		if err != nil {
			log.Debugf("Failed to delete token from the system keyring: %s", err)
		}
		return ""
	}
	tokenFromKeychain, err := getCloudToken(id)
	if err == nil && tokenFromKeychain != "" {
		msg.WarningMessage(
			"Got %s from the system keyring, declare %s env variable or run %s to override it",
			msg.PrimaryBold(qdenv.QodanaToken),
			msg.PrimaryBold(qdenv.QodanaToken),
			msg.PrimaryBold("qodana init -f"),
		)
		log.Debugf("Loaded token from the system keyring with id %s", id)
		return tokenFromKeychain
	}
	return ""
}

func getTokenFromUserInput(projectDir string, repositoryRoot string, id string, logDir string) string {
	if msg.IsInteractive() {
		msg.WarningMessage(cloud.EmptyTokenMessage, cloud.GetCloudRootEndpoint().Url)
		var token string
		for {
			token = setupToken(projectDir, repositoryRoot, id, logDir)
			if token == "q" {
				return ""
			}
			if token != "" {
				return token
			}
		}
	}
	return ""
}
