package repository

import (
	"context"
	"fmt"
	"log"
	codeword_rpc "penahub.gitlab.yandexcloud.net/pena-services/customer/internal/proto/codeword"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/utils"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.uber.org/zap"
	mongoWrapper "penahub.gitlab.yandexcloud.net/backend/penahub_common/mongo"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/errors"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/fields"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/models"
	"penahub.gitlab.yandexcloud.net/pena-services/customer/internal/service/history"
)

type HistoryRepositoryDeps struct {
	Logger  *zap.Logger
	MongoDB *mongo.Collection
}

type HistoryRepository struct {
	logger  *zap.Logger
	mongoDB *mongo.Collection
}

func NewHistoryRepository(deps HistoryRepositoryDeps) *HistoryRepository {
	if deps.Logger == nil {
		log.Panicln("logger is nil on <NewHistoryRepository>")
	}

	if deps.MongoDB == nil {
		log.Panicln("mongodb is nil on <NewHistoryRepository>")
	}

	return &HistoryRepository{
		logger:  deps.Logger,
		mongoDB: deps.MongoDB,
	}
}

func (receiver *HistoryRepository) Insert(ctx context.Context, history *models.History) (*models.History, errors.Error) {
	result, err := receiver.mongoDB.InsertOne(ctx, history.Sanitize())
	if err != nil {
		receiver.logger.Error("failed to insert history on <Insert> of <HistoryRepository>",
			zap.Any("history", history),
			zap.Error(err),
		)

		return nil, errors.New(
			fmt.Errorf("failed to insert history on <Insert> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}

	insertedID := result.InsertedID.(primitive.ObjectID).Hex()
	history.ID = insertedID

	return history, nil
}

func (receiver *HistoryRepository) FindMany(ctx context.Context, dto *history.GetHistories) ([]models.History, errors.Error) {
	findOptions := options.Find()

	findOptions.SetSkip((dto.Pagination.Page - 1) * dto.Pagination.Limit)
	findOptions.SetLimit(dto.Pagination.Limit)
	findOptions.SetSort(bson.D{{
		Key: "createdAt", Value: -1,
	}})

	histories, err := mongoWrapper.Find[models.History](ctx, &mongoWrapper.RequestSettings{
		Driver:  receiver.mongoDB,
		Options: findOptions,
		Filter:  dto.BSON(),
	})
	if err != nil {
		receiver.logger.Error("failed to find many histories on <FindMany> of <HistoryRepository>",
			zap.Int64("page", dto.Pagination.Page),
			zap.Int64("limit", dto.Pagination.Limit),
			zap.Int64("skip", (dto.Pagination.Page-1)*dto.Pagination.Limit),
			zap.Error(err),
		)

		return nil, errors.New(
			fmt.Errorf("failed to find many histories on <FindMany> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}

	return histories, nil
}

func (receiver *HistoryRepository) CountAll(ctx context.Context, dto *history.GetHistories) (int64, errors.Error) {
	count, err := receiver.mongoDB.CountDocuments(ctx, dto.BSON())
	if err != nil {
		receiver.logger.Error("failed to count all documents on <CountAll> of <HistoryRepository>",
			zap.Error(err),
		)

		return 0, errors.New(
			fmt.Errorf("failed to count all documents on <CountAll> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}

	return count, nil
}

// TODO:tests
// GetRecentTariffs method for processing a user request with data aggregation with a limit of 100 sorted in descending order.
func (receiver *HistoryRepository) GetRecentTariffs(ctx context.Context, userID string) ([]models.TariffID, errors.Error) {
	matchStage := bson.D{
		{Key: "$match", Value: bson.D{
			{Key: fields.History.UserID, Value: userID},
			{Key: fields.History.IsDeleted, Value: false},
			{Key: fields.History.Type, Value: models.CustomerHistoryKeyPayCart},
		}},
	}

	unwindStage := bson.D{
		{Key: "$unwind", Value: bson.D{
			{Key: "path", Value: "$rawDetails.tariffs"},
		}},
	}

	groupStage := bson.D{
		{Key: "$group", Value: bson.D{
			{Key: "_id", Value: "$rawDetails.tariffs.id"},
		}},
	}

	sortStage := bson.D{
		{Key: "$sort", Value: bson.D{
			{Key: "createdAt", Value: -1},
		}},
	}

	limitStage := bson.D{
		{Key: "$limit", Value: 100},
	}

	cursor, err := receiver.mongoDB.Aggregate(ctx, mongo.Pipeline{matchStage, unwindStage, sortStage, groupStage, limitStage})
	if err != nil {
		receiver.logger.Error("failed to get recent tariffs on <GetRecentTariffs> of <HistoryRepository>",
			zap.String("userId", userID),
			zap.Error(err),
		)
		return nil, errors.New(
			fmt.Errorf("failed to get recent tariffs on <GetRecentTariffs> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}

	var result []models.TariffID
	if err := cursor.All(ctx, &result); err != nil {
		receiver.logger.Error("failed to decode recent tariffs on <GetRecentTariffs> of <HistoryRepository>",
			zap.String("userId", userID),
			zap.Error(err),
		)
		return nil, errors.New(
			fmt.Errorf("failed to decode recent tariffs on <GetRecentTariffs> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}

	return result, nil
}

// TODO:tests.
func (receiver *HistoryRepository) GetHistoryByID(ctx context.Context, historyID string) (*models.ReportHistory, errors.Error) {
	history := &models.ReportHistory{}

	objID, err := primitive.ObjectIDFromHex(historyID)
	if err != nil {
		return nil, errors.New(fmt.Errorf("failed to convert history ID: %w", err), errors.ErrInternalError)
	}

	err = receiver.mongoDB.FindOne(ctx, bson.M{"_id": objID}).Decode(history)
	if err != nil {
		receiver.logger.Error(
			"failed to find by id in <GetHistoryById> of <HistoryRepository>",
			zap.String("historyID", historyID),
			zap.Error(err),
		)

		if err == mongo.ErrNoDocuments {
			return nil, errors.New(
				fmt.Errorf("history not found with ID: %s", historyID),
				errors.ErrNotFound,
			)
		}

		return nil, errors.New(
			fmt.Errorf("failed to find by id: %w", err),
			errors.ErrInternalError,
		)
	}

	return history, nil
}

// TODO:tests.
func (receiver *HistoryRepository) GetDocNumber(ctx context.Context, userID string) (map[string]int, errors.Error) {
	findOptions := options.Find()
	findOptions.SetSort(bson.D{{Key: "createdAt", Value: 1}})

	filter := bson.M{
		fields.History.UserID: userID,
	}

	cursor, err := receiver.mongoDB.Find(ctx, filter, findOptions)
	if err != nil {
		receiver.logger.Error("failed to get DocNumber list on <GetDocNumber> of <HistoryRepository>",
			zap.String("userId", userID),
			zap.Error(err),
		)
		return nil, errors.New(
			fmt.Errorf("failed to get DocNumber list on <GetDocNumber> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}

	defer func() {
		if err := cursor.Close(ctx); err != nil {
			receiver.logger.Error("failed to close cursor on <GetDocNumber> of <HistoryRepository>",
				zap.String("userId", userID),
				zap.Error(err),
			)
		}
	}()

	result := make(map[string]int)
	var count int
	for cursor.Next(ctx) {
		var history models.History
		if err := cursor.Decode(&history); err != nil {
			receiver.logger.Error("failed to decode history on <GetDocNumber> of <HistoryRepository>",
				zap.String("userId", userID),
				zap.Error(err),
			)
			return nil, errors.New(
				fmt.Errorf("failed to decode history on <GetDocNumber> of <HistoryRepository>: %w", err),
				errors.ErrInternalError,
			)
		}

		result[history.ID] = count
		count++
	}

	if err := cursor.Err(); err != nil {
		receiver.logger.Error("cursor error on <GetDocNumber> of <HistoryRepository>",
			zap.String("userId", userID),
			zap.Error(err),
		)
		return nil, errors.New(
			fmt.Errorf("cursor error on <GetDocNumber> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}

	return result, nil
}

func (receiver *HistoryRepository) CalculateCustomerLTV(ctx context.Context, from, to int64) (int64, errors.Error) {
	timeFilter := bson.M{}
	if from != 0 || to != 0 {
		timeRange := bson.M{}
		if from != 0 {
			timeRange["$gte"] = time.Unix(from, 0).UTC().Format(time.RFC3339Nano)
		}
		if to != 0 {
			timeRange["$lte"] = time.Unix(to, 0).UTC().Format(time.RFC3339Nano)
		}
		timeFilter["createdAt"] = timeRange
	}
	pipeline := mongo.Pipeline{
		{{Key: "$match", Value: bson.M{"key": models.CustomerHistoryKeyPayCart, "isDeleted": false}}},
		{{Key: "$match", Value: timeFilter}},
		{{Key: "$group", Value: bson.M{
			"_id":          "$userId",
			"firstPayment": bson.M{"$min": "$createdAt"},
			"lastPayment":  bson.M{"$max": "$createdAt"},
		}}},
		{{Key: "$project", Value: bson.M{
			"lifeTimeInDays": bson.M{"$divide": []interface{}{
				bson.M{"$subtract": []interface{}{bson.M{"$toDate": "$lastPayment"}, bson.M{"$toDate": "$firstPayment"}}},
				86400000,
			}},
		}}},
		{{Key: "$group", Value: bson.M{
			"_id":        nil,
			"averageLTV": bson.M{"$avg": "$lifeTimeInDays"},
		}}},
	}

	cursor, err := receiver.mongoDB.Aggregate(ctx, pipeline)
	if err != nil {
		receiver.logger.Error("failed to calculate customer LTV <CalculateCustomerLTV> of <HistoryRepository>",
			zap.Error(err),
		)
		return 0, errors.New(
			fmt.Errorf("failed to calculate customer LTV <CalculateCustomerLTV> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}
	defer func() {
		if err := cursor.Close(ctx); err != nil {
			receiver.logger.Error("failed to close cursor", zap.Error(err))
		}
	}()
	var results []struct{ AverageLTV float64 }
	if err := cursor.All(ctx, &results); err != nil {
		receiver.logger.Error("failed to getting result LTV <CalculateCustomerLTV> of <HistoryRepository>",
			zap.Error(err),
		)
		return 0, errors.New(
			fmt.Errorf("failed to getting result LTV <CalculateCustomerLTV> of <HistoryRepository>: %w", err),
			errors.ErrInternalError,
		)
	}
	if len(results) == 0 {
		return 0, nil
	}

	averageLTV := int64(results[0].AverageLTV)
	return averageLTV, nil
}

func (receiver *HistoryRepository) GetHistoryByListUsers(ctx context.Context, accounts []models.Account) (map[string][]models.History, error) {
	historyMap := make(map[string][]models.History)

	var IDs []string
	for _, account := range accounts {
		IDs = append(IDs, account.UserID)
	}
	filter := bson.M{"userId": bson.M{"$in": IDs}}

	cursor, err := receiver.mongoDB.Find(ctx, filter)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)

	for cursor.Next(ctx) {
		var history models.History
		if err := cursor.Decode(&history); err != nil {
			return nil, err
		}
		historyMap[history.UserID] = append(historyMap[history.UserID], history)
	}

	if err := cursor.Err(); err != nil {
		return nil, err
	}

	return historyMap, nil
}

func (receiver *HistoryRepository) GetPayUsersPromoHistory(ctx context.Context, codewordData map[string][]*codeword_rpc.PromoActivationResp_UserTime, from, to int64) (map[string]int64, error) {
	match := utils.MatchGen(codewordData, from, to)

	pipeline := []bson.M{
		match,
		{
			"$group": bson.M{
				"_id":      "$userId",
				"priceSum": bson.M{"$sum": "$rawDetails.price"},
			},
		},
	}

	cursor, err := receiver.mongoDB.Aggregate(ctx, pipeline)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)

	userSum := make(map[string]int64)
	for cursor.Next(ctx) {
		var result struct {
			UserID   string `bson:"_id"`
			PriceSum int64  `bson:"priceSum"`
		}
		if err := cursor.Decode(&result); err != nil {
			return nil, err
		}
		userSum[result.UserID] = result.PriceSum
	}
	if err := cursor.Err(); err != nil {
		return nil, err
	}
	return userSum, nil
}
