package service

import (
	"bytes"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"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/pdf/config"
	"github.com/UpmeshLTD/urus-aio/pdf/server/grpc/pb"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/credentials"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/aws/aws-sdk-go/service/s3/s3manager"
	"github.com/boombuler/barcode"
	"github.com/boombuler/barcode/code128"
	"github.com/boombuler/barcode/qr"
	"github.com/google/uuid"
	"html/template"
	"image/png"
	"io/ioutil"
	"log"
	"math"
	"os"
	"reflect"
	"sync"
	"time"

	"github.com/SebastiaanKlippert/go-wkhtmltopdf"
)

const (
	MaxColumns        = 3
	MaxItemsPerColumn = 8
)

//go:generate mockery --name=Service --case=snake
type Service interface {
	Ping(ctx context.Context, req *pb.PingRequest) (*pb.PingResponse, error)
	Generate(ctx context.Context, req *pb.GenerateRequest) (*pb.GenerateResponse, error)
}

type serviceImpl struct {
	config config.Config
}

func NewService(config config.Config) Service {
	return &serviceImpl{
		config: config,
	}
}

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) Generate(ctx context.Context, req *pb.GenerateRequest) (*pb.GenerateResponse, error) {
	var htmlFiles []string
	failedShipments := make([]*pb.FailedShipment, 0)
	parseFiles := map[string]*template.Template{}
	templatePath := s.config.GetPDFConfig().TemplatePath

	if req.Language == "" || !utils.InArray(req.Language, []string{
		entities.LanguageEN,
		entities.LanguageTH,
		entities.LanguageCN,
		entities.LanguageMY,
	}) {
		req.Language = entities.LanguageEN
	}

	// Init template files courier mapping
	templateFiles := map[string]string{
		entities.CourierCodeJTExpressTH:    templatePath + "/jnt_non_cod.html",
		entities.CourierCodeJTExpressMY:    templatePath + "/jnt_my_non_cod.html",
		entities.CourierCodeJTExpressSG:    templatePath + "/jnt_sg.html",
		entities.CourierCodeFlashExpressTH: templatePath + "/flash_express.html",
		entities.CourierCodeFlashExpressPH: templatePath + "/flash_express.html",
		entities.CourierCodeKerryExpressTH: templatePath + "/kerry_express_th.html",
	}

	// Init template parse files
	for courierCode, templateFile := range templateFiles {
		t, err := template.ParseFiles(templateFile)
		if err != nil {
			log.Printf("[GRPC_PDF_Generate] failed to init template parse files - courier code: %s - template: %s - error: `%s`\n", courierCode, templateFile, err.Error())
			continue
		}
		parseFiles[courierCode] = t
	}

	// Generate html files from the shipment label data
	total := len(req.ShipmentLabels)
	files := make([]string, total)
	wg := sync.WaitGroup{}
	wg.Add(total)
	for i, label := range req.ShipmentLabels {
		i := i
		label := label
		func() {
			defer wg.Done()

			// Check parse files already init
			if _, ok := parseFiles[label.CourierCode]; !ok {
				log.Printf("[GRPC_PDF_Generate] courier not support to generate pdf: %s\n", label.CourierCode)
				failedShipments = append(failedShipments, &pb.FailedShipment{
					ID:    label.ID,
					Error: fmt.Sprintf("courier not support to generate pdf - shipment: %s - courier code: %s", label.ID, label.CourierCode),
				})
				return
			}

			// Get template data
			data, err := s.transformShipmentLabelToTemplateData(req, label)
			if err != nil {
				log.Printf("[GRPC_PDF_Generate] failed to transform shipment label to template data - shipment: %s - error: `%s`\n", label.ID, err.Error())
				failedShipments = append(failedShipments, &pb.FailedShipment{
					ID:    label.ID,
					Error: fmt.Sprintf("failed to transform shipment label to template data: %s", err.Error()),
				})
				return
			}

			data["Shipment"] = label
			data["Language"] = req.Language
			data["Translated"] = s.getTextMapping(req.Language)
			if data["Barcode"] == nil {
				// Generate barcode 128
				barcodeBase64, err := s.generateBarcodeBase64(label.TrackingNumber, 640, 100)
				if err != nil {
					log.Printf("[GRPC_PDF_Generate] failed to generate barcode - shipment: %s - error: `%s`\n", label.ID, err.Error())
					return
				}
				data["Barcode"] = template.URL(barcodeBase64)
			}

			// Generate html content from template file
			buf := new(bytes.Buffer)
			if err := parseFiles[label.CourierCode].Execute(buf, data); err != nil {
				log.Printf("[GRPC_PDF_Generate] failed to execute template - shipment: %s - error: `%s`\n", label.ID, err.Error())
				failedShipments = append(failedShipments, &pb.FailedShipment{
					ID:    label.ID,
					Error: fmt.Sprintf("failed to exec template: %s", err.Error()),
				})
				return
			}

			// Create temp file
			tempFile, err := ioutil.TempFile(s.config.GetPDFConfig().TempDir, "label-*.html")
			if err != nil {
				log.Printf("[GRPC_PDF_Generate] failed to create temp file - shipment: %s - error: `%s`\n", label.ID, err.Error())
				failedShipments = append(failedShipments, &pb.FailedShipment{
					ID:    label.ID,
					Error: fmt.Sprintf("failed to create temp file: %s", err.Error()),
				})
				return
			}

			// Write html content into temp file
			if _, err := tempFile.Write(buf.Bytes()); err != nil {
				log.Printf("[GRPC_PDF_Generate] failed to write temp file: %s - shipment: %s - error: `%s`\n", tempFile.Name(), label.ID, err.Error())
				failedShipments = append(failedShipments, &pb.FailedShipment{
					ID:    label.ID,
					Error: fmt.Sprintf("failed to write file: %s", err.Error()),
				})
				return
			}

			// Close temp file
			if err := tempFile.Close(); err != nil {
				log.Printf("[GRPC_PDF_Generate] failed to close temp file: %s - shipment: %s - error: `%s`\n", tempFile.Name(), label.ID, err.Error())
			}

			// Keep files index as original
			files[i] = tempFile.Name()

			log.Printf("[GRPC_PDF_Generate] created temp file: %s - shipment: %s\n", tempFile.Name(), label.ID)
		}()
	}
	wg.Wait()

	// Filter empty path
	for _, path := range files {
		if path != "" {
			htmlFiles = append(htmlFiles, path)
		}
	}

	if len(htmlFiles) == 0 {
		log.Printf("[GRPC_PDF_Generate] no html files to generate - failed shipments: %d\n", len(failedShipments))
		return &pb.GenerateResponse{
			Error:           "no html files to generate",
			FailedShipments: failedShipments,
		}, nil
	}

	// Merge multiple html files into one pdf
	pdf, err := s.generatePDF(htmlFiles, req.Title)
	if err != nil {
		log.Printf("[GRPC_PDF_Generate] failed to generate pdf file - error: `%s`\n", err.Error())
		return &pb.GenerateResponse{
			Error: err.Error(),
		}, nil
	}

	// Upload pdf file to S3log.Printf("[GRPC_PDF_Generate] generated pdf file: %s\n", url)
	pdfFileName := uuid.NewString() + ".pdf"
	log.Printf("[GRPC_PDF_Generate] uploading file: %s\n", pdfFileName)
	url, err := s.uploadPdfToS3(pdfFileName, pdf)
	errMsg := ""
	if err != nil {
		errMsg = err.Error()
		log.Printf("[GRPC_PDF_Generate] failed to upload file to S3: %s - error: `%s`\n", pdfFileName, errMsg)
	}

	log.Printf("[GRPC_PDF_Generate] uploaded file: %s - %s\n", pdfFileName, url)

	// Clear temp files
	s.removeTempFiles(htmlFiles)

	return &pb.GenerateResponse{
		URL:             url,
		Error:           errMsg,
		FailedShipments: failedShipments,
	}, nil
}

// https://github.com/SebastiaanKlippert/go-wkhtmltopdf
func (s *serviceImpl) generatePDF(htmlFiles []string, pdfTitle string) ([]byte, error) {
	wkhtmltopdf.SetPath(s.config.GetPDFConfig().BinPath)
	pdfGenerator, err := wkhtmltopdf.NewPDFGenerator()
	if err != nil {
		return nil, err
	}

	// Set global options
	pdfGenerator.Title.Set(pdfTitle)
	pdfGenerator.Dpi.Set(96)
	pdfGenerator.PageWidth.Set(100)
	pdfGenerator.PageHeight.Set(180)
	pdfGenerator.MarginTop.Set(0)
	pdfGenerator.MarginBottom.Set(0)
	pdfGenerator.MarginLeft.Set(0)
	pdfGenerator.MarginRight.Set(0)
	pdfGenerator.Orientation.Set(wkhtmltopdf.OrientationPortrait)

	for _, path := range htmlFiles {
		pdfGenerator.AddPage(wkhtmltopdf.NewPage(path))
	}

	// Create PDF document in internal buffer
	if err := pdfGenerator.Create(); err != nil {
		return nil, err
	}

	return pdfGenerator.Bytes(), nil
}

func (s *serviceImpl) uploadPdfToS3(name string, b []byte) (string, error) {
	awsKey := s.config.GetPDFConfig().AwsKey
	awsSecret := s.config.GetPDFConfig().AwsSecret
	filePath := fmt.Sprintf("%s/%s", s.config.GetPDFConfig().AwsS3Folder, name)
	awsConfig := &aws.Config{
		Region: aws.String(s.config.GetPDFConfig().AwsRegion),
	}

	if awsKey != "" && awsSecret != "" {
		awsConfig.Credentials = credentials.NewStaticCredentials(awsKey, awsSecret, "")
	}

	awsSession := session.Must(session.NewSession(awsConfig))
	uploader := s3manager.NewUploader(awsSession)
	_, err := uploader.Upload(&s3manager.UploadInput{
		Key:    aws.String(filePath),
		Bucket: aws.String(s.config.GetPDFConfig().AwsS3Bucket),
		ACL:    aws.String("private"),
		Body:   bytes.NewReader(b),
	})

	if err != nil {
		log.Println(err)
		return "", err
	}

	awsClient := s3.New(awsSession)
	req, _ := awsClient.GetObjectRequest(&s3.GetObjectInput{
		Key:                        aws.String(filePath),
		Bucket:                     aws.String(s.config.GetPDFConfig().AwsS3Bucket),
		ResponseContentType:        aws.String("application/pdf"),
		ResponseContentDisposition: aws.String("inline"),
	})

	ttl := time.Duration(s.config.GetPDFConfig().AwsS3PresignedTTL) * time.Minute
	url, err := req.Presign(ttl)
	if err != nil {
		return "", fmt.Errorf("failed to presign GetObjectRequest for key %q: %v", filePath, err)
	}

	return url, nil
}

func (s *serviceImpl) removeTempFiles(htmlFiles []string) {
	log.Printf("[GRPC_PDF_Generate] removing %d temp files\n", len(htmlFiles))
	wg := sync.WaitGroup{}
	wg.Add(len(htmlFiles))
	for _, path := range htmlFiles {
		path := path
		go func() {
			defer wg.Done()
			if path != "" {
				if err := os.Remove(path); err != nil {
					log.Printf("[GRPC_PDF_Generate] failed to remove temp file: %s\n", path)
				} else {
					log.Printf("[GRPC_PDF_Generate] removed temp file: %s\n", path)
				}
			}
		}()
	}
	wg.Wait()
}

// GenerateBarcodeBase64 https://github.com/boombuler/barcode
func (s *serviceImpl) generateBarcodeBase64(str string, width int, height int) (string, error) {
	encodedBarcode, err := code128.Encode(str)
	if err != nil {
		return "", err
	}

	resizedBarcode, err := barcode.Scale(encodedBarcode, width, height)
	if err != nil {
		return "", err
	}

	// encode the barcode as png
	var buf bytes.Buffer
	if err := png.Encode(&buf, resizedBarcode); err != nil {
		return "", err
	}

	pngBytes, err := ioutil.ReadAll(&buf)
	if err != nil {
		return "", err
	}

	return "data:image/png;base64," + base64.StdEncoding.EncodeToString(pngBytes), nil
}

func (s *serviceImpl) generateQRCodeBase64(str string, width int, height int) (string, error) {
	encodedBarcode, err := qr.Encode(str, qr.M, qr.Auto)
	if err != nil {
		return "", err
	}

	resizedBarcode, err := barcode.Scale(encodedBarcode, width, height)
	if err != nil {
		return "", err
	}

	// encode the barcode as png
	var buf bytes.Buffer
	if err := png.Encode(&buf, resizedBarcode); err != nil {
		return "", err
	}

	pngBytes, err := ioutil.ReadAll(&buf)
	if err != nil {
		return "", err
	}

	return "data:image/png;base64," + base64.StdEncoding.EncodeToString(pngBytes), nil
}

func (s *serviceImpl) getTextMapping(lang string) map[string]interface{} {
	if lang == "" {
		lang = "en"
	}

	if data, ok := localizedText[lang]; ok {
		return data
	}

	return localizedText["en"]
}

func (s *serviceImpl) splitToChunks(slice interface{}, chunkSize int) interface{} {
	sliceType := reflect.TypeOf(slice)
	sliceVal := reflect.ValueOf(slice)
	length := sliceVal.Len()
	if sliceType.Kind() != reflect.Slice {
		panic("parameter must be []T")
	}
	n := 0
	if length%chunkSize > 0 {
		n = 1
	}
	SST := reflect.MakeSlice(reflect.SliceOf(sliceType), 0, length/chunkSize+n)
	st, ed := 0, 0
	for st < length {
		ed = st + chunkSize
		if ed > length {
			ed = length
		}
		SST = reflect.Append(SST, sliceVal.Slice(st, ed))
		st = ed
	}
	return SST.Interface()
}

func (s *serviceImpl) transformShipmentLabelToTemplateData(req *pb.GenerateRequest, label *pb.ShipmentLabel) (map[string]interface{}, error) {
	switch label.CourierCode {
	case entities.CourierCodeJTExpressTH:
		return s.transformShipmentLabelToTemplateDataJntThai(req, label)
	case entities.CourierCodeJTExpressMY:
		return s.transformShipmentLabelToTemplateDataJntMalaysia(req, label)
	case entities.CourierCodeJTExpressSG:
		return s.transformShipmentLabelToTemplateDataJntSingapore(req, label)
	case entities.CourierCodeFlashExpressTH, entities.CourierCodeFlashExpressPH:
		return s.transformShipmentLabelToTemplateDataFlashExpress(req, label)
	case entities.CourierCodeKerryExpressTH:
		return s.transformShipmentLabelToTemplateDataKerryExpressTH(req, label)
	}
	return map[string]interface{}{}, nil
}

func (s *serviceImpl) transformShipmentLabelToTemplateDataJntThai(_ *pb.GenerateRequest, label *pb.ShipmentLabel) (map[string]interface{}, error) {
	items := label.Items
	if len(items) > MaxColumns*MaxItemsPerColumn {
		items = items[:MaxColumns*MaxItemsPerColumn]
	}

	totalItems := len(items)
	groupItems := make([]map[string]interface{}, 0)
	totalColumns := int(math.Ceil(float64(totalItems) / float64(MaxItemsPerColumn)))
	itemsPerColumn := int(math.Ceil(float64(totalItems) / float64(totalColumns)))
	for i, v := range s.splitToChunks(items, itemsPerColumn).([][]*pb.ShipmentLabelItem) {
		for _, item := range v {
			if totalColumns == 2 && len(item.Name) > 20 {
				item.Name = item.Name[0:20]
			} else if totalColumns == 3 && len(item.Name) > 10 {
				item.Name = item.Name[0:10]
			}
		}
		groupItems = append(groupItems, map[string]interface{}{
			"Start": i*itemsPerColumn + 1,
			"Items": v,
		})
	}

	return map[string]interface{}{
		"GroupItems": groupItems,
	}, nil
}

func (s *serviceImpl) transformShipmentLabelToTemplateDataJntMalaysia(_ *pb.GenerateRequest, label *pb.ShipmentLabel) (map[string]interface{}, error) {
	location, err := time.LoadLocation(enums.TimezoneMY)
	if err != nil {
		return nil, errors.New(fmt.Sprintf("failed to load location time zone - %s", err.Error()))
	}

	return map[string]interface{}{
		"CreatedDate": label.CreatedAt.AsTime().In(location).Format("2006/01/02"),
	}, nil
}

func (s *serviceImpl) transformShipmentLabelToTemplateDataJntSingapore(_ *pb.GenerateRequest, label *pb.ShipmentLabel) (map[string]interface{}, error) {
	qrcode, err := s.generateQRCodeBase64(label.TrackingNumber, 420, 420)
	if err != nil {
		log.Printf("[GRPC_PDF_Generate] failed to generate QR code - shipment: %s - error: `%s`\n", label.ID, err.Error())
		return nil, errors.New(fmt.Sprintf("failed to generate QR code - %s", err.Error()))
	}

	return map[string]interface{}{
		"QRCode": template.URL(qrcode),
	}, nil
}

func (s *serviceImpl) transformShipmentLabelToTemplateDataFlashExpress(_ *pb.GenerateRequest, label *pb.ShipmentLabel) (map[string]interface{}, error) {
	maxItems := 6
	items := make([]map[string]interface{}, 0)
	for i, v := range label.Items {
		if i < maxItems {
			items = append(items, map[string]interface{}{
				"Name":     v.Name,
				"Quantity": v.Quantity,
			})
		}
	}
	return map[string]interface{}{
		"Items":        items,
		"HasMoreItems": len(label.Items) > maxItems,
	}, nil
}

func (s *serviceImpl) transformShipmentLabelToTemplateDataKerryExpressTH(_ *pb.GenerateRequest, label *pb.ShipmentLabel) (map[string]interface{}, error) {
	qrcode, err := s.generateQRCodeBase64(label.TrackingNumber, 420, 420)
	if err != nil {
		log.Printf("[GRPC_PDF_Generate] failed to generate QR code - shipment: %s - error: `%s`\n", label.ID, err.Error())
		return nil, errors.New(fmt.Sprintf("failed to generate QR code - %s", err.Error()))
	}

	maxItems := 6
	items := make([]map[string]interface{}, 0)
	hasMore := len(label.Items) > maxItems
	max := maxItems
	if hasMore {
		max--
	}

	for i, v := range label.Items {
		if i < max {
			items = append(items, map[string]interface{}{
				"Name":     v.Name,
				"Code":     v.Code,
				"Quantity": v.Quantity,
			})
		}
	}

	return map[string]interface{}{
		"QRCode":       template.URL(qrcode),
		"Items":        items,
		"HasMoreItems": hasMore,
	}, nil
}
