package stats

import (
	"encoding/json"
	"fmt"
	"math"
	"slices"

	"github.com/ncruces/go-sqlite3"
	"github.com/ncruces/go-sqlite3/internal/util"
	"github.com/ncruces/sort/quick"
)

// Compatible with:
// https://sqlite.org/src/file/ext/misc/percentile.c

const (
	median = iota
	percentile_100
	percentile_cont
	percentile_disc
)

func newPercentile(kind int) sqlite3.AggregateConstructor {
	return func() sqlite3.AggregateFunction { return &percentile{kind: kind} }
}

type percentile struct {
	nums []float64
	arg1 []byte
	kind int
}

func (q *percentile) Step(ctx sqlite3.Context, arg ...sqlite3.Value) {
	a := arg[0]
	f := a.Float()
	if f != 0.0 || a.Type() != sqlite3.NULL {
		q.nums = append(q.nums, f)
	}
	if q.kind != median && q.arg1 == nil {
		q.arg1 = append(q.arg1, arg[1].RawText()...)
	}
}

func (q *percentile) Inverse(ctx sqlite3.Context, arg ...sqlite3.Value) {
	a := arg[0]
	f := a.Float()
	if f != 0.0 || a.Type() != sqlite3.NULL {
		i := slices.Index(q.nums, f)
		l := len(q.nums) - 1
		q.nums[i] = q.nums[l]
		q.nums = q.nums[:l]
	}
}

func (q *percentile) Value(ctx sqlite3.Context) {
	if len(q.nums) == 0 {
		return
	}

	var (
		err    error
		float  float64
		floats []float64
	)
	if q.kind == median {
		float, err = q.at(0.5)
		ctx.ResultFloat(float)
	} else if err = json.Unmarshal(q.arg1, &floats); err == nil {
		err = q.atMore(floats)
		ctx.ResultJSON(floats)
	} else if err = json.Unmarshal(q.arg1, &float); err == nil {
		float, err = q.at(float)
		ctx.ResultFloat(float)
	}
	if err != nil {
		ctx.ResultError(fmt.Errorf("percentile: %w", err)) // notest
	}
}

func (q *percentile) at(pos float64) (float64, error) {
	if q.kind == percentile_100 {
		pos = pos / 100
	}
	if pos < 0 || pos > 1 {
		return 0, util.ErrorString("invalid pos")
	}

	i, f := math.Modf(pos * float64(len(q.nums)-1))
	m0 := quick.Select(q.nums, int(i))

	if f == 0 || q.kind == percentile_disc {
		return m0, nil
	}

	m1 := slices.Min(q.nums[int(i)+1:])
	return util.Lerp(m0, m1, f), nil
}

func (q *percentile) atMore(pos []float64) error {
	for i := range pos {
		v, err := q.at(pos[i])
		if err != nil {
			return err
		}
		pos[i] = v
	}
	return nil
}
