package runsummary

import (
	"encoding/json"
	"fmt"
	"os"
	"sync"
	"time"

	"github.com/vercel/turbo/cli/internal/chrometracing"
	"github.com/vercel/turbo/cli/internal/fs"
	"github.com/vercel/turbo/cli/internal/turbopath"

	"github.com/mitchellh/cli"
)

// executionEvent represents a single event in the build process, i.e. a target starting or finishing
// building, or reaching some milestone within those steps.
type executionEvent struct {
	// Timestamp of this event
	Time time.Time
	// Duration of this event
	Duration time.Duration
	// Target which has just changed
	Label string
	// Its current status
	Status executionEventName
	// Error, only populated for failure statuses
	Err string

	exitCode *int
}

// executionEventName represents the status of a target when we log a build result.
type executionEventName int

// The collection of expected build result statuses.
const (
	targetInitialized executionEventName = iota
	TargetBuilding
	TargetBuildStopped
	TargetExecuted
	TargetBuilt
	TargetCached
	TargetBuildFailed
)

func (en executionEventName) toString() string {
	switch en {
	case targetInitialized:
		return "initialized"
	case TargetBuilding:
		return "building"
	case TargetBuildStopped:
		return "buildStopped"
	case TargetExecuted:
		return "executed"
	case TargetBuilt:
		return "built"
	case TargetCached:
		return "cached"
	case TargetBuildFailed:
		return "buildFailed"
	}

	return ""
}

// TaskExecutionSummary contains data about the state of a single task in a turbo run.
// Some fields are updated over time as the task prepares to execute and finishes execution.
type TaskExecutionSummary struct {
	startAt  time.Time          // set once
	status   executionEventName // current status, updated during execution
	err      string             // only populated for failure statuses
	Duration time.Duration      // updated during the task execution
	exitCode *int               // pointer so we can distinguish between 0 and unknown.
}

func (ts *TaskExecutionSummary) endTime() time.Time {
	return ts.startAt.Add(ts.Duration)
}

// MarshalJSON munges the TaskExecutionSummary into a format we want
// We'll use an anonmyous, private struct for this, so it's not confusingly duplicated
func (ts *TaskExecutionSummary) MarshalJSON() ([]byte, error) {
	serializable := struct {
		Start    int64  `json:"startTime"`
		End      int64  `json:"endTime"`
		Err      string `json:"error,omitempty"`
		ExitCode *int   `json:"exitCode"`
	}{
		Start:    ts.startAt.UnixMilli(),
		End:      ts.endTime().UnixMilli(),
		Err:      ts.err,
		ExitCode: ts.exitCode,
	}

	return json.Marshal(&serializable)
}

// ExitCode access exit code nil means no exit code was received
func (ts *TaskExecutionSummary) ExitCode() *int {
	var exitCode int
	if ts.exitCode == nil {
		return nil
	}
	exitCode = *ts.exitCode
	return &exitCode
}

// executionSummary is the state of the entire `turbo run`. Individual task state in `Tasks` field
type executionSummary struct {
	// mu guards reads/writes to the `state` field
	mu              sync.Mutex
	tasks           map[string]*TaskExecutionSummary // key is a taskID
	profileFilename string

	// These get serialized to JSON
	command   string                       // a synthesized turbo command to produce this invocation
	repoPath  turbopath.RelativeSystemPath // the (possibly empty) path from the turborepo root to where the command was run
	success   int                          // number of tasks that exited successfully (does not include cache hits)
	failure   int                          // number of tasks that exited with failure
	cached    int                          // number of tasks that had a cache hit
	attempted int                          // number of tasks that started
	startedAt time.Time
	endedAt   time.Time
	exitCode  int
}

func (es *executionSummary) Duration() time.Duration {
	ended := es.endedAt
	if ended.IsZero() {
		ended = time.Now()
	}

	return ended.Sub(es.startedAt)
}

// MarshalJSON munges the executionSummary into a format we want
// We'll use an anonmyous, private struct for this, so it's not confusingly duplicated.
func (es *executionSummary) MarshalJSON() ([]byte, error) {
	serializable := struct {
		Command   string `json:"command"`
		RepoPath  string `json:"repoPath"`
		Success   int    `json:"success"`
		Failure   int    `json:"failed"`
		Cached    int    `json:"cached"`
		Attempted int    `json:"attempted"`
		StartTime int64  `json:"startTime"`
		EndTime   int64  `json:"endTime"`
		ExitCode  int    `json:"exitCode"`
	}{
		Command:   es.command,
		RepoPath:  es.repoPath.ToString(),
		StartTime: es.startedAt.UnixMilli(),
		EndTime:   es.endedAt.UnixMilli(),
		Success:   es.success,
		Failure:   es.failure,
		Cached:    es.cached,
		Attempted: es.attempted,
		ExitCode:  es.exitCode,
	}

	return json.Marshal(&serializable)
}

// newExecutionSummary creates a executionSummary instance to track events in a `turbo run`.`
func newExecutionSummary(command string, repoPath turbopath.RelativeSystemPath, start time.Time, tracingProfile string) *executionSummary {
	if tracingProfile != "" {
		chrometracing.EnableTracing()
	}

	return &executionSummary{
		command:         command,
		repoPath:        repoPath,
		success:         0,
		failure:         0,
		cached:          0,
		attempted:       0,
		tasks:           make(map[string]*TaskExecutionSummary),
		startedAt:       start,
		profileFilename: tracingProfile,
	}
}

// Run starts the Execution of a single task. It returns a function that can
// be used to update the state of a given taskID with the executionEventName enum
func (es *executionSummary) run(taskID string) (func(outcome executionEventName, err error, exitCode *int), *TaskExecutionSummary) {
	start := time.Now()
	taskExecutionSummary := es.add(&executionEvent{
		Time:   start,
		Label:  taskID,
		Status: targetInitialized,
	})

	tracer := chrometracing.Event(taskID)

	// This function can be called with an enum and an optional error to update
	// the state of a given taskID.
	tracerFn := func(outcome executionEventName, err error, exitCode *int) {
		defer tracer.Done()
		now := time.Now()
		result := &executionEvent{
			Time:     now,
			Duration: now.Sub(start),
			Label:    taskID,
			Status:   outcome,
			// We'll assign this here regardless of whether it is nil, but we'll check for nil
			// when we assign it to the taskExecutionSummary.
			exitCode: exitCode,
		}

		if err != nil {
			result.Err = err.Error()
		}

		// Ignore the return value here
		es.add(result)
	}

	return tracerFn, taskExecutionSummary
}

func (es *executionSummary) add(event *executionEvent) *TaskExecutionSummary {
	es.mu.Lock()
	defer es.mu.Unlock()

	var taskExecSummary *TaskExecutionSummary
	if ts, ok := es.tasks[event.Label]; ok {
		// If we already know about this task, we'll update it with the new event
		taskExecSummary = ts
	} else {
		// If we don't know about it yet, init and add it into the parent struct
		// (event.Status should always be `targetBuilding` here.)
		taskExecSummary = &TaskExecutionSummary{startAt: event.Time}
		es.tasks[event.Label] = taskExecSummary
	}

	// Update the Status, Duration, and Err fields
	taskExecSummary.status = event.Status
	taskExecSummary.err = event.Err
	taskExecSummary.Duration = event.Duration

	if event.exitCode != nil {
		taskExecSummary.exitCode = event.exitCode
	}

	switch {
	case event.Status == TargetBuilding:
		es.attempted++
	case event.Status == TargetBuildFailed:
		es.failure++
	case event.Status == TargetCached:
		es.cached++
	case event.Status == TargetBuilt:
		es.success++
	}

	return es.tasks[event.Label]
}

// writeChromeTracing writes to a profile name if the `--profile` flag was passed to turbo run
func writeChrometracing(filename string, terminal cli.Ui) error {
	outputPath := chrometracing.Path()
	if outputPath == "" {
		// tracing wasn't enabled
		return nil
	}

	name := fmt.Sprintf("turbo-%s.trace", time.Now().Format(time.RFC3339))
	if filename != "" {
		name = filename
	}
	if err := chrometracing.Close(); err != nil {
		terminal.Warn(fmt.Sprintf("Failed to flush tracing data: %v", err))
	}
	cwdRaw, err := os.Getwd()
	if err != nil {
		return err
	}
	root, err := fs.GetCwd(cwdRaw)
	if err != nil {
		return err
	}
	// chrometracing.Path() is absolute by default, but can still be relative if overriden via $CHROMETRACING_DIR
	// so we have to account for that before converting to turbopath.AbsoluteSystemPath
	if err := fs.CopyFile(&fs.LstatCachedFile{Path: fs.ResolveUnknownPath(root, outputPath)}, name); err != nil {
		return err
	}
	return nil
}
