package devices

import (
	"errors"
	"fmt"
	"log"
	"time"

	"golang.org/x/net/context"

	"code.secondbit.org/uuid.hg"
)

var (
	// ErrDeviceNotFound is returned when the specified device couldn't be found.
	ErrDeviceNotFound = errors.New("device not found")
)

// Device represents a specific device that updates can be pushed to.
type Device struct {
	ID        uuid.ID
	Name      string
	Owner     uuid.ID
	Type      DeviceType
	Created   time.Time
	LastSeen  time.Time
	PushToken string
}

// ApplyChange returns a Device that is a copy of the passed Device,
// but with the passed DeviceChange applied.
func ApplyChange(d Device, change DeviceChange) Device {
	result := d
	if change.Name != nil {
		result.Name = *change.Name
	}
	if change.Owner != nil {
		result.Owner = *change.Owner
	} else {
		// We don't want to accidentally leave a slice that
		// is owned by both behind.
		result.Owner = d.Owner.Copy()
	}
	if change.Type != nil {
		result.Type = *change.Type
	}
	if change.Created != nil {
		result.Created = *change.Created
	}
	if change.LastSeen != nil {
		result.LastSeen = *change.LastSeen
	}
	if change.PushToken != nil {
		result.PushToken = *change.PushToken
	}
	return result
}

// DeviceChange represents a set of changes to a Device that will be used
// to update a Device.
type DeviceChange struct {
	DeviceID  uuid.ID
	Name      *string
	Owner     *uuid.ID
	Type      *DeviceType
	Created   *time.Time
	LastSeen  *time.Time
	PushToken *string
}

// Storer is an interface to control how data is stored in and retrieved from
// the datastore.
type Storer interface {
	GetDevices(ids []uuid.ID, c context.Context) (map[string]Device, error)
	UpdateDevice(change DeviceChange, c context.Context) error
	DeleteDevices(ids []uuid.ID, c context.Context) error
	CreateDevices(devices []Device, c context.Context) error
	ListDevicesByOwner(user uuid.ID, c context.Context) ([]Device, error)

	// These are used for testing only.
	Factory(c context.Context) (Storer, error)
	Destroy(c context.Context) error
}

type ErrDeviceAlreadyExists uuid.ID

func (e ErrDeviceAlreadyExists) Error() string {
	return fmt.Sprintf("device with ID %s already exists in datastore", uuid.ID(e).String())
}

// GetMany returns as many of the Devices specified by the passed IDs as possible.
// They are returned as a map, with the key being the string version of the ID.
// No error will be returned if a Device can't be found.
func GetMany(ids []uuid.ID, c context.Context) (map[string]Device, error) {
	results := map[string]Device{}
	storer, err := getStorer(c)
	if err != nil {
		log.Printf("Error retrieving Storer: %+v\n", err)
		return results, err
	}
	results, err = storer.GetDevices(ids, c)
	if err != nil {
		log.Printf("Error retrieving Devices from %T: %+v\n", storer, err)
		return results, err
	}
	return results, nil
}

// Get returns the Device specified by the passed ID. If the Device can't be found,
// an ErrDeviceNotFound error is returned.
func Get(id uuid.ID, c context.Context) (Device, error) {
	results, err := GetMany([]uuid.ID{id}, c)
	if err != nil {
		return Device{}, err
	}
	result, ok := results[id.String()]
	if !ok {
		return Device{}, ErrDeviceNotFound
	}
	return result, nil
}

// Update applies the DeviceChange to the passed Device, and returns the result. If
// the Device can't be found, an ErrDeviceNotFound error was returned.
func Update(device Device, change DeviceChange, c context.Context) (Device, error) {
	storer, err := getStorer(c)
	if err != nil {
		log.Printf("Error retrieving Storer: %+v\n", err)
		return Device{}, err
	}
	change.DeviceID = device.ID
	err = storer.UpdateDevice(change, c)
	if err != nil {
		return Device{}, err
	}
	return ApplyChange(device, change), nil
}

// DeleteMany removes the passed IDs from the datastore. No error is returned if the
// ID doesn't correspond to a Device in the datastore.
func DeleteMany(ids []uuid.ID, c context.Context) error {
	storer, err := getStorer(c)
	if err != nil {
		log.Printf("Error retrieving Storer: %+v\n", err)
		return err
	}
	return storer.DeleteDevices(ids, c)
}

// Delete removes the passed ID from the datastore. No error is returned if the ID doesn't
// correspond to a Device in the datastore.
func Delete(id uuid.ID, c context.Context) error {
	return DeleteMany([]uuid.ID{id}, c)
}

// CreateMany stores the passed Devices in the datastore, assigning default values if
// necessary. The Devices that were ultimately stored (including any default values, if
// applicable) are returned.
func CreateMany(devices []Device, c context.Context) ([]Device, error) {
	storer, err := getStorer(c)
	if err != nil {
		log.Printf("Error retrieving Storer: %+v\n", err)
		return []Device{}, err
	}
	modified := make([]Device, 0, len(devices))
	for _, device := range devices {
		if device.ID.IsZero() {
			device.ID = uuid.NewID()
		}
		if device.Created.IsZero() {
			device.Created = time.Now()
		}
		if device.LastSeen.IsZero() {
			device.LastSeen = time.Now()
		}
		modified = append(modified, device)
	}
	err = storer.CreateDevices(devices, c)
	if err != nil {
		return []Device{}, err
	}
	return modified, nil
}

// Create stores the passed Device in the datastore, assigning default values if
// necessary. The Devices that were ultimately stored (including any default values, if
// applicable) are returned.
func Create(device Device, c context.Context) (Device, error) {
	devices, err := CreateMany([]Device{device}, c)
	if err != nil {
		return Device{}, err
	}
	// There should never be a case where we don't return a result.
	// Ideally, we'd return an error here instead of letting the panic
	// happen, but seeing as I can't come up with a reason the error would
	// occur, I'm having trouble coming up with a reasonable error to return.
	return devices[0], nil
}

// ListByOwner returns a slice of all the Devices with an Owner property that
// matches the passed ID. There's no guarantee on the order the Devices will be
// returned in.
func ListByOwner(user uuid.ID, c context.Context) ([]Device, error) {
	// BUG(paddy): Eventually, we'll need to support paging for devices. But right now, I don't foresee any user creating enough of them to make pagination worthwhile.
	storer, err := getStorer(c)
	if err != nil {
		log.Printf("Error retrieving Storer: %+v\n", err)
		return []Device{}, err
	}
	devices, err := storer.ListDevicesByOwner(user, c)
	return devices, err
}
