package ninjavan

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/UpmeshLTD/urus-aio/infrastructure/db/entities"
	e "github.com/UpmeshLTD/urus-aio/infrastructure/errors"
	m "github.com/UpmeshLTD/urus-aio/infrastructure/errors/messages"
	pdfpb "github.com/UpmeshLTD/urus-aio/pdf/server/grpc/pb"
	shipmentConfig "github.com/UpmeshLTD/urus-aio/shipment/config"
	"github.com/UpmeshLTD/urus-aio/shipment/external/shipment"
	shipmentLog "github.com/UpmeshLTD/urus-aio/shipment/log"
	"github.com/UpmeshLTD/urus-aio/shipment/server/http/dto"
	"github.com/mitchellh/mapstructure"
	"io"
	"io/ioutil"
	"log"
	"net/http"
)

const (
	ApiPathAccessToken = "/2.0/oauth/access_token"
	ApiPathCreateOrder = "/4.1/orders"
	ApiPathCancelOrder = "/2.2/orders/%s"
	ApiPathGetWaybill  = "/2.0/reports/waybill?tids=%s&h=1"
)

var (
	accessToken = ""
)

type ninjaVanImpl struct {
	config shipmentConfig.Config
	logger shipmentLog.Logger
}

func NewService(config shipmentConfig.Config, logger shipmentLog.Logger) shipment.IShipment {
	return &ninjaVanImpl{
		config: config,
		logger: logger,
	}
}

/*- Authenticate -----------------------------------------------------------------------------------------------------*/

func (i *ninjaVanImpl) EncryptCourierAuthenticationInfo(info dto.CourierAuthenticationInfo, workspaceID string) (encryptedInfo string, salt string, err error) {
	panic("implement me")
}

func (i *ninjaVanImpl) DecryptCourierAuthenticationInfo(encrypted string, salt string) (dto.CourierAuthenticationInfo, error) {
	panic("implement me")
}

/*- Authenticate -----------------------------------------------------------------------------------------------------*/

/*- Shipment ---------------------------------------------------------------------------------------------------------*/

func (i *ninjaVanImpl) CreateShipment(ctx context.Context, req *entities.Shipment, sellerCourier *entities.SellerCourier) (*entities.Shipment, *shipment.Error) {
	request := CreateShipmentRequest{}
	res, err := i.requestWithRetry(http.MethodPost, ApiPathCreateOrder, request)
	if err != nil {
		log.Println("[CreateShipment][NinjaVan] Failed to call API", err)
		return nil, shipment.ToShipmentError(e.NewFormatted(ctx, e.InternalServer, m.FailedToCallAPI, err.Error()))
	}

	response := CreateShipmentResponse{}
	if err = mapstructure.Decode(res, &response); err != nil {
		log.Println("[CreateShipment][NinjaVan] Failed to decode", err)
		return nil, shipment.ToShipmentError(e.NewFormatted(ctx, e.InternalServer, m.FailedToCallAPI, err.Error()))
	}

	return &entities.Shipment{
		CourierTrackingID: &response.TrackingNumber,
		LabelMetadata:     res,
	}, nil
}

func (i *ninjaVanImpl) CancelShipment(ctx context.Context, req *entities.Shipment, sellerCourier *entities.SellerCourier) (*entities.Shipment, *shipment.Error) {
	if req.CourierTrackingID == nil || *req.CourierTrackingID == "" {
		return nil, shipment.ToShipmentError(e.NewFormatted(ctx, e.OtherShipmentError, m.VariableEmpty, "CourierTrackingID"))
	}

	path := fmt.Sprintf(ApiPathCancelOrder, *req.CourierTrackingID)
	request := CancelShipmentRequest{}

	res, err := i.requestWithRetry(http.MethodDelete, path, request)
	if err != nil {
		log.Println("[CreateShipment][NinjaVan] Failed to call API", err)
		return nil, shipment.ToShipmentError(e.NewFormatted(ctx, e.InternalServer, m.FailedToCallAPI, err.Error()))
	}

	response := CancelShipmentResponse{}
	if err = mapstructure.Decode(res, &response); err != nil {
		log.Println("[CancelShipment][NinjaVan] Failed to decode", err)
		return nil, shipment.ToShipmentError(e.NewFormatted(ctx, e.InternalServer, m.FailedToCallAPI, err.Error()))
	}

	return req, nil
}

/*- Shipment ---------------------------------------------------------------------------------------------------------*/

/*- Label ------------------------------------------------------------------------------------------------------------*/

func (i *ninjaVanImpl) TransformShipmentEntityToShipmentLabel(ctx context.Context, shipment *entities.Shipment, language string) (*pdfpb.ShipmentLabel, error) {
	panic("implement me")
}

/*- Label ------------------------------------------------------------------------------------------------------------*/

/*- Private function -------------------------------------------------------------------------------------------------*/

func (i *ninjaVanImpl) login() bool {
	clientID := i.config.GetShipmentProviderConfig().NinjaVan.ApiClientID
	clientKey := i.config.GetShipmentProviderConfig().NinjaVan.ApiClientKey
	params := map[string]string{
		"client_id":     clientID,
		"client_secret": clientKey,
		"grant_type":    "client_credentials",
	}
	payload, err := json.Marshal(params)
	if err != nil {
		log.Println("[NinjaVan][login] Failed to encode", err)
		return false
	}

	res, err := i.doRequest(http.MethodPost, ApiPathAccessToken, payload, map[string]string{
		"Content-Type": "application/json",
	})
	if err != nil {
		log.Println("[NinjaVan][login] Failed to login", err)
		return false
	}

	loginResponse := LoginResponse{}
	if err := mapstructure.Decode(res, &loginResponse); err != nil {
		log.Println("[NinjaVan][login] Failed to decode", err)
		return false
	}
	accessToken = loginResponse.AccessToken
	return true
}

func (i *ninjaVanImpl) requestWithRetry(method, path string, params interface{}) (map[string]interface{}, error) {
	res, status, err := i.request(method, path, params)
	if err != nil {
		return res, err
	}

	if status == http.StatusUnauthorized {
		if i.login() {
			fmt.Println("[NinjaVan][requestWithRetry] Start try again")
			res, _, err = i.request(method, path, params)
			if err != nil {
				return res, err
			}
		}
	}

	errorResponse := ErrorResponse{}
	if err := mapstructure.Decode(res, &errorResponse); err != nil {
		fmt.Println("[NinjaVan][requestWithRetry] Failed to decode", err)
		return res, err
	}
	return res, err
}

func (i *ninjaVanImpl) request(method, path string, params interface{}) (map[string]interface{}, int, error) {
	endpoint := i.config.GetShipmentProviderConfig().NinjaVan.ApiUrl + path
	payload, err := json.Marshal(params)
	if err != nil {
		return nil, 0, err
	}

	fmt.Println("[NinjaVan] Endpoint:", endpoint)
	fmt.Println("[NinjaVan] Request:", string(payload))

	res, err := i.doRequest(method, endpoint, payload, map[string]string{
		"Content-Type":  "application/json",
		"Authorization": "Bearer " + accessToken,
	})

	fmt.Println("[NinjaVan] Response:", string(res))

	data := map[string]interface{}{}
	if err := json.Unmarshal(res, &data); err != nil {
		return nil, 0, err
	}
	return data, status, nil
}

func (i *ninjaVanImpl) doRequest(method, endpoint string, payload []byte, headers map[string]string) (*http.Response, error) {
	req, err := http.NewRequest(method, endpoint, bytes.NewBuffer(payload))
	if err != nil {
		return nil, err
	}

	for k, v := range headers {
		req.Header.Add(k, v)
	}

	client := &http.Client{}
	res, err := client.Do(req)
	if err != nil {
		return nil, 0, err
	}
	defer func() {
		_ = res.Body.Close()
	}()

	result, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, 0, err
	}

	return result, res.StatusCode, err
}

/*- Private function -------------------------------------------------------------------------------------------------*/

/*- Not implemented --------------------------------------------------------------------------------------------------*/

func (i *ninjaVanImpl) CreateShipments(ctx context.Context, reqs []*entities.Shipment, sellerCourier *entities.SellerCourier) ([]*entities.Shipment, []*shipment.CreateShipmentError) {
	panic("implement me")
}

func (i *ninjaVanImpl) UpdateShipment(ctx context.Context, req *entities.Shipment, sellerCourier *entities.SellerCourier) (*entities.Shipment, *shipment.Error) {
	panic("implement me")
}

func (i *ninjaVanImpl) GenerateLinkAccountURL(ctx context.Context, workspaceID string) (string, error) {
	return "", errors.New("courier not support")
}

func (i *ninjaVanImpl) GetAccountAssociateStatus(ctx context.Context, mchId string) (bool, error) {
	return false, errors.New("courier not support")
}

func (i *ninjaVanImpl) GetShipmentLabelUrl(ctx context.Context, req *entities.Shipment, sellerCourier *entities.SellerCourier) (string, error) {
	panic("implement me")
}

func (i *ninjaVanImpl) PrintLabel(ctx context.Context, req *entities.Shipment, sellerCourier *entities.SellerCourier) (io.Reader, error) {
	panic("implement me")
}

func (i *ninjaVanImpl) GetRawShipmentRoutes(ctx context.Context, courierTrackingID string) (interface{}, error) {
	panic("implement me")
}

func (i *ninjaVanImpl) CheckRequestParam(data map[string]interface{}) bool {
	panic("implement me")
}

func (i *ninjaVanImpl) NotifyCourierPickup(ctx context.Context, req *entities.PickupPoint, sellerCourier *entities.SellerCourier, estimateParcelNumber int) (*shipment.NotifyCourierResponseData, error) {
	return nil, errors.New("not implement")
}

func (i *ninjaVanImpl) SetCourierWebhook(ctx context.Context, req *shipment.SetCourierWebhookParams, info *dto.CourierAuthenticationInfo) error {
	panic("implement me")
}

/*- Not implemented --------------------------------------------------------------------------------------------------*/
