// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

package lunatask

import (
	"context"
	"fmt"
	"net/http"
	"net/url"
)

// unwrap extracts an entity from a response wrapper.
type unwrap[E, R any] func(R) E

// unwrapSlice extracts a slice of entities from a response wrapper.
type unwrapSlice[E, R any] func(R) []E

// SourceFilter provides optional source filtering for list operations.
type SourceFilter interface {
	GetSource() *string
	GetSourceID() *string
}

// get fetches a single entity by ID.
func get[E, R any](ctx context.Context, c *Client, path, id, name string, extract unwrap[E, R]) (*E, error) {
	if id == "" {
		return nil, fmt.Errorf("%w: %s ID cannot be empty", ErrBadRequest, name)
	}

	resp, _, err := doJSON[R](ctx, c, http.MethodGet, path+"/"+id, nil)
	if err != nil {
		return nil, err
	}

	e := extract(*resp)

	return &e, nil
}

// list fetches all entities, optionally filtered by source.
func list[E, R any](
	ctx context.Context, c *Client, path string, opts SourceFilter, extract unwrapSlice[E, R],
) ([]E, error) {
	if opts != nil {
		params := url.Values{}

		if s := opts.GetSource(); s != nil && *s != "" {
			params.Set("source", *s)
		}

		if s := opts.GetSourceID(); s != nil && *s != "" {
			params.Set("source_id", *s)
		}

		if len(params) > 0 {
			path = fmt.Sprintf("%s?%s", path, params.Encode())
		}
	}

	resp, _, err := doJSON[R](ctx, c, http.MethodGet, path, nil)
	if err != nil {
		return nil, err
	}

	return extract(*resp), nil
}

// create adds a new entity. Returns (nil, nil) if duplicate exists (HTTP 204).
func create[E, R any](ctx context.Context, c *Client, path string, body any, extract unwrap[E, R]) (*E, error) {
	resp, noContent, err := doJSON[R](ctx, c, http.MethodPost, path, body)
	if err != nil {
		return nil, err
	}

	if noContent {
		return nil, nil //nolint:nilnil
	}

	e := extract(*resp)

	return &e, nil
}

// update modifies an entity by ID.
func update[E, R any](
	ctx context.Context, c *Client, path, id, name string, body any, extract unwrap[E, R],
) (*E, error) {
	if id == "" {
		return nil, fmt.Errorf("%w: %s ID cannot be empty", ErrBadRequest, name)
	}

	resp, _, err := doJSON[R](ctx, c, http.MethodPut, path+"/"+id, body)
	if err != nil {
		return nil, err
	}

	e := extract(*resp)

	return &e, nil
}

// del removes an entity by ID and returns its final state.
func del[E, R any](ctx context.Context, c *Client, path, id, name string, extract unwrap[E, R]) (*E, error) {
	if id == "" {
		return nil, fmt.Errorf("%w: %s ID cannot be empty", ErrBadRequest, name)
	}

	resp, _, err := doJSON[R](ctx, c, http.MethodDelete, path+"/"+id, nil)
	if err != nil {
		return nil, err
	}

	e := extract(*resp)

	return &e, nil
}
