package server

import (
	"bytes"
	"encoding/base64"
	"errors"
	"fmt"
	"image"
	"net/url"
	"strconv"

	"github.com/yeqown/go-qrcode/v2"
	"github.com/yeqown/go-qrcode/writer/standard"
	"go.uber.org/zap"

	"github.com/status-im/status-go/images"
	"github.com/status-im/status-go/multiaccounts"
)

type WriterCloserByteBuffer struct {
	*bytes.Buffer
}

func (wc WriterCloserByteBuffer) Close() error {
	return nil
}

func NewWriterCloserByteBuffer() *WriterCloserByteBuffer {
	return &WriterCloserByteBuffer{bytes.NewBuffer([]byte{})}
}

type QRConfig struct {
	DecodedQRURL    string
	WithLogo        bool
	CorrectionLevel qrcode.EncodeOption
	KeyUID          string
	ImageName       string
	Size            int
	Params          url.Values
}

func NewQRConfig(params url.Values, logger *zap.Logger) (*QRConfig, error) {
	config := &QRConfig{}
	config.Params = params
	err := config.setQrURL()

	if err != nil {
		logger.Error("[qrops-error] error in setting QRURL", zap.Error(err))
		return nil, err
	}

	config.setAllowProfileImage()
	config.setErrorCorrectionLevel()
	err = config.setSize()

	if err != nil {
		logger.Error("[qrops-error] could not convert string to int for size param ", zap.Error(err))
		return nil, err
	}

	if config.WithLogo {
		err = config.setKeyUID()

		if err != nil {
			logger.Error(err.Error())
			return nil, err
		}

		config.setImageName()
	}

	return config, nil
}

func (q *QRConfig) setQrURL() error {
	qrURL, ok := q.Params["url"]

	if !ok || len(qrURL) == 0 {
		return errors.New("[qrops-error] no qr url provided")
	}

	decodedURL, err := base64.StdEncoding.DecodeString(qrURL[0])

	if err != nil {
		return err
	}

	q.DecodedQRURL = string(decodedURL)
	return nil
}

func (q *QRConfig) setAllowProfileImage() {
	allowProfileImage, ok := q.Params["allowProfileImage"]

	if !ok || len(allowProfileImage) == 0 {
		// we default to false when this flag was not provided
		// so someone does not want to allowProfileImage on their QR Image
		// fine then :)
		q.WithLogo = false
	}

	LogoOnImage, err := strconv.ParseBool(allowProfileImage[0])

	if err != nil {
		// maybe for fun someone tries to send non-boolean values to this flag
		// we also default to false in that case
		q.WithLogo = false
	}

	// if we reach here its most probably true
	q.WithLogo = LogoOnImage
}

func (q *QRConfig) setErrorCorrectionLevel() {
	level, ok := q.Params["level"]
	if !ok || len(level) == 0 {
		// we default to MediumLevel of error correction when the level flag
		// is not passed.
		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
	}

	levelInt, err := strconv.Atoi(level[0])
	if err != nil || levelInt < 0 {
		// if there is any issue with string to int conversion
		// we still default to MediumLevel of error correction
		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
	}

	switch levelInt {
	case 1:
		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionLow)
	case 2:
		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
	case 3:
		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionQuart)
	case 4:
		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionHighest)
	default:
		q.CorrectionLevel = qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionMedium)
	}
}

func (q *QRConfig) setSize() error {
	size, ok := q.Params["size"]

	if ok {
		imageToBeResized, err := strconv.Atoi(size[0])

		if err != nil {
			return err
		}

		if imageToBeResized <= 0 {
			return errors.New("[qrops-error] Got an invalid size parameter, it should be greater than zero")
		}

		q.Size = imageToBeResized

	}

	return nil
}

func (q *QRConfig) setKeyUID() error {
	keyUID, ok := q.Params["keyUid"]
	// the keyUID was not passed, which is a requirement to get the multiaccount image,
	// so we log this error
	if !ok || len(keyUID) == 0 {
		return errors.New("[qrops-error] A keyUID is required to put logo on image and it was not passed in the parameters")
	}

	q.KeyUID = keyUID[0]
	return nil
}

func (q *QRConfig) setImageName() {
	imageName, ok := q.Params["imageName"]
	//if the imageName was not passed, we default to const images.LargeDimName
	if !ok || len(imageName) == 0 {
		q.ImageName = images.LargeDimName
	}

	q.ImageName = imageName[0]
}

func ToLogoImageFromBytes(imageBytes []byte, padding int) ([]byte, error) {
	img, _, err := image.Decode(bytes.NewReader(imageBytes))
	if err != nil {
		return nil, fmt.Errorf("decoding image failed: %v", err)
	}
	circle := images.CreateCircleWithPadding(img, padding)
	resultBytes, err := images.EncodePNG(circle)
	if err != nil {
		return nil, fmt.Errorf("encoding PNG failed: %v", err)
	}
	return resultBytes, nil
}

func GetLogoImage(multiaccountsDB *multiaccounts.Database, keyUID string, imageName string) ([]byte, error) {
	var (
		padding   int
		LogoBytes []byte
	)

	staticImageData, err := images.Asset("_assets/tests/qr/status.png")
	if err != nil { // Asset was not found.
		return nil, err
	}
	identityImageObjectFromDB, err := multiaccountsDB.GetIdentityImage(keyUID, imageName)

	if err != nil {
		return nil, err
	}

	// default padding to 10 to make the QR with profile image look as per
	// the designs
	padding = 10

	if identityImageObjectFromDB == nil {
		LogoBytes, err = ToLogoImageFromBytes(staticImageData, padding)
	} else {
		LogoBytes, err = ToLogoImageFromBytes(identityImageObjectFromDB.Payload, padding)
	}

	return LogoBytes, err
}

func GetPadding(imgBytes []byte) int {
	const (
		defaultPadding = 20
	)
	size, _, err := images.GetImageDimensions(imgBytes)
	if err != nil {
		return defaultPadding
	}
	return size / 5
}

func generateQRBytes(params url.Values, logger *zap.Logger, multiaccountsDB *multiaccounts.Database) []byte {

	qrGenerationConfig, err := NewQRConfig(params, logger)

	if err != nil {
		logger.Error("could not generate QRConfig please rectify the errors with input parameters", zap.Error(err))
		return nil
	}

	qrc, err := qrcode.NewWith(qrGenerationConfig.DecodedQRURL,
		qrcode.WithEncodingMode(qrcode.EncModeAuto),
		qrGenerationConfig.CorrectionLevel,
	)

	if err != nil {
		logger.Error("could not generate QRCode with provided options", zap.Error(err))
		return nil
	}

	buf := NewWriterCloserByteBuffer()
	nw := standard.NewWithWriter(buf)
	err = qrc.Save(nw)

	if err != nil {
		logger.Error("could not save image", zap.Error(err))
		return nil
	}

	payload := buf.Bytes()

	if qrGenerationConfig.WithLogo {
		logo, err := GetLogoImage(multiaccountsDB, qrGenerationConfig.KeyUID, qrGenerationConfig.ImageName)

		if err != nil {
			logger.Error("could not get logo image from multiaccountsDB", zap.Error(err))
			return nil
		}

		qrWidth, qrHeight, err := images.GetImageDimensions(payload)

		if err != nil {
			logger.Error("could not get image dimensions from payload", zap.Error(err))
			return nil
		}

		logo, err = images.ResizeImage(logo, qrWidth/5, qrHeight/5)

		if err != nil {
			logger.Error("could not resize logo image ", zap.Error(err))
			return nil
		}

		payload = images.SuperimposeLogoOnQRImage(payload, logo)
	}

	if qrGenerationConfig.Size > 0 {

		payload, err = images.ResizeImage(payload, qrGenerationConfig.Size, qrGenerationConfig.Size)

		if err != nil {
			logger.Error("could not resize final logo image ", zap.Error(err))
			return nil
		}
	}

	return payload

}