// Package cmd holds the root cobra command for turbo
package cmd

import (
	"context"
	"fmt"
	"os"
	"runtime/pprof"
	"runtime/trace"

	"github.com/pkg/errors"
	"github.com/vercel/turbo/cli/internal/cmdutil"
	"github.com/vercel/turbo/cli/internal/daemon"
	"github.com/vercel/turbo/cli/internal/process"
	"github.com/vercel/turbo/cli/internal/run"
	"github.com/vercel/turbo/cli/internal/signals"
	"github.com/vercel/turbo/cli/internal/turbostate"
	"github.com/vercel/turbo/cli/internal/util"
)

func initializeOutputFiles(helper *cmdutil.Helper, parsedArgs *turbostate.ParsedArgsFromRust) error {
	if parsedArgs.Trace != "" {
		cleanup, err := createTraceFile(parsedArgs.Trace)
		if err != nil {
			return fmt.Errorf("failed to create trace file: %v", err)
		}
		helper.RegisterCleanup(cleanup)
	}
	if parsedArgs.Heap != "" {
		cleanup, err := createHeapFile(parsedArgs.Heap)
		if err != nil {
			return fmt.Errorf("failed to create heap file: %v", err)
		}
		helper.RegisterCleanup(cleanup)
	}
	if parsedArgs.CPUProfile != "" {
		cleanup, err := createCpuprofileFile(parsedArgs.CPUProfile)
		if err != nil {
			return fmt.Errorf("failed to create CPU profile file: %v", err)
		}
		helper.RegisterCleanup(cleanup)
	}

	return nil
}

// RunWithExecutionState runs turbo with the ParsedArgsFromRust that is passed from the Rust side.
func RunWithExecutionState(executionState *turbostate.ExecutionState, turboVersion string) int {
	util.InitPrintf()
	// TODO: replace this with a context
	signalWatcher := signals.NewWatcher()
	helper := cmdutil.NewHelper(turboVersion, &executionState.CLIArgs)
	ctx := context.Background()

	err := initializeOutputFiles(helper, &executionState.CLIArgs)
	if err != nil {
		fmt.Printf("%v", err)
		return 1
	}
	defer helper.Cleanup(&executionState.CLIArgs)

	doneCh := make(chan struct{})
	var execErr error
	go func() {
		command := executionState.CLIArgs.Command
		if command.Daemon != nil {
			execErr = daemon.ExecuteDaemon(ctx, helper, signalWatcher, executionState)
		} else if command.Run != nil {
			execErr = run.ExecuteRun(ctx, helper, signalWatcher, executionState)
		} else {
			execErr = fmt.Errorf("unknown command: %v", command)
		}

		close(doneCh)
	}()

	// Wait for either our command to finish, in which case we need to clean up,
	// or to receive a signal, in which case the signal handler above does the cleanup
	select {
	case <-doneCh:
		// We finished whatever task we were running
		signalWatcher.Close()
		exitErr := &process.ChildExit{}
		if errors.As(execErr, &exitErr) {
			return exitErr.ExitCode
		} else if execErr != nil {
			return 1
		}
		return 0
	case <-signalWatcher.Done():
		// We caught a signal, which already called the close handlers
		return 1
	}
}

type profileCleanup func() error

// Close implements io.Close for profileCleanup
func (pc profileCleanup) Close() error {
	return pc()
}

// To view a CPU trace, use "go tool trace [file]". Note that the trace
// viewer doesn't work under Windows Subsystem for Linux for some reason.
func createTraceFile(traceFile string) (profileCleanup, error) {
	f, err := os.Create(traceFile)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create trace file: %v", traceFile)
	}
	if err := trace.Start(f); err != nil {
		return nil, errors.Wrap(err, "failed to start tracing")
	}
	return func() error {
		trace.Stop()
		return f.Close()
	}, nil
}

// To view a heap trace, use "go tool pprof [file]" and type "top". You can
// also drop it into https://speedscope.app and use the "left heavy" or
// "sandwich" view modes.
func createHeapFile(heapFile string) (profileCleanup, error) {
	f, err := os.Create(heapFile)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create heap file: %v", heapFile)
	}
	return func() error {
		if err := pprof.WriteHeapProfile(f); err != nil {
			// we don't care if we fail to close the file we just failed to write to
			_ = f.Close()
			return errors.Wrapf(err, "failed to write heap file: %v", heapFile)
		}
		return f.Close()
	}, nil
}

// To view a CPU profile, drop the file into https://speedscope.app.
// Note: Running the CPU profiler doesn't work under Windows subsystem for
// Linux. The profiler has to be built for native Windows and run using the
// command prompt instead.
func createCpuprofileFile(cpuprofileFile string) (profileCleanup, error) {
	f, err := os.Create(cpuprofileFile)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create cpuprofile file: %v", cpuprofileFile)
	}
	if err := pprof.StartCPUProfile(f); err != nil {
		return nil, errors.Wrap(err, "failed to start CPU profiling")
	}
	return func() error {
		pprof.StopCPUProfile()
		return f.Close()
	}, nil
}
