package fetcher

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"slices"

	"github.com/go-errors/errors"
)

type Fetcher struct {
	server  string
	client  *http.Client
	editors []RequestEditor
	status  []int
}

type FetcherOption func(*Fetcher)

func NewFetcher(server string, opts ...FetcherOption) *Fetcher {
	api := &Fetcher{
		server: server,
		client: http.DefaultClient,
	}
	for _, apply := range opts {
		apply(api)
	}
	return api
}

func WithHTTPClient(client *http.Client) FetcherOption {
	return func(s *Fetcher) {
		s.client = client
	}
}

func WithExpectedStatus(statusCode ...int) FetcherOption {
	return func(s *Fetcher) {
		s.status = statusCode
	}
}

func WithBearerToken(token string) FetcherOption {
	addHeader := func(req *http.Request) {
		req.Header.Add("Authorization", "Bearer "+token)
	}
	return WithRequestEditor(addHeader)
}

func WithUserAgent(agent string) FetcherOption {
	addHeader := func(req *http.Request) {
		req.Header.Add("User-Agent", agent)
	}
	return WithRequestEditor(addHeader)
}

func WithRequestEditor(fn RequestEditor) FetcherOption {
	return func(s *Fetcher) {
		s.editors = append(s.editors, fn)
	}
}

type RequestEditor func(req *http.Request)

func (s *Fetcher) Send(ctx context.Context, method, path string, reqBody any, reqEditors ...RequestEditor) (*http.Response, error) {
	body, ok := reqBody.(io.Reader)
	if !ok && reqBody != nil {
		var buf bytes.Buffer
		enc := json.NewEncoder(&buf)
		if err := enc.Encode(reqBody); err != nil {
			return nil, errors.Errorf("failed to encode request body: %w", err)
		}
		reqEditors = append(reqEditors, func(req *http.Request) {
			req.Header.Set("Content-Type", "application/json")
		})
		body = &buf
	}
	// Creates request
	req, err := http.NewRequestWithContext(ctx, method, s.server+path, body)
	if err != nil {
		return nil, errors.Errorf("failed to initialise http request: %w", err)
	}
	for _, apply := range s.editors {
		apply(req)
	}
	for _, apply := range reqEditors {
		apply(req)
	}
	// Sends request
	resp, err := s.client.Do(req)
	if err != nil {
		return nil, errors.Errorf("failed to execute http request: %w", err)
	}
	if slices.Contains(s.status, resp.StatusCode) {
		return resp, nil
	}
	// Reject unexpected status codes as error
	if len(s.status) > 0 || resp.StatusCode >= http.StatusBadRequest {
		defer resp.Body.Close()
		data, err := io.ReadAll(resp.Body)
		if err != nil {
			return resp, errors.Errorf("Error status %d: %w", resp.StatusCode, err)
		}
		return resp, errors.Errorf("Error status %d: %s", resp.StatusCode, data)
	}
	return resp, nil
}

func ParseJSON[T any](r io.ReadCloser) (T, error) {
	defer r.Close()
	var data T
	dec := json.NewDecoder(r)
	if err := dec.Decode(&data); err != nil {
		return data, errors.Errorf("failed to parse response body: %w", err)
	}
	return data, nil
}
