package linodego

import (
	"context"
	"encoding/json"
	"fmt"
	"net/url"
	"reflect"
)

// paginatedResponse represents a single response from a paginated
// endpoint.
type paginatedResponse[T any] struct {
	Page    int `json:"page"    url:"page,omitempty"`
	Pages   int `json:"pages"   url:"pages,omitempty"`
	Results int `json:"results" url:"results,omitempty"`
	Data    []T `json:"data"`
}

// handlePaginatedResults aggregates results from the given
// paginated endpoint using the provided ListOptions and HTTP method.
// nolint:funlen
func handlePaginatedResults[T any, O any](
	ctx context.Context,
	client *Client,
	endpoint string,
	opts *ListOptions,
	method string,
	options ...O,
) ([]T, error) {
	result := make([]T, 0)

	if opts == nil {
		opts = &ListOptions{PageOptions: &PageOptions{Page: 0}}
	}

	if opts.PageOptions == nil {
		opts.PageOptions = &PageOptions{Page: 0}
	}

	// Validate options
	numOpts := len(options)
	if numOpts > 1 {
		return nil, fmt.Errorf("invalid number of options: expected 0 or 1, got %d", numOpts)
	}

	// Prepare request body if options are provided
	var reqBody string

	if numOpts > 0 && !isNil(options[0]) {
		body, err := json.Marshal(options[0])
		if err != nil {
			return nil, fmt.Errorf("failed to marshal request body: %w", err)
		}

		reqBody = string(body)
	}

	// Makes a request to a particular page and appends the response to the result
	handlePage := func(page int) error {
		var resultType paginatedResponse[T]

		// Override the page to be applied in applyListOptionsToRequest(...)
		opts.Page = page

		// This request object cannot be reused for each page request
		// because it can lead to possible data corruption
		req := client.R(ctx).SetResult(&resultType)

		// Apply all user-provided list options to the request
		if err := applyListOptionsToRequest(opts, req); err != nil {
			return err
		}

		// Set request body if provided
		if reqBody != "" {
			req.SetBody(reqBody)
		}

		var response *paginatedResponse[T]
		// Execute the appropriate HTTP method
		switch method {
		case "GET":
			res, err := coupleAPIErrors(req.Get(endpoint))
			if err != nil {
				return err
			}

			response = res.Result().(*paginatedResponse[T])
		case "PUT":
			res, err := coupleAPIErrors(req.Put(endpoint))
			if err != nil {
				return err
			}

			response = res.Result().(*paginatedResponse[T])
		case "POST":
			res, err := coupleAPIErrors(req.Post(endpoint))
			if err != nil {
				return err
			}

			response = res.Result().(*paginatedResponse[T])
		default:
			return fmt.Errorf("unsupported HTTP method: %s", method)
		}

		// Update pagination metadata
		opts.Page = page
		opts.Pages = response.Pages
		opts.Results = response.Results
		result = append(result, response.Data...)

		return nil
	}

	// Determine starting page
	startingPage := 1
	pageDefined := opts.Page > 0

	if pageDefined {
		startingPage = opts.Page
	}

	// Get the first page
	if err := handlePage(startingPage); err != nil {
		return nil, err
	}

	// If the user has explicitly specified a page, we don't
	// need to get any other pages.
	if pageDefined {
		return result, nil
	}

	// Get the rest of the pages
	for page := 2; page <= opts.Pages; page++ {
		if err := handlePage(page); err != nil {
			return nil, err
		}
	}

	return result, nil
}

// getPaginatedResults aggregates results from the given
// paginated endpoint using the provided ListOptions.
func getPaginatedResults[T any](
	ctx context.Context,
	client *Client,
	endpoint string,
	opts *ListOptions,
) ([]T, error) {
	return handlePaginatedResults[T, any](ctx, client, endpoint, opts, "GET")
}

// putPaginatedResults sends a PUT request and aggregates the results from the given
// paginated endpoint using the provided ListOptions.
func putPaginatedResults[T, O any](
	ctx context.Context,
	client *Client,
	endpoint string,
	opts *ListOptions,
	options ...O,
) ([]T, error) {
	return handlePaginatedResults[T, O](ctx, client, endpoint, opts, "PUT", options...)
}

// postPaginatedResults sends a POST request and aggregates the results from the given
// paginated endpoint using the provided ListOptions.
func postPaginatedResults[T, O any](
	ctx context.Context,
	client *Client,
	endpoint string,
	opts *ListOptions,
	options ...O,
) ([]T, error) {
	return handlePaginatedResults[T, O](ctx, client, endpoint, opts, "POST", options...)
}

// doGETRequest runs a GET request using the given client and API endpoint,
// and returns the result
func doGETRequest[T any](
	ctx context.Context,
	client *Client,
	endpoint string,
) (*T, error) {
	var resultType T

	req := client.R(ctx).SetResult(&resultType)

	r, err := coupleAPIErrors(req.Get(endpoint))
	if err != nil {
		return nil, err
	}

	return r.Result().(*T), nil
}

// doPOSTRequest runs a PUT request using the given client, API endpoint,
// and options/body.
func doPOSTRequest[T, O any](
	ctx context.Context,
	client *Client,
	endpoint string,
	options ...O,
) (*T, error) {
	var resultType T

	numOpts := len(options)

	if numOpts > 1 {
		return nil, fmt.Errorf("invalid number of options: %d", len(options))
	}

	req := client.R(ctx).SetResult(&resultType)

	if numOpts > 0 && !isNil(options[0]) {
		body, err := json.Marshal(options[0])
		if err != nil {
			return nil, err
		}

		req.SetBody(string(body))
	}

	r, err := coupleAPIErrors(req.Post(endpoint))
	if err != nil {
		return nil, err
	}

	return r.Result().(*T), nil
}

// doPOSTRequestNoResponseBody runs a POST request using the given client, API endpoint,
// and options/body. It expects only empty response from the endpoint.
func doPOSTRequestNoResponseBody[T any](
	ctx context.Context,
	client *Client,
	endpoint string,
	options ...T,
) error {
	_, err := doPOSTRequest[any, T](ctx, client, endpoint, options...)
	return err
}

// doPOSTRequestNoRequestResponseBody runs a POST request where no request body is needed and no response body
// is expected from the endpoints.
func doPOSTRequestNoRequestResponseBody(
	ctx context.Context,
	client *Client,
	endpoint string,
) error {
	return doPOSTRequestNoResponseBody(ctx, client, endpoint, struct{}{})
}

// doPUTRequest runs a PUT request using the given client, API endpoint,
// and options/body.
func doPUTRequest[T, O any](
	ctx context.Context,
	client *Client,
	endpoint string,
	options ...O,
) (*T, error) {
	var resultType T

	numOpts := len(options)

	if numOpts > 1 {
		return nil, fmt.Errorf("invalid number of options: %d", len(options))
	}

	req := client.R(ctx).SetResult(&resultType)

	if numOpts > 0 && !isNil(options[0]) {
		body, err := json.Marshal(options[0])
		if err != nil {
			return nil, err
		}

		req.SetBody(string(body))
	}

	r, err := coupleAPIErrors(req.Put(endpoint))
	if err != nil {
		return nil, err
	}

	return r.Result().(*T), nil
}

// doDELETERequest runs a DELETE request using the given client
// and API endpoint.
func doDELETERequest(
	ctx context.Context,
	client *Client,
	endpoint string,
) error {
	req := client.R(ctx)
	_, err := coupleAPIErrors(req.Delete(endpoint))

	return err
}

// formatAPIPath allows us to safely build an API request with path escaping
func formatAPIPath(format string, args ...any) string {
	escapedArgs := make([]any, len(args))
	for i, arg := range args {
		if typeStr, ok := arg.(string); ok {
			arg = url.PathEscape(typeStr)
		}

		escapedArgs[i] = arg
	}

	return fmt.Sprintf(format, escapedArgs...)
}

func isNil(i any) bool {
	if i == nil {
		return true
	}

	// Check for nil pointers
	v := reflect.ValueOf(i)

	return v.Kind() == reflect.Ptr && v.IsNil()
}
