//go:build darwin || (linux && amd64)
// +build darwin linux,amd64

package engine

import (
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/kopia/kopia/internal/clock"
)

var (
	repoBuildTime   = "unknown"
	repoGitRevision = "unknown"
	repoGitBranch   = "unknown"
	testBuildTime   = "unknown"
	testGitRevision = "unknown"
	testGitBranch   = "unknown"
)

// Stats prints the engine stats, cumulative and from the current run.
func (e *Engine) Stats() string {
	b := &strings.Builder{}

	fmt.Fprintln(b, "==================================")
	fmt.Fprintln(b, "Build Info")
	fmt.Fprintln(b, "==================================")
	fmt.Fprintf(b, "  Repo build time:      %25v\n", repoBuildTime)
	fmt.Fprintf(b, "  Repo git revision:    %25v\n", repoGitRevision)
	fmt.Fprintf(b, "  Repo git branch:      %25v\n", repoGitBranch)
	fmt.Fprintln(b, "")
	fmt.Fprintf(b, "  Engine build time:    %25v\n", testBuildTime)
	fmt.Fprintf(b, "  Engine git revision:  %25v\n", testGitRevision)
	fmt.Fprintf(b, "  Engine git branch:    %25v\n", testGitBranch)
	fmt.Fprintln(b, "")
	fmt.Fprintln(b, "==================================")
	fmt.Fprintln(b, "Engine Action Summary (Cumulative)")
	fmt.Fprintln(b, "==================================")
	fmt.Fprintf(b, "  Engine runtime:   %10vs\n", e.getRuntimeSeconds())
	fmt.Fprintln(b, "")
	fmt.Fprint(b, e.CumulativeStats.Stats())
	fmt.Fprintln(b, "")

	fmt.Fprintln(b, "==================================")
	fmt.Fprintln(b, "Engine Action Summary (This Run)")
	fmt.Fprintln(b, "==================================")
	fmt.Fprint(b, e.RunStats.Stats())
	fmt.Fprintln(b, "")

	return b.String()
}

// Stats tracks statistics during engine runtime.
type Stats struct {
	RunCounter     int64
	ActionCounter  int64
	CreationTime   time.Time
	RunTime        time.Duration
	PerActionStats map[ActionKey]*ActionStats

	DataRestoreCount   int64
	DataPurgeCount     int64
	ErrorRecoveryCount int64
	NoOpCount          int64
}

// Stats returns a string report of the engine's stats.
func (stats *Stats) Stats() string {
	b := &strings.Builder{}

	fmt.Fprintln(b, "=============")
	fmt.Fprintln(b, "Stat summary")
	fmt.Fprintln(b, "=============")
	fmt.Fprintf(b, "  Number of runs:     %10v\n", stats.RunCounter)
	fmt.Fprintf(b, "  Engine lifetime:   %10vs\n", stats.getLifetimeSeconds())
	fmt.Fprintf(b, "  Actions run:        %10v\n", stats.ActionCounter)
	fmt.Fprintf(b, "  Errors recovered:   %10v\n", stats.ErrorRecoveryCount)
	fmt.Fprintf(b, "  Data dir restores:  %10v\n", stats.DataRestoreCount)
	fmt.Fprintf(b, "  Data dir purges:    %10v\n", stats.DataPurgeCount)
	fmt.Fprintf(b, "  NoOp count:         %10v\n", stats.NoOpCount)
	fmt.Fprintln(b, "")
	fmt.Fprintln(b, "=============")
	fmt.Fprintln(b, "Action stats")
	fmt.Fprintln(b, "=============")

	for actionKey, actionStat := range stats.PerActionStats {
		fmt.Fprintf(b, "%s:\n", actionKey)
		fmt.Fprintf(b, "  Count:            %10d\n", actionStat.Count)
		fmt.Fprintf(b, "  Avg Runtime:      %10v\n", actionStat.avgRuntimeString())
		fmt.Fprintf(b, "  Max Runtime:     %10vs\n", durationToSec(actionStat.MaxRuntime))
		fmt.Fprintf(b, "  Min Runtime:     %10vs\n", durationToSec(actionStat.MinRuntime))
		fmt.Fprintf(b, "  Error Count:      %10v\n", actionStat.ErrorCount)
		fmt.Fprintln(b, "")
	}

	return b.String()
}

// ActionStats tracks runtime statistics for an action.
type ActionStats struct {
	Count        int64
	TotalRuntime time.Duration
	MinRuntime   time.Duration
	MaxRuntime   time.Duration
	ErrorCount   int64
}

// AverageRuntime returns the average run time for the action.
func (s *ActionStats) AverageRuntime() time.Duration {
	return time.Duration(int64(s.TotalRuntime) / s.Count)
}

// Record records the current time against the provided start time
// and updates the stats accordingly.
func (s *ActionStats) Record(st time.Time, err error) {
	thisRuntime := clock.Now().Sub(st)
	s.TotalRuntime += thisRuntime

	if thisRuntime > s.MaxRuntime {
		s.MaxRuntime = thisRuntime
	}

	if s.Count == 0 || thisRuntime < s.MinRuntime {
		s.MinRuntime = thisRuntime
	}

	s.Count++

	if err != nil {
		s.ErrorCount++
	}
}

func (stats *Stats) getLifetimeSeconds() int64 {
	return durationToSec(clock.Now().Sub(stats.CreationTime))
}

func durationToSec(dur time.Duration) int64 {
	return int64(dur.Round(time.Second).Seconds())
}

func (s *ActionStats) avgRuntimeString() string {
	if s.Count == 0 {
		return "--"
	}

	return fmt.Sprintf("%vs", durationToSec(s.AverageRuntime()))
}

func (e *Engine) getTimestampS() int64 {
	return e.getRuntimeSeconds()
}

func (e *Engine) getRuntimeSeconds() int64 {
	return durationToSec(e.CumulativeStats.RunTime + clock.Now().Sub(e.RunStats.CreationTime))
}

type statsUpdatetype int

const (
	statsIncrNoOp statsUpdatetype = iota
	statsIncrDataPurge
	statsIncrDataRestore
	statsIncrErrorRecovery
)

func (e *Engine) statsUpdateCounters(updateType statsUpdatetype) {
	e.statsMux.Lock()
	defer e.statsMux.Unlock()

	switch updateType {
	case statsIncrNoOp:
		e.RunStats.NoOpCount++
		e.CumulativeStats.NoOpCount++
	case statsIncrDataPurge:
		e.RunStats.DataPurgeCount++
		e.CumulativeStats.DataPurgeCount++
	case statsIncrDataRestore:
		e.RunStats.DataRestoreCount++
		e.CumulativeStats.DataRestoreCount++
	case statsIncrErrorRecovery:
		e.RunStats.ErrorRecoveryCount++
		e.CumulativeStats.ErrorRecoveryCount++
	}
}

func (e *Engine) statsIncrActionCountAndLog(actionKey ActionKey) {
	e.statsMux.Lock()
	defer e.statsMux.Unlock()

	e.RunStats.ActionCounter++
	e.CumulativeStats.ActionCounter++

	log.Printf(
		"Engine executing ACTION: name=%q actionCount=%v totActCount=%v t=%vs (%vs)",
		actionKey,
		e.RunStats.ActionCounter,
		e.CumulativeStats.ActionCounter,
		e.RunStats.getLifetimeSeconds(),
		e.getRuntimeSeconds(),
	)
}

func (e *Engine) statsUpdatePerAction(actionKey ActionKey, st time.Time, err error) {
	e.statsMux.Lock()
	defer e.statsMux.Unlock()

	if e.RunStats.PerActionStats != nil && e.RunStats.PerActionStats[actionKey] == nil {
		e.RunStats.PerActionStats[actionKey] = new(ActionStats)
	}

	if e.CumulativeStats.PerActionStats != nil && e.CumulativeStats.PerActionStats[actionKey] == nil {
		e.CumulativeStats.PerActionStats[actionKey] = new(ActionStats)
	}

	e.RunStats.PerActionStats[actionKey].Record(st, err)
	e.CumulativeStats.PerActionStats[actionKey].Record(st, err)
}
