package service

import (
	"context"
	"fmt"
	"github.com/UpmeshLTD/urus-aio/infrastructure/common"
	"github.com/UpmeshLTD/urus-aio/infrastructure/db/entities"
	"github.com/UpmeshLTD/urus-aio/infrastructure/enums"
	"github.com/UpmeshLTD/urus-aio/infrastructure/utils"
	"github.com/UpmeshLTD/urus-aio/infrastructure/utils/workerpool"
	"github.com/UpmeshLTD/urus-aio/shipment/server/grpc/pb"
	"github.com/pkg/errors"
	"log"
	"time"
)

const (
	RowsPerQuery       = 1000
	UpdatesConcurrency = 10
	MaxProcessingTime  = 30 * time.Minute
)

type iGRPC interface {
	GRPCPing(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error)
	GRPCArchive(ctx context.Context, req *pb.ArchiveRequest) (*pb.ArchiveResponse, error)
	GRPCArchiveBatch(ctx context.Context, req *pb.ArchiveBatchRequest) (*pb.ArchiveBatchResponse, error)
	GRPCSyncCourierShipmentStatus(ctx context.Context, req *pb.SyncCourierShipmentStatusRequest) (*pb.SyncCourierShipmentStatusResponse, error)
}

func (s *serviceImpl) GRPCPing(ctx context.Context, _ *pb.PingRequest) (*pb.PingResponse, error) {
	info := utils.GetInfo(s.config.IsDevelopment())
	return &pb.PingResponse{
		Timestamps:  info["timestamps"].(int64),
		Environment: info["environment"].(string),
		Hostname:    info["hostname"].(string),
	}, nil
}

func (s *serviceImpl) GRPCArchive(ctx context.Context, req *pb.ArchiveRequest) (*pb.ArchiveResponse, error) {
	shippingGroupId := entities.EmptyShippingGroupID
	if req.ShippingGroupId != nil && *req.ShippingGroupId != "" {
		shippingGroupId = *req.ShippingGroupId
	}

	shipments, err := s.shipmentDomain.GetByCartID(ctx, req.CartId)
	if err != nil {
		return nil, err
	}

	var shipment *entities.Shipment
	for _, sm := range shipments {
		if sm.ShippingGroupID == nil || *sm.ShippingGroupID == shippingGroupId {
			shipment = sm
			break
		}
	}

	if shipment == nil {
		return nil, errors.New("shipment not found")
	}

	if shipment.Status != entities.ShipmentStatusFailedDelivery &&
		shipment.Status != entities.ShipmentStatusDelivered &&
		shipment.Status != entities.ShipmentStatusCancelled {
		return nil, errors.New("cannot archive this shipment")
	}

	_, err = s.shipmentDomain.Archive(ctx, shipment)
	if err != nil {
		return nil, err
	}

	return &pb.ArchiveResponse{Status: "ok"}, nil
}

func (s *serviceImpl) GRPCArchiveBatch(ctx context.Context, req *pb.ArchiveBatchRequest) (*pb.ArchiveBatchResponse, error) {
	cartIds := make([]string, 0)
	for _, shipmentReq := range req.Shipments {
		cartIds = append(cartIds, shipmentReq.CartId)
	}

	if len(cartIds) == 0 {
		return &pb.ArchiveBatchResponse{Status: "ok"}, nil
	}

	sms, err := s.shipmentDomain.GetByCartIDs(ctx, cartIds)
	if err != nil {
		return nil, err
	}

	smsMaps := map[string]*entities.Shipment{}
	for _, sm := range sms {
		smsMaps[utils.GenerateKeyFromCartIdAndShippingGroupId(sm.CartID, sm.ShippingGroupID)] = sm
	}

	shipments := make([]*entities.Shipment, 0)
	for _, shipmentReq := range req.Shipments {
		key := utils.GenerateKeyFromCartIdAndShippingGroupId(shipmentReq.CartId, shipmentReq.ShippingGroupId)
		if _, has := smsMaps[key]; has {
			shipments = append(shipments, smsMaps[key])
		}
	}

	if len(shipments) == 0 {
		return nil, errors.New("exists at least one shipment not found")
	}

	for _, shipment := range shipments {
		if shipment.Status != entities.ShipmentStatusFailedDelivery &&
			shipment.Status != entities.ShipmentStatusDelivered &&
			shipment.Status != entities.ShipmentStatusCancelled {
			return nil, errors.New("cannot archive at least one shipment")
		}
	}

	if err := s.shipmentDomain.ArchiveBatch(ctx, shipments); err != nil {
		return nil, err
	}

	return &pb.ArchiveBatchResponse{Status: "ok"}, nil
}

func (s *serviceImpl) GRPCSyncCourierShipmentStatus(ctx context.Context, req *pb.SyncCourierShipmentStatusRequest) (*pb.SyncCourierShipmentStatusResponse, error) {
	log.Println("[SyncCourierShipmentStatus] CourierID:", req.CourierID)

	shippingStatus := "shipping"
	preload := make([]string, 0) // disable preload
	fields := []string{"id", "cart_id", "shipping_group_id", "courier_id", "courier_tracking_id", "state_date"}
	duration, err := time.ParseDuration(fmt.Sprintf("-%dm", req.Interval))
	if err != nil {
		return nil, errors.Wrap(err, "Failed to parse duration")
	}

	// Get and check courier status
	c, err := s.courierDomain.GetCourier(ctx, &entities.GetCourierParams{
		CourierID: req.CourierID,
	})
	if err != nil {
		log.Println("[SyncCourierShipmentStatus] Failed to get courier:", err)
		return nil, errors.Wrap(err, "Failed to get courier")
	}
	if c.Deactivate {
		log.Println("[SyncCourierShipmentStatus] Courier has been deactivated")
		return nil, errors.New("Courier has been deactivated")
	}

	st := time.Now()
	id := st.Unix()

	log.Printf("[SyncCourierShipmentStatus] Job %d started at %s\n", id, st.Format(enums.TimeFormatYMDHIS))

	for {
		if time.Now().Sub(st) > MaxProcessingTime {
			log.Printf("[SyncCourierShipmentStatus] Job %d has been stopped at %s due to long processing\n", id, time.Now().Format(enums.TimeFormatYMDHIS))
			return nil, errors.New("Job has been stopped due to long processing")
		}

		// Get in-shipping shipments
		checkedAtBefore := time.Now().Add(duration)
		shipments, total, err := s.shipmentDomain.GetList(ctx, &entities.GetShipmentsParams{
			Fields:                   &fields,
			Preload:                  &preload,
			CourierID:                &req.CourierID,
			Status:                   &shippingStatus,
			CheckedAtBefore:          &checkedAtBefore,
			CourierTrackingIDNotNull: true,
			Pagination: &common.Pagination{
				Sort:  "checked_at ASC",
				Limit: RowsPerQuery,
			},
		})
		if err != nil {
			log.Println("[SyncCourierShipmentStatus] Failed to get shipments:", err)
			return nil, errors.Wrap(err, "Failed to get shipments")
		}

		log.Printf("[SyncCourierShipmentStatus] Total shipments: %d / %d", len(shipments), total)

		if len(shipments) == 0 {
			log.Println("[SyncCourierShipmentStatus] All shipments are up to date")
			break
		}

		courierService, err := s.externalShipmentFactory.Get(c.Code)
		if err != nil {
			log.Println("[SyncCourierShipmentStatus] Failed to get shipment provider:", err)
			return nil, errors.Wrap(err, "Failed to get shipment provider")
		}

		// Call to courier to get shipment status
		courierShipments, shipmentErr := courierService.GetShipmentStatus(ctx, shipments, nil)
		if shipmentErr != nil {
			log.Println("[SyncCourierShipmentStatus] Failed to get courier shipment status:", err)
			return nil, errors.Wrap(err, "Failed to get courier shipment status")
		}

		courierShipmentsMap := map[string]*entities.CourierShipmentStatus{}
		for _, v := range courierShipments {
			courierShipmentsMap[v.CourierTrackingID] = v
		}

		wp := workerpool.NewPool(UpdatesConcurrency)
		for _, sm := range shipments {
			sm := sm
			wp.AddTask(func() {
				if sm.CourierTrackingID != nil {
					if v, ok := courierShipmentsMap[*sm.CourierTrackingID]; ok {
						if v.IsNotFound {
							// Shipment not found
							log.Println("[SyncCourierShipmentStatus] Courier shipment not found:", v.CourierTrackingID)
						} else if !v.StatusDate.IsZero() && v.StatusDate.After(sm.StateDate) {
							// The courier shipment has new status
							// Transform courier shipment status to upmesh shipment status
							status := courierService.TransformShipmentStatus(ctx, v.StatusCode)
							if status != nil {
								// Update shipment status
								if err := s.webhookDomain.UpdateShipmentStatus(ctx, sm, &entities.ShipmentStatusLog{
									Status:            *status,
									CourierID:         sm.CourierID,
									CourierTrackingID: v.CourierTrackingID,
									CourierStatus:     v.StatusCode,
									StateText:         v.StatusText,
									StateDate:         v.StatusDate,
								}); err != nil {
									log.Println("[SyncCourierShipmentStatus] Failed to update shipment status:", err)
								}
							} else {
								log.Printf("[SyncCourierShipmentStatus] Courier shipment status not exist in mapping: %s - %s", v.CourierTrackingID, v.StatusCode)
							}
						}
					}
				}

				// Update checked_at
				if err := s.shipmentDomain.UpdatePartial(ctx, sm.ID.String(), map[string]interface{}{
					"checked_at": time.Now(),
				}); err != nil {
					log.Println("[SyncCourierShipmentStatus] Failed to update shipment checked_at:", err)
				}
			})
		}
		wp.Run()
	}

	log.Printf("[SyncCourierShipmentStatus] Job %d finished at %s\n", id, time.Now().Format(enums.TimeFormatYMDHIS))

	return &pb.SyncCourierShipmentStatusResponse{
		Status: "success",
	}, nil
}
