package apiv1

import (
	"fmt"
	"log"
	"net/http"

	"code.secondbit.org/api.hg"
	"code.secondbit.org/ducky/devices.hg"
	"code.secondbit.org/trout.hg"
	"code.secondbit.org/uuid.hg"

	"golang.org/x/net/context"
)

type createRequest struct {
	Devices []Device `json:"devices"`
}

type updateRequest struct {
	DeviceChange DeviceChange `json:"device"`
}

func handleCreateDevices(ctx context.Context, w http.ResponseWriter, r *http.Request) {
	var req createRequest

	userID, err := api.AuthUser(r)
	if err != nil {
		api.Encode(w, r, http.StatusUnauthorized, Response{Errors: api.AccessDeniedError})
		return
	}

	err = api.Decode(r, &req)
	if err != nil {
		api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
		return
	}

	devicesToCreate := devicesFromAPI(req.Devices)
	passedScopes := api.GetScopes(r)
	for pos, device := range devicesToCreate {
		err := validateDeviceCreation(device, passedScopes, userID)
		if err == nil {
			continue
		}
		var requestErr api.RequestError
		switch err {
		case errUnauthorizedLastSeen:
			requestErr.Slug = api.RequestErrAccessDenied
			requestErr.Field = fmt.Sprintf("devices/%d/lastSeen", pos)
		case errUnauthorizedCreated:
			requestErr.Slug = api.RequestErrAccessDenied
			requestErr.Field = fmt.Sprintf("devices/%d/created", pos)
		case errUnauthorizedOwner:
			requestErr.Slug = api.RequestErrAccessDenied
			requestErr.Field = fmt.Sprintf("devices/%d/owner", pos)
		case errInvalidDeviceType:
			requestErr.Slug = api.RequestErrInvalidValue
			requestErr.Field = fmt.Sprintf("devices/%d/type", pos)
		case errDeviceNameTooShort:
			if len(device.Name) == 0 {
				requestErr.Slug = api.RequestErrMissing
			} else {
				requestErr.Slug = api.RequestErrInsufficient
			}
			requestErr.Field = fmt.Sprintf("devices/%d/name", pos)
		case errDeviceNameTooLong:
			requestErr.Slug = api.RequestErrOverflow
			requestErr.Field = fmt.Sprintf("devices/%d/name", pos)
		}
		if requestErr.Slug != "" {
			api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{requestErr}})
			return
		}
		api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
	}
	createdDevices, err := devices.CreateMany(devicesToCreate, ctx)
	if err != nil {
		// BUG(paddy): we should filter out non-internal errors here and expose better error responses
		log.Printf("Error creating devices: %+v\n", err)
		api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
		return
	}
	var resp Response
	for _, device := range createdDevices {
		resp.Devices = append(resp.Devices, apiDeviceFromCore(device, api.CheckScopes(passedScopes, ScopeViewPushToken.ID)))
	}
	api.Encode(w, r, http.StatusCreated, resp)
}

func handleGetDevices(ctx context.Context, w http.ResponseWriter, r *http.Request) {
	userID, err := api.AuthUser(r)
	if err != nil {
		api.Encode(w, r, http.StatusUnauthorized, Response{Errors: api.AccessDeniedError})
		return
	}

	passedScopes := api.GetScopes(r)
	if !api.CheckScopes(passedScopes, ScopeViewDevices.ID) {
		api.Encode(w, r, http.StatusForbidden, Response{Errors: api.AccessDeniedError})
		return
	}

	var retrievedDevices []devices.Device
	requestedIDStrs := r.URL.Query()["id"]
	requestedIDStrs = append(requestedIDStrs, trout.RequestVars(r)[http.CanonicalHeaderKey("id")]...)

	if len(requestedIDStrs) < 1 {
		retrievedDevices, err = devices.ListByOwner(userID, ctx)
		if err != nil {
			log.Printf("Error listing devices: %+v\n", err)
			api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
			return
		}
	} else {
		requestedIDs := make([]uuid.ID, 0, len(requestedIDStrs))
		var reqErrs []api.RequestError
		for pos, idStr := range requestedIDStrs {
			id, err := uuid.Parse(idStr)
			if err != nil {
				reqErrs = append(reqErrs, api.RequestError{Slug: api.RequestErrInvalidFormat, Param: fmt.Sprintf("id[%d]", pos)})
				continue
			}
			requestedIDs = append(requestedIDs, id)
		}
		if len(reqErrs) > 0 {
			api.Encode(w, r, http.StatusBadRequest, Response{Errors: reqErrs})
			return
		}
		getDevices, err := devices.GetMany(requestedIDs, ctx)
		if err != nil {
			api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
			return
		}
		for _, device := range getDevices {
			if device.Owner.Equal(userID) {
				retrievedDevices = append(retrievedDevices, device)
			}
		}
	}

	var resp Response
	for _, device := range retrievedDevices {
		resp.Devices = append(resp.Devices, apiDeviceFromCore(device, api.CheckScopes(passedScopes, ScopeViewPushToken.ID)))
	}
	api.Encode(w, r, http.StatusOK, resp)
}

func handleUpdateDevice(ctx context.Context, w http.ResponseWriter, r *http.Request) {
	userID, err := api.AuthUser(r)
	if err != nil {
		api.Encode(w, r, http.StatusUnauthorized, Response{Errors: api.AccessDeniedError})
		return
	}

	id, err := uuid.Parse(trout.RequestVars(r).Get("id"))
	if err != nil {
		api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{{Slug: api.RequestErrInvalidFormat, Param: "id"}}})
		return
	}
	device, err := devices.Get(id, ctx)
	if err != nil {
		if err == devices.ErrDeviceNotFound {
			api.Encode(w, r, http.StatusNotFound, Response{Errors: []api.RequestError{{Slug: api.RequestErrNotFound, Param: "id"}}})
			return
		}
		log.Printf("Error retrieving device %s: %+v\n", id, err)
		api.Encode(w, r, http.StatusInternalServerError, api.ActOfGodError)
		return
	}

	var req updateRequest
	err = api.Decode(r, &req)
	if err != nil {
		api.Encode(w, r, http.StatusBadRequest, Response{Errors: api.InvalidFormatError})
		return
	}

	passedScopes := api.GetScopes(r)
	err = validateDeviceUpdate(apiDeviceFromCore(device, true), req.DeviceChange, passedScopes, userID)
	var requestErr api.RequestError
	switch err {
	case errUnauthorizedLastSeen:
		requestErr.Slug = api.RequestErrAccessDenied
		requestErr.Field = "device/lastSeen"
	case errUnauthorizedCreated:
		requestErr.Slug = api.RequestErrAccessDenied
		requestErr.Field = "device/created"
	case errUnauthorizedOwner:
		requestErr.Slug = api.RequestErrAccessDenied
		requestErr.Field = "device/owner"
	case errInvalidDeviceType:
		requestErr.Slug = api.RequestErrInvalidValue
		requestErr.Field = "device/type"
	case errDeviceNameTooShort:
		if len(device.Name) == 0 {
			requestErr.Slug = api.RequestErrMissing
		} else {
			requestErr.Slug = api.RequestErrInsufficient
		}
		requestErr.Field = "device/name"
	case errDeviceNameTooLong:
		requestErr.Slug = api.RequestErrOverflow
		requestErr.Field = "device/name"
	}
	if requestErr.Slug != "" {
		api.Encode(w, r, http.StatusBadRequest, Response{Errors: []api.RequestError{requestErr}})
		return
	}
	if err != nil {
		api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
		return
	}

	change := changeFromAPI(req.DeviceChange)
	updatedDevice, err := devices.Update(device, change, ctx)
	if err != nil {
		// BUG(paddy): we should filter out non-internal errors here and expose better error responses
		log.Printf("Error updating device %s: %+v\n", id, err)
		api.Encode(w, r, http.StatusInternalServerError, Response{Errors: api.ActOfGodError})
		return
	}
	resp := Response{Devices: []Device{apiDeviceFromCore(updatedDevice, api.CheckScopes(passedScopes, ScopeViewPushToken.ID))}}
	api.Encode(w, r, http.StatusOK, resp)
}
