package service

import (
	"context"
	"errors"
	"gorm.io/gorm"
	"strconv"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/types/known/timestamppb"

	"github.com/UpmeshLTD/urus-aio/infrastructure/db/entities"
	"github.com/UpmeshLTD/urus-aio/infrastructure/utils"
	"github.com/UpmeshLTD/urus-aio/paid-order/external/cart"
	"github.com/UpmeshLTD/urus-aio/paid-order/server/grpc/pb"
)

type iGRPC interface {
	Ping(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error)

	GetFulfillmentByID(ctx context.Context, req *pb.GetFulfillmentRequest) (*pb.GetFulfillmentResponse, error)
	UpdateFulfillmentDraftStatus(ctx context.Context, req *pb.UpdateFulfillmentDraftStatusRequest) error

	GRPCGetPaidOrders(ctx context.Context, req *pb.GetPaidOrdersRequest) (*pb.GetPaidOrderResponse, error)
	UpdatePaidOrderIncorrectInformation(ctx context.Context, req *pb.UpdatePaidOrderIncorrectInformationRequest) error
	UpdatePaidOrderShipmentId(ctx context.Context, req *pb.UpdatePaidOrderShipmentIdRequest) error

	GetDeliveryAddressByCartID(ctx context.Context, req *pb.GetDeliveryAddressByCartIDRequest) (*pb.GetDeliveryAddressByCartIDResponse, error)
	GetDeliveryAddressByCartIDs(ctx context.Context, req *pb.GetDeliveryAddressByCartIDsRequest) (*pb.GetDeliveryAddressByCartIDsResponse, error)

	CancelArrangedShipPaidOrder(ctx context.Context, req *pb.CancelArrangedShipPaidOrderRequest) error
	ReArrangeShipPaidOrdersGRPC(ctx context.Context, req *pb.ReArrangeShipPaidOrdersRequest) (*pb.ReArrangeShipPaidOrdersResponse, error)
}

func (s *serviceImpl) Ping(ctx context.Context, req *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) GetFulfillmentByID(ctx context.Context, req *pb.GetFulfillmentRequest) (*pb.GetFulfillmentResponse, error) {
	fulfillment, err := s.fulfillmentDomain.GetWithAssociates(ctx, req.FulfillmentId)
	if err != nil {
		return nil, err
	}
	if fulfillment.WorkspaceID != req.WorkspaceId {
		return nil, errors.New("fulfillment not found")
	}

	values := make([]string, 0)

	md, ok := metadata.FromIncomingContext(ctx)
	if ok {
		values = md.Get("authorization")
	}

	if len(values) == 0 {
		return nil, status.Error(codes.Unauthenticated, "Unauthenticated")
	}
	token := values[0]

	cartIds := make([]string, 0)
	for _, f := range fulfillment.PaidOrders {
		cartIds = append(cartIds, f.CartID)
	}

	cartDetails, err := s.cartService.GetCartByIDs(ctx, token, cartIds)
	if err != nil {
		return nil, err
	}

	cartsMap := make(map[string]*cart.CartDetail, len(cartDetails))
	for _, v := range cartDetails {
		cartsMap[v.CartID] = v
	}

	deliveryAddresses, err := s.deliveryAddressDomain.GetList(ctx, cartIds)
	if err != nil {
		return nil, err
	}

	deliveryAddressesMap := make(map[string]*entities.DeliveryAddress)
	for _, v := range deliveryAddresses {
		deliveryAddressesMap[v.CartID] = v
	}

	orderDetails := make([]*pb.OrderDetail, 0)
	pos := fulfillment.PaidOrders

	for _, po := range pos {
		if _, has := cartsMap[po.CartID]; !has {
			continue
		}

		orderItems := make([]*pb.OrderItem, 0)
		for _, v := range cartsMap[po.CartID].OrderItems {
			if po.ShippingGroupID == nil ||
				(v.ShippingGroupID != nil && *po.ShippingGroupID == *v.ShippingGroupID) ||
				(v.ShippingGroupID == nil && *po.ShippingGroupID == entities.EmptyShippingGroupID) {
				orderItems = append(orderItems, &pb.OrderItem{
					Id:           v.ID,
					ItemId:       v.ItemID,
					ItemPostId:   v.ItemPostId,
					ItemName:     v.ItemName,
					ItemCode:     v.ItemCode,
					ItemPrice:    v.ItemPrice,
					ItemQuantity: v.ItemQuantity,
					ItemSubTotal: v.ItemSubTotal,
				})
			}
		}

		deliveryAddress, ok := deliveryAddressesMap[po.CartID]
		if !ok {
			deliveryAddress = &entities.DeliveryAddress{
				Address: entities.Address{},
			}
		}

		var subLocations = make([]*pb.Location, 0)
		for _, v := range deliveryAddress.Address.SortSubLevelLocations() {
			subLocations = append(subLocations, &pb.Location{
				InternationalName: v.InternationalName,
				LocalizedName:     v.LocalizedName,
				ParentId:          v.ParentID,
			})
		}

		// For case force paid doesn't save recipient's name and phone number
		recipientName := deliveryAddress.RecipientName
		recipientPhoneNumber := deliveryAddress.RecipientPhoneNumber

		if recipientName == "" {
			recipientName = cartsMap[po.CartID].DeliveryDetails.Recipient
		}
		if recipientPhoneNumber == "" {
			recipientPhoneNumber = cartsMap[po.CartID].GeneralInformation.CustomerPhoneNumber
		}

		orderDetails = append(orderDetails, &pb.OrderDetail{
			Id:              po.ID,
			CartId:          po.CartID,
			ShippingGroupId: po.ShippingGroupID,
			DeliveryAddress: &pb.Address{
				Address1:          deliveryAddress.Address.Address1,
				Address2:          deliveryAddress.Address.Address2,
				PostalCode:        deliveryAddress.Address.PostalCode,
				UnitNumber:        deliveryAddress.Address.UnitNumber,
				SubLevelLocations: subLocations,
			},
			Dimension: &pb.Dimension{
				Width:  po.Dimension.Width,
				Length: po.Dimension.Height,
				Height: po.Dimension.Length,
				Weight: &po.Dimension.Weight,
			},
			GeneralInformation: &pb.GeneralInformation{
				SellerId:            cartsMap[po.CartID].GeneralInformation.SellerID,
				PageName:            cartsMap[po.CartID].GeneralInformation.PageName,
				CustomerId:          cartsMap[po.CartID].GeneralInformation.CustomerID,
				CustomerName:        cartsMap[po.CartID].GeneralInformation.CustomerName,
				CustomerEmail:       cartsMap[po.CartID].GeneralInformation.CustomerEmail,
				CustomerPhoneNumber: cartsMap[po.CartID].GeneralInformation.CustomerPhoneNumber,
			},
			DeliveryDetails: &pb.DeliveryDetails{
				DeliveryOptionId:     cartsMap[po.CartID].DeliveryDetails.DeliveryOptionID,
				DeliveryOption:       cartsMap[po.CartID].DeliveryDetails.DeliveryOption,
				DeliveryAddress:      cartsMap[po.CartID].DeliveryDetails.DeliveryAddress,
				DeliverySurcharge:    cartsMap[po.CartID].DeliveryDetails.DeliverySurcharge,
				DeliveryInstruction:  cartsMap[po.CartID].DeliveryDetails.DeliveryInstruction,
				PostalCode:           cartsMap[po.CartID].DeliveryDetails.PostalCode,
				RecipientName:        recipientName,
				RecipientPhoneNumber: recipientPhoneNumber,
			},
			OrderItems: orderItems,
		})
	}

	var pickupTime *timestamppb.Timestamp
	var pickupStartTime *timestamppb.Timestamp
	var pickupEndTime *timestamppb.Timestamp

	if fulfillment.PickupTime != nil {
		pickupTime = timestamppb.New(*fulfillment.PickupTime)
	}
	if fulfillment.PickupStartTime != nil {
		pickupStartTime = timestamppb.New(*fulfillment.PickupStartTime)
	}
	if fulfillment.PickupEndTime != nil {
		pickupEndTime = timestamppb.New(*fulfillment.PickupEndTime)
	}

	return &pb.GetFulfillmentResponse{
		Id:                fulfillment.ID.String(),
		CourierId:         fulfillment.CourierID,
		DeliveryId:        fulfillment.CourierDeliveryPlanID,
		PickupPointId:     fulfillment.PickupPointID,
		PickupTime:        pickupTime,
		PickupStartTime:   pickupStartTime,
		PickupEndTime:     pickupEndTime,
		PickupInstruction: fulfillment.PickupInstruction,
		OrderDetails:      orderDetails,
	}, nil
}

func (s *serviceImpl) UpdateFulfillmentDraftStatus(ctx context.Context, req *pb.UpdateFulfillmentDraftStatusRequest) error {
	return s.fulfillmentDomain.UpdateDraftStatus(ctx, req.Id, req.Draft)
}

func (s *serviceImpl) UpdatePaidOrderIncorrectInformation(ctx context.Context, req *pb.UpdatePaidOrderIncorrectInformationRequest) error {
	po, err := s.paidOrderDomain.Get(ctx, req.CartId, req.ShippingGroupId)
	if err != nil {
		return err
	}
	return s.paidOrderDomain.UpdateIncorrectInformation(ctx, po.ID, req.IncorrectInformation)
}

func (s *serviceImpl) UpdatePaidOrderShipmentId(ctx context.Context, req *pb.UpdatePaidOrderShipmentIdRequest) error {
	po, err := s.paidOrderDomain.Get(ctx, req.CartId, req.ShippingGroupId)
	if err != nil {
		return err
	}
	return s.paidOrderDomain.UpdatePaidOrderShipmentID(ctx, po.ID, req.ShipmentId)
}

func (s *serviceImpl) GetDeliveryAddressByCartID(ctx context.Context, req *pb.GetDeliveryAddressByCartIDRequest) (*pb.GetDeliveryAddressByCartIDResponse, error) {
	da, err := s.deliveryAddressDomain.Get(ctx, req.CartID)
	if err != nil {
		return nil, status.Error(codes.NotFound, "delivery address not found")
	}

	subLevelLocations := make([]*pb.SubLevelLocation, 0)
	for _, v := range da.Address.SortSubLevelLocations() {
		subLevelLocations = append(subLevelLocations, &pb.SubLevelLocation{
			InternationalName: v.InternationalName,
			LocalizedName:     v.LocalizedName,
		})
	}

	return &pb.GetDeliveryAddressByCartIDResponse{
		CartID:         da.CartID,
		Owner:          da.Owner,
		RecipientName:  da.RecipientName,
		RecipientPhone: da.RecipientPhoneNumber,
		Address: &pb.GetDeliveryAddressByCartIDResponseAddress{
			ID:                strconv.Itoa(int(da.AddressID)),
			Address1:          da.Address.Address1,
			Address2:          da.Address.Address2,
			PostalCode:        da.Address.PostalCode,
			CountryID:         da.Address.CountryID,
			UnitNumber:        da.Address.UnitNumber,
			SubLevelLocations: subLevelLocations,
		},
	}, nil
}

func (s *serviceImpl) GetDeliveryAddressByCartIDs(ctx context.Context, req *pb.GetDeliveryAddressByCartIDsRequest) (*pb.GetDeliveryAddressByCartIDsResponse, error) {
	res, err := s.deliveryAddressDomain.GetList(ctx, req.CartIDs)
	if err != nil {
		return nil, status.Error(codes.NotFound, "delivery address not found")
	}

	das := make([]*pb.GetDeliveryAddressByCartIDResponse, 0)
	for _, v1 := range res {
		subLevelLocations := make([]*pb.SubLevelLocation, 0)
		for _, v := range v1.Address.SortSubLevelLocations() {
			subLevelLocations = append(subLevelLocations, &pb.SubLevelLocation{
				InternationalName: v.InternationalName,
				LocalizedName:     v.LocalizedName,
			})
		}
		das = append(das, &pb.GetDeliveryAddressByCartIDResponse{
			CartID:         v1.CartID,
			Owner:          v1.Owner,
			RecipientName:  v1.RecipientName,
			RecipientPhone: v1.RecipientPhoneNumber,
			Address: &pb.GetDeliveryAddressByCartIDResponseAddress{
				ID:                strconv.Itoa(int(v1.AddressID)),
				Address1:          v1.Address.Address1,
				Address2:          v1.Address.Address2,
				PostalCode:        v1.Address.PostalCode,
				CountryID:         v1.Address.CountryID,
				UnitNumber:        v1.Address.UnitNumber,
				SubLevelLocations: subLevelLocations,
			},
		})
	}

	return &pb.GetDeliveryAddressByCartIDsResponse{
		DeliveryAddresses: das,
	}, nil
}

func (s *serviceImpl) GRPCGetPaidOrders(ctx context.Context, req *pb.GetPaidOrdersRequest) (*pb.GetPaidOrderResponse, error) {
	result, _, err := s.paidOrderDomain.GetList(ctx, &entities.GetPaidOrdersParams{
		CartIDs: req.CartIds,
	})
	if err != nil {
		return nil, err
	}
	pos := make([]*pb.PaidOrder, 0)
	for _, v := range result {
		pos = append(pos, &pb.PaidOrder{
			CartId:          v.CartID,
			ShippingGroupId: v.ShippingGroupID,
			CreatedAt:       timestamppb.New(v.CreatedAt),
		})
	}
	return &pb.GetPaidOrderResponse{
		PaidOrders: pos,
	}, nil
}

func (s *serviceImpl) CancelArrangedShipPaidOrder(ctx context.Context, req *pb.CancelArrangedShipPaidOrderRequest) error {
	return s.paidOrderDomain.CancelArrangedShipPaidOrder(ctx, req.PaidOrderId)
}

func (s *serviceImpl) ReArrangeShipPaidOrdersGRPC(ctx context.Context, req *pb.ReArrangeShipPaidOrdersRequest) (*pb.ReArrangeShipPaidOrdersResponse, error) {
	// Create new fulfillment
	fulfillment, err := s.fulfillmentDomain.Create(ctx, &entities.Fulfillment{
		WorkspaceID: req.WorkspaceID,
	})
	if err != nil {
		return nil, err
	}

	fulfillmentID := fulfillment.ID.String()

	// Reset paid orders
	if err := s.paidOrderDomain.UpdateBatch(ctx, req.PaidOrderIDs, map[string]interface{}{
		"FulfillmentID":        fulfillmentID,
		"ShipmentID":           gorm.Expr("NULL"),
		"IncorrectInformation": gorm.Expr("NULL"),
		"Dimension":            entities.WeightDimension{},
	}); err != nil {
		return nil, err
	}

	return &pb.ReArrangeShipPaidOrdersResponse{
		FulfillmentID: fulfillmentID,
	}, nil
}
