package products

import (
	"context"
	"fmt"
	pb "sipro-mps/api/generated/v1/ozon/products"
	"sipro-mps/internal/marketplace"
	"sipro-mps/internal/ozon"
	"sipro-mps/internal/ozon/products/mapping/generated"
	"sipro-mps/internal/redis"
	"sipro-mps/internal/tasks/client"
	"sipro-mps/internal/tasks/types"
	"sipro-mps/pkg/utils"
	"strconv"
	"sync"

	api "git.denco.store/fakz9/ozon-api-client/ozon"
	"github.com/samber/lo"
)

type apiRepository struct {
	marketplaceRepository marketplace.Repository
}

func GetProductsKey(identifier string) string {
	return fmt.Sprintf("ozon:products:%s:compressed", identifier)
}

func NewAPIRepository(marketplaceRepository marketplace.Repository) Repository {
	return &apiRepository{
		marketplaceRepository: marketplaceRepository,
	}
}

func fetchProductIds(ctx context.Context, client *api.Client, resultChan chan<- []int64, errChan chan<- error) {
	defer close(resultChan)
	lastId := ""
	for {
		resp, err := client.Products().GetListOfProducts(ctx, &api.GetListOfProductsParams{
			Filter: api.GetListOfProductsFilter{Visibility: "ALL"},
			LastId: lastId,
			Limit:  1000,
		})
		if err != nil {
			// dev
			//panic(err)
			errChan <- fmt.Errorf("fetching product IDs: %w", err)
			return
		}

		items := resp.Result.Items
		if len(items) == 0 {
			break
		}
		productIds := lo.Map(items, func(item api.GetListOfProductsResultItem, _ int) int64 { return item.ProductId })

		resultChan <- productIds

		lastId = resp.Result.LastId
		if lastId == "" {
			break
		}
	}
}

func fetchProducts(ctx context.Context, client *api.Client, productIdsChan <-chan []int64, resultChan chan<- []OzonProduct, errChan chan<- error) {
	defer close(resultChan)
	defer close(errChan)
	wg := sync.WaitGroup{}

	for productIds := range productIdsChan {
		wg.Add(1)
		go func() {
			defer wg.Done()
			resp, err := client.Products().ListProductsByIDs(ctx, &api.ListProductsByIDsParams{
				ProductId: productIds,
			})
			if err != nil {
				// dev
				//panic(err)
				errChan <- fmt.Errorf("fetching products: %w", err)
				return
			}
			items := resp.Items
			resultChan <- items
		}()
	}
	wg.Wait()
}

func (a *apiRepository) GetAllProducts(ctx context.Context, marketplaceId int) ([]OzonProduct, error) {
	mp, err := a.marketplaceRepository.GetMarketplaceByID(ctx, marketplaceId)
	if err != nil {
		return nil, err
	}
	clientFromMarketplace, err := ozon.GetClientFromMarketplace(mp)
	if err != nil {
		return nil, err
	}
	var items []OzonProduct
	productIdsChan := make(chan []int64)
	producsChan := make(chan []OzonProduct)
	errChan := make(chan error)
	go fetchProductIds(ctx, clientFromMarketplace, productIdsChan, errChan)
	go fetchProducts(ctx, clientFromMarketplace, productIdsChan, producsChan, errChan)
	for products := range producsChan {
		for _, product := range products {
			items = append(items, product)
		}
	}
	return items, nil
}

func (a *apiRepository) StreamAllProducts(ctx context.Context, marketplaceId int, resultChan chan<- []OzonProduct, errChan chan<- error) {
	mp, err := a.marketplaceRepository.GetMarketplaceByID(ctx, marketplaceId)
	if err != nil {
		errChan <- err
		return
	}
	clientFromMarketplace, err := ozon.GetClientFromMarketplace(mp)
	if err != nil {
		errChan <- err
		return
	}
	productIdsChan := make(chan []int64)
	go fetchProductIds(ctx, clientFromMarketplace, productIdsChan, errChan)
	go fetchProducts(ctx, clientFromMarketplace, productIdsChan, resultChan, errChan)
}

func (a *apiRepository) StreamAllProductsCache(ctx context.Context, marketplaceId int, resultChan chan<- []PbProduct, errChan chan<- error) {
	defer close(resultChan)
	defer close(errChan)
	mp, err := a.marketplaceRepository.GetMarketplaceByID(ctx, marketplaceId)
	if err != nil {
		errChan <- err
		return
	}
	identifier, err := mp.GetIdentifier()
	if err != nil {
		errChan <- fmt.Errorf("getting marketplace identifier: %w", err)
		return
	}
	key := GetProductsKey(identifier)
	var cachedMessage pb.GetListOfProductsResponse
	err = redis.ReadProtoMessage(ctx, key, &cachedMessage)
	if err == nil && len(cachedMessage.Products) > 0 {
		resultChan <- utils.DerefSlice(cachedMessage.Products)
		_ = client.EnqueueFetchProductsTask(types.TypeOzonFetchProducts, marketplaceId)

		return
	}

	innerResultChan := make(chan []OzonProduct)
	innerErrChan := make(chan error)
	go a.StreamAllProducts(ctx, marketplaceId, innerResultChan, innerErrChan)

	converter := generated.ConverterImpl{}
	var allProducts []PbProduct
	defer func() {
		if len(allProducts) == 0 {
			return
		}
		message := pb.GetListOfProductsResponse{Products: utils.ToPtrs(allProducts)}
		_ = redis.WriteProtoMessage(ctx, key, &message)
	}()
	for {
		select {
		case err, ok := <-innerErrChan:
			if !ok {
				return
			}
			if err != nil {
				errChan <- fmt.Errorf("streaming products: %w", err)
				return
			}
		case products, ok := <-innerResultChan:
			if !ok && len(products) == 0 {
				return
			}
			pbProducts := lo.Map(products, func(p OzonProduct, _ int) PbProduct {
				return *converter.ToProto(&p)
			})
			allProducts = append(allProducts, pbProducts...)
			resultChan <- pbProducts
			if !ok {
				return
			}
		}
	}
}

func (a *apiRepository) StreamProductAttributesCache(ctx context.Context, marketplaceId int, productIds []int, resultChan chan<- []PbProductAttributes, errChan chan<- error) {
	defer close(resultChan)
	defer close(errChan)

	mp, err := a.marketplaceRepository.GetMarketplaceByID(ctx, marketplaceId)
	if err != nil {
		errChan <- err
		return
	}
	ozonClient, err := ozon.GetClientFromMarketplace(mp)
	if err != nil {
		errChan <- err
		return
	}
	converter := generated.ConverterImpl{}

	for _, chunk := range lo.Chunk(productIds, 1000) {
		chunkStrings := lo.Map(chunk, func(item int, index int) string {
			return strconv.Itoa(item)
		})
		request := api.GetDescriptionOfProductsParams{
			LastId:        "",
			Limit:         1000,
			SortBy:        "id",
			SortDirection: "asc",
			Filter:        api.GetDescriptionOfProductsFilter{ProductId: chunkStrings},
		}
		response, err := ozonClient.Products().GetDescriptionOfProducts(ctx, &request)
		if err != nil {
			errChan <- err
			return
		}
		attrs := lo.Map(response.Result, func(item api.GetDescriptionOfProductsResult, index int) pb.ProductAttributes {
			return *converter.AttributesToProto(&item)
		})
		resultChan <- attrs
	}
}

func (a *apiRepository) DeleteProducts(ctx context.Context, marketplaceId int, items []*PbDeleteProductRequestItem) ([]*PbDeleteProductResponseItem, error) {
	mp, err := a.marketplaceRepository.GetMarketplaceByID(ctx, marketplaceId)

	if err != nil {

		return nil, err
	}
	ozonClient, err := ozon.GetClientFromMarketplace(mp)
	if err != nil {
		return nil, err
	}
	// Step 1: map the items into a slice
	mapped := lo.Map(items, func(item *PbDeleteProductRequestItem, _ int) *PbDeleteProductResponseItem {
		return &PbDeleteProductResponseItem{
			ProductId: item.ProductId,
			OfferId:   item.OfferId,
			IsDeleted: false,
		}
	})

	// Step 2: create the map keyed by OfferId
	result := lo.KeyBy(mapped, func(item *PbDeleteProductResponseItem) string {
		return item.OfferId
	})

	for _, chunk := range lo.Chunk(items, 100) {
		productIds := lo.Map(chunk, func(item *PbDeleteProductRequestItem, index int) int64 {
			return item.ProductId
		})
		request := api.ArchiveProductParams{ProductId: productIds}
		_, err := ozonClient.Products().ArchiveProduct(ctx, &request)
		if err != nil {
			return nil, err
		}
	}

	for _, chunk := range lo.Chunk(items, 500) {
		products := lo.Map(chunk, func(item *PbDeleteProductRequestItem, index int) api.RemoveProductWithoutSKUProduct {
			return api.RemoveProductWithoutSKUProduct{OfferId: item.OfferId}
		})
		request := api.RemoveProductWithoutSKUParams{Products: products}
		response, err := ozonClient.Products().RemoveProductWithoutSKU(ctx, &request)
		if err != nil {
			return nil, err
		}
		for _, status := range response.Status {
			if item, ok := result[status.OfferId]; ok {
				item.IsDeleted = status.IsDeleted
			}
		}
	}
	return lo.Values(result), nil
}

func (a *apiRepository) CreateOrUpdateProducts(ctx context.Context, marketplaceId int, items []*PbCreateOrUpdateItem) (*PbCreateOrUpdateProductsResponse, error) {
	mp, err := a.marketplaceRepository.GetMarketplaceByID(ctx, marketplaceId)

	if err != nil {

		return nil, err
	}
	ozonClient, err := ozon.GetClientFromMarketplace(mp)
	if err != nil {
		return nil, err
	}
	converter := generated.ConverterImpl{}
	pageSize := 100
	result := make([]int64, (len(items)+pageSize-1)/pageSize)
	for idx, chunk := range lo.Chunk(items, pageSize) {
		mappedItems := lo.Map(chunk, func(item *PbCreateOrUpdateItem, index int) api.CreateOrUpdateProductItem {
			return *converter.ProtoToUpdateItem(item)
		})
		request := api.CreateOrUpdateProductParams{Items: mappedItems}
		response, err := ozonClient.Products().CreateOrUpdateProduct(ctx, &request)
		if err != nil {
			return nil, err
		}
		result[idx] = response.Result.TaskId
	}

	return &pb.CreateOrUpdateProductsResponse{TaskId: result}, nil
}
