package main

import (
	"net/http"
	"time"

	"github.com/emicklei/go-restful"
	"github.com/emicklei/go-restful-swagger12"
	"google.golang.org/appengine"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/user"
)

// This example demonstrates a reasonably complete suite of RESTful operations backed
// by DataStore on Google App Engine.

// Our simple example struct.
type Profile struct {
	LastModified time.Time `json:"-" xml:"-"`
	Email        string    `json:"-" xml:"-"`
	FirstName    string    `json:"first_name" xml:"first-name"`
	NickName     string    `json:"nick_name" xml:"nick-name"`
	LastName     string    `json:"last_name" xml:"last-name"`
}

type ProfileApi struct {
	Path string
}

func gaeUrl() string {
	if appengine.IsDevAppServer() {
		return "http://localhost:8080"
	} else {
		// Include your URL on App Engine here.
		// I found no way to get AppID without appengine.Context and this always
		// based on a http.Request.
		return "http://federatedservices.appspot.com"
	}
}

func init() {
	u := ProfileApi{Path: "/profiles"}
	u.register()

	// Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
	// You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
	// Open <your_app_id>.appspot.com/apidocs and enter
	// Place the Swagger UI files into a folder called static/swagger if you wish to use Swagger
	// http://<your_app_id>.appspot.com/apidocs.json in the api input field.
	// For testing, you can use http://localhost:8080/apidocs.json
	config := swagger.Config{
		// You control what services are visible
		WebServices:    restful.RegisteredWebServices(),
		WebServicesUrl: gaeUrl(),
		ApiPath:        "/apidocs.json",

		// Optionally, specify where the UI is located
		SwaggerPath: "/apidocs/",

		// GAE support static content which is configured in your app.yaml.
		// This example expect the swagger-ui in static/swagger so you should place it there :)
		SwaggerFilePath: "static/swagger"}
	swagger.InstallSwaggerService(config)
}

func (u ProfileApi) register() {
	ws := new(restful.WebService)

	ws.
		Path(u.Path).
		// You can specify consumes and produces per route as well.
		Consumes(restful.MIME_JSON, restful.MIME_XML).
		Produces(restful.MIME_JSON, restful.MIME_XML)

	ws.Route(ws.POST("").To(u.insert).
		// Swagger documentation.
		Doc("insert a new profile").
		Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")).
		Reads(Profile{}))

	ws.Route(ws.GET("/{profile-id}").To(u.read).
		// Swagger documentation.
		Doc("read a profile").
		Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")).
		Writes(Profile{}))

	ws.Route(ws.PUT("/{profile-id}").To(u.update).
		// Swagger documentation.
		Doc("update an existing profile").
		Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")).
		Param(ws.BodyParameter("Profile", "representation of a profile").DataType("main.Profile")).
		Reads(Profile{}))

	ws.Route(ws.DELETE("/{profile-id}").To(u.remove).
		// Swagger documentation.
		Doc("remove a profile").
		Param(ws.PathParameter("profile-id", "identifier for a profile").DataType("string")))

	restful.Add(ws)
}

// POST http://localhost:8080/profiles
// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"}
//
func (u *ProfileApi) insert(r *restful.Request, w *restful.Response) {
	c := appengine.NewContext(r.Request)

	// Marshall the entity from the request into a struct.
	p := new(Profile)
	err := r.ReadEntity(&p)
	if err != nil {
		w.WriteError(http.StatusNotAcceptable, err)
		return
	}

	// Ensure we start with a sensible value for this field.
	p.LastModified = time.Now()

	// The profile belongs to this user.
	p.Email = user.Current(c).String()

	k, err := datastore.Put(c, datastore.NewIncompleteKey(c, "profiles", nil), p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Let them know the location of the newly created resource.
	// TODO: Use a safe Url path append function.
	w.AddHeader("Location", u.Path+"/"+k.Encode())

	// Return the resultant entity.
	w.WriteHeader(http.StatusCreated)
	w.WriteEntity(p)
}

// GET http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
//
func (u ProfileApi) read(r *restful.Request, w *restful.Response) {
	c := appengine.NewContext(r.Request)

	// Decode the request parameter to determine the key for the entity.
	k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Retrieve the entity from the datastore.
	p := Profile{}
	if err := datastore.Get(c, k, &p); err != nil {
		if err.Error() == "datastore: no such entity" {
			http.Error(w, err.Error(), http.StatusNotFound)
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	// Check we own the profile before allowing them to view it.
	// Optionally, return a 404 instead to help prevent guessing ids.
	// TODO: Allow admins access.
	if p.Email != user.Current(c).String() {
		http.Error(w, "You do not have access to this resource", http.StatusForbidden)
		return
	}

	w.WriteEntity(p)
}

// PUT http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
// {"first_name": "Ivan", "nick_name": "Socks", "last_name": "Hawkes"}
//
func (u *ProfileApi) update(r *restful.Request, w *restful.Response) {
	c := appengine.NewContext(r.Request)

	// Decode the request parameter to determine the key for the entity.
	k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Marshall the entity from the request into a struct.
	p := new(Profile)
	err = r.ReadEntity(&p)
	if err != nil {
		w.WriteError(http.StatusNotAcceptable, err)
		return
	}

	// Retrieve the old entity from the datastore.
	old := Profile{}
	if err := datastore.Get(c, k, &old); err != nil {
		if err.Error() == "datastore: no such entity" {
			http.Error(w, err.Error(), http.StatusNotFound)
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	// Check we own the profile before allowing them to update it.
	// Optionally, return a 404 instead to help prevent guessing ids.
	// TODO: Allow admins access.
	if old.Email != user.Current(c).String() {
		http.Error(w, "You do not have access to this resource", http.StatusForbidden)
		return
	}

	// Since the whole entity is re-written, we need to assign any invariant fields again
	// e.g. the owner of the entity.
	p.Email = user.Current(c).String()

	// Keep track of the last modification date.
	p.LastModified = time.Now()

	// Attempt to overwrite the old entity.
	_, err = datastore.Put(c, k, p)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	// Let them know it succeeded.
	w.WriteHeader(http.StatusNoContent)
}

// DELETE http://localhost:8080/profiles/ahdkZXZ-ZmVkZXJhdGlvbi1zZXJ2aWNlc3IVCxIIcHJvZmlsZXMYgICAgICAgAoM
//
func (u *ProfileApi) remove(r *restful.Request, w *restful.Response) {
	c := appengine.NewContext(r.Request)

	// Decode the request parameter to determine the key for the entity.
	k, err := datastore.DecodeKey(r.PathParameter("profile-id"))
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// Retrieve the old entity from the datastore.
	old := Profile{}
	if err := datastore.Get(c, k, &old); err != nil {
		if err.Error() == "datastore: no such entity" {
			http.Error(w, err.Error(), http.StatusNotFound)
		} else {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
		return
	}

	// Check we own the profile before allowing them to delete it.
	// Optionally, return a 404 instead to help prevent guessing ids.
	// TODO: Allow admins access.
	if old.Email != user.Current(c).String() {
		http.Error(w, "You do not have access to this resource", http.StatusForbidden)
		return
	}

	// Delete the entity.
	if err := datastore.Delete(c, k); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}

	// Success notification.
	w.WriteHeader(http.StatusNoContent)
}
