/*
 * 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 platform

import (
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/JetBrains/qodana-cli/internal/platform/strutil"

	"github.com/JetBrains/qodana-cli/internal/cloud"
	platformcmd "github.com/JetBrains/qodana-cli/internal/platform/cmd"
	"github.com/JetBrains/qodana-cli/internal/platform/commoncontext"
	"github.com/JetBrains/qodana-cli/internal/platform/effectiveconfig"
	"github.com/JetBrains/qodana-cli/internal/platform/msg"
	"github.com/JetBrains/qodana-cli/internal/platform/qdenv"
	"github.com/JetBrains/qodana-cli/internal/platform/qdyaml"
	"github.com/JetBrains/qodana-cli/internal/platform/thirdpartyscan"
	"github.com/JetBrains/qodana-cli/internal/platform/tokenloader"
	"github.com/JetBrains/qodana-cli/internal/platform/utils"
	"github.com/JetBrains/qodana-cli/internal/tooling"
	log "github.com/sirupsen/logrus"
)

func RunThirdPartyLinterAnalysis(
	cliOptions platformcmd.CliOptions,
	linter ThirdPartyLinter,
	linterInfo thirdpartyscan.LinterInfo,
) (int, error) {
	qdenv.InitializeQodanaGlobalEnv(cliOptions)

	var err error

	commonCtx := commoncontext.Compute3rdParty(
		linterInfo.LinterName,
		linterInfo.IsEap,
		cliOptions.CacheDir,
		cliOptions.ResultsDir,
		cliOptions.ReportDir,
		qdenv.GetQodanaGlobalEnv(qdenv.QodanaToken),
		cliOptions.ClearCache,
		cliOptions.ProjectDir,
		cliOptions.RepositoryRoot,
	)
	commonCtx, err = correctInitArgsForThirdParty(commonCtx)
	if err != nil {
		msg.ErrorMessage(err.Error())
		return 1, err
	}
	resultDir := commonCtx.ResultsDir
	defer changeResultDirPermissionsInContainer(resultDir)

	thirdPartyCloudData := checkLinterLicense(commonCtx)

	printLinterLicense(thirdPartyCloudData.LicensePlan, linterInfo)
	printQodanaLogo(commonCtx.LogDir(), commonCtx.CacheDir, linterInfo)

	mountInfo := extractUtils(linter, commonCtx.CacheDir)

	localQodanaYamlFullPath := qdyaml.GetLocalNotEffectiveQodanaYamlFullPath(
		commonCtx.ProjectDir,
		cliOptions.ConfigName,
	)

	effectiveDir, cleanup, err := utils.CreateTempDir("qodana-effective-config")
	if err != nil {
		return 1, fmt.Errorf("failed to create qodana effective configuration dir %v", err)
	}
	defer cleanup()

	qodanaConfigEffectiveFiles, err := effectiveconfig.CreateEffectiveConfigFiles(
		localQodanaYamlFullPath,
		cliOptions.GlobalConfigurationsDir,
		cliOptions.GlobalConfigurationId,
		mountInfo.JavaPath,
		effectiveDir,
		commonCtx.LogDir(),
	)
	if err != nil {
		log.Fatalf("Failed to load Qodana configuration %s", err)
	}
	qodanaYamlConfig := thirdpartyscan.QodanaYamlConfig{}
	if qodanaConfigEffectiveFiles.EffectiveQodanaYamlPath != "" {
		yaml := qdyaml.LoadQodanaYamlByFullPath(qodanaConfigEffectiveFiles.EffectiveQodanaYamlPath)
		qodanaYamlConfig = thirdpartyscan.YamlConfig(yaml)
	}

	context := thirdpartyscan.ComputeContext(
		cliOptions,
		commonCtx,
		linterInfo,
		mountInfo,
		thirdPartyCloudData,
		qodanaYamlConfig,
	)

	LogContext(&context)

	events := make([]tooling.FuserEvent, 0)
	eventsCh := createFuserEventChannel(&events)

	projectIdHash := thirdPartyCloudData.ProjectIdHash
	defer func() {
		logProjectClose(eventsCh, linterInfo, projectIdHash)
		sendFuserEvents(eventsCh, &events, context, GetDeviceIdSalt()[0])
	}()
	logOs(eventsCh, linterInfo, projectIdHash)
	logProjectOpen(eventsCh, linterInfo, projectIdHash)

	if err = linter.RunAnalysis(context); err != nil {
		msg.ErrorMessage(err.Error())
		return 1, err
	}
	log.Debugf("Java executable path: %s", mountInfo.JavaPath)

	thresholds := getFailureThresholds(context)
	var analysisResult int
	if analysisResult, err = computeBaselinePrintResults(context, thresholds); err != nil {
		msg.ErrorMessage(err.Error())
		return 1, err
	}
	if qodanaConfigEffectiveFiles.EffectiveQodanaYamlPath != "" {
		err = copyQodanaYamlToLogDir(qodanaConfigEffectiveFiles.EffectiveQodanaYamlPath, context.LogDir())
		if err != nil {
			msg.ErrorMessage(err.Error())
			return 1, err
		}
	}
	if err = convertReportToCloudFormat(context); err != nil {
		msg.ErrorMessage(err.Error())
		return 1, err
	}
	sendReportToQodanaServer(context)
	newReportUrl := cloud.GetReportUrl(context.ResultsDir())
	ProcessSarif(
		filepath.Join(context.ResultsDir(), commoncontext.QodanaSarifName),
		context.AnalysisId(),
		newReportUrl,
		false,
		context.GenerateCodeClimateReport(),
		context.SendBitBucketInsights(),
	)
	err = writeShortSarifReport(context)
	if err != nil {
		log.Warnf("Problems writing short SARIF report: %v", err)
	}
	return analysisResult, nil
}

func correctInitArgsForThirdParty(commonCtx commoncontext.Context) (commoncontext.Context, error) {
	empty := commoncontext.Context{}
	var err error

	if commonCtx.ResultsDir, err = filepath.Abs(commonCtx.ResultsDir); err != nil {
		return empty, fmt.Errorf("failed to get absolute path to results directory: %w", err)
	}

	if commonCtx.ReportDir, err = filepath.Abs(commonCtx.ReportDir); err != nil {
		return empty, fmt.Errorf("failed to get absolute path to report directory: %w", err)
	}

	tmpResultsDir := GetTmpResultsDir(commonCtx.ResultsDir)
	if _, err := os.Stat(tmpResultsDir); err == nil {
		if err := os.RemoveAll(tmpResultsDir); err != nil {
			return empty, fmt.Errorf("failed to remove folder with temporary data: %w", err)
		}
	}

	directories := []string{
		commonCtx.ResultsDir,
		commonCtx.LogDir(),
		commonCtx.CacheDir,
		ReportResultsPath(commonCtx.ReportDir),
		tmpResultsDir,
	}
	for _, dir := range directories {
		if _, err := os.Stat(dir); os.IsNotExist(err) {
			if err := os.MkdirAll(dir, 0755); err != nil {
				return empty, fmt.Errorf("failed to create directory %s: %w", dir, err)
			}
		}
	}
	return commonCtx, nil
}

func checkLinterLicense(loader tokenloader.CloudTokenLoader) thirdpartyscan.ThirdPartyStartupCloudData {
	licensePlan := cloud.CommunityLicensePlan
	token := tokenloader.LoadCloudUploadToken(loader, false, false, true)
	projectIdHash := ""
	cloud.SetupLicenseToken(token)
	if cloud.Token.Token != "" {
		licenseData := cloud.GetCloudApiEndpoints().GetLicenseData(cloud.Token.Token)
		tokenloader.ValidateTokenPrintProject(cloud.Token.Token)
		licensePlan = licenseData.LicensePlan
		projectIdHash = licenseData.ProjectIdHash
	}
	return thirdpartyscan.ThirdPartyStartupCloudData{
		LicensePlan:   licensePlan,
		QodanaToken:   token,
		ProjectIdHash: projectIdHash,
	}
}

func printLinterLicense(licensePlan string, linterInfo thirdpartyscan.LinterInfo) {
	licenseString := licensePlan
	if cloud.Token.Token == "" && linterInfo.IsEap {
		licenseString = "EAP license"
	}
	msg.SuccessMessage("Qodana license plan: %s", licenseString)
}

func sendReportToQodanaServer(c thirdpartyscan.Context) {
	if cloud.Token.IsAllowedToSendReports() {
		fmt.Println("Publishing report ...")
		publisher := Publisher{
			ResultsDir: c.ResultsDir(),
			LogDir:     c.LogDir(),
			AnalysisId: c.AnalysisId(),
		}

		SendReport(
			publisher,
			cloud.Token.Token,
			strutil.QuoteForWindows(c.MountInfo().JavaPath),
		)
	} else {
		fmt.Println("Skipping report publishing")
	}
}

func copyQodanaYamlToLogDir(qodanaYamlFullPath string, logDir string) error {
	if _, err := os.Stat(qodanaYamlFullPath); errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err := utils.CopyFile(qodanaYamlFullPath, path.Join(logDir, "qodana.yaml")); err != nil {
		log.Errorf("Error while copying qodana.yaml: %s", err)
		return err
	}
	return nil
}

func printQodanaLogo(logDir string, cacheDir string, linterInfo thirdpartyscan.LinterInfo) {
	fmt.Println("\nLog directory: " + logDir)
	fmt.Println("Cache directory: " + cacheDir)
	fmt.Print(qodanaLogo(linterInfo.LinterPresentableName, linterInfo.LinterVersion, linterInfo.IsEap))
}

// qodanaLogo prepares the info message for the tool
func qodanaLogo(toolDesc string, version string, eap bool) string {
	eapString := ""
	if eap {
		eapString = "EAP"
	}
	return fmt.Sprintf(
		`
          _              _
         /\ \           /\ \        %s %s %s
        /  \ \         /  \ \       Documentation
       / /\ \ \       / /\ \ \      https://jb.gg/qodana-docs
      / / /\ \ \     / / /\ \ \     Contact us at
     / / /  \ \_\   / / /  \ \_\    qodana-support@jetbrains.com
    / / / _ / / /  / / /   / / /    Or via our issue tracker
   / / / /\ \/ /  / / /   / / /     https://jb.gg/qodana-issue
  / / /__\ \ \/  / / /___/ / /      Or share your feedback at our forum
 / / /____\ \ \ / / /____\/ /       https://jb.gg/qodana-forum
 \/________\_\/ \/_________/

`, toolDesc, version, eapString,
	)
}

func changeResultDirPermissionsInContainer(resultDir string) {
	if !qdenv.IsContainer() {
		return
	}
	err := ChangeResultsPermissionsRecursively(resultDir)
	if err != nil {
		msg.ErrorMessage("Unable to change permissions in %s: %s", resultDir, err)
	}
}

func convertReportToCloudFormat(context thirdpartyscan.Context) error {
	log.Debugf("Generating report to %s...", context.ReportDir())
	args := converterArgs(context, context.MountInfo())
	stdout, _, res, err := utils.LaunchAndLog(context.LogDir(), "converter", args...)
	if res != 0 {
		return fmt.Errorf("converter exited with non-zero status code: %d", res)
	}
	if err != nil {
		return fmt.Errorf("error while running converter: %s", err)
	}
	if strings.Contains(stdout, "java.lang") {
		return fmt.Errorf("exception occured while generating report: %s", stdout)
	}
	return nil
}

func converterArgs(options thirdpartyscan.Context, mountInfo thirdpartyscan.MountInfo) []string {
	return []string{
		strutil.QuoteForWindows(mountInfo.JavaPath),
		"-jar",
		strutil.QuoteForWindows(mountInfo.Converter),
		"-s",
		strutil.QuoteForWindows(options.ProjectDir()),
		"-d",
		strutil.QuoteForWindows(options.ResultsDir()),
		"-o",
		strutil.QuoteForWindows(options.ReportDir()),
		"-n",
		"result-allProblems.json",
		"-f",
	}
}

// TODO: think about removing short sarif generation from ultimate
func writeShortSarifReport(context thirdpartyscan.Context) error {
	outputPath := GetShortSarifPath(context.ResultsDir())
	sarifPath := GetSarifPath(context.ResultsDir())
	log.Debugf("Creating short SARIF report at %s from %s...", outputPath, sarifPath)
	return MakeShortSarif(sarifPath, outputPath)
}
