package linodego

/**
 * Pagination and Filtering types and helpers
 */

import (
	"context"
	"fmt"
	"log"
	"strconv"

	"github.com/go-resty/resty/v2"
)

// PageOptions are the pagination parameters for List endpoints
type PageOptions struct {
	Page    int `url:"page,omitempty" json:"page"`
	Pages   int `url:"pages,omitempty" json:"pages"`
	Results int `url:"results,omitempty" json:"results"`
}

// ListOptions are the pagination and filtering (TODO) parameters for endpoints
type ListOptions struct {
	*PageOptions
	Filter string
}

// NewListOptions simplified construction of ListOptions using only
// the two writable properties, Page and Filter
func NewListOptions(page int, filter string) *ListOptions {
	return &ListOptions{PageOptions: &PageOptions{Page: page}, Filter: filter}
}

// listHelper abstracts fetching and pagination for GET endpoints that
// do not require any Ids (top level endpoints).
// When opts (or opts.Page) is nil, all pages will be fetched and
// returned in a single (endpoint-specific)PagedResponse
// opts.results and opts.pages will be updated from the API response
// nolint
func (c *Client) listHelper(ctx context.Context, i interface{}, opts *ListOptions) error {
	req := c.R(ctx)
	if opts != nil && opts.PageOptions != nil && opts.Page > 0 {
		req.SetQueryParam("page", strconv.Itoa(opts.Page))
	}

	var (
		err     error
		pages   int
		results int
		r       *resty.Response
	)

	if opts != nil && len(opts.Filter) > 0 {
		req.SetHeader("X-Filter", opts.Filter)
	}

	switch v := i.(type) {
	case *LinodeKernelsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(LinodeKernelsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*LinodeKernelsPagedResponse).Pages
			results = r.Result().(*LinodeKernelsPagedResponse).Results
			v.appendData(r.Result().(*LinodeKernelsPagedResponse))
		}
	case *LinodeTypesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(LinodeTypesPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*LinodeTypesPagedResponse).Pages
			results = r.Result().(*LinodeTypesPagedResponse).Results
			v.appendData(r.Result().(*LinodeTypesPagedResponse))
		}
	case *ImagesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(ImagesPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*ImagesPagedResponse).Pages
			results = r.Result().(*ImagesPagedResponse).Results
			v.appendData(r.Result().(*ImagesPagedResponse))
		}
	case *StackscriptsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(StackscriptsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*StackscriptsPagedResponse).Pages
			results = r.Result().(*StackscriptsPagedResponse).Results
			v.appendData(r.Result().(*StackscriptsPagedResponse))
		}
	case *InstancesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(InstancesPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*InstancesPagedResponse).Pages
			results = r.Result().(*InstancesPagedResponse).Results
			v.appendData(r.Result().(*InstancesPagedResponse))
		}
	case *RegionsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(RegionsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*RegionsPagedResponse).Pages
			results = r.Result().(*RegionsPagedResponse).Results
			v.appendData(r.Result().(*RegionsPagedResponse))
		}
	case *VolumesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(VolumesPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*VolumesPagedResponse).Pages
			results = r.Result().(*VolumesPagedResponse).Results
			v.appendData(r.Result().(*VolumesPagedResponse))
		}
	case *DomainsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(DomainsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			response, ok := r.Result().(*DomainsPagedResponse)
			if !ok {
				return fmt.Errorf("response is not a *DomainsPagedResponse")
			}
			pages = response.Pages
			results = response.Results
			v.appendData(response)
		}
	case *EventsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(EventsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*EventsPagedResponse).Pages
			results = r.Result().(*EventsPagedResponse).Results
			v.appendData(r.Result().(*EventsPagedResponse))
		}
	case *FirewallsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(FirewallsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*FirewallsPagedResponse).Pages
			results = r.Result().(*FirewallsPagedResponse).Results
			v.appendData(r.Result().(*FirewallsPagedResponse))
		}
	case *LKEClustersPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(LKEClustersPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*LKEClustersPagedResponse).Pages
			results = r.Result().(*LKEClustersPagedResponse).Results
			v.appendData(r.Result().(*LKEClustersPagedResponse))
		}
	case *LKEVersionsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(LKEVersionsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*LKEVersionsPagedResponse).Pages
			results = r.Result().(*LKEVersionsPagedResponse).Results
			v.appendData(r.Result().(*LKEVersionsPagedResponse))
		}
	case *LongviewSubscriptionsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(LongviewSubscriptionsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*LongviewSubscriptionsPagedResponse).Pages
			results = r.Result().(*LongviewSubscriptionsPagedResponse).Results
			v.appendData(r.Result().(*LongviewSubscriptionsPagedResponse))
		}
	case *LongviewClientsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(LongviewClientsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*LongviewClientsPagedResponse).Pages
			results = r.Result().(*LongviewClientsPagedResponse).Results
			v.appendData(r.Result().(*LongviewClientsPagedResponse))
		}
	case *IPAddressesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(IPAddressesPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*IPAddressesPagedResponse).Pages
			results = r.Result().(*IPAddressesPagedResponse).Results
			v.appendData(r.Result().(*IPAddressesPagedResponse))
		}
	case *IPv6PoolsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(IPv6PoolsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*IPv6PoolsPagedResponse).Pages
			results = r.Result().(*IPv6PoolsPagedResponse).Results
			v.appendData(r.Result().(*IPv6PoolsPagedResponse))
		}
	case *IPv6RangesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(IPv6RangesPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*IPv6RangesPagedResponse).Pages
			results = r.Result().(*IPv6RangesPagedResponse).Results
			v.appendData(r.Result().(*IPv6RangesPagedResponse))
			// @TODO consolidate this type with IPv6PoolsPagedResponse?
		}
	case *SSHKeysPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(SSHKeysPagedResponse{}).Get(v.endpoint(c))); err == nil {
			response, ok := r.Result().(*SSHKeysPagedResponse)
			if !ok {
				return fmt.Errorf("response is not a *SSHKeysPagedResponse")
			}
			pages = response.Pages
			results = response.Results
			v.appendData(response)
		}
	case *TicketsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(TicketsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*TicketsPagedResponse).Pages
			results = r.Result().(*TicketsPagedResponse).Results
			v.appendData(r.Result().(*TicketsPagedResponse))
		}
	case *InvoicesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(InvoicesPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*InvoicesPagedResponse).Pages
			results = r.Result().(*InvoicesPagedResponse).Results
			v.appendData(r.Result().(*InvoicesPagedResponse))
		}
	case *NotificationsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(NotificationsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*NotificationsPagedResponse).Pages
			results = r.Result().(*NotificationsPagedResponse).Results
			v.appendData(r.Result().(*NotificationsPagedResponse))
		}
	case *OAuthClientsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(OAuthClientsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*OAuthClientsPagedResponse).Pages
			results = r.Result().(*OAuthClientsPagedResponse).Results
			v.appendData(r.Result().(*OAuthClientsPagedResponse))
		}
	case *PaymentsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(PaymentsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*PaymentsPagedResponse).Pages
			results = r.Result().(*PaymentsPagedResponse).Results
			v.appendData(r.Result().(*PaymentsPagedResponse))
		}
	case *NodeBalancersPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(NodeBalancersPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*NodeBalancersPagedResponse).Pages
			results = r.Result().(*NodeBalancersPagedResponse).Results
			v.appendData(r.Result().(*NodeBalancersPagedResponse))
		}
	case *TagsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(TagsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*TagsPagedResponse).Pages
			results = r.Result().(*TagsPagedResponse).Results
			v.appendData(r.Result().(*TagsPagedResponse))
		}
	case *TokensPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(TokensPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*TokensPagedResponse).Pages
			results = r.Result().(*TokensPagedResponse).Results
			v.appendData(r.Result().(*TokensPagedResponse))
		}
	case *UsersPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(UsersPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*UsersPagedResponse).Pages
			results = r.Result().(*UsersPagedResponse).Results
			v.appendData(r.Result().(*UsersPagedResponse))
		}
	case *ObjectStorageBucketsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(ObjectStorageBucketsPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*ObjectStorageBucketsPagedResponse).Pages
			results = r.Result().(*ObjectStorageBucketsPagedResponse).Results
			v.appendData(r.Result().(*ObjectStorageBucketsPagedResponse))
		}
	case *ObjectStorageClustersPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(ObjectStorageClustersPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*ObjectStorageClustersPagedResponse).Pages
			results = r.Result().(*ObjectStorageClustersPagedResponse).Results
			v.appendData(r.Result().(*ObjectStorageClustersPagedResponse))
		}
	case *ObjectStorageKeysPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(ObjectStorageKeysPagedResponse{}).Get(v.endpoint(c))); err == nil {
			pages = r.Result().(*ObjectStorageKeysPagedResponse).Pages
			results = r.Result().(*ObjectStorageKeysPagedResponse).Results
			v.appendData(r.Result().(*ObjectStorageKeysPagedResponse))
		}
	/**
	case ProfileAppsPagedResponse:
	case ProfileWhitelistPagedResponse:
	case ManagedContactsPagedResponse:
	case ManagedCredentialsPagedResponse:
	case ManagedIssuesPagedResponse:
	case ManagedLinodeSettingsPagedResponse:
	case ManagedServicesPagedResponse:
	**/
	default:
		log.Fatalf("listHelper interface{} %+v used", i)
	}

	if err != nil {
		return err
	}

	if opts == nil {
		for page := 2; page <= pages; page++ {
			if err := c.listHelper(ctx, i, &ListOptions{PageOptions: &PageOptions{Page: page}}); err != nil {
				return err
			}
		}
	} else {
		if opts.PageOptions == nil {
			opts.PageOptions = &PageOptions{}
		}

		if opts.Page == 0 {
			for page := 2; page <= pages; page++ {
				opts.Page = page
				if err := c.listHelper(ctx, i, opts); err != nil {
					return err
				}
			}
		}
		opts.Results = results
		opts.Pages = pages
	}

	return nil
}

// listHelperWithID abstracts fetching and pagination for GET endpoints that
// require an Id (second level endpoints).
// When opts (or opts.Page) is nil, all pages will be fetched and
// returned in a single (endpoint-specific)PagedResponse
// opts.results and opts.pages will be updated from the API response
// nolint
func (c *Client) listHelperWithID(ctx context.Context, i interface{}, idRaw interface{}, opts *ListOptions) error {
	req := c.R(ctx)
	if opts != nil && opts.Page > 0 {
		req.SetQueryParam("page", strconv.Itoa(opts.Page))
	}

	var (
		err     error
		pages   int
		results int
		r       *resty.Response
	)

	id, _ := idRaw.(int)

	if opts != nil && len(opts.Filter) > 0 {
		req.SetHeader("X-Filter", opts.Filter)
	}

	switch v := i.(type) {
	case *DomainRecordsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(DomainRecordsPagedResponse{}).Get(v.endpointWithID(c, id))); err == nil {
			response, ok := r.Result().(*DomainRecordsPagedResponse)
			if !ok {
				return fmt.Errorf("response is not a *DomainRecordsPagedResponse")
			}
			pages = response.Pages
			results = response.Results
			v.appendData(response)
		}
	case *InstanceConfigsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(InstanceConfigsPagedResponse{}).Get(v.endpointWithID(c, id))); err == nil {
			pages = r.Result().(*InstanceConfigsPagedResponse).Pages
			results = r.Result().(*InstanceConfigsPagedResponse).Results
			v.appendData(r.Result().(*InstanceConfigsPagedResponse))
		}
	case *InstanceDisksPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(InstanceDisksPagedResponse{}).Get(v.endpointWithID(c, id))); err == nil {
			pages = r.Result().(*InstanceDisksPagedResponse).Pages
			results = r.Result().(*InstanceDisksPagedResponse).Results
			v.appendData(r.Result().(*InstanceDisksPagedResponse))
		}
	case *InstanceVolumesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(InstanceVolumesPagedResponse{}).Get(v.endpointWithID(c, id))); err == nil {
			pages = r.Result().(*InstanceVolumesPagedResponse).Pages
			results = r.Result().(*InstanceVolumesPagedResponse).Results
			v.appendData(r.Result().(*InstanceVolumesPagedResponse))
		}
	case *InvoiceItemsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(InvoiceItemsPagedResponse{}).Get(v.endpointWithID(c, id))); err == nil {
			pages = r.Result().(*InvoiceItemsPagedResponse).Pages
			results = r.Result().(*InvoiceItemsPagedResponse).Results
			v.appendData(r.Result().(*InvoiceItemsPagedResponse))
		}
	case *LKEClusterPoolsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(LKEClusterPoolsPagedResponse{}).Get(v.endpointWithID(c, id))); err == nil {
			pages = r.Result().(*LKEClusterPoolsPagedResponse).Pages
			results = r.Result().(*LKEClusterPoolsPagedResponse).Results
			v.appendData(r.Result().(*LKEClusterPoolsPagedResponse))
		}
	case *NodeBalancerConfigsPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(NodeBalancerConfigsPagedResponse{}).Get(v.endpointWithID(c, id))); err == nil {
			pages = r.Result().(*NodeBalancerConfigsPagedResponse).Pages
			results = r.Result().(*NodeBalancerConfigsPagedResponse).Results
			v.appendData(r.Result().(*NodeBalancerConfigsPagedResponse))
		}
	case *TaggedObjectsPagedResponse:
		idStr := idRaw.(string)

		if r, err = coupleAPIErrors(req.SetResult(TaggedObjectsPagedResponse{}).Get(v.endpointWithID(c, idStr))); err == nil {
			pages = r.Result().(*TaggedObjectsPagedResponse).Pages
			results = r.Result().(*TaggedObjectsPagedResponse).Results
			v.appendData(r.Result().(*TaggedObjectsPagedResponse))
		}
	/**
	case TicketAttachmentsPagedResponse:
		if r, err = req.SetResult(v).Get(v.endpoint(c)); r.Error() != nil {
			return NewError(r)
		} else if err == nil {
			pages = r.Result().(*TicketAttachmentsPagedResponse).Pages
			results = r.Result().(*TicketAttachmentsPagedResponse).Results
			v.appendData(r.Result().(*TicketAttachmentsPagedResponse))
		}
	case TicketRepliesPagedResponse:
		if r, err = req.SetResult(v).Get(v.endpoint(c)); r.Error() != nil {
			return NewError(r)
		} else if err == nil {
			pages = r.Result().(*TicketRepliesPagedResponse).Pages
			results = r.Result().(*TicketRepliesPagedResponse).Results
			v.appendData(r.Result().(*TicketRepliesPagedResponse))
		}
	**/
	default:
		log.Fatalf("Unknown listHelperWithID interface{} %T used", i)
	}

	if err != nil {
		return err
	}

	if opts == nil {
		for page := 2; page <= pages; page++ {
			if err := c.listHelperWithID(ctx, i, id, &ListOptions{PageOptions: &PageOptions{Page: page}}); err != nil {
				return err
			}
		}
	} else {
		if opts.PageOptions == nil {
			opts.PageOptions = &PageOptions{}
		}
		if opts.Page == 0 {
			for page := 2; page <= pages; page++ {
				opts.Page = page
				if err := c.listHelperWithID(ctx, i, id, opts); err != nil {
					return err
				}
			}
		}
		opts.Results = results
		opts.Pages = pages
	}

	return nil
}

// listHelperWithTwoIDs abstracts fetching and pagination for GET endpoints that
// require twos IDs (third level endpoints).
// When opts (or opts.Page) is nil, all pages will be fetched and
// returned in a single (endpoint-specific)PagedResponse
// opts.results and opts.pages will be updated from the API response
func (c *Client) listHelperWithTwoIDs(ctx context.Context, i interface{}, firstID, secondID int, opts *ListOptions) error {
	req := c.R(ctx)

	if opts != nil && opts.Page > 0 {
		req.SetQueryParam("page", strconv.Itoa(opts.Page))
	}

	var (
		err     error
		pages   int
		results int
		r       *resty.Response
	)

	if opts != nil && len(opts.Filter) > 0 {
		req.SetHeader("X-Filter", opts.Filter)
	}

	switch v := i.(type) {
	case *NodeBalancerNodesPagedResponse:
		if r, err = coupleAPIErrors(req.SetResult(NodeBalancerNodesPagedResponse{}).Get(v.endpointWithTwoIDs(c, firstID, secondID))); err == nil {
			pages = r.Result().(*NodeBalancerNodesPagedResponse).Pages
			results = r.Result().(*NodeBalancerNodesPagedResponse).Results
			v.appendData(r.Result().(*NodeBalancerNodesPagedResponse))
		}
	default:
		log.Fatalf("Unknown listHelperWithTwoIDs interface{} %T used", i)
	}

	if err != nil {
		return err
	}

	if opts == nil {
		for page := 2; page <= pages; page++ {
			if err := c.listHelper(ctx, i, &ListOptions{PageOptions: &PageOptions{Page: page}}); err != nil {
				return err
			}
		}
	} else {
		if opts.PageOptions == nil {
			opts.PageOptions = &PageOptions{}
		}
		if opts.Page == 0 {
			for page := 2; page <= pages; page++ {
				opts.Page = page
				if err := c.listHelperWithTwoIDs(ctx, i, firstID, secondID, opts); err != nil {
					return err
				}
			}
		}
		opts.Results = results
		opts.Pages = pages
	}

	return nil
}
