package httpserver

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

	"github.com/AFASystems/presence/internal/pkg/model"
	"github.com/AFASystems/presence/internal/pkg/persistence"
	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	"github.com/gorilla/websocket"
)

var (
	upgrader = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}
)

const (
	writeWait    = 10 * time.Second
	pongWait     = 60 * time.Second
	pingPeriod   = (pongWait * 9) / 10
	beaconPeriod = 2 * time.Second
)

// Init store in main or anywhere else and pass it to all initializer functions
// called in main, then with controllers or handlers use wrapper that takes entire store
// allocates only the properties that need to be passed into the controller

func StartHTTPServer(addr string, ctx *model.AppContext) {
	headersOk := handlers.AllowedHeaders([]string{"X-Requested-With"})
	originsOk := handlers.AllowedOrigins([]string{"*"})
	methodsOk := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"})

	// Set up HTTP server
	r := mux.NewRouter()
	r.HandleFunc("/api/results", resultsHandler(&ctx.HTTPResults))

	r.HandleFunc("/api/beacons/{beacon_id}", BeaconsDeleteHandler(&ctx.Beacons, ctx.ButtonsList)).Methods("DELETE")
	r.HandleFunc("/api/beacons", BeaconsListHandler(&ctx.Beacons)).Methods("GET")
	r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("POST") //since beacons are hashmap, just have put and post be same thing. it'll either add or modify that entry
	r.HandleFunc("/api/beacons", BeaconsAddHandler(&ctx.Beacons)).Methods("PUT")

	r.HandleFunc("/api/latest-beacons", latestBeaconsListHandler(&ctx.LatestList)).Methods("GET")

	r.HandleFunc("/api/settings", SettingsListHandler(&ctx.Settings)).Methods("GET")
	r.HandleFunc("/api/settings", SettingsEditHandler(&ctx.Settings)).Methods("POST")

	r.PathPrefix("/js/").Handler(http.StripPrefix("/js/", http.FileServer(http.Dir("static_html/js/"))))
	r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir("static_html/css/"))))
	r.PathPrefix("/img/").Handler(http.StripPrefix("/img/", http.FileServer(http.Dir("static_html/img/"))))
	r.PathPrefix("/").Handler(http.FileServer(http.Dir("static_html/")))

	http.Handle("/", r)

	mxWS := mux.NewRouter()
	mxWS.HandleFunc("/ws/api/beacons", serveWs(&ctx.HTTPResults))
	mxWS.HandleFunc("/ws/api/beacons/latest", serveLatestBeaconsWs(&ctx.LatestList))
	mxWS.HandleFunc("/ws/broadcast", handleConnections(ctx.Clients, &ctx.Broadcast))
	http.Handle("/ws/", mxWS)

	go handleMessages(ctx.Clients, &ctx.Broadcast)

	http.ListenAndServe(addr, handlers.CORS(originsOk, headersOk, methodsOk)(r))

}

func resultsHandler(httpResults *model.HTTPResultsList) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		httpResults.HTTPResultsLock.Lock()
		defer httpResults.HTTPResultsLock.Unlock()
		js, err := json.Marshal(httpResults.HTTPResults)

		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Write(js)
	}
}

func BeaconsListHandler(beacons *model.BeaconsList) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		beacons.Lock.RLock()
		js, err := json.Marshal(beacons.Beacons)
		beacons.Lock.RUnlock()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Write(js)
	}
}

func BeaconsAddHandler(beacons *model.BeaconsList) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		decoder := json.NewDecoder(r.Body)
		var inBeacon model.Beacon
		err := decoder.Decode(&inBeacon)

		if err != nil {
			http.Error(w, err.Error(), 400)
			return
		}

		if (len(strings.TrimSpace(inBeacon.Name)) == 0) || (len(strings.TrimSpace(inBeacon.Beacon_id)) == 0) {
			http.Error(w, "name and beacon_id cannot be blank", 400)
			return
		}

		beacons.Beacons[inBeacon.Beacon_id] = inBeacon

		err = persistence.PersistBeacons(beacons)

		if err != nil {
			http.Error(w, "trouble persisting beacons list, create bucket", 500)
			return
		}

		w.Write([]byte("ok"))
	}
}

func BeaconsDeleteHandler(beacons *model.BeaconsList, buttonsList map[string]model.Button) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		fmt.Println("route param: ", vars)
		beaconId := vars["beacon_id"]
		_, ok := beacons.Beacons[beaconId]
		if !ok {
			http.Error(w, "no beacon with the specified id", 400) // change the status code
			return
		}
		delete(beacons.Beacons, beaconId)

		_, ok = buttonsList[beaconId]
		if ok {
			delete(buttonsList, beaconId)
		}

		err := persistence.PersistBeacons(beacons)
		if err != nil {
			http.Error(w, "trouble persisting beacons list, create bucket", 500)
			return
		}

		w.Write([]byte("ok"))
	}
}

func latestBeaconsListHandler(latestList *model.LatestBeaconsList) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		latestList.Lock.RLock()
		var la = make([]model.Beacon, 0)
		for _, b := range latestList.LatestList {
			la = append(la, b)
		}
		latestList.Lock.RUnlock()
		js, err := json.Marshal(la)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Write(js)
	}
}

func SettingsListHandler(settings *model.Settings) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		js, err := json.Marshal(settings)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Write(js)
	}
}

func SettingsEditHandler(settings *model.Settings) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		decoder := json.NewDecoder(r.Body)
		var inSettings model.Settings
		err := decoder.Decode(&inSettings)
		if err != nil {
			http.Error(w, err.Error(), 400)
			return
		}

		//make sure values are > 0
		if (inSettings.Location_confidence <= 0) ||
			(inSettings.Last_seen_threshold <= 0) ||
			(inSettings.HA_send_interval <= 0) {
			http.Error(w, "values must be greater than 0", 400)
			return
		}

		*settings = inSettings

		err = persistence.PersistSettings(settings)
		if err != nil {
			http.Error(w, "trouble persisting settings, create bucket", 500)
			return
		}

		w.Write([]byte("ok"))
	}
}

func reader(ws *websocket.Conn) {
	defer ws.Close()
	ws.SetReadLimit(512)
	ws.SetReadDeadline(time.Now().Add(pongWait))
	ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongWait)); return nil })
	for {
		_, _, err := ws.ReadMessage()
		if err != nil {
			break
		}
	}
}

func writer(ws *websocket.Conn, httpResult *model.HTTPResultsList) {
	pingTicker := time.NewTicker(pingPeriod)
	beaconTicker := time.NewTicker(beaconPeriod)
	defer func() {
		pingTicker.Stop()
		beaconTicker.Stop()
		ws.Close()
	}()
	for {
		select {
		case <-beaconTicker.C:
			httpResult.HTTPResultsLock.Lock()
			defer httpResult.HTTPResultsLock.Unlock()
			js, err := json.Marshal(httpResult.HTTPResults)

			if err != nil {
				js = []byte("error")
			}

			ws.SetWriteDeadline(time.Now().Add(writeWait))
			if err := ws.WriteMessage(websocket.TextMessage, js); err != nil {
				return
			}
		case <-pingTicker.C:
			ws.SetWriteDeadline(time.Now().Add(writeWait))
			if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
				return
			}
		}
	}
}

func serveWs(httpResult *model.HTTPResultsList) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ws, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			if _, ok := err.(websocket.HandshakeError); !ok {
				log.Println(err)
			}
			return
		}

		go writer(ws, httpResult)
		reader(ws)
	}
}

func latestBeaconWriter(ws *websocket.Conn, latestBeaconsList map[string]model.Beacon, lock *sync.RWMutex) {
	pingTicker := time.NewTicker(pingPeriod)
	beaconTicker := time.NewTicker(beaconPeriod)
	defer func() {
		pingTicker.Stop()
		beaconTicker.Stop()
		ws.Close()
	}()
	for {
		select {
		case <-beaconTicker.C:

			lock.RLock()
			var la = make([]model.Beacon, 0)
			for _, b := range latestBeaconsList {
				la = append(la, b)
			}
			lock.RUnlock()
			js, err := json.Marshal(la)

			if err != nil {
				js = []byte("error")
			}

			ws.SetWriteDeadline(time.Now().Add(writeWait))
			if err := ws.WriteMessage(websocket.TextMessage, js); err != nil {
				return
			}
		case <-pingTicker.C:
			ws.SetWriteDeadline(time.Now().Add(writeWait))
			if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
				return
			}
		}
	}
}

func serveLatestBeaconsWs(latestList *model.LatestBeaconsList) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ws, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			if _, ok := err.(websocket.HandshakeError); !ok {
				log.Println(err)
			}
			return
		}

		go latestBeaconWriter(ws, latestList.LatestList, &latestList.Lock)
		reader(ws)
	}
}

func handleConnections(clients map[*websocket.Conn]bool, broadcast *chan model.Message) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		ws, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			log.Fatal(err)
		}

		defer ws.Close()

		clients[ws] = true

		for {
			var msg model.Message
			err := ws.ReadJSON(&msg)
			if err != nil {
				log.Printf("error: %v", err)
				delete(clients, ws)
				break
			}
			*broadcast <- msg
		}
	}
}

func handleMessages(clients map[*websocket.Conn]bool, broadcast *chan model.Message) {
	for {
		msg := <-*broadcast
		for client := range clients {
			err := client.WriteJSON(msg)
			if err != nil {
				log.Printf("error: %v", err)
				client.Close()
				delete(clients, client)
			}
		}
	}
}
