package datasource

import (
	"reflect"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"go.uber.org/zap"

	"github.com/pingcap-inc/clinic/pkg/auth/session"
	"github.com/pingcap-inc/clinic/pkg/datasource/view"
)

type DataSource func(ctx *session.CtxUser, query view.QueryFilter) (any, int64, error)

// joinDataSourceToSlice return specify data from accessor1 and accessor2
//
// There are 3 cases:
// 1. accessor1 contains all wanted records
// 2. accessor1 contains part of the data. Need to fetch data from accessor2
// 3. accessor2 contains all wanted records, the results need to skip the data in case2
func joinDataSourceToSlice(accessor1, accessor2 DataSource, ctx *session.CtxUser, query view.QueryFilter, dst any) (int64, error) {
	if reflect.TypeOf(dst).Kind() != reflect.Ptr {
		return 0, errors.New("the type must be prt type")
	}

	dstRV := reflect.ValueOf(dst).Elem()

	v1, total1, err := accessor1(ctx, query)
	if err != nil {
		return 0, err
	}
	log.Info("joinDataSourceToSlice", zap.Any("total1", total1))
	rv1 := reflect.ValueOf(v1)
	v1Len := 0

	if v1 != nil && !rv1.IsZero() {
		r1 := reflect.TypeOf(v1)
		if r1.Kind() != dstRV.Kind() {
			return 0, errors.New("accessor1 must be slice return type")
		}

		v1Len = rv1.Len()
	}

	results := reflect.MakeSlice(dstRV.Type(), 0, 0)
	for i := 0; i < v1Len; i++ {
		results = reflect.Append(results, rv1.Index(i))
	}
	defer func() {
		dstRV.Set(results)
	}()
	// case1: accessor1 include all wanted data
	if v1Len >= query.Limit {
		_, total2, err := accessor2(ctx, query)
		if err != nil {
			return 0, err
		}
		return total1 + total2, nil
	}

	result2 := reflect.MakeSlice(dstRV.Type(), 0, 0)
	queryPages := calculatePages(int(total1), query)

	var (
		total2   int64
		v2       any
		queryErr error
	)

	for _, page := range queryPages {
		query2 := view.QueryFilter{
			Page:        page,
			Limit:       query.Limit,
			ExtraFilter: query.ExtraFilter,
		}

		v2, total2, queryErr = accessor2(ctx, query2)
		if queryErr != nil {
			return 0, queryErr
		}

		if v2 == nil {
			break
		}

		r2 := reflect.TypeOf(v2)
		if r2.Kind() != dstRV.Kind() {
			return 0, errors.New("accessor2 must be slice return type")
		}

		// accessor2 has been drained
		rv2 := reflect.ValueOf(v2)
		if rv2.IsZero() {
			break
		}

		v2Len := rv2.Len()
		for i := 0; i < v2Len; i++ {
			result2 = reflect.Append(result2, rv2.Index(i))
		}

		if v2Len < query.Limit {
			// accessor2 has been drained
			break
		}
	}
	log.Info("joinDataSourceToSlice", zap.Any("total2", total2))

	skip := 0
	if result2.Len() > 0 {
		skip = calculateSkip(int(total1), queryPages, query)
	}

	for i, j := skip, 0; i < min(result2.Len(), skip+query.Limit) && j < query.Limit-v1Len; i, j = i+1, j+1 {
		results = reflect.Append(results, result2.Index(i))
	}

	return total1 + total2, nil
}

func min(v1, v2 int) int {
	if v1 > v2 {
		return v2
	}

	return v1
}

func calculatePages(total1 int, pq view.QueryFilter) []int {
	if total1 >= pq.Page*pq.Limit {
		return nil
	}
	if total1%pq.Limit == 0 {
		return []int{pq.Page - total1/pq.Limit}
	}

	page := pq.Page - total1/pq.Limit
	if page <= 1 {
		return []int{page}
	}

	return []int{page - 1, page}
}

func calculateSkip(total1 int, pages []int, pq view.QueryFilter) int {
	if len(pages) <= 1 {
		return 0
	}

	return pq.Limit - total1%pq.Limit
}
