package trace

import (
	"errors"
	"math"
	"runtime"
	"sync"
	"time"

	"github.com/projectdiscovery/utils/generic"
)

const (
	// DefaultMemorySnapshotInterval is the default interval for taking memory snapshots
	DefaultMemorySnapshotInterval = 100 * time.Millisecond
)

type MemorySnapshot struct {
	Time  time.Time
	Alloc uint64
}

type Metrics struct {
	StartTime         time.Time
	FinishTime        time.Time
	ExecutionDuration time.Duration
	Snapshots         []MemorySnapshot
	MinAllocMemory    uint64
	MaxAllocMemory    uint64
	AvgAllocMemory    uint64
}

type FunctionContext struct {
	strategy ActionStrategy
	action   func()
	before   func()
	after    func()
}

func (f *FunctionContext) Execute() {
	if f.before != nil {
		f.before()
	}

	f.strategy.Before()
	f.action()
	f.strategy.After()

	if f.after != nil {
		f.after()
	}
}

type ActionStrategy interface {
	Before()
	After()
	GetMetrics() *Metrics
}

type DefaultStrategy struct {
	metrics generic.Lockable[*Metrics]
	ticker  *time.Ticker
	done    chan bool
	wg      sync.WaitGroup
}

func (d *DefaultStrategy) Before() {
	d.metrics.Do(func(m *Metrics) {
		m.StartTime = time.Now()

		d.ticker = time.NewTicker(DefaultMemorySnapshotInterval)
		d.done = make(chan bool)
		d.wg.Add(1)
		go func() {
			defer d.wg.Done()
			for {
				select {
				case <-d.done:
					return
				case t := <-d.ticker.C:
					var mem runtime.MemStats
					runtime.ReadMemStats(&mem)
					m.Snapshots = append(m.Snapshots, MemorySnapshot{
						Time:  t,
						Alloc: mem.Alloc,
					})
				}
			}
		}()
	})
}

func (d *DefaultStrategy) After() {
	close(d.done)
	d.wg.Wait()
	d.ticker.Stop()
	d.metrics.Do(func(m *Metrics) {
		m.FinishTime = time.Now()
		m.ExecutionDuration = m.FinishTime.Sub(m.StartTime)

		var totalMemory uint64 = 0
		if len(m.Snapshots) > 0 {
			m.MinAllocMemory = m.Snapshots[0].Alloc
			m.MaxAllocMemory = m.Snapshots[0].Alloc

			for _, s := range m.Snapshots {
				if s.Alloc < m.MinAllocMemory {
					m.MinAllocMemory = s.Alloc
				}
				m.MinAllocMemory = uint64(math.Min(float64(m.MinAllocMemory), float64(s.Alloc)))
				m.MaxAllocMemory = uint64(math.Max(float64(m.MaxAllocMemory), float64(s.Alloc)))
				totalMemory += s.Alloc
			}
			m.AvgAllocMemory = totalMemory / uint64(len(m.Snapshots))
		}
	})

}

func (d *DefaultStrategy) GetMetrics() *Metrics {
	var metrics *Metrics
	d.metrics.Do(func(m *Metrics) {
		metrics = m
	})
	return metrics
}

type TraceOptions struct {
	strategy ActionStrategy
	before   func()
	after    func()
}

type TraceOptionSetter func(opts *TraceOptions)

func WithStrategy(s ActionStrategy) TraceOptionSetter {
	return func(opts *TraceOptions) {
		opts.strategy = s
	}
}

func WithBefore(b func()) TraceOptionSetter {
	return func(opts *TraceOptions) {
		opts.before = b
	}
}

func WithAfter(a func()) TraceOptionSetter {
	return func(opts *TraceOptions) {
		opts.after = a
	}
}

func Trace(f func(), setters ...TraceOptionSetter) (*Metrics, error) {
	opts := &TraceOptions{
		strategy: &DefaultStrategy{metrics: generic.Lockable[*Metrics]{V: &Metrics{}}},
	}

	// Apply option if provided
	for _, setter := range setters {
		setter(opts)
	}

	if opts.strategy == nil {
		return nil, errors.New("strategy should not be nil")
	}

	context := &FunctionContext{
		strategy: opts.strategy,
		action:   f,
		before:   opts.before,
		after:    opts.after,
	}

	context.Execute()
	return opts.strategy.GetMetrics(), nil
}
