package server

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"

	"bitbucket.org/ww/goautoneg"
	"code.secondbit.org/feature.hg"
)

const (
	ErrMissing  = "missing"
	ErrNotFound = "not_found"
	ErrConflict = "conflict"
	ErrActOfGod = "act_of_god"
)

var (
	encoders = []string{"application/json"}
)

type flagCheck struct {
	ID     string `json:"id,omitempty"`
	Passes bool   `json:"pass,omitempty"`
}

type response struct {
	status      int
	contentType string
	Checks      []flagCheck    `json:"checks,omitempty"`
	Flags       []feature.Flag `json:"flags,omitempty"`
	Errors      []respError    `json:"errors,omitempty"`
}

func (r response) addErrors(e ...respError) {
	if r.Errors == nil {
		r.Errors = []respError{}
	}
	r.Errors = append(r.Errors, e...)
}

func (r response) write(w http.ResponseWriter) {
	w.Header().Set("Content-Type", r.contentType)
	if r.status == 0 {
		r.status = 200
	}
	w.WriteHeader(r.status)
	var err error
	switch r.contentType {
	case "application/json":
		enc := json.NewEncoder(w)
		err = enc.Encode(r)
	default:
		_, err = w.Write([]byte(strings.Join(encoders, "\n") + "\n"))
	}
	if err != nil {
		log.Printf("Error writing response: %+v\n", err)
	}
}

func responseFromRequest(r *http.Request) response {
	resp := responseFromRequest(r)
	if r.Header.Get("Accept") != "" {
		resp.contentType = goautoneg.Negotiate(r.Header.Get("Accept"), encoders)
		if resp.contentType == "" {
			resp.status = http.StatusNotAcceptable
		}
	} else {
		resp.contentType = "application/json"
	}
	return resp
}

type respError struct {
	Code  string `json:"code"`
	Param string `json:"param,omitempty"`
	Field string `json:"field,omitempty"`
}

func FlagSetHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
	resp := responseFromRequest(r)
	flags, err := store.list()
	if err != nil {
		switch err.code {
		default:
			log.Printf("Error listing flags: %+v\n", err)
			resp.status = http.StatusInternalServerError
			resp.addErrors(respError{Code: ErrActOfGod})
		}
		resp.write(w)
		return
	}
	shard := r.URL.Query().Get("shard")
	if shard == "" {
		resp.Flags = flags
		resp.write(w)
		return
	}
	checks := make([]flagCheck, len(flags))
	for pos, flag := range flags {
		checks[pos] = flagCheck{ID: flag.ID, Passes: flag.Permit(shard)}
	}
	resp.Checks = checks
	resp.write(w)
}

func CreateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
	resp := responseFromRequest(r)
	var f feature.Flag
	// BUG(paddY): Need to unmarshal the request body
	if f.ID == "" {
		resp.status = http.StatusBadRequest
		resp.addErrors(respError{Code: ErrMissing, Field: "/id"})
		resp.write(w)
		return
	}
	err := store.create(f)
	if err != nil {
		switch err.code {
		case errAlreadyExists:
			resp.status = http.StatusBadRequest
			resp.addErrors(respError{Code: ErrConflict, Field: "/id"})
		default:
			log.Printf("Error creating flag: %+v\n", err)
			resp.status = http.StatusInternalServerError
			resp.addErrors(respError{Code: ErrActOfGod})
		}
		resp.write(w)
		return
	}
	resp.Flags = []feature.Flag{f}
	resp.write(w)
}

func UpdateFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
	resp := responseFromRequest(r)
	var flags []feature.Flag
	// BUG(paddy): Need to unmarshal the request body
	for pos, f := range flags {
		if f.ID == "" {
			resp.addErrors(respError{Code: ErrMissing, Field: fmt.Sprintf("/id/%d", pos)})
			resp.status = http.StatusBadRequest
		}
	}
	errs := store.update(flags)
	if errs != nil {
		for _, err := range errs {
			switch err.code {
			case errNotFound:
				resp.status = http.StatusNotFound
				resp.addErrors(respError{Code: ErrNotFound, Field: fmt.Sprintf("/id/%d", err.pos)})
			default:
				log.Printf("Error updating flag: %+v\n", err)
				resp.status = http.StatusInternalServerError
				resp.addErrors(respError{Code: ErrActOfGod})
			}
		}
		resp.write(w)
		return
	}
	resp.Flags = flags
	resp.write(w)
}

func DeleteFlagHandler(w http.ResponseWriter, r *http.Request, store flagStore) {
	resp := responseFromRequest(r)
	var id string
	// BUG(paddY): Need to pick a routing library and get the ID
	err := store.destroy(id)
	if err != nil {
		switch err.code {
		case ErrNotFound:
			resp.status = http.StatusNotFound
			resp.addErrors(respError{Code: ErrNotFound, Param: "id"})
		default:
			resp.status = http.StatusInternalServerError
			resp.addErrors(respError{Code: ErrActOfGod})
		}
		resp.write(w)
		return
	}
	resp.Flags = []feature.Flag{{ID: id}}
	resp.write(w)
}
