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

import (
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/JetBrains/qodana-cli/internal/core/corescan"
	"github.com/JetBrains/qodana-cli/internal/core/startup"
	"github.com/JetBrains/qodana-cli/internal/platform"
	"github.com/JetBrains/qodana-cli/internal/platform/product"
	"github.com/JetBrains/qodana-cli/internal/platform/qdcontainer"
	"github.com/JetBrains/qodana-cli/internal/platform/strutil"
	"github.com/JetBrains/qodana-cli/internal/platform/utils"
	"github.com/JetBrains/qodana-cli/internal/sarif"

	log "github.com/sirupsen/logrus"
)

// getIdeExitCode gets IDEA "exitCode" from SARIF.
func getIdeExitCode(resultsDir string, c int) (res int) {
	if c != 0 {
		return c
	}
	s, err := platform.ReadReport(platform.GetShortSarifPath(resultsDir))
	if err != nil {
		log.Fatal(err)
	}
	if len(s.Runs) > 0 && len(s.Runs[0].Invocations) > 0 {
		res := int(s.Runs[0].Invocations[0].ExitCode)
		if res < utils.QodanaSuccessExitCode || res > utils.QodanaFailThresholdExitCode {
			log.Printf("Wrong exitCode in sarif: %d", res)
			return 1
		}
		log.Printf("IDE exit code: %d", res)
		return res
	}
	log.Printf("IDE process exit code: %d", c)
	return c
}

// getInvocationProperties gets invocation properties from SARIF.
func getInvocationProperties(resultsDir string) *sarif.PropertyBag {
	s, err := platform.ReadReport(platform.GetShortSarifPath(resultsDir))
	if err != nil {
		log.Fatal(err)
	}
	if len(s.Runs) > 0 && len(s.Runs[0].Invocations) > 0 {
		if s.Runs[0].Invocations[0].Properties == nil {
			return &sarif.PropertyBag{}
		}
		return s.Runs[0].Invocations[0].Properties
	}
	return &sarif.PropertyBag{}
}

func runQodanaLocal(c corescan.Context) (int, error) {
	writeProperties(c)
	args := getIdeRunCommand(c)
	ideProcess, err := utils.RunCmdWithTimeout(
		"",
		os.Stdout, os.Stderr,
		c.GetAnalysisTimeout(),
		utils.QodanaTimeoutExitCodePlaceholder,
		args...,
	)
	res := getIdeExitCode(c.ResultsDir(), ideProcess)
	if res > utils.QodanaSuccessExitCode && res != utils.QodanaFailThresholdExitCode {
		postAnalysis(c)
		return res, err
	}

	saveReport(c)
	postAnalysis(c)
	return res, err
}

func getIdeRunCommand(c corescan.Context) []string {
	args := []string{strutil.QuoteIfSpace(c.Prod().IdeScript)}
	if !c.Prod().Is242orNewer() {
		args = append(args, "inspect")
	}
	args = append(args, "qodana")

	args = append(args, GetIdeArgs(c)...)
	args = append(args, strutil.QuoteIfSpace(c.ProjectDir()), strutil.QuoteIfSpace(c.ResultsDir()))
	return args
}

// GetIdeArgs returns qodana command options.
func GetIdeArgs(c corescan.Context) []string {
	arguments := make([]string, 0)
	if c.CustomLocalQodanaYamlPath() != "" {
		arguments = append(arguments, "--config", strutil.QuoteForWindows(c.CustomLocalQodanaYamlPath()))
	}
	if c.Analyser().IsContainer() && c.SaveReport() {
		arguments = append(arguments, "--save-report")
	}
	if c.OnlyDirectory() != "" {
		arguments = append(arguments, "--only-directory", strutil.QuoteForWindows(c.OnlyDirectory()))
	}
	if c.DisableSanity() {
		arguments = append(arguments, "--disable-sanity")
	}
	if c.ProfileName() != "" {
		arguments = append(arguments, "--profile-name", strutil.QuoteIfSpace(c.ProfileName()))
	}
	if c.ProfilePath() != "" {
		arguments = append(arguments, "--profile-path", strutil.QuoteForWindows(c.ProfilePath()))
	}
	if c.RunPromo() != "" {
		arguments = append(arguments, "--run-promo", c.RunPromo())
	}
	if c.Script() != "" && c.Script() != "default" {
		arguments = append(arguments, "--script", c.Script())
	}
	if c.Baseline() != "" {
		arguments = append(arguments, "--baseline", strutil.QuoteForWindows(c.Baseline()))
	}
	if c.BaselineIncludeAbsent() {
		arguments = append(arguments, "--baseline-include-absent")
	}
	if c.FailThreshold() != "" {
		arguments = append(arguments, "--fail-threshold", c.FailThreshold())
	}
	if rel := c.ProjectDirPathRelativeToRepositoryRoot(); rel != "" && rel != "." {
		if c.Analyser().IsContainer() {
			// it is safe to use / here because it's a path inside the container
			arguments = append(arguments, "--project-dir", qdcontainer.MountDir+"/"+rel)
			arguments = append(arguments, "--repository-root", qdcontainer.MountDir)
		}
	}

	linter := c.Analyser().GetLinter()
	if linter.SupportFixes {
		applyFixes := c.ApplyFixes()
		cleanup := c.Cleanup()
		if c.FixesStrategy() != "" {
			switch strings.ToLower(c.FixesStrategy()) {
			case "apply":
				applyFixes = true
			case "cleanup":
				cleanup = true
			default:
				break
			}
		}
		if !c.Analyser().IsContainer() && c.Prod().Is233orNewer() {
			if applyFixes {
				arguments = append(arguments, "--apply-fixes")
			} else if cleanup {
				arguments = append(arguments, "--cleanup")
			}
		} else { // remove this block in 2023.3 or later
			if applyFixes {
				arguments = append(arguments, "--fixes-strategy", "apply")
			} else if cleanup {
				arguments = append(arguments, "--fixes-strategy", "cleanup")
			}
		}
	}

	// TODO : think how it could be better handled in presence of random 3rd party linters
	if linter == product.DotNetCommunityLinter || linter == product.ClangLinter {
		// third party common options
		if c.NoStatistics() {
			arguments = append(arguments, "--no-statistics")
		}
		if linter == product.DotNetCommunityLinter {
			// cdnet options
			if c.CdnetSolution() != "" {
				arguments = append(arguments, "--solution", strutil.QuoteForWindows(c.CdnetSolution()))
			}
			if c.CdnetProject() != "" {
				arguments = append(arguments, "--project", strutil.QuoteForWindows(c.CdnetProject()))
			}
			if c.CdnetConfiguration() != "" {
				arguments = append(arguments, "--configuration", c.CdnetConfiguration())
			}
			if c.CdnetPlatform() != "" {
				arguments = append(arguments, "--platform", c.CdnetPlatform())
			}
			if c.CdnetNoBuild() {
				arguments = append(arguments, "--no-build")
			}
		} else {
			// clang options
			if c.ClangCompileCommands() != "" {
				arguments = append(arguments, "--compile-commands", strutil.QuoteForWindows(c.ClangCompileCommands()))
			}
			if c.ClangArgs() != "" {
				arguments = append(arguments, "--clang-args", c.ClangArgs())
			}
		}
	}

	if c.Analyser().IsContainer() {
		if startHash, err := c.StartHash(); startHash != "" && err == nil && c.Script() == "default" {
			arguments = append(arguments, "--diff-start", startHash)
		}
		if c.DiffEnd() != "" && c.Script() == "default" {
			arguments = append(arguments, "--diff-end", c.DiffEnd())
		}
		if c.ForceLocalChangesScript() && c.Script() == "default" {
			arguments = append(arguments, "--force-local-changes-script")
		}

		if c.AnalysisId() != "" {
			arguments = append(arguments, "--analysis-id", c.AnalysisId())
		}

		if c.CoverageDir() != "" {
			arguments = append(arguments, "--coverage-dir", c.CoverageDir())
		}

		if c.JvmDebugPort() > 0 {
			arguments = append(arguments, "--jvm-debug-port", strconv.Itoa(c.JvmDebugPort()))
		}
		if c.GlobalConfigurationsDir() != "" {
			arguments = append(arguments, "--global-config-dir", qdcontainer.DataGlobalConfigDir)
		}
		if c.GlobalConfigurationId() != "" {
			arguments = append(arguments, "--global-config-id", c.GlobalConfigurationId())
		}
		if c.GenerateCodeClimateReport() {
			arguments = append(arguments, "--code-climate")
		}
		for _, property := range c.Property() {
			arguments = append(arguments, "--property="+property)
		}
	} else if c.Prod().Is251orNewer() {
		arguments = append(arguments, "--config-dir", strutil.QuoteForWindows(c.EffectiveConfigurationDir()))
	}
	return arguments
}

// postAnalysis post-analysis stage: wait for FUS stats to upload
func postAnalysis(c corescan.Context) {
	err := startup.SyncIdeaCache(c.ProjectDir(), c.CacheDir(), true)
	if err != nil {
		log.Warnf("failed to sync .idea directory: %v", err)
	}
	startup.SyncConfigCache(c.Prod(), c.ConfigDir(), c.CacheDir(), false)
	for i := 1; i <= 600; i++ {
		if utils.FindProcess("statistics-uploader") {
			time.Sleep(time.Second)
		} else {
			break
		}
	}
}

// installPlugins runs plugin installer for every plugin id in qodana.yaml.
func installPlugins(c corescan.Context) {
	if c.Analyser().IsContainer() {
		return
	}

	plugins := c.QodanaYamlConfig().Plugins
	if len(plugins) > 0 {
		setInstallPluginsVmoptions(c)
	}
	for _, plugin := range plugins {
		log.Printf("Installing plugin %s", plugin.Id)
		if res, err := utils.RunCmd(
			"",
			strutil.QuoteIfSpace(c.Prod().IdeScript),
			"installPlugins",
			strutil.QuoteIfSpace(plugin.Id),
		); res > 0 || err != nil {
			os.Exit(res)
		}
	}
}
