package products

import (
	"context"
	"fmt"
	"io"
	"math"
	"net/http"
	"sipro-mps/internal/redis"
	"sipro-mps/pkg/utils"
	"strconv"
	"strings"
	"time"

	pb "sipro-mps/api/generated/v1/yandexmarket/products"
	"sipro-mps/internal/marketplace"
	"sipro-mps/internal/ym"
	"sipro-mps/internal/ym/products/mapping/generated"

	"git.denco.store/fakz9/yandex-go-client"
	"github.com/samber/lo"
	"github.com/tidwall/gjson"
)

const (
	defaultChunkSize       = 200
	offerMappingsRateLimit = 600
	tariffsRateLimit       = 95
	rateLimitWindow        = time.Minute
	maxPageSize            = math.MaxInt32
)

// apiRepository implements the Repository interface using Yandex Market API
type apiRepository struct {
	marketplaceRepository marketplace.Repository
	converter             *generated.ConverterImpl
}

// NewAPIRepository creates a new API-based repository implementation
func NewAPIRepository(marketplaceRepository marketplace.Repository) Repository {
	return &apiRepository{
		marketplaceRepository: marketplaceRepository,
		converter:             &generated.ConverterImpl{},
	}
}

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

// getBusinessID retrieves the business ID for a given marketplace by looking up the campaign
func (r *apiRepository) getBusinessID(ctx context.Context, mp *marketplace.Marketplace) (int64, error) {
	if mp.CampaignID == "" {
		return 0, fmt.Errorf("campaign ID is not set for marketplace %d", mp.ID)
	}

	campaignID, err := r.validateCampaignID(mp.CampaignID, mp.ID)
	if err != nil {
		return 0, err
	}

	client, err := ym.GetClientFromMarketplace(mp)
	if err != nil {
		return 0, fmt.Errorf("failed to create Yandex Market client: %w", err)
	}

	businessID, err := r.fetchBusinessIDFromCampaigns(ctx, client, campaignID, mp.ID)
	if err != nil {
		return 0, err
	}

	return businessID, nil
}

// validateCampaignID validates and parses the campaign ID string
func (r *apiRepository) validateCampaignID(campaignIDStr string, marketplaceID int) (int64, error) {
	campaignID, err := strconv.ParseInt(campaignIDStr, 10, 64)
	if err != nil {
		return 0, fmt.Errorf("invalid campaign ID '%s' for marketplace %d: %w", campaignIDStr, marketplaceID, err)
	}
	return campaignID, nil
}

// fetchBusinessIDFromCampaigns retrieves business ID by searching through campaigns
func (r *apiRepository) fetchBusinessIDFromCampaigns(ctx context.Context, client *ymclient.APIClient, campaignID int64, marketplaceID int) (int64, error) {
	rsp, _, err := client.CampaignsAPI.GetCampaigns(ctx).Page(1).PageSize(maxPageSize).Execute()
	if err != nil {
		return 0, fmt.Errorf("failed to call GetCampaigns: %w", err)
	}
	if rsp == nil {
		return 0, fmt.Errorf("GetCampaigns returned nil response")
	}

	for _, campaign := range rsp.Campaigns {
		if campaign.GetId() == campaignID {
			return campaign.Business.GetId(), nil
		}
	}

	return 0, fmt.Errorf("campaign ID %d not found in GetCampaigns response for marketplace %d", campaignID, marketplaceID)
}

// GetProducts retrieves products from Yandex Market API in chunks and sends results to channels
func (r *apiRepository) GetProducts(ctx context.Context, marketplaceID int, req *pb.GetProductsRequest, resultChan chan<- []*pb.GetProductsResponse_Offer, errChan chan<- error) {
	defer close(resultChan)
	defer close(errChan)

	_, client, businessID, err := r.setupMarketplaceClient(ctx, marketplaceID)
	if err != nil {
		errChan <- err
		return
	}
	key := GetProductKey(strconv.Itoa(int(businessID)))
	var cachedMessage pb.GetProductsResponse
	err = redis.ReadProtoMessage(ctx, key, &cachedMessage)
	if err == nil && len(cachedMessage.Offers) > 0 {
		resultChan <- cachedMessage.Offers
		return
	}
	r.setOfferMappingsRateLimit(businessID)
	// all offers
	var allOffers []*pb.GetProductsResponse_Offer
	defer func() {
		// store only if offers is 90% or more then requested
		if len(allOffers) < int(float64(len(req.OfferIds))*0.9) {
			return
		}
		if len(allOffers) == 0 {
			return
		}
		message := pb.GetProductsResponse{
			Offers: allOffers,
		}
		_ = redis.WriteProtoMessage(ctx, key, &message)
	}()
	for _, chunk := range lo.Chunk(req.OfferIds, defaultChunkSize) {
		offers, err := r.fetchOfferMappings(ctx, client, businessID, chunk)
		if err != nil {
			errChan <- err
			return
		}
		resultChan <- offers
		allOffers = append(allOffers, offers...)
	}
}

// setupMarketplaceClient initializes marketplace, API client, and business ID
func (r *apiRepository) setupMarketplaceClient(ctx context.Context, marketplaceID int) (*marketplace.Marketplace, *ymclient.APIClient, int64, error) {
	mp, err := r.marketplaceRepository.GetMarketplaceByID(ctx, marketplaceID)
	if err != nil {
		return nil, nil, 0, fmt.Errorf("failed to get marketplace: %w", err)
	}

	client, err := ym.GetClientFromMarketplace(mp)
	if err != nil {
		return nil, nil, 0, fmt.Errorf("failed to create Yandex Market client: %w", err)
	}

	businessID, err := r.getBusinessID(ctx, mp)
	if err != nil {
		return nil, nil, 0, fmt.Errorf("failed to get business ID: %w", err)
	}

	return mp, client, businessID, nil
}

// setOfferMappingsRateLimit configures rate limiting for offer mappings endpoint
func (r *apiRepository) setOfferMappingsRateLimit(businessID int64) {
	path := "/businesses/" + strconv.Itoa(int(businessID)) + "/offer-mappings"
	ym.SetPathLimit(path, rateLimitWindow, offerMappingsRateLimit)
}

// fetchOfferMappings retrieves offer mappings for a given set of offer IDs
func (r *apiRepository) fetchOfferMappings(ctx context.Context, client *ymclient.APIClient, businessID int64, offerIDs []string) ([]*pb.GetProductsResponse_Offer, error) {
	req := ymclient.NewGetOfferMappingsRequest()
	req.OfferIds = offerIDs

	rsp, _, err := client.BusinessOfferMappingsAPI.GetOfferMappings(ctx, businessID).GetOfferMappingsRequest(*req).Execute()
	if err != nil {
		return nil, fmt.Errorf("failed to call GetOfferMappings: %w", err)
	}
	if rsp == nil {
		return nil, fmt.Errorf("GetOfferMappings returned nil response")
	}

	return r.processOfferMappings(rsp.Result.GetOfferMappings())
}

// processOfferMappings converts YM offer mappings to protobuf format
func (r *apiRepository) processOfferMappings(offerMappings []ymclient.GetOfferMappingDTO) ([]*pb.GetProductsResponse_Offer, error) {
	var resultOffers []*pb.GetProductsResponse_Offer

	for _, offerMapping := range offerMappings {
		protoOffer := r.converter.YmOfferToProtoOffer(offerMapping.Offer)
		if protoOffer == nil {
			fmt.Printf("Warning: received nil offer for ID %s\n", offerMapping.Offer.OfferId)
			continue
		}
		if mapping, ok := offerMapping.GetMappingOk(); ok {
			if marketCategoryId, ok := mapping.GetMarketCategoryIdOk(); ok {
				protoOffer.MarketCategoryId = *marketCategoryId
			}
		}
		resultOffers = append(resultOffers, protoOffer)

	}

	return resultOffers, nil
}

// CalculateProductTariffs calculates tariffs for products using Yandex Market API
func (r *apiRepository) CalculateProductTariffs(ctx context.Context, marketplaceID int, req *pb.CalculateProductTariffsRequest, resultChan chan<- []*pb.CalculateProductTariffsResponse, errChan chan<- error) {
	defer close(resultChan)
	defer close(errChan)

	_, client, _, err := r.setupMarketplaceClient(ctx, marketplaceID)
	if err != nil {
		errChan <- err
		return
	}

	r.setTariffsRateLimit()

	ymParameters := r.converter.ProtoParametersToYmParameters(req.Parameters)
	if ymParameters == nil {
		errChan <- fmt.Errorf("failed to convert request parameters")
		return
	}

	offerChunks := lo.Chunk(req.Offers, defaultChunkSize)
	maxRetries := 5
	for chunkIndex, offerChunk := range offerChunks {
		fmt.Printf("Processing chunk %d/%d with %d offers\n", chunkIndex+1, len(offerChunks), len(offerChunk))
		var globalError error = nil
		for range maxRetries {
			response, err := r.processTariffChunk(ctx, client, ymParameters, offerChunk, chunkIndex)
			if err != nil {
				globalError = err
				continue
			}

			if response != nil {
				resultChan <- []*pb.CalculateProductTariffsResponse{response}
				break
			}
		}
		if globalError != nil {
			fmt.Printf("Error processing chunk %d: %s\n", chunkIndex+1, err)
		}
	}
}

// setTariffsRateLimit configures rate limiting for tariffs calculation endpoint
func (r *apiRepository) setTariffsRateLimit() {
	ym.SetPathLimit("/tariffs/calculate", rateLimitWindow, tariffsRateLimit)
}
func GetCategoriesError(bodyData string) []int64 {
	var result []int64
	for _, v := range gjson.Get(bodyData, "errors.#.message").Array() {
		errorMessage := v.String()
		split := strings.Split(errorMessage, "categories: ")
		if len(split) < 2 {
			continue
		}
		categories := strings.Split(strings.TrimSpace(split[1]), ",")
		for _, category := range categories {
			category = strings.TrimSpace(category)
			if category == "" {
				continue
			}
			isDigit, categoryId := utils.IsDigit(category)
			if !isDigit || categoryId == nil {
				continue
			}
			result = append(result, *categoryId)
		}
	}
	return result
}

func filterOffersByCategories(offers []*pb.CalculateProductTariffsRequest_Offer, categories []int64) []*pb.CalculateProductTariffsRequest_Offer {
	return lo.Filter(offers, func(offer *pb.CalculateProductTariffsRequest_Offer, _ int) bool {
		return !lo.Contains(categories, offer.CategoryId)
	})
}

func (r *apiRepository) makeTariffRequest(context context.Context, client *ymclient.APIClient, ymParameters *ymclient.CalculateTariffsParametersDTO, offerChunk []*pb.CalculateProductTariffsRequest_Offer) (*ymclient.CalculateTariffsResponse, *http.Response, error) {
	ymOffers := r.convertOffersToYM(offerChunk)
	if len(ymOffers) == 0 {
		return nil, nil, fmt.Errorf("no valid offers in chunk")
	}

	if ymParameters.CampaignId != nil && *ymParameters.CampaignId > 0 {
		ymParameters.SellingProgram = nil
		ymParameters.Frequency = nil
		ymParameters.Currency = nil
	} else {
		ymParameters.CampaignId = nil
	}

	ymRequest := ymclient.NewCalculateTariffsRequest(*ymParameters, ymOffers)
	response, httpResp, err := client.TariffsAPI.CalculateTariffs(context).
		CalculateTariffsRequest(*ymRequest).
		Execute()
	return response, httpResp, err
}

// processTariffChunk processes a single chunk of offers for tariff calculation
func (r *apiRepository) processTariffChunk(ctx context.Context, client *ymclient.APIClient, ymParameters *ymclient.CalculateTariffsParametersDTO, offerChunk []*pb.CalculateProductTariffsRequest_Offer, chunkIndex int) (*pb.CalculateProductTariffsResponse, error) {
	globalError := fmt.Errorf("failed to process chunk %d", chunkIndex+1)
	for range 2 {
		response, httpResp, err := r.makeTariffRequest(ctx, client, ymParameters, offerChunk)

		if err != nil {
			globalError = err
			if httpResp == nil || httpResp.Body == nil {
				return nil, fmt.Errorf("failed to call Yandex Market API for chunk %d: %w", chunkIndex+1, err)
			}
			bodyData, httpErr := io.ReadAll(httpResp.Body)
			if httpErr != nil {
				return nil, fmt.Errorf("failed to read response body for chunk %d: %w", chunkIndex+1, httpErr)
			}
			fmt.Printf("Error response for chunk %d: %s\n", chunkIndex+1, string(bodyData))
			categoriesErrors := GetCategoriesError(string(bodyData))
			if len(categoriesErrors) == 0 {
				return nil, fmt.Errorf("failed to call Yandex Market API for chunk %d: %w", chunkIndex+1, err)
			}

			offerChunk = filterOffersByCategories(offerChunk, categoriesErrors)
			continue
		}

		if response == nil || response.Result == nil {

			return nil, fmt.Errorf("Warning: received empty response for chunk %d\n", chunkIndex+1)
		}

		return r.convertResponseToProto(response), nil
	}
	return nil, globalError
}

// convertOffersToYM converts protobuf offers to Yandex Market format
func (r *apiRepository) convertOffersToYM(offers []*pb.CalculateProductTariffsRequest_Offer) []ymclient.CalculateTariffsOfferDTO {
	var ymOffers []ymclient.CalculateTariffsOfferDTO
	for _, offer := range offers {
		ymOffer := r.converter.ProtoOfferToYmOffer(offer)
		if ymOffer != nil {
			ymOffers = append(ymOffers, *ymOffer)
		}
	}
	return ymOffers
}

// convertResponseToProto converts Yandex Market response to protobuf format
func (r *apiRepository) convertResponseToProto(response *ymclient.CalculateTariffsResponse) *pb.CalculateProductTariffsResponse {
	var offers []*pb.CalculateProductTariffsResponse_Offers

	if response.Result.Offers != nil {
		for _, ymOfferInfo := range response.Result.Offers {
			pbResponseOffer := r.converter.YmOfferToProtoResponseOffer(&ymOfferInfo)
			if pbResponseOffer != nil {
				offers = append(offers, pbResponseOffer)
			}
		}
	}

	result := &pb.CalculateProductTariffsResponse{}
	result.Offers = offers
	return result
}
