package storage

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"sync"

	"primesys_command_producer/src/logger"
	"primesys_command_producer/src/types"
	"primesys_command_producer/src/utils"
)

// command.json is the queue of every command, for all users
var commandFileMu sync.Mutex

// delivered message set when the device socket accepted the command
const DeliveredSuccess = "Command sent successfully"

const queueCommandExpiry = 15 * 60

// RunQueue executes the queue in command.json if it has any command to send.
func RunQueue(execute func() error) {
	commands, err := GetCommands()
	if err != nil {
		logger.Error("Error reading command.json: %v", err)
		return
	}

	pending := 0
	for _, cmd := range commands {
		// only commands with a server flag that are not delivered yet
		if cmd.ServerFlag != "" && cmd.DevliveredMessage == "" {
			pending++
		}
	}

	if pending == 0 {
		logger.Info("No queues to execute")
		return
	}

	logger.Info("Executing %d commands", pending)
	if err := execute(); err != nil {
		logger.Error("Error processing queue: %v", err)
	}
}

// GetCommands reads every command in data/command.json.
func GetCommands() ([]types.QueueCommand, error) {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	return readCommandFile()
}

// SaveCommand appends a command to data/command.json.
func SaveCommand(cmd types.QueueCommand) error {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	commands, err := readCommandFile()
	if err != nil {
		return err
	}

	return writeCommandFile(append(commands, cmd))
}

// WriteCommands replaces the contents of data/command.json.
func WriteCommands(commands []types.QueueCommand) error {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	return writeCommandFile(commands)
}

// FindCommandByServerFlag searches data/command.json for a command with the given server flag.
func FindCommandByServerFlag(serverFlag string) (types.QueueCommand, bool, error) {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	commands, err := readCommandFile()
	if err != nil {
		return types.QueueCommand{}, false, err
	}

	for _, cmd := range commands {
		if cmd.ServerFlag == serverFlag {
			return cmd, true, nil
		}
	}

	return types.QueueCommand{}, false, nil
}

func readCommandFile() ([]types.QueueCommand, error) {
	data, err := os.ReadFile(filepath.Join(dataDir, commandFile))
	if err != nil {
		return nil, err
	}

	commands := make([]types.QueueCommand, 0)
	if len(data) == 0 {
		return commands, nil
	}
	if err := json.Unmarshal(data, &commands); err != nil {
		return nil, err
	}
	return commands, nil
}

func writeCommandFile(commands []types.QueueCommand) error {
	data, err := json.MarshalIndent(commands, "", "  ")
	if err != nil {
		return err
	}

	return os.WriteFile(filepath.Join(dataDir, commandFile), data, 0644)
}

// DeleteCommandByServerFlag removes the command with the given server flag from data/command.json.
func DeleteCommandByServerFlag(serverFlag string) error {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	commands, err := readCommandFile()
	if err != nil {
		return err
	}

	remaining := make([]types.QueueCommand, 0, len(commands))
	for _, cmd := range commands {
		if cmd.ServerFlag != serverFlag {
			remaining = append(remaining, cmd)
		}
	}

	return writeCommandFile(remaining)
}

func ClearQueue() error {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	commands, err := readCommandFile()
	if err != nil {
		return err
	}

	now := utils.IndianEpoch()

	remaining := make([]types.QueueCommand, 0, len(commands))

	for _, cmd := range commands {
		if now-cmd.Command.TimeStamp > queueCommandExpiry {
			continue
		}
		remaining = append(remaining, cmd)
	}

	return writeCommandFile(remaining)
}

// UpdateDeliveredMessage finds the command by server flag in data/command.json and sets its delivered message.
func UpdateDeliveredMessage(serverFlag, message string) (bool, error) {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	commands, err := readCommandFile()
	if err != nil {
		return false, err
	}

	found := false
	for i := range commands {
		if commands[i].ServerFlag == serverFlag {
			commands[i].DevliveredMessage = message
			found = true
			break
		}
	}

	if !found {
		return false, nil
	}

	return true, writeCommandFile(commands)
}

// DeleteCommandAndAssignFlag removes the command with the given server flag and gives its flag
// to the same user's first command that has no server flag. Returns the command that got the flag, if any.
func DeleteCommandAndAssignFlag(serverFlag string) (*types.QueueCommand, error) {
	commandFileMu.Lock()
	defer commandFileMu.Unlock()

	commands, err := readCommandFile()
	if err != nil {
		return nil, err
	}

	// hold the deleted command's user id and inner server flag
	var userId, freeFlag string
	remaining := make([]types.QueueCommand, 0, len(commands))
	for _, cmd := range commands {
		if cmd.ServerFlag == serverFlag {
			userId = cmd.UserId
			freeFlag = cmd.Command.ServerFlag
			continue
		}
		remaining = append(remaining, cmd)
	}

	// give the freed flag to the user's first command without a server flag
	var assigned *types.QueueCommand
	if userId != "" && freeFlag != "" {
		for i := range remaining {
			if remaining[i].UserId == userId && remaining[i].Command.ServerFlag == "" {
				count, err := serverFlagCount(freeFlag)
				if err != nil {
					return nil, err
				}

				remaining[i].Command.ServerFlag = freeFlag
				remaining[i].ServerFlag = strconv.FormatInt(remaining[i].Command.Imei, 10) + freeFlag
				remaining[i].ServerFlagCount = count
				assigned = &remaining[i]
				break
			}
		}
	}

	if err := writeCommandFile(remaining); err != nil {
		return nil, err
	}

	if assigned != nil {
		cmd := *assigned
		return &cmd, nil
	}
	return nil, nil
}

// serverFlagCount converts the last four characters of a server flag to its count, e.g. 00E00001 -> 1
func serverFlagCount(serverFlag string) (int, error) {
	if len(serverFlag) < 4 {
		return 0, fmt.Errorf("server flag %q is shorter than 4 characters", serverFlag)
	}
	return strconv.Atoi(serverFlag[len(serverFlag)-4:])
}
