package sale_order

import (
	"bytes"
	"context"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

	"go.uber.org/zap"

	SOProto "github.com/AlchemyTelcoSolutions/proto/gen/go/callisto/so/v1"
	"github.com/AlchemyTelcoSolutions/xutils-go/xlogger"

	"github.com/AlchemyTelcoSolutions/callisto-so-bff/cmd/app/config"
	"github.com/AlchemyTelcoSolutions/callisto-so-bff/internal/auth"
	"github.com/AlchemyTelcoSolutions/callisto-so-bff/internal/domain/model"
	"github.com/AlchemyTelcoSolutions/callisto-so-bff/internal/proxy"
	soConverter "github.com/AlchemyTelcoSolutions/callisto-so-bff/internal/sale_order/converter"
)

const (
	CALLISTO_API     = "callisto api"
	CALLISTO_SO_GRPC = "callisto so grpc"

	ErrorNoShipmentFound = "no shipments data found for this order"
	ErrorNoASNFound      = "no ASN data found for this shipment"
)

// NewSaleOrderService return a nes Sale Order Service
func NewSaleOrderService() *Service {
	return &Service{}
}

// SetLogger is a setter for logger
func (s *Service) SetLogger(l xlogger.Logger) *Service {
	s.logger = l
	return s
}

// SetConfigs is a setter for Configs
func (s *Service) SetConfigs(c config.AppConfig) *Service {
	s.configs = c
	return s
}

// SetProxyService SetCallistoAPiClient is a setter for callisto api client
func (s *Service) SetProxyService(h proxy.ProxyService) *Service {
	s.httpProxySvc = h
	return s
}

// SetAuthService is a setter for auth service
func (s *Service) SetAuthService(a auth.AuthService) *Service {
	s.authService = a
	return s
}

// SetCallistoSOGRPC for Callisto SO service client GRPC
func (s *Service) SetCallistoSOGRPC(c SOServiceClient) *Service {
	s.callistoSOClient = c
	return s
}

// GetSOSummary is a function that get sale order summary to callisto-so
func (s *Service) GetSOSummary(req *http.Request) *model.Response[model.SaleOrderSummaryResponse] {
	// get auth me information
	authResp, err := s.authService.AuthMeByRequest(req)
	if err != nil {
		return defaultErrorResponse[model.SaleOrderSummaryResponse](nil, "unauthorized", http.StatusUnauthorized)
	}

	// get summary to callisto-so
	summaryResp, err := s.callistoSOClient.GetSummary(req.Context(), &SOProto.GetSummaryRequest{
		BuyerCompanyId: authResp.CompanyId,
	})
	if err != nil {
		return defaultErrorResponse[model.SaleOrderSummaryResponse](nil, "failed get summary to callisto-so", http.StatusInternalServerError)
	}

	// convert proto response to model response
	result, err := soConverter.FromProtoToSummaryResponse(summaryResp)
	if err != nil {
		return defaultErrorResponse[model.SaleOrderSummaryResponse](nil, "failed to convert response", http.StatusInternalServerError)
	}

	return &model.Response[model.SaleOrderSummaryResponse]{
		Code:    http.StatusOK,
		Success: true,
		Result:  result,
	}
}

// sendOrderToSOService send a grpc call to SO service if data is correct and available to be sent
func (s *Service) sendOrderToSOService(ctx context.Context, orderApi *model.CallistoApiResponse) error {
	configs := s.configs.GetConfigurations()
	if orderApi.SOData != nil && configs.CallistoSO.Enabled {
		callistoSORequest, err := soConverter.ToProtoFromCallistoApiClientCreate(orderApi)
		if err != nil {
			return fmt.Errorf("not able to convert _bff response from api into proto message: %w", err)
		}
		soResponse, err := s.callistoSOClient.CreateSaleOrder(ctx, callistoSORequest)
		if err != nil {
			return fmt.Errorf("error creating order in sale order service: %w", err)
		}
		// log the response of callisto so if this is success
		s.logger.Info(
			fmt.Sprintf("message successfully sent to callisto so service, response: %v", soResponse),
			zap.String("ctx2", CALLISTO_SO_GRPC),
		)
	} else {
		s.logger.Info("so service disabled, not sending request", zap.String("ctx2", CALLISTO_SO_GRPC))
	}

	return nil
}

// sendRequestToAPI makes a requests to API
func (s *Service) sendRequestToAPI(r *http.Request, path string, response interface{}) (int, error) {
	// in here we define new headers if required
	extraHeaders := map[string]string{}
	resp, err := s.httpProxySvc.SendRequestToClient(r, path, extraHeaders)
	if err != nil {
		return 0, fmt.Errorf("error sending request to callisto api,  %v", err)
	}
	// try to unmarshall the reponse from callisto-api bff endpoint
	if err := json.Unmarshal(resp.BodyBytes, &response); err != nil {
		return resp.StatusCode, fmt.Errorf("%s", string(resp.BodyBytes[:]))
	}

	return resp.StatusCode, nil
}

var (
	supportedExportedFileASN = map[string]bool{
		"csv": true,
	}
)

// ExportASNToFile is a function that create a file for ASN data which coming from SO service.
// The file format could be driven by the passing format value from API `fileFormat`, but only supported for certain format for now.
func (s *Service) ExportASNToFile(r *http.Request, orderReference string, fileFormat string) *model.SaleOrderExportASNResponse {
	ctx := r.Context()

	fileFormat = strings.ToLower(fileFormat)

	s.logger.Info(
		fmt.Sprintf("exporting file for ASN Data of %s, in format %s", orderReference, fileFormat),
		zap.String("ctx2", CALLISTO_SO_GRPC),
	)

	if !supportedExportedFileASN[fileFormat] {
		// just adding another layer of checking for the supported format here,
		// but this must be handled on handler layer already
		return errorResponseExportASNFile(http.StatusUnsupportedMediaType, "file format is not supported")
	}

	// get auth me information
	authResp, err := s.authService.AuthMeByRequest(r)
	if err != nil {
		return errorResponseExportASNFile(http.StatusUnauthorized, "unauthorized")
	}

	// check if the user has access to this order
	if err := s.checkOrderAffiliation(ctx, orderReference, authResp); err != nil {
		return errorResponseExportASNFile(http.StatusForbidden, "forbidden")
	}

	// Get Shipment for so_reference, we need its id
	shipments, err := s.getShipmentsByOrderReference(ctx, orderReference)
	if err != nil {
		return errorResponseExportASNFile(http.StatusInternalServerError, "failed to get shipments data")
	}

	if len(shipments) == 0 {
		s.logger.Error("no shipments data found for this order", err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
			zap.String("order_reference", orderReference),
		)
		return errorResponseExportASNFile(http.StatusNotFound, ErrorNoShipmentFound)
	}

	collectionASNData := []model.SaleOrderASNData{}

	for _, v := range shipments {
		// Get ASN for shipment id Y, then populate into collection data
		shipmentID := v.ID
		asnData, err := s.getAsnData(ctx, shipmentID)
		if err != nil {
			// should we continue with no error?
			return errorResponseExportASNFile(http.StatusInternalServerError, "failed to get ASN data")
		}

		if len(asnData) == 0 {
			// should we continue with no error?
			s.logger.Error("no ASN data found for this shipment", err,
				zap.String("ctx2", CALLISTO_SO_GRPC),
				zap.Uint64("shipment_id", shipmentID),
			)
			return errorResponseExportASNFile(http.StatusNotFound, ErrorNoASNFound)
		}

		collectionASNData = append(collectionASNData, asnData...)
	}

	response := s.generateASNFile(ctx, collectionASNData, fileFormat, orderReference)

	s.logger.Info(
		fmt.Sprintf("DONE exporting file for ASN Data of %s, in format %s", orderReference, fileFormat),
		zap.String("ctx2", CALLISTO_SO_GRPC),
	)

	return response
}

// getShipmentsByOrderReference a function to get shipments data for specific order reference `orderReference`
// and the data we need for this only its ID for now.
func (s *Service) getShipmentsByOrderReference(ctx context.Context, orderReference string) ([]model.SaleOrderShipmentData, error) {
	callistoSORequest := soConverter.ToProtoGetShipments(orderReference)

	soResponse, err := s.callistoSOClient.GetShipments(ctx, callistoSORequest)
	if err != nil {
		s.logger.Error(fmt.Sprintf("failed to get shipments data: %s", err.Error()), err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
			zap.String("order_ref", orderReference),
		)
		return nil, fmt.Errorf("error get shipments data from sale order service: %w", err)
	}

	shipments := make([]model.SaleOrderShipmentData, len(soResponse.Shipments))

	for i, v := range soResponse.Shipments {
		shipments[i] = model.SaleOrderShipmentData{
			ID: v.ShipmentId,
		}
	}

	return shipments, nil
}

func (s *Service) checkOrderAffiliation(ctx context.Context, orderReference string, authResp *model.AuthMeResponse) error {
	// get auth me information
	if authResp.CompanyId == 0 {
		return fmt.Errorf("forbidden, missing company id in auth response")
	}

	resp, err := s.callistoSOClient.CheckOrderAffiliation(ctx, &SOProto.CheckOrderAffiliationRequest{
		Reference:      orderReference,
		BuyerCompanyId: authResp.CompanyId,
	})

	if err != nil {
		s.logger.Error("failed to check order affiliation", err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
			zap.String("order_reference", orderReference),
			zap.Uint64("company_id", authResp.CompanyId),
		)
		return fmt.Errorf("error checking order affiliation: %w", err)
	}

	if !resp.BuyerAffiliated {
		err := fmt.Errorf("forbidden, order not affiliated with this company id")
		s.logger.Error("order affiliated check", err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
			zap.String("order_reference", orderReference),
			zap.Uint64("company_id", authResp.CompanyId),
		)

		return err
	}

	return nil
}

// getAsnData a function to get ASN data for specific shipment `shipmentID`
// and the ASN data we need for this only its SKU, Product Name, Serial, and IMEI
// which may be added in the future
func (s *Service) getAsnData(ctx context.Context, shipmentID uint64) ([]model.SaleOrderASNData, error) {
	callistoSORequest := soConverter.ToProtoGetAsnData(shipmentID)

	soResponse, err := s.callistoSOClient.GetAsnData(ctx, callistoSORequest)
	if err != nil {
		s.logger.Error("failed to get asn data", err,
			zap.String("ctx2", CALLISTO_SO_GRPC),
			zap.Uint64("shipment_id", shipmentID),
		)
		return nil, fmt.Errorf("error get asn data from sale order service: %w", err)
	}

	asnData := make([]model.SaleOrderASNData, len(soResponse.AsnItems))

	for i, v := range soResponse.AsnItems {
		asnData[i] = model.SaleOrderASNData{
			SKU:         v.Sku,
			ProductName: v.ProductName,
			Serial:      v.Serial,
			IMEI:        v.Imei,
		}
	}

	return asnData, nil
}

// generateASNFile a function to generate a file based on the `fileFormat` value
// and the file content will be the ASN data that passed into this function.
// Only supported for certain `fileFormat`.
func (s *Service) generateASNFile(ctx context.Context, asnData []model.SaleOrderASNData, fileFormat string, orderReference string) *model.SaleOrderExportASNResponse {
	filename := fmt.Sprintf("ASN-%s", orderReference)

	response := &model.SaleOrderExportASNResponse{}

	var err error
	var file *model.File

	switch fileFormat {
	case "csv":
		data := [][]string{
			{"sku", "product_name", "serial", "imei"},
		}

		for _, v := range asnData {
			data = append(data, []string{
				v.SKU, v.ProductName, v.Serial, v.IMEI,
			})
		}

		file, err = s.writeCSV(ctx, data, filename)
		if err != nil {
			return errorResponseExportASNFile(http.StatusInternalServerError, "failed to generate ASN file")
		}
	default:
		// just adding another layer of checking for the supported format here,
		// but this must be handled on handler layer already
		return errorResponseExportASNFile(http.StatusUnsupportedMediaType, "file format is not supported")
	}

	response.Success = true
	response.File = file

	return response
}

// writeCSV a function to generate a CSV file for the `data` that passed into this function.
// This is reusable func, just need the data and filename to be passed into this,
// then the `File` object will gives the appropriate values
// like its content stored as bytes value, its file name, and mime type
func (s *Service) writeCSV(ctx context.Context, data [][]string, filename string) (*model.File, error) {
	b := &bytes.Buffer{}
	csvWriter := csv.NewWriter(b)

	// writing CSV data
	for i, row := range data {
		if err := csvWriter.Write(row); err != nil {
			if err != nil {
				s.logger.Error("failed to write row data into csv", err,
					zap.String("filename", filename),
					zap.Int("row", i+1),
				)
				return nil, err
			}
			return nil, err
		}
	}

	csvWriter.Flush()

	if err := csvWriter.Error(); err != nil {
		if err != nil {
			s.logger.Error("failed to completely write all data into csv", err,
				zap.String("filename", filename),
			)
			return nil, err
		}
		return nil, err
	}

	return &model.File{
		Content:  b.Bytes(),
		Name:     filename,
		MIMEType: "text/csv",
	}, nil
}

// errorResponseExportASNFile is a function to generate error response
// for any error occurred during `ExportASNFile` processes
func errorResponseExportASNFile(code int, msg string) *model.SaleOrderExportASNResponse {
	return &model.SaleOrderExportASNResponse{
		Code:    code,
		Success: false,
		Error: map[string]interface{}{
			"message": msg,
		},
	}
}

// defaultResponse is the default response from service
func defaultResponse(r *model.SaleOrderResponse, msg string, code int) *model.SaleOrderResponse {
	defaultResponse := &model.CallistoApiResponse{
		Success: false,
	}
	r.Code = code
	defaultResponse.Error = map[string]interface{}{
		"message": strings.Trim(msg, `\"`),
		"data":    nil,
	}
	r.Response = defaultResponse
	return r
}

// defaultResponse is the default response from service
func defaultErrorResponse[T any](r *T, msg string, code int) *model.Response[T] {
	return &model.Response[T]{
		Code:   code,
		Result: r,
		Error: &map[string]interface{}{
			"message": strings.Trim(msg, `\"`),
		},
	}
}

func logRequestBody(r *http.Request, path string, msg string, log xlogger.Logger) {
	b, err := io.ReadAll(r.Body)
	if err != nil {
		log.Info("not able to read body from request", zap.String("ctx2", CALLISTO_API))
		return
	}

	// after success read put back the body content in the request
	r.Body = io.NopCloser(bytes.NewBuffer(b))
	body := string(b)

	// clean the body for the logs
	body = strings.ReplaceAll(body, "\t", "")
	body = strings.ReplaceAll(body, "\n", "")

	log.Info(fmt.Sprintf("%s, request: %v", msg, body), zap.String("ctx2", CALLISTO_API))
}

func getLegacyOrderID(ctx context.Context) string {
	if val, ok := ctx.Value(proxy.LegacyOrderIDCtxValue{}).(string); ok {
		return val
	}
	return ""
}
