package gitlab

import (
	"fmt"
	"iter"
)

type PaginationOptionFunc = RequestOptionFunc

// Scan scans all pages for the given request function f and returns individual items in an iterator.
// If an error happens during pagination, the iterator stops immediately.
// The caller must consume the returned error function to retrieve potential errors.
//
//	opts := &ListProjectsOptions{}
//	it, hasErr := Scan(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
//		return c.Projects.ListProjects(opts, p)
//	})
//	projects := slices.Collect(it)
//	if err := hasErr(); err != nil {
//		return err
//	}
//
// or with keyset-based pagination:
//
//	opts := &ListProjectsOptions{
//		ListOptions: ListOptions{
//			OrderBy:    "id",
//			Pagination: "keyset",
//		},
//	}
//	it, hasErr := Scan(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
//		return c.Projects.ListProjects(opts, p)
//	})
//	projects := slices.Collect(it)
//	if err := hasErr(); err != nil {
//		return err
//	}
//
// Attention: This API is experimental and may be subject to breaking changes to improve the API in the future.
func Scan[T any](f func(p PaginationOptionFunc) ([]T, *Response, error)) (iter.Seq[T], func() error) {
	exhausted := false
	var e error
	it := func(yield func(T) bool) {
		defer func() {
			exhausted = true
		}()
		for t, err := range Scan2(f) {
			if err != nil {
				e = err
				return
			}

			if !yield(t) {
				return
			}
		}
	}
	hasErr := func() error {
		if !exhausted {
			panic("called error function of Scan iterator before iterator was exhausted")
		}
		return e
	}
	return it, hasErr
}

// Scan2 scans all pages for the given request function f and returns individual items and potential errors in an iterator.
// The caller must consume the error element of the iterator during each iteration
// to ensure that no errors happened.
//
//	opts := &ListProjectsOptions{}
//	for p, err := range Scan2(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
//		return c.Projects.ListProjects(opts, p)
//	}) {
//		if err != nil {
//			return err
//		}
//		// do something with p
//	}
//
// or with keyset-based pagination:
//
//	opts := &ListProjectsOptions{
//		ListOptions: ListOptions{
//			OrderBy:    "id",
//			Pagination: "keyset",
//		},
//	}
//	for p, err := range Scan2(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
//		return c.Projects.ListProjects(opts, p)
//	}) {
//		if err != nil {
//			return err
//		}
//		// do something with p
//	}
//
// Attention: This API is experimental and may be subject to breaking changes to improve the API in the future.
func Scan2[T any](f func(p PaginationOptionFunc) ([]T, *Response, error)) iter.Seq2[T, error] {
	return func(yield func(T, error) bool) {
		var page PaginationOptionFunc

	Pagination:
		for {
			ts, resp, err := f(page)
			if err != nil {
				var t T
				yield(t, err)
				return
			}

			for _, t := range ts {
				if !yield(t, nil) {
					return
				}
			}

			// the f request function was either configured for offset- or keyset-based
			// pagination. We support both here, by checking if the next link is provided (keyset)
			// or not. If both are provided, keyset-based pagination takes precedence.
			next, ok := WithNext(resp)
			if !ok {
				// no more pages
				break Pagination
			}

			page = next
		}
	}
}

// Must provides a single item iterator for the provided two item iterator and panics if an error happens.
//
//	opts := &ListProjectsOptions{}
//	for p := range Must(Scan2(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
//		return c.Projects.ListProjects(opts, p)
//	})) {
//		// do something with p
//	}
//
// Attention: This API is experimental and may be subject to breaking changes to improve the API in the future.
func Must[T any](it iter.Seq2[T, error]) iter.Seq[T] {
	return func(yield func(T) bool) {
		for x, err := range it {
			if err != nil {
				panic(fmt.Errorf("iterator produced an error: %w", err))
			}

			if !yield(x) {
				return
			}
		}
	}
}

// ScanAndCollect is a convenience function that collects all results and returns them as a slice as well as an error if one happens.
//
//	opts := &ListProjectsOptions{}
//	projects, err := ScanAndCollect(func(p PaginationOptionFunc) ([]*Project, *Response, error) {
//		return c.Projects.ListProjects(opts, p)
//	}))
//	if err != nil {
//		// handle the error
//	}
//	// do something with projects
//
// Attention: This API is experimental and may be subject to breaking changes to improve the API in the future.
func ScanAndCollect[T any](f func(p PaginationOptionFunc) ([]T, *Response, error)) ([]T, error) {
	return ScanAndCollectN(f, -1)
}

// ScanAndCollectN is a convenience function that collects at most n results and
// returns them as a slice as well as an error if one happens.
//
// This is useful when you need a slice, e.g. for marshaling the data
// structures, passing the data to a function expecting a slice, or implementing
// custom sorting logic. If you want to iterate over all items, the iterator
// returned by [Scan2] is a more memory efficient alternative.
//
// n determines the number of items to collect:
//   - n > 0: at most n items are returned
//   - n == 0: the result is a nil slice (zero items)
//   - n < 0: all items  are returned (no limit)
//
// Attention: This API is experimental and may be subject to breaking changes to
// improve the API in the future.
func ScanAndCollectN[T any](f func(p PaginationOptionFunc) ([]T, *Response, error), n int) ([]T, error) {
	var items []T

	for item, err := range Scan2(f) {
		if err != nil {
			return nil, err
		}

		if n >= 0 && len(items) >= n {
			break
		}

		items = append(items, item)
	}

	return items, nil
}
