package services

import (
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"os"
	"strconv"
	"sync"
	"time"

	appConfig "bitbucket.org/outfrontmedia/rolling-stock-display-simulation-tool/config"
	"github.com/twinj/uuid"
	"gosrc.io/xmpp"
	"gosrc.io/xmpp/stanza"
)

const rolingStockPresenceMessage = `ROLLING STOCK PRESENCE MESSAGE
{
	"serial": "79101X02Y0029450001CJ2000",
	"model": "VEN032FSNWM00",
	"displayOrientation": "left",
	"macAddress": "1c:c0:e1:42:5a:c4",
	"node": "v8.16.2",
	"nodeJS": "v8.16.2",
	"fbi": "2.10-2ubuntu1",
	"libgif7": "5.1.4-2ubuntu0.1",
	"bsEdgeServer": "13.3.9-1",
	"initopciVersion": "0.4~24",
	"bspInstallVersion": "1.0~36",
	"awsConfigsVersion": "2021.21.26-e7c5738",
	"outGuard": "2021.36.23-dev-028972a",
	"val": "1.1.11~129",
	"broadSignPlayer": "13.3.9-1",
	"displayUnitExternalID": "L-0141-M7-7281-Right-3SM-G02-02-79101X02Y0020040003EJ2001",
	"privateIPaddress": "10.1.1.94",
	"linux": {
	  "os": "Ubuntu 18.04.2 LTS",
	  "kernel": "4.18.20+10+opci",
	  "architecture": "x86-64"
	},
	"digiID": "digi-00000000-00000000-0040FFFF-FF801D30"
}`

const commandMessageExample = `{"device_jid":"device1@rolling-stock-sandbox2-openfire.onsmartengineering.com",
"message_id":"df26acfa-bdd4-40f4-a980-0638bbdb096e-1632473349400",
"api_version":1,
"from_id":"agent@rolling-stock-sandbox2-openfire.onsmartengineering.com",
"commands":[{"name":"demo_command","uuid":"0388ffd0-3f69-4600-b545-ed19f6378a0a",
"params":{"user_id":"21","api_version":1,"command":"demo_command","player_id":"8","arg":"senad.uka@toptal.com help"}}]}`

const commandResponse = `{"api_version": 1, 
"message_id": "766318f3-d1d3-4359-bf2d-e69d508733f4-1632410186320", 
"responses": [{"params": {"message": "Unsupported command. Type help for the list of supported commands", "response_code": "SUCCESS"}, 
"uuid": "5a041416-4184-4529-a7d3-7bfdd9fc1739", 
"name": "demo_command"}], 
"player_id": "3"}`

// CommandMessage -
type CommandMessage struct {
	APIVersion int       `json:"api_version"`
	MessageID  string    `json:"message_id"`
	Commands   []Command `json:"commands"`
}

// Command -
type Command struct {
	Params CommandParams `json:"params"`
	UUID   string        `json:"uuid"`
	Name   string        `json:"name"`
}

// CommandParams -
type CommandParams struct {
	Command  string `json:"message"`
	PlayerID string `json:"player_id"`
}

// CommandMessageResponse -
type CommandMessageResponse struct {
	APIVersion int        `json:"api_version"`
	MessageID  string     `json:"message_id"`
	Responses  []Response `json:"responses"`
	PlayerID   string     `json:"player_id"`
}

// Response -
type Response struct {
	Params ResponseParams `json:"params"`
	UUID   string         `json:"uuid"`
	Name   string         `json:"name"`
}

// ResponseParams -
type ResponseParams struct {
	Message      string `json:"message"`
	ResponseCode string `json:"response_code"`
}

// XMPPService - struct containing
type XMPPService struct {
	Router      *xmpp.Router
	XMPPClients []XMPPClient
}

// XMPPClient - struct containing client and client configuration
type XMPPClient struct {
	Config xmpp.Config
	Client *xmpp.Client
}

// ClientCredentials - credentials info for client configuration
type ClientCredentials struct {
	Jid        string `json:"username"`
	Credential string `json:"password"`
	Host       string `json:"host"`
	Port       int    `json:"port"`
	Ping       int    `json:"ping"`
}

const format = "2006/01/02 15:04:05"

// Instance - get instance of xmpp service
func Instance() *XMPPService {
	return &xmppService
}

var xmppService XMPPService

// Init Initialise XMPP servie, and router
func Init() {

	xmppService = XMPPService{
		Router: xmpp.NewRouter(),
	}
	xmppService.Router.HandleFunc("message", handleMessage)

	//Get Client credentials from file
	clientCredentials, err := getClientConfigsFromFile(appConfig.AppConfig.Credentials.CredentialsFileLocation)

	if err != nil {
		log.Fatalf("Unable to load credentials from file path %v, ERROR : %v TERMINATING", appConfig.AppConfig.Credentials.CredentialsFileLocation, err)
	}

	log.Printf("Client credentials length %v", len(clientCredentials))

	xmppService.XMPPClients = make([]XMPPClient, 0)
	var wg sync.WaitGroup

	for _, credential := range clientCredentials {
		wg.Add(1)
		log.Printf("Host %v", credential.Host+":"+strconv.Itoa(credential.Port))
		log.Printf("Jid %v", credential.Jid+"@"+credential.Host)
		log.Printf("Port %v", credential.Port)
		log.Printf("Credential %v", credential.Credential)

		xmppClient := XMPPClient{
			Config: xmpp.Config{

				TransportConfiguration: xmpp.TransportConfiguration{
					Address:   credential.Host + ":" + strconv.Itoa(credential.Port),
					Domain:    credential.Host,
					TLSConfig: &tls.Config{InsecureSkipVerify: true},
				},
				Jid:          credential.Jid + "@" + credential.Host,
				Credential:   xmpp.Password(credential.Credential),
				StreamLogger: os.Stdout,
				Insecure:     true,
			},
		}

		go func() {
			defer wg.Done()
			statusMesageDelay := int64(rand.Intn(appConfig.AppConfig.GeneralOptions.CommandReplyDelay))
			log.Printf("Delaying client connection for: %v secconds", statusMesageDelay)
			// Delay For two seccond to allow all clients to connect
			time.Sleep(time.Duration(statusMesageDelay * 1000000000))

			client, err := xmpp.NewClient(&xmppClient.Config, xmppService.Router, errorHandler)

			// Client connection
			if err := client.Connect(); err != nil {
				msg := fmt.Sprintf("XMPP connection failed: %v", err)

				fmt.Printf("Failed to connect to server. Exiting... %v", msg)
				return
			}

			if err != nil {
				log.Printf("Unable to initialise client for %v", xmppClient.Config.Jid)
			}
			xmppClient.Client = client
			xmppService.XMPPClients = append(xmppService.XMPPClients, xmppClient)
		}()

	}
	// Wait untill all client connections are open sucessfuly
	wg.Wait()

}

// SendOnlinePresenceStanza - send online presence stnza with data, to server
func SendOnlinePresenceStanza(client *xmpp.Client, jid string) error {

	statusMesageDelay := int64(rand.Intn(appConfig.AppConfig.GeneralOptions.CommandReplyDelay))
	log.Printf("Delaying online stanza presence message for: %v, secconds", statusMesageDelay*1000000000)
	// Delay For two seccond to allow all clients to connect
	time.Sleep(time.Duration(statusMesageDelay * 1000000000))

	onlinePresencePacket := stanza.NewPresence(stanza.Attrs{From: jid, Type: stanza.StanzaType(stanza.PresenceShowChat)})
	onlinePresencePacket.Status = rolingStockPresenceMessage

	err := client.Send(onlinePresencePacket)

	if err != nil {
		return err
	}

	log.Printf("<%v> sent online status", jid)
	return nil
}

// RunOnlinePresenceStatusIndefinetly - keep sending online presence status messages indefinetly for all clients with delay
func (xmppService *XMPPService) RunOnlinePresenceStatusIndefinetly() {

	for {
		for _, xmppClient := range xmppService.XMPPClients {

			// Send online presence stanza in go rutines
			go func(xmppClient XMPPClient) {
				err := SendOnlinePresenceStanza(xmppClient.Client, xmppClient.Config.Jid)

				if err != nil {
					log.Printf("There was an error while sending online presence stanza %v", err)
					return
				}

			}(xmppClient)

		}

		// Delay before sending another message
		time.Sleep(time.Duration(appConfig.AppConfig.GeneralOptions.PresenceStatusDelay * 1000000000))

	}

}

// ExecuteSingleStatusMessage - Wait for all clients to send online presence stanza messages just once
func (xmppService *XMPPService) ExecuteSingleStatusMessage() {

	var wg sync.WaitGroup

	for _, xmppClient := range xmppService.XMPPClients {
		wg.Add(1)

		// Send online presence stanza in go rutines
		go func(xmppClient XMPPClient) {
			defer wg.Done()
			err := SendOnlinePresenceStanza(xmppClient.Client, xmppClient.Config.Jid)

			if err != nil {
				log.Printf("There was an error while sending online presence stanza %v", err)
				return
			}

		}(xmppClient)
	}

	// Wait until all clients sent their online prezence staza messages
	wg.Wait()
}

func getClientConfigsFromFile(filePath string) ([]ClientCredentials, error) {

	var clientCredentials []ClientCredentials

	f, err := os.Open(filePath)
	if err != nil {
		return clientCredentials, err
	}
	defer f.Close()

	byteValue, err := ioutil.ReadAll(f)

	if err != nil {
		return clientCredentials, err
	}

	err = json.Unmarshal(byteValue, &clientCredentials)

	if err != nil {
		return clientCredentials, err
	}

	return clientCredentials, nil

}

func handleMessage(s xmpp.Sender, p stanza.Packet) {

	commandMessageResponse := CommandMessageResponse{}
	err := json.Unmarshal([]byte(commandResponse), &commandMessageResponse)

	if err != nil {
		log.Printf("unable to unmarshal Command Meassage Response: %v", commandResponse)
	}

	msg, ok := p.(stanza.Message)
	if !ok {
		log.Printf("  message not OK")
		_, _ = fmt.Fprintf(os.Stdout, "Ignoring packet: %T\n", p)
		return
	}

	if len(msg.Body) == 0 {
		log.Printf("<%v> received empty message, and will not respond", msg.To)
		return
	}

	commandMessage := CommandMessage{}
	err = json.Unmarshal([]byte(msg.Body), &commandMessage)

	if err != nil {
		log.Printf("<%v> unable to unmarshal Command Meassage: %v", msg.To, msg.Body)
	}
	if len(commandMessageResponse.Responses) > 0 || len(commandMessage.Commands) > 0 {
		// messageIDUUID := uuid.NewV4().String()
		commandMessageUUID := uuid.NewV4().String()
		commandMessageResponse.Responses[0].UUID = commandMessageUUID
		commandMessageResponse.PlayerID = commandMessage.Commands[0].Params.PlayerID
		commandMessageResponse.MessageID = commandMessage.MessageID

	} else {
		log.Printf("<%v> There are no commands inside command message : %v", msg.To, commandMessage)
	}

	m, err := json.Marshal(commandMessageResponse)

	if err != nil {
		log.Printf("<%v> unable to marshal Command Meassage response : %v", msg.To, commandMessageResponse)
	}

	log.Printf("<%v> received following message: %v", msg.To, msg.Body)
	commandReplyDelay := int64(rand.Intn(appConfig.AppConfig.GeneralOptions.CommandReplyDelay))
	log.Printf("Waiting FOR: %v", commandReplyDelay*1000000000)
	// Delay For two seccond to allow all clients to connect
	time.Sleep(time.Duration(commandReplyDelay * 1000000000))

	reply := stanza.Message{Attrs: stanza.Attrs{To: msg.From}, Body: string(m)}
	err = s.Send(reply)

	if err != nil {
		log.Printf("Error sending  to %v  message %v", msg.From, err)
	}
}

func errorHandler(err error) {
	fmt.Println(err.Error())
}
