package sale_order

import (
	"context"
	"fmt"
	"net/http"
	"strings"
	"time"

	SOProto "github.com/AlchemyTelcoSolutions/proto/gen/go/callisto/so/v1"
	"go.uber.org/zap"

	v1 "github.com/AlchemyTelcoSolutions/callisto-so-bff/api/v1"
	"github.com/AlchemyTelcoSolutions/callisto-so-bff/internal/domain/model"
	"github.com/AlchemyTelcoSolutions/callisto-so-bff/internal/sale_order/converter"
	soConverter "github.com/AlchemyTelcoSolutions/callisto-so-bff/internal/sale_order/converter"
)

// UpdateSaleOrder is a function that update an existing order in SO service
// And Sending Request to Callsito-api
//
// IMPORTANT!!!
// Callisto api as is using lumen framework(PHP), and  does not have a proper method to use PUT or PATCH HTTP Methods
// when use "form-data", those are mimiqued using method POST with the request paramether "_methoD: PUT" or "method:_PATCH"
// Update sale order should pass the request using post in this case
func (s *Service) UpdateSaleOrder(r *http.Request, req *v1.UpdateSaleOrderMultipartBody, orderReference string) *model.SaleOrderResponse {
	// preare path in callisto api
	configs := s.configs.GetConfigurations()
	urlUpdate := strings.Join([]string{configs.CallistoAPI.Host, configs.CallistoAPI.SaleOrder.Update}, "")
	response := &model.SaleOrderResponse{}
	updateOrderApi := &model.CallistoApiResponse{}

	// send request to callisto api and get the _bff response
	//
	// @TODO still need to define if all missing logic will be from bff or new endpoints in callisto-api used as a service
	// to complete that we should create and inject the new services(callisto-api calls)
	var err error
	var responseCode int

	// callisto api endpoint v1/sale-order and v1`/bff/sale-order for update have different structure, so for those cases
	// we are not able to map properly, for this case we should have different mappinf one for bff endpoint and another one for non bff

	// We need to pull the legacy order id from the context
	legacyOrderID := s.getApiOrderID(r)
	path := urlUpdate + "/" + legacyOrderID

	switch true {
	case strings.HasPrefix(configs.CallistoAPI.SaleOrder.Update, "/v1/bff"):
		responseCode, err = s.requestApiBFFEndpoint(r, updateOrderApi, path)
	default:
		responseCode, err = s.requestApiNoBFFEndpoint(r, updateOrderApi, path)
	}
	if err != nil {
		msg := "error proxy call to callisto-api for update order"
		s.logger.Error(msg, err, zap.String("ctx2", CALLISTO_API), zap.String("path", path))
		return defaultResponse(response, msg, http.StatusInternalServerError)
	}
	if configs.CallistoSO.Enabled && updateOrderApi.SOData != nil {
		// performing this operation after request is sent to callisto
		// because all docs should persist before read
		if err := req.Scan(r); err != nil {
			s.logger.Error("failed to scan request", err)
		}

		// if Put method exist and has the Method Put use the "PUT" logic
		switch true {
		case req.Method != nil && *req.Method == "PUT":
			err = s.updateToSOServicePut(r.Context(), updateOrderApi, req, orderReference)
		default:
			// in here we should implement the POST Method
			err = s.updateToSOServicePost(r.Context(), updateOrderApi, orderReference)
		}
		if err != nil {
			// nothing to do, only log the error if any
			// at this moment was decided to continue the execution even if Callisto SO returns with error
			msg := fmt.Sprintf("error sending update to callisto sale order service %s", err.Error())
			s.logger.Info(msg, zap.String("ctx2", CALLISTO_SO_GRPC))
		}
	}

	response.Code = responseCode
	response.Response = updateOrderApi
	return response
}

// requestApiNoBFFEndpoint request for callisto Api original update method
func (s *Service) requestApiNoBFFEndpoint(r *http.Request, updateOrderApi *model.CallistoApiResponse, path string) (int, error) {
	var updateOrderMap any
	responseCode, err := s.sendRequestToAPI(r, path, &updateOrderMap)
	if err != nil {
		return 0, err
	}
	updateOrderApi.Result = updateOrderMap
	return responseCode, nil
}

// requestApiBFFEndpoint request for callisto Api bff update method
func (s *Service) requestApiBFFEndpoint(r *http.Request, updateOrderApi *model.CallistoApiResponse, path string) (int, error) {
	responseCode, err := s.sendRequestToAPI(r, path, updateOrderApi)
	if err != nil {
		return 0, err
	}
	return responseCode, nil
}

// updateToSOService is a legacy function to add note/upload document/change incoterms/add shipment information
// everything happening in the same endpoint, is expected this will be splited in the future
// at that moment we should map independently the struct of the body request for each independent function
func (s *Service) updateToSOServicePut(ctx context.Context, apiUpdate *model.CallistoApiResponse, req *v1.UpdateSaleOrderMultipartBody, orderReference string) error {
	bffData, err := soConverter.ToStructFromCallistoApiClientUpdate(apiUpdate)
	if err != nil {
		s.logger.Error("not able to get bff data", err, zap.String("ctx2", CALLISTO_SO_GRPC))
		return err
	}
	// add note to order
	bffData.OrderRef = orderReference
	if req.Note != nil {
		addNoteReq := &model.UpdateNoteData{
			CreatedAt:      time.Now(),
			OrderReference: orderReference,
			UserID:         bffData.UserID,
			Note:           *req.Note,
		}
		if err = s.AddNoteToOrder(ctx, addNoteReq); err != nil {
			msg := fmt.Sprintf("error sending update to callisto sale order service %s", err.Error())
			s.logger.Info(msg, zap.String("ctx2", CALLISTO_SO_GRPC))
			return err
		}
	}

	// update incoterms
	// TODO to be implemented, define contract and method in SO service

	// shipments
	if bffData.Shipment.TrackingUrl != "" {
		shipmentData, err := converter.ToAddShipmentFromReq(req)
		if err != nil {
			msg := fmt.Sprintf("error parsing data from request %s", err.Error())
			s.logger.Info(msg, zap.String("ctx2", CALLISTO_SO_GRPC))
			return err
		}
		shipmentData.OrderReference = orderReference
		shipmentData.UserID = bffData.UserID
		shipment, err := s.AddShipmentToOrder(ctx, shipmentData)
		if err != nil {
			msg := fmt.Sprintf("error sending shipping to callisto sale order service %s", err.Error())
			s.logger.Info(msg, zap.String("ctx2", CALLISTO_SO_GRPC))
			return err
		}
		s.logger.Info(fmt.Sprintf("shipment created with shipment id: %s", shipment), zap.String("ctx2", CALLISTO_SO_GRPC))
	}

	// document
	if bffData.Document.Url != "" {
		documentData := &model.AddDocumentData{
			OrderReference: orderReference,
			CreatedBy:      bffData.UserID,
			DocumentType:   int32(*req.DocTypeID),
			Url:            bffData.Document.Url,
			Hash:           bffData.Document.Hash,
		}
		if err = s.AddDocumentToOrder(ctx, documentData); err != nil {
			msg := fmt.Sprintf("error sending document to callisto sale order service %s", err.Error())
			s.logger.Info(msg, zap.String("ctx2", CALLISTO_SO_GRPC))
			return err
		}
	}
	return nil
}

// AddNoteToOrder is a function to add note to one order
func (s *Service) AddNoteToOrder(ctx context.Context, req *model.UpdateNoteData) error {
	noteReq := converter.ToProtoFromNoteRequest(req)
	// Response is only a boolean value, so, if not error we assume the update is correct
	_, err := s.callistoSOClient.AddNote(ctx, noteReq)
	if err != nil {
		s.logger.Error(
			"error adding note to sale order service",
			err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
		)
		return err
	}
	return nil
}

// AddShipmentToOrder is a function to add shipment infornation to one order
func (s *Service) AddShipmentToOrder(ctx context.Context, req *model.AddShipmentData) (*SOProto.AddShipmentResponse, error) {
	shipment := converter.ToProtoFromShipmentRequest(req)
	// Response is only a boolean value, so, if not error we assume the update is correct
	response, err := s.callistoSOClient.AddShipment(ctx, shipment)
	if err != nil {
		s.logger.Error(
			"error adding shipment to sale order service",
			err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
		)
		return nil, err
	}
	return response, nil
}

// AddDocumentToOrder is a function to add document to one order
func (s *Service) AddDocumentToOrder(ctx context.Context, req *model.AddDocumentData) error {
	document := converter.ToAddDocumentToProto(req)
	// Response is only a boolean value, so, if not error we assume the update is correct
	_, err := s.callistoSOClient.AddDocument(ctx, document)
	if err != nil {
		s.logger.Error(
			"error adding document to sale order service",
			err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
		)
		return err
	}
	return nil
}

// UpdateOrderNetsuite is a function that updates the order with data from Netsuite
func (s *Service) updateToSOServicePost(ctx context.Context, apiUpdate *model.CallistoApiResponse, orderRef string) error {
	bffData, err := soConverter.ToStructFromCallistoApiClientUpdate(apiUpdate)
	if err != nil {
		s.logger.Error("not able to get bff data", err, zap.String("ctx2", CALLISTO_SO_GRPC))
		return err
	}

	invoicesReq, err := s.callistoSOClient.GetInvoices(ctx, &SOProto.GetInvoicesRequest{OrderRef: orderRef})
	if err != nil {
		s.logger.Error(
			fmt.Sprintf("error getting invoice by order ref %s", orderRef),
			err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
		)
		return err
	}

	if len(invoicesReq.Invoices) < 1 {
		err = fmt.Errorf("empty invoices array")
		s.logger.Error(
			fmt.Sprintf("no invoice found for order ref %s", orderRef),
			err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
		)
		return err
	}

	invoiceID := invoicesReq.Invoices[len(invoicesReq.Invoices)-1].Id
	documentTypeID := SOProto.Doctype_value[bffData.Document.DocumentType]

	addDocumentReq := &model.AddDocumentData{
		OrderReference: orderRef,
		CreatedBy:      bffData.UserID,
		DocumentType:   documentTypeID,
		Url:            bffData.Document.Url,
		Hash:           bffData.Document.Hash,
		InvoiceId:      int64(invoiceID),
	}

	if err = s.AddDocumentToOrder(ctx, addDocumentReq); err != nil {
		s.logger.Error(
			fmt.Sprintf("error adding document to order ref %s", orderRef),
			err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
		)
		return err
	}

	if documentTypeID == int32(SOProto.Doctype_DOCTYPE_INVOICE_PROFORMA) {
		updateStatusReq := &SOProto.UpdateStatusRequest{
			OrderRef:  orderRef,
			Status:    SOProto.Status_STATUS_AWAITING_INVOICE,
			CreatedBy: uint64(bffData.UserID),
		}

		if _, err = s.callistoSOClient.UpdateStatus(ctx, updateStatusReq); err != nil {
			s.logger.Error(
				fmt.Sprintf("error updating status of order %s", orderRef),
				err,
				zap.String("ctx2", CALLISTO_SO_GRPC),
			)
			return err
		}
	}

	return nil
}
