package nucleus

import (
	"encoding/json"
	"io/ioutil"
	"sync"

	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/device"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/errors"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/filesystem"
	"code.fbi.h-da.de/danet/gosdn/controller/store"
	"github.com/google/uuid"
)

// FilesystemDeviceStore is the filesystem implementation of the device store
type FilesystemDeviceStore struct {
	sbiStore         southbound.Store
	pndUUID          uuid.UUID
	fileMutex        sync.Mutex
	pathToDeviceFile string
}

// NewFilesystemDeviceStore returns a filesystem implementation for a pnd store.
func NewFilesystemDeviceStore(pndUUID uuid.UUID) device.Store {
	deviceFilenameForUUID := store.GetStoreFilenameForUUID(pndUUID, filesystem.DeviceFilenameSuffix)

	store.EnsureFilesystemStorePathExists(deviceFilenameForUUID)
	return &FilesystemDeviceStore{
		pathToDeviceFile: store.GetCompletePathToFileStore(deviceFilenameForUUID),
		fileMutex:        sync.Mutex{},
		pndUUID:          pndUUID,
	}
}

func (s *FilesystemDeviceStore) readAllDevicesFromFile() ([]device.LoadedDevice, error) {
	var loadedDevices []device.LoadedDevice

	content, err := ioutil.ReadFile(s.pathToDeviceFile)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(content, &loadedDevices)
	if err != nil {
		return nil, err
	}

	return loadedDevices, nil
}

func (s *FilesystemDeviceStore) writeAllDevicesToFile(devices []device.LoadedDevice) error {
	serializedData, err := json.Marshal(devices)
	if err != nil {
		return err
	}

	err = ioutil.WriteFile(s.pathToDeviceFile, serializedData, 0600)
	if err != nil {
		return err
	}

	return nil
}

// Get takes a Device's UUID or name and returns the Device.
func (s *FilesystemDeviceStore) Get(query store.Query) (device.LoadedDevice, error) {
	s.fileMutex.Lock()
	defer s.fileMutex.Unlock()

	var device device.LoadedDevice

	devices, err := s.readAllDevicesFromFile()
	if err != nil {
		return device, err
	}

	for _, device := range devices {
		if device.ID == query.ID.String() {
			return device, nil
		}
	}

	return device, &errors.ErrNotFound{ID: query.ID}
}

// GetAll returns all stored devices.
func (s *FilesystemDeviceStore) GetAll() ([]device.LoadedDevice, error) {
	s.fileMutex.Lock()
	defer s.fileMutex.Unlock()

	devices, err := s.readAllDevicesFromFile()

	return devices, err
}

// Add adds a device to the device store.
func (s *FilesystemDeviceStore) Add(deviceToAdd device.Device) error {
	s.fileMutex.Lock()
	defer s.fileMutex.Unlock()

	devices, err := s.readAllDevicesFromFile()
	if err != nil {
		return err
	}

	var loadedDevice device.LoadedDevice
	loadedDevice, err = store.TransformObjectToLoadedObject[device.Device, device.LoadedDevice](deviceToAdd)
	if err != nil {
		return err
	}

	devices = append(devices, loadedDevice)

	err = s.writeAllDevicesToFile(devices)
	if err != nil {
		return err
	}

	return nil
}

// Update updates a existing device.
func (s *FilesystemDeviceStore) Update(deviceToUpdate device.Device) error {
	s.fileMutex.Lock()
	defer s.fileMutex.Unlock()

	loadedDeviceToUpdate, err := store.TransformObjectToLoadedObject[device.Device, device.LoadedDevice](deviceToUpdate)

	devices, err := s.readAllDevicesFromFile()
	if err != nil {
		return err
	}

	for i, device := range devices {
		if device.ID == deviceToUpdate.ID().String() {
			devices[i] = loadedDeviceToUpdate
			err = s.writeAllDevicesToFile(devices)
			if err != nil {
				return err
			}
			return nil
		}
	}

	return &errors.ErrNotFound{ID: deviceToUpdate.ID().String()}
}

// Delete deletes a device from the device store.
func (s *FilesystemDeviceStore) Delete(deviceToDelete device.Device) error {
	s.fileMutex.Lock()
	defer s.fileMutex.Unlock()

	devices, err := s.readAllDevicesFromFile()
	if err != nil {
		return err
	}

	for i, device := range devices {
		if device.ID == deviceToDelete.ID().String() {
			//remove item from slice
			devices[i] = devices[len(devices)-1]
			devices = devices[:len(devices)-1]

			err = s.writeAllDevicesToFile(devices)
			if err != nil {
				return err
			}

			return nil
		}
	}

	return &errors.ErrNotFound{ID: deviceToDelete.ID}
}