// Package googlechat provides functionality to send google chat notifications
package googlechat

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/sasha-s/go-deadlock"

	"quantex.com/qfixpt/src/app/version"
	common "quantex.com/qfixpt/src/client/notify"
	"quantex.com/qfixpt/src/common/logger"
	"quantex.com/qfixpt/src/common/tracerr"
	"quantex.com/qfixpt/src/domain"
)

//revive:disable:line-length-limit It's just links

type SpaceID string

// spaces URLs
const googleChatURL = "https://chat.googleapis.com/v1/spaces/"

type Notify struct {
	spaces   map[domain.MessageChannel]SpaceID
	messages map[string]time.Time
	m        deadlock.Mutex
}

//nolint:lll // It's just a link
func New(chls domain.Channels) *Notify {
	if err := common.ValidateChannelConfig(chls); err != nil {
		panic(tracerr.Errorf("google config error: %w", err))
	}

	messages := make(map[string]time.Time)
	spaces := make(map[domain.MessageChannel]SpaceID)

	spaces[domain.MessageChannelTest] = SpaceID(chls.Test)
	spaces[domain.MessageChannelWeb] = SpaceID(chls.Web)
	spaces[domain.MessageChannelPanic] = SpaceID(chls.Panic)
	spaces[domain.MessageChannelError] = SpaceID(chls.Error)

	return &Notify{
		spaces:   spaces,
		messages: messages,
	}
}

func (g *Notify) Example() {
	wg := sync.WaitGroup{}
	wg.Add(3)

	go g.SendMsg(domain.MessageChannelTest, "Hello World!", domain.MessageStatusGood, &wg)
	go g.SendMsg(domain.MessageChannelTest, "Error", domain.MessageStatusStopper, &wg)
	go g.SendMsg(domain.MessageChannelTest, "Warning", domain.MessageStatusWarning, &wg)

	wg.Wait()
}

func (g *Notify) SendMsg(chat domain.MessageChannel, text string, status domain.MessageStatus, waitgroup *sync.WaitGroup) {
	var space SpaceID

	if s, ok := g.spaces[chat]; !ok {
		space = g.spaces[domain.MessageChannelError]

		err := tracerr.Errorf("error sending google notification, there's no space id for chat: %s", chat)

		logger.ErrorWoNotifier("%v", err.Error())
	} else {
		space = s
	}

	s := fmt.Sprintf("%s-%s-%s", text, status.String(), string(space))

	if ok := g.shouldSendMessage(s); ok {
		go common.SendWithRetry(text, status, waitgroup, googleChatURL+string(space), getMessage)
	}
}

//nolint:lll // It's just a link
const (
	errImg  = "https://media.gettyimages.com/id/1359003186/es/foto/illustration-of-a-tick-or-an-x-indicating-right-and-wrong.jpg?s=612x612&w=0&k=20&c=FdYIXI1qCPpVcY6phcIJom8sRhZw4hgYVtwOC6lNlmA="
	goodImg = "https://media.gettyimages.com/id/1352723074/es/foto/drawing-of-green-tick-check-mark.jpg?s=612x612&w=0&k=20&c=RgoCJ-n0OpZSClCWhJstoXAfiGrBZhnggUvOp2ooJqI="
	warnImg = "https://media.gettyimages.com/id/1407160246/es/vector/icono-de-tri%C3%A1ngulo-de-peligro.jpg?s=612x612&w=0&k=20&c=rI0IYmZmkg62txtNQFhyTbHx5oW311_OupBkbWODfjg="
)

func getImage(status domain.MessageStatus) (string, error) {
	switch status {
	case domain.MessageStatusGood:
		return goodImg, nil
	case domain.MessageStatusWarning:
		return warnImg, nil
	case domain.MessageStatusStopper:
		return errImg, nil
	}

	return "", tracerr.Errorf("unknown Message Status Type")
}

func getMessage(text string, status domain.MessageStatus) string {
	image, err := getImage(status)
	if err != nil {
		err = tracerr.Errorf("error getting image at getMessage, error: %w", err)

		logger.ErrorWoNotifier("%v", err.Error())

		var e error

		image, e = getImage(domain.MessageStatusWarning)
		if e != nil {
			e = tracerr.Errorf("error getting image at getMessage, error: %w", e)

			logger.ErrorWoNotifier("%v", e.Error())
		}
	}

	env := version.Environment()
	appVersion := version.Base()

	text = strings.ReplaceAll(text, "'", "\"")

	msg := `
    {
      'cardsV2': [{
        'cardId': 'createCardMessage',
        'card': {
          'header': {
            'title': 'qfixpt',
			'subtitle': 'Notification',
			'imageUrl': '%s',
			'imageType': 'CIRCLE'
          },
          'sections': [
            {
				'widgets':[
					{
						'textParagraph': {
							'text': '<b>Environment:</b> %s'
						}
					},
					{
						'textParagraph': {
							'text': '<b>Message:</b> %s'						
						}
					},
					{
						'textParagraph': {
							'text': '<b>Build:</b> %s'
						}
					},
					{
						'textParagraph': {
							'text': '<b>Time:</b> %s'
						}
					},
					{
						'textParagraph': {
							'text': '<b>Hostname:</b> %s'
						}
					},
					{
						'textParagraph': {
							'text': '<b>IP:</b> %s'
						}
					}
				]
            }
          ]
        }
      }]
    }`

	loc, err := time.LoadLocation("America/Argentina/Buenos_Aires")
	if err != nil {
		err := tracerr.Errorf("error loading timezone %v, setting default UTC", err)

		logger.ErrorWoNotifier("%v", err.Error())

		loc = time.UTC
	}

	now := time.Now().In(loc).Format("15:04:05 02-01-2006 -0700")
	jsonMsg := fmt.Sprintf(msg, image, env, text, appVersion, now,
		getHostname(), getOutboundIP())

	return jsonMsg
}

func (g *Notify) shouldSendMessage(s string) (ok bool) {
	sha, err := generateShaString(s)
	if err != nil {
		err = tracerr.Errorf("error generating sha string at shouldSendMessage, error: %w", err)

		logger.ErrorWoNotifier("%v", err.Error())

		return true
	}

	g.m.Lock()
	defer g.m.Unlock()

	if t, ok := g.messages[sha]; ok && time.Since(t) < time.Minute {
		return false
	}

	g.messages[sha] = time.Now()

	return true
}

func generateShaString(s string) (sha string, err error) {
	hash := sha256.New()
	if _, err := hash.Write([]byte(s)); err != nil {
		err = tracerr.Errorf("error generating hash at generateShaString, error: %w", err)
		logger.ErrorWoNotifier("%v", err.Error())

		return sha, err
	}

	return hex.EncodeToString(hash.Sum(nil)), nil
}

//revive:enable:line-length-limit

//nolint:gochecknoglobals // need to be global
var outboundIP string

func getOutboundIP() string {
	if outboundIP == "" {
		conn, err := net.Dial("udp", "8.8.8.8:80")
		if err != nil {
			logger.ErrorWoNotifier("failed to get UDP address")

			return ""
		}

		defer func(conn net.Conn) {
			err = conn.Close()
			if err != nil {
				logger.ErrorWoNotifier("error closing net connection")
			}
		}(conn)

		localAddr, ok := conn.LocalAddr().(*net.UDPAddr)
		if !ok {
			logger.ErrorWoNotifier("error: conn.LocalAddr() is not a *net.UDPAddr type")
		}

		outboundIP = localAddr.IP.String()
	}

	return outboundIP
}

func getHostname() string {
	hostname, err := os.Hostname()
	if err != nil {
		logger.ErrorWoNotifier("failed to get hostname")

		return ""
	}

	return hostname
}
