package execute

import (
	"context"
	"fmt"
	"strings"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/collections"
	"github.com/microsoft/typescript-go/internal/compiler"
	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/diagnostics"
	"github.com/microsoft/typescript-go/internal/execute/build"
	"github.com/microsoft/typescript-go/internal/execute/incremental"
	"github.com/microsoft/typescript-go/internal/execute/tsc"
	"github.com/microsoft/typescript-go/internal/format"
	"github.com/microsoft/typescript-go/internal/json"
	"github.com/microsoft/typescript-go/internal/locale"
	"github.com/microsoft/typescript-go/internal/ls/lsutil"
	"github.com/microsoft/typescript-go/internal/parser"
	"github.com/microsoft/typescript-go/internal/pprof"
	"github.com/microsoft/typescript-go/internal/tracing"
	"github.com/microsoft/typescript-go/internal/tsoptions"
	"github.com/microsoft/typescript-go/internal/tspath"
)

func startTracingIfNeeded(sys tsc.System, config *tsoptions.ParsedCommandLine, testing tsc.CommandLineTesting) *tracing.Tracing {
	traceDir := config.CompilerOptions().GenerateTrace
	if traceDir == "" {
		return nil
	}
	configFilePath := ""
	if config.ConfigFile != nil && config.ConfigFile.SourceFile != nil {
		configFilePath = config.ConfigFile.SourceFile.FileName()
	}
	tr, err := tracing.StartTracing(sys.FS(), traceDir, configFilePath, testing != nil)
	if err != nil {
		fmt.Fprintf(sys.Writer(), "Warning: Failed to start tracing: %v\n", err)
	}
	return tr
}

func stopTracing(sys tsc.System, tr *tracing.Tracing) {
	if tr == nil {
		return
	}
	if err := tr.StopTracing(); err != nil {
		fmt.Fprintf(sys.Writer(), "Warning: Failed to stop tracing: %v\n", err)
	}
}

func CommandLine(ctx context.Context, sys tsc.System, commandLineArgs []string, testing tsc.CommandLineTesting) tsc.CommandLineResult {
	if len(commandLineArgs) > 0 {
		switch strings.ToLower(commandLineArgs[0]) {
		case "-b", "--b", "-build", "--build":
			return tscBuildCompilation(ctx, sys, tsoptions.ParseBuildCommandLine(commandLineArgs, sys), testing)
			// case "-f":
			// 	return fmtMain(sys, commandLineArgs[1], commandLineArgs[1])
		}
	}

	return tscCompilation(ctx, sys, tsoptions.ParseCommandLine(commandLineArgs, sys), testing)
}

func fmtMain(sys tsc.System, input, output string) tsc.ExitStatus {
	ctx := format.WithFormatCodeSettings(context.Background(), lsutil.GetDefaultFormatCodeSettings(), "\n")
	input = string(tspath.ToPath(input, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames()))
	output = string(tspath.ToPath(output, sys.GetCurrentDirectory(), sys.FS().UseCaseSensitiveFileNames()))
	fileContent, ok := sys.FS().ReadFile(input)
	if !ok {
		fmt.Fprintln(sys.Writer(), "File not found:", input)
		return tsc.ExitStatusNotImplemented
	}
	text := fileContent
	pathified := tspath.ToPath(input, sys.GetCurrentDirectory(), true)
	sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{
		FileName: string(pathified),
		Path:     pathified,
	}, text, core.GetScriptKindFromFileName(string(pathified)))
	edits := format.FormatDocument(ctx, sourceFile)
	newText := core.ApplyBulkEdits(text, edits)

	if err := sys.FS().WriteFile(output, newText); err != nil {
		fmt.Fprintln(sys.Writer(), err.Error())
		return tsc.ExitStatusNotImplemented
	}
	return tsc.ExitStatusSuccess
}

func tscBuildCompilation(ctx context.Context, sys tsc.System, buildCommand *tsoptions.ParsedBuildCommandLine, testing tsc.CommandLineTesting) tsc.CommandLineResult {
	locale := buildCommand.Locale()
	reportDiagnostic := tsc.CreateDiagnosticReporter(sys, sys.Writer(), locale, buildCommand.CompilerOptions)

	if len(buildCommand.Errors) > 0 {
		for _, err := range buildCommand.Errors {
			reportDiagnostic(err)
		}
		return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
	}

	if pprofDir := buildCommand.CompilerOptions.PprofDir; pprofDir != "" {
		// !!! stderr?
		profileSession := pprof.BeginProfiling(pprofDir, sys.Writer())
		defer profileSession.Stop()
	}

	if buildCommand.CompilerOptions.Help.IsTrue() {
		tsc.PrintVersion(sys, locale)
		tsc.PrintBuildHelp(sys, locale, tsoptions.BuildOpts)
		return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess}
	}

	orchestrator := build.NewOrchestrator(build.Options{
		Sys:     sys,
		Command: buildCommand,
		Testing: testing,
	})
	return orchestrator.Start(ctx)
}

func tscCompilation(ctx context.Context, sys tsc.System, commandLine *tsoptions.ParsedCommandLine, testing tsc.CommandLineTesting) tsc.CommandLineResult {
	configFileName := ""
	locale := commandLine.Locale()
	reportDiagnostic := tsc.CreateDiagnosticReporter(sys, sys.Writer(), locale, commandLine.CompilerOptions())

	if len(commandLine.Errors) > 0 {
		for _, e := range commandLine.Errors {
			reportDiagnostic(e)
		}
		return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
	}

	if pprofDir := commandLine.CompilerOptions().PprofDir; pprofDir != "" {
		// !!! stderr?
		profileSession := pprof.BeginProfiling(pprofDir, sys.Writer())
		defer profileSession.Stop()
	}

	if commandLine.CompilerOptions().Init.IsTrue() {
		tsc.WriteConfigFile(sys, locale, reportDiagnostic, commandLine.Raw.(*collections.OrderedMap[string, any]))
		return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess}
	}

	if commandLine.CompilerOptions().Version.IsTrue() {
		tsc.PrintVersion(sys, locale)
		return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess}
	}

	if commandLine.CompilerOptions().Help.IsTrue() || commandLine.CompilerOptions().All.IsTrue() {
		tsc.PrintHelp(sys, locale, commandLine)
		return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess}
	}

	if commandLine.CompilerOptions().Watch.IsTrue() && commandLine.CompilerOptions().ListFilesOnly.IsTrue() {
		reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "watch", "listFilesOnly"))
		return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
	}

	if commandLine.CompilerOptions().Project != "" {
		if len(commandLine.FileNames()) != 0 {
			reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Option_project_cannot_be_mixed_with_source_files_on_a_command_line))
			return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
		}

		fileOrDirectory := tspath.NormalizePath(commandLine.CompilerOptions().Project)
		if sys.FS().DirectoryExists(fileOrDirectory) {
			configFileName = tspath.CombinePaths(fileOrDirectory, "tsconfig.json")
			if !sys.FS().FileExists(configFileName) {
				reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Cannot_find_a_tsconfig_json_file_at_the_current_directory_Colon_0, configFileName))
				return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
			}
		} else {
			configFileName = fileOrDirectory
			if !sys.FS().FileExists(configFileName) {
				reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.The_specified_path_does_not_exist_Colon_0, fileOrDirectory))
				return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
			}
		}
	} else if !commandLine.CompilerOptions().IgnoreConfig.IsTrue() || len(commandLine.FileNames()) == 0 {
		searchPath := tspath.NormalizePath(sys.GetCurrentDirectory())
		configFileName = findConfigFile(searchPath, sys.FS().FileExists, "tsconfig.json")
		if len(commandLine.FileNames()) != 0 {
			if configFileName != "" {
				// Error to not specify config file
				reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.X_tsconfig_json_is_present_but_will_not_be_loaded_if_files_are_specified_on_commandline_Use_ignoreConfig_to_skip_this_error))
				return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
			}
		} else if configFileName == "" {
			if commandLine.CompilerOptions().ShowConfig.IsTrue() {
				reportDiagnostic(ast.NewCompilerDiagnostic(diagnostics.Cannot_find_a_tsconfig_json_file_at_the_current_directory_Colon_0, tspath.NormalizePath(sys.GetCurrentDirectory())))
			} else {
				tsc.PrintVersion(sys, locale)
				tsc.PrintHelp(sys, locale, commandLine)
			}
			return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsSkipped}
		}
	}

	// !!! convert to options with absolute paths is usually done here, but for ease of implementation, it's done in `tsoptions.ParseCommandLine()`
	compilerOptionsFromCommandLine := commandLine.CompilerOptions()
	configForCompilation := commandLine
	extendedConfigCache := &tsc.ExtendedConfigCache{}
	var compileTimes tsc.CompileTimes
	var commandLineRaw *collections.OrderedMap[string, any]
	if configFileName != "" {
		configStart := sys.Now()
		if raw, ok := commandLine.Raw.(*collections.OrderedMap[string, any]); ok {
			// Wrap command line options in a "compilerOptions" key to match tsconfig.json structure
			wrapped := &collections.OrderedMap[string, any]{}
			wrapped.Set("compilerOptions", raw)
			commandLineRaw = wrapped
		}
		configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(configFileName, compilerOptionsFromCommandLine, commandLineRaw, sys, extendedConfigCache)
		compileTimes.ConfigTime = sys.Now().Sub(configStart)
		if len(errors) != 0 {
			// these are unrecoverable errors--exit to report them as diagnostics
			for _, e := range errors {
				reportDiagnostic(e)
			}
			return tsc.CommandLineResult{Status: tsc.ExitStatusDiagnosticsPresent_OutputsGenerated}
		}
		configForCompilation = configParseResult
		// Updater to reflect pretty
		reportDiagnostic = tsc.CreateDiagnosticReporter(sys, sys.Writer(), locale, commandLine.CompilerOptions())
	}

	reportErrorSummary := tsc.CreateReportErrorSummary(sys, locale, configForCompilation.CompilerOptions())
	if compilerOptionsFromCommandLine.ShowConfig.IsTrue() {
		showConfig(sys, configForCompilation, configFileName)
		return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess}
	}
	if configForCompilation.CompilerOptions().Watch.IsTrue() {
		watcher := createWatcher(
			sys,
			configForCompilation,
			compilerOptionsFromCommandLine,
			commandLineRaw,
			reportDiagnostic,
			reportErrorSummary,
			testing,
		)
		watcher.start(ctx)
		return tsc.CommandLineResult{Status: tsc.ExitStatusSuccess, Watcher: watcher}
	} else if configForCompilation.CompilerOptions().IsIncremental() {
		return performIncrementalCompilation(
			sys,
			configForCompilation,
			reportDiagnostic,
			reportErrorSummary,
			extendedConfigCache,
			&compileTimes,
			testing,
		)
	}
	return performCompilation(
		sys,
		configForCompilation,
		reportDiagnostic,
		reportErrorSummary,
		extendedConfigCache,
		&compileTimes,
		testing,
	)
}

func findConfigFile(searchPath string, fileExists func(string) bool, configName string) string {
	result, ok := tspath.ForEachAncestorDirectory(searchPath, func(ancestor string) (string, bool) {
		fullConfigName := tspath.CombinePaths(ancestor, configName)
		if fileExists(fullConfigName) {
			return fullConfigName, true
		}
		return fullConfigName, false
	})
	if !ok {
		return ""
	}
	return result
}

func getTraceFromSys(sys tsc.System, locale locale.Locale, testing tsc.CommandLineTesting) func(msg *diagnostics.Message, args ...any) {
	return tsc.GetTraceWithWriterFromSys(sys.Writer(), locale, testing)
}

func performIncrementalCompilation(
	sys tsc.System,
	config *tsoptions.ParsedCommandLine,
	reportDiagnostic tsc.DiagnosticReporter,
	reportErrorSummary tsc.DiagnosticsReporter,
	extendedConfigCache tsoptions.ExtendedConfigCache,
	compileTimes *tsc.CompileTimes,
	testing tsc.CommandLineTesting,
) tsc.CommandLineResult {
	host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(sys, config.Locale(), testing))
	buildInfoReadStart := sys.Now()
	oldProgram := incremental.ReadBuildInfoProgram(config, incremental.NewBuildInfoReader(host), host)
	compileTimes.BuildInfoReadTime = sys.Now().Sub(buildInfoReadStart)

	tr := startTracingIfNeeded(sys, config, testing)

	parseStart := sys.Now()
	program := compiler.NewProgram(compiler.ProgramOptions{
		Config:  config,
		Host:    host,
		Tracing: tr,
	})
	compileTimes.ParseTime = sys.Now().Sub(parseStart)
	changesComputeStart := sys.Now()
	incrementalProgram := incremental.NewProgram(program, oldProgram, incremental.CreateHost(host), testing != nil)
	compileTimes.ChangesComputeTime = sys.Now().Sub(changesComputeStart)
	result, _ := tsc.EmitAndReportStatistics(tsc.EmitInput{
		Sys:                sys,
		ProgramLike:        incrementalProgram,
		Program:            incrementalProgram.GetProgram(),
		Config:             config,
		ReportDiagnostic:   reportDiagnostic,
		ReportErrorSummary: reportErrorSummary,
		Writer:             sys.Writer(),
		CompileTimes:       compileTimes,
		Testing:            testing,
		Tracing:            tr,
	})

	stopTracing(sys, tr)

	if testing != nil {
		testing.OnProgram(incrementalProgram)
	}
	return tsc.CommandLineResult{
		Status: result.Status,
	}
}

func performCompilation(
	sys tsc.System,
	config *tsoptions.ParsedCommandLine,
	reportDiagnostic tsc.DiagnosticReporter,
	reportErrorSummary tsc.DiagnosticsReporter,
	extendedConfigCache tsoptions.ExtendedConfigCache,
	compileTimes *tsc.CompileTimes,
	testing tsc.CommandLineTesting,
) tsc.CommandLineResult {
	host := compiler.NewCachedFSCompilerHost(sys.GetCurrentDirectory(), sys.FS(), sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(sys, config.Locale(), testing))

	tr := startTracingIfNeeded(sys, config, testing)

	parseStart := sys.Now()
	program := compiler.NewProgram(compiler.ProgramOptions{
		Config:  config,
		Host:    host,
		Tracing: tr,
	})
	compileTimes.ParseTime = sys.Now().Sub(parseStart)
	result, _ := tsc.EmitAndReportStatistics(tsc.EmitInput{
		Sys:                sys,
		ProgramLike:        program,
		Program:            program,
		Config:             config,
		ReportDiagnostic:   reportDiagnostic,
		ReportErrorSummary: reportErrorSummary,
		Writer:             sys.Writer(),
		CompileTimes:       compileTimes,
		Testing:            testing,
		Tracing:            tr,
	})

	stopTracing(sys, tr)

	return tsc.CommandLineResult{
		Status: result.Status,
	}
}

func showConfig(sys tsc.System, config *tsoptions.ParsedCommandLine, configFileName string) {
	tsConfig := tsoptions.ConvertToTSConfig(config, configFileName)
	_ = json.MarshalIndentWrite(sys.Writer(), tsConfig, "", "    ")
}
