package websocket

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/url"
	"primesys_command_producer/src/logger"
	"primesys_command_producer/src/models"
	"primesys_command_producer/src/repositories"
	"primesys_command_producer/src/storage"
	"primesys_command_producer/src/types"
	. "primesys_command_producer/src/types"
	"primesys_command_producer/src/utils"
	"strconv"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

var queueMu sync.Mutex

// handleMessage validates the incoming command request and pushes it in kafka
func (c *Client) handleMessage(message []byte) {

	commands, err := GetUserQueue(c.UserID)

	if err != nil {
		logger.Error("No Such command exists")
	}

	queueCount := len(commands)
	serverCount := 0
	if len(commands) > 0 {
		serverCount = commands[len(commands)-1].ServerFlagCount + 1
	}

	logger.Info("server %d", queueCount)

	var packet DivisionCommandRequest

	if err := json.Unmarshal(message, &packet); err != nil {
		logger.Error("userId=%s invalid json: %v", c.UserID, err)
		c.reply(CommandResponse{Status: false, Message: "Invalid json"})
		return
	}

	for _, cmd := range packet.Command {

		if queueCount < 800 {
			packet := CommandRequestPacket{
				Imei:       cmd.DeviceImei,
				Command:    withHash(cmd.Command),
				UserID:     c.UserID,
				TimeStamp:  utils.IndianEpoch(),
				ServerFlag: getServerFlagCount(c.ServerFlag, serverCount),
			}

			logger.Debug("Packet sending %v", packet)

			err := WriteCommand(c.UserID, types.QueueCommand{
				UserId:          c.UserID,
				ServerFlagCount: serverCount,
				ServerFlag:      strconv.FormatInt(packet.Imei, 10) + getServerFlagCount(c.ServerFlag, serverCount),
				Command:         packet,
			})
			if err != nil {
				log.Println(err)
			}

			if err := sendToDevice(packet); err != nil {
				logger.Error("Send to device failed imei=%d: %v", packet.Imei, err)
			}
			serverCount = serverCount + 1
		} else {
			packet := CommandRequestPacket{
				Imei:      cmd.DeviceImei,
				Command:   withHash(cmd.Command),
				UserID:    c.UserID,
				TimeStamp: utils.IndianEpoch(),
			}

			err := WriteCommand(c.UserID, types.QueueCommand{
				UserId:  c.UserID,
				Command: packet,
			})
			if err != nil {
				log.Println(err)
			}
		}

		queueCount++

	}

}

// WriteCommand appends a command to data/command.json.
func WriteCommand(userID string, cmd types.QueueCommand) error {
	queueMu.Lock()
	defer queueMu.Unlock()

	return storage.SaveCommand(cmd)
}

const deviceSocketURL = "wss://prime-producer-socket.mykidtrackers.in/ws"

// ProcessQueue sends every command in data/command.json to its device.
// Commands are never removed from the file.
func ProcessQueue() error {
	queueMu.Lock()
	defer queueMu.Unlock()

	commands, err := storage.GetCommands()
	if err != nil {
		return err
	}

	for _, qc := range commands {
		// only commands with a server flag that are not delivered yet are sent
		if qc.ServerFlag == "" || qc.DevliveredMessage != "" {
			continue
		}

		// device command always ends with #
		qc.Command.Command = withHash(qc.Command.Command)

		err := sendToDevice(qc.Command)
		if err != nil {
			logger.Error("Error sending command to imei %d: %v", qc.Command.Imei, err)
			continue
		}

		// if err != nil {
		// 	logger.Error("Error marshaling response for imei %d: %v", qc.Command.Imei, err)
		// 	continue
		// }
	}

	return nil
}

// SendCommand sends a queued command to its device.
func SendCommand(qc types.QueueCommand) error {
	// device command always ends with #
	qc.Command.Command = withHash(qc.Command.Command)
	return sendToDevice(qc.Command)
}

// sendToDevice connects to the device socket for the command's imei and sends the command.
func sendToDevice(cmd types.CommandRequestPacket) error {
	u, err := url.Parse(deviceSocketURL)
	if err != nil {
		return err
	}
	q := u.Query()
	key := strconv.FormatInt(cmd.Imei, 10) + cmd.ServerFlag
	q.Set("key", key)
	u.RawQuery = q.Encode()

	dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second}
	conn, httpResp, err := dialer.Dial(u.String(), nil)
	if err != nil {
		if httpResp != nil {
			body, _ := io.ReadAll(httpResp.Body)
			httpResp.Body.Close()
			logger.Error("Dial %s failed: %v | status=%d %s | body=%q",
				u.String(), err, httpResp.StatusCode, httpResp.Status, string(body))
		} else {
			logger.Error("Dial %s failed: %v (no HTTP response)", u.String(), err)
		}
		return err
	}
	defer conn.Close()

	req := types.DeviceCommandRequest{
		Imei:       cmd.Imei,
		ServerFlag: cmd.ServerFlag,
		Command:    cmd.Command,
	}

	conn.SetWriteDeadline(time.Now().Add(writeWait))
	if err := conn.WriteJSON(req); err != nil {
		return err
	}

	// wait for the device response, command stays in queue if none comes
	conn.SetReadDeadline(time.Now().Add(pongWait))
	_, resp, err := conn.ReadMessage()
	if err != nil {
		return err
	}

	var response DeviceCommandResponse

	if err := json.Unmarshal(resp, &response); err != nil {
		logger.Error("Error in socket response: %v", err)
	}

	logger.Info("Response Send To Device %+v", response)

	logger.Info("Success command %t", response.Success)

	// search key in command.json is imei + server flag
	searchKey := strconv.FormatInt(cmd.Imei, 10) + response.ServerFlag

	// not processed: save the response in command history and remove the command
	if !response.Success {
		queueCmd, found, err := storage.FindCommandByServerFlag(searchKey)
		if err != nil {
			logger.Error("Error searching command.json for server_flag %s: %v", searchKey, err)
			return nil
		}
		if !found {
			logger.Error("No command found in command.json for server_flag %s", searchKey)
			return nil
		}

		if err := insertIntoCommandHistory(response, queueCmd); err != nil {
			logger.Error("Error inserting command history for server_flag %s: %v", searchKey, err)
			return nil
		}

		assigned, err := storage.DeleteCommandAndAssignFlag(searchKey)
		if err != nil {
			logger.Error("Error deleting command for server_flag %s: %v", searchKey, err)
			return nil
		}

		logger.Info("Command failed (%s), history saved and command removed for server_flag %s", response.Response, searchKey)
		if assigned != nil {
			logger.Info("Server flag %s assigned to imei=%d userId=%s", assigned.ServerFlag, assigned.Command.Imei, assigned.UserId)

			// send the command that just got the server flag, its response updates delivered_message
			if err := SendCommand(*assigned); err != nil {
				logger.Error("Error sending command to imei %d: %v", assigned.Command.Imei, err)
			}
		}
		return nil
	}

	found, err := storage.UpdateDeliveredMessage(searchKey, response.Response)
	if err != nil {
		logger.Error("Error updating delivered message for server_flag %s: %v", searchKey, err)
	} else if !found {
		logger.Error("No command found in command.json for server_flag %s", searchKey)
	} else {
		logger.Info("Delivered message updated for server_flag %s", searchKey)
	}
	return nil
}

func insertIntoCommandHistory(response DeviceCommandResponse, queueCmd QueueCommand) error {

	device, err := repositories.GetDeviceByImei(context.Background(), queueCmd.Command.Imei)
	if err != nil {
		return err
	}
	if device == nil {
		return fmt.Errorf("device not found for imei %d", queueCmd.Command.Imei)
	}

	division, err := repositories.GetDivisionByID(context.Background(), device.DivisionID)
	if err != nil {
		return err
	}
	userLogin, err := repositories.GetDivisionByID(context.Background(), queueCmd.UserId)
	if err != nil {
		return err
	}
	if division == nil {
		return fmt.Errorf("division not found for id %s", device.DivisionID)
	}

	command := models.CommandHistory{
		DeviceImei:         queueCmd.Command.Imei,
		Timestamp:          queueCmd.Command.TimeStamp,
		DeviceName:         device.DeviceName,
		LoginName:          userLogin.Name,
		DivisionID:         device.DivisionID,
		OriginalCommand:    queueCmd.Command.Command,
		Command:            queueCmd.Command.Command,
		DeviceResponse:     response.Response,
		DeviceResponseTime: utils.IndianEpoch(),
	}

	return repositories.InsertCommandHistory(context.Background(), &command)
}
