package cli

import (
	"context"
	"os"
	"path/filepath"
	"runtime"
	"runtime/pprof"

	"github.com/alecthomas/kingpin/v2"
	"github.com/pkg/errors"
)

const profDirName = "profiles"

type profileFlags struct {
	profileGCBeforeSaving bool
	profileCPU            bool
	profileBlockingRate   int
	profileMemoryRate     int
	profileMutexFraction  int
	saveProfiles          bool

	outputDirectory  string
	cpuProfileCloser func()
}

func (c *profileFlags) setup(app *kingpin.Application) {
	c.profileBlockingRate = -1
	c.profileMemoryRate = -1
	c.profileMutexFraction = -1

	app.Flag("profile-store-on-exit", "Writes profiling data on exit. It writes a file per profile type (heap, goroutine, threadcreate, block, mutex) in a sub-directory in the directory specified with the --diagnostics-output-directory").Hidden().BoolVar(&c.saveProfiles) //nolint:lll
	app.Flag("profile-go-gc-before-dump", "Perform a Go GC before writing out memory profiles").Hidden().BoolVar(&c.profileGCBeforeSaving)
	app.Flag("profile-blocking-rate", "Blocking profiling rate, a value of 0 turns off block profiling").Hidden().IntVar(&c.profileBlockingRate)
	app.Flag("profile-cpu", "Enable CPU profiling").Hidden().BoolVar(&c.profileCPU)
	app.Flag("profile-memory-rate", "Memory profiling rate").Hidden().IntVar(&c.profileMemoryRate)
	app.Flag("profile-mutex-fraction", "Mutex profiling, a value of 0 turns off mutex profiling").Hidden().IntVar(&c.profileMutexFraction)
}

func (c *profileFlags) start(ctx context.Context, outputDirectory string) error {
	pBlockingRate := c.profileBlockingRate
	pMemoryRate := c.profileMemoryRate
	pMutexFraction := c.profileMutexFraction

	if c.saveProfiles {
		// when saving profiles ensure profiling parameters have sensible values
		// unless explicitly modified.
		// runtime.MemProfileRate has a default value, no need to reset it.
		if pBlockingRate == -1 {
			pBlockingRate = 1
		}

		if pMutexFraction == -1 {
			pMutexFraction = 1
		}
	}

	// set profiling parameters if they have been changed from defaults
	if pBlockingRate != -1 {
		runtime.SetBlockProfileRate(pBlockingRate)
	}

	if pMemoryRate != -1 {
		runtime.MemProfileRate = pMemoryRate
	}

	if pMutexFraction != -1 {
		runtime.SetMutexProfileFraction(pMutexFraction)
	}

	if !c.profileCPU && !c.saveProfiles {
		return nil
	}

	c.outputDirectory = outputDirectory

	// ensure upfront that the pprof output dir can be created.
	profDir, err := mkSubdirectories(c.outputDirectory, profDirName)
	if err != nil {
		return err
	}

	if !c.profileCPU {
		return nil
	}

	// start CPU profile dumper
	f, err := os.Create(filepath.Join(profDir, "cpu.pprof")) //nolint:gosec
	if err != nil {
		return errors.Wrap(err, "could not create CPU profile output file")
	}

	// CPU profile closer
	closer := func() {
		pprof.StopCPUProfile()

		if err := f.Close(); err != nil {
			log(ctx).Warn("error closing CPU profile output file:", err)
		}
	}

	if err := pprof.StartCPUProfile(f); err != nil {
		closer()

		return errors.Wrap(err, "could not start CPU profile")
	}

	c.cpuProfileCloser = closer

	return nil
}

func (c *profileFlags) stop(ctx context.Context) {
	if c.cpuProfileCloser != nil {
		c.cpuProfileCloser()
		c.cpuProfileCloser = nil
	}

	if !c.saveProfiles {
		return
	}

	if c.profileGCBeforeSaving {
		// update profiles, otherwise they may not include activity after the last GC
		runtime.GC()
	}

	profDir, err := mkSubdirectories(c.outputDirectory, profDirName)
	if err != nil {
		log(ctx).Warn("cannot create directory to save profiles:", err)
	}

	for _, p := range pprof.Profiles() {
		func() {
			fname := filepath.Join(profDir, p.Name()+".pprof")

			f, err := os.Create(fname) //nolint:gosec
			if err != nil {
				log(ctx).Warnf("unable to create profile output file '%s': %v", fname, err)

				return
			}

			defer func() {
				if err := f.Close(); err != nil {
					log(ctx).Warnf("unable to close profile output file '%s': %v", fname, err)
				}
			}()

			if err := p.WriteTo(f, 0); err != nil {
				log(ctx).Warnf("unable to write profile to file '%s': %v", fname, err)
			}
		}()
	}
}
