package products

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"time"

	"route256/cart/internal/domain/entity"
	"route256/cart/internal/domain/model"
	"route256/cart/internal/infra/errgroup"

	"github.com/rs/zerolog/log"
	"golang.org/x/time/rate"
)

type ProductService struct {
	httpClient http.Client
	token      string
	address    string

	limiter *rate.Limiter

	workers int
}

func NewProductService(httpClient http.Client, token, address string, limitRPS, burst, workers int) *ProductService {
	log.Debug().Msgf("creating product server with %d worker limit", workers)

	return &ProductService{
		httpClient: httpClient,
		token:      token,
		address:    address,

		limiter: rate.NewLimiter(rate.Limit(limitRPS), burst),
		workers: workers,
	}
}

func (s *ProductService) GetProductBySku(ctx context.Context, sku entity.Sku) (*model.Product, error) {
	if err := s.limiter.Wait(ctx); err != nil {
		return nil, fmt.Errorf("limiter.Wait: %w", err)
	}

	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(
		ctx,
		http.MethodGet,
		fmt.Sprintf("http://%s/product/%d", s.address, sku),
		http.NoBody,
	)
	if err != nil {
		return nil, fmt.Errorf("http.NewRequestWithContext: %w", err)
	}

	req.Header.Add("X-API-KEY", s.token)

	response, err := s.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("httpClient.Do: %w", err)
	}
	defer response.Body.Close()

	if response.StatusCode == http.StatusNotFound {
		return nil, model.ErrProductNotFound
	}

	if response.StatusCode != http.StatusOK {
		return nil, errors.New("status not ok")
	}

	resp := &GetProductResponse{}
	if err := json.NewDecoder(response.Body).Decode(resp); err != nil {
		return nil, fmt.Errorf("json.NewDecoder: %w", err)
	}

	return &model.Product{
		Name:  resp.Name,
		Price: resp.Price,
		Sku:   entity.Sku(resp.Sku),
	}, nil
}

type GetProductResponse struct {
	Name  string `json:"name"`
	Price int32  `json:"price"`
	Sku   int64  `json:"sku"`
}

func (s *ProductService) GetProducts(ctx context.Context, skus []entity.Sku) ([]*model.Product, error) {
	if len(skus) == 0 {
		return []*model.Product{}, nil
	}

	results := make([]*model.Product, len(skus))

	g, _ := errgroup.WithContext(ctx, s.workers)

	for i, sku := range skus {
		g.Go(func(groupCtx context.Context) error {
			p, err := s.GetProductBySku(groupCtx, sku)
			if err != nil {
				return err
			}

			results[i] = p

			return nil
		})
	}

	if err := g.Wait(); err != nil {
		return nil, err
	}

	return results, nil
}
