package proxy

import (
	"context"
	"encoding/base64"
	"encoding/json"

	"github.com/rancher/steve/pkg/schemaserver/types"
	"golang.org/x/sync/errgroup"
	"golang.org/x/sync/semaphore"
)

type ParallelPartitionLister struct {
	Lister      PartitionLister
	Concurrency int64
	Partitions  []Partition
	state       *listState
	revision    string
	err         error
}

type PartitionLister func(ctx context.Context, partition Partition, cont string, revision string, limit int) (types.APIObjectList, error)

func (p *ParallelPartitionLister) Err() error {
	return p.err
}

func (p *ParallelPartitionLister) Revision() string {
	return p.revision
}

func (p *ParallelPartitionLister) Continue() string {
	if p.state == nil {
		return ""
	}
	bytes, err := json.Marshal(p.state)
	if err != nil {
		return ""
	}
	return base64.StdEncoding.EncodeToString(bytes)
}

func indexOrZero(partitions []Partition, namespace string) int {
	if namespace == "" {
		return 0
	}
	for i, partition := range partitions {
		if partition.Namespace == namespace {
			return i
		}
	}
	return 0
}

func (p *ParallelPartitionLister) List(ctx context.Context, limit int, resume string) (<-chan []types.APIObject, error) {
	var state listState
	if resume != "" {
		bytes, err := base64.StdEncoding.DecodeString(resume)
		if err != nil {
			return nil, err
		}
		if err := json.Unmarshal(bytes, &state); err != nil {
			return nil, err
		}

		if state.Limit > 0 {
			limit = state.Limit
		}
	}

	result := make(chan []types.APIObject)
	go p.feeder(ctx, state, limit, result)
	return result, nil
}

type listState struct {
	Revision           string `json:"r,omitempty"`
	PartitionNamespace string `json:"p,omitempty"`
	Continue           string `json:"c,omitempty"`
	Offset             int    `json:"o,omitempty"`
	Limit              int    `json:"l,omitempty"`
}

func (p *ParallelPartitionLister) feeder(ctx context.Context, state listState, limit int, result chan []types.APIObject) {
	var (
		sem      = semaphore.NewWeighted(p.Concurrency)
		capacity = limit
		last     chan struct{}
	)

	eg, ctx := errgroup.WithContext(ctx)
	defer func() {
		err := eg.Wait()
		if p.err == nil {
			p.err = err
		}
		close(result)
	}()

	for i := indexOrZero(p.Partitions, state.PartitionNamespace); i < len(p.Partitions); i++ {
		if capacity <= 0 || isDone(ctx) {
			break
		}

		var (
			partition = p.Partitions[i]
			tickets   = int64(1)
			turn      = last
			next      = make(chan struct{})
		)

		// setup a linked list of channel to control insertion order
		last = next

		if state.Revision == "" {
			// don't have a revision yet so grab all tickets to set a revision
			tickets = 3
		}
		if err := sem.Acquire(ctx, tickets); err != nil {
			p.err = err
			break
		}

		// make state local
		state := state
		eg.Go(func() error {
			defer sem.Release(tickets)
			defer close(next)

			for {
				cont := ""
				if partition.Namespace == state.PartitionNamespace {
					cont = state.Continue
				}
				list, err := p.Lister(ctx, partition, cont, state.Revision, limit)
				if err != nil {
					return err
				}

				waitForTurn(ctx, turn)
				if p.state != nil {
					return nil
				}

				if state.Revision == "" {
					state.Revision = list.Revision
				}

				if p.revision == "" {
					p.revision = list.Revision
				}

				if state.PartitionNamespace == partition.Namespace && state.Offset > 0 && state.Offset < len(list.Objects) {
					list.Objects = list.Objects[state.Offset:]
				}

				if len(list.Objects) > capacity {
					result <- list.Objects[:capacity]
					// save state to redo this list at this offset
					p.state = &listState{
						Revision:           list.Revision,
						PartitionNamespace: partition.Namespace,
						Continue:           cont,
						Offset:             capacity,
						Limit:              limit,
					}
					capacity = 0
					return nil
				} else {
					result <- list.Objects
					capacity -= len(list.Objects)
					if list.Continue == "" {
						return nil
					}
					// loop again and get more data
					state.Continue = list.Continue
					state.PartitionNamespace = partition.Namespace
					state.Offset = 0
				}
			}
		})
	}

	p.err = eg.Wait()
}

func waitForTurn(ctx context.Context, turn chan struct{}) {
	if turn == nil {
		return
	}
	select {
	case <-turn:
	case <-ctx.Done():
	}
}

func isDone(ctx context.Context) bool {
	select {
	case <-ctx.Done():
		return true
	default:
		return false
	}
}
