package main

import (
	"context"
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"log"
	"math/rand"
	"net/http"
	"strings"

	glws "git.alfi.li/gamelang/frontend/webserver"
	glwss "git.alfi.li/gamelang/frontend/wsserver"
	bingoman "git.alfi.li/gamelang/games/bingo/bingoManager"
	gamelangpb "git.alfi.li/gamelang/protobuf/golang"
	glbingopb "git.alfi.li/gamelang/protobuf/golang-bingo"
	userman "git.alfi.li/gamelang/systems/usermanager"
	worldman "git.alfi.li/gamelang/systems/worldmanager"
	"github.com/labstack/echo/v4"
	"gopkg.in/gcfg.v1"
)

// Template wraps go's html template
type Template struct {
	templates *template.Template
}

// Render renders a Template
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
	return t.templates.ExecuteTemplate(w, name, data)
}

func initTemplates(e *echo.Echo) {
	t := &Template{
		templates: template.Must(template.ParseGlob("public/*.html")),
	}
	e.Renderer = t
}

type BingoConfig struct {
	Http struct {
		ListenIP   string
		ListenPort string
	}
	WS struct {
		ListenIP   string
		ListenPort string
	}
	Manager struct {
		UserManager  string
		WorldManager string
		BingoManager string
	}
}

func getDefaultConfig() BingoConfig {
	bc := BingoConfig{}
	bc.Http.ListenIP = "127.0.0.1"
	bc.Http.ListenPort = "8080"
	bc.WS.ListenIP = "127.0.0.1"
	bc.WS.ListenPort = "8081"
	bc.Manager.UserManager = "localhost:9090"
	bc.Manager.WorldManager = "localhost:9091"
	bc.Manager.BingoManager = "localhost:9092"
	return bc
}

type BingoGame struct {
	um           *userman.UserManager   //gamelangpb.UserServiceClient
	wm           *worldman.WorldManager //gamelangpb.WorldServiceClient
	bingomanager *bingoman.BingoManager

	webserver *glws.Webserver
	wssserver *glwss.WSServer

	notichan chan glbingopb.Event

	config BingoConfig
}

func (bg *BingoGame) Init(config BingoConfig) {

	//init userclient
	//userconn, err := grpc.Dial(config.Manager.UserManager, grpc.WithInsecure())
	//if err != nil {
	//	panic(err.Error())
	//}
	//defer userconn.Close()
	bg.um = userman.NewUserManager([]string{}) //gamelangpb.NewUserServiceClient(userconn)

	//init wm
	//worldconn, err := grpc.Dial(config.Manager.WorldManager, grpc.WithInsecure())
	//if err != nil {
	//	panic(err.Error())
	//}
	//defer worldconn.Close()
	bg.wm = worldman.NewWorldManager([]string{}) //gamelangpb.NewWorldServiceClient(worldconn)

	bg.notichan = make(chan glbingopb.Event)
	bg.bingomanager = bingoman.NewBingoManager([]string{}, bg.notichan)
	go bg.workNotifications()

	//init webserver
	wsConfig := glws.Config{}
	wsConfig.Http.ListenIP = config.Http.ListenIP
	wsConfig.Http.ListenPort = config.Http.ListenPort
	bg.webserver = glws.NewWebserver(wsConfig)

}

func (bg *BingoGame) workNotifications() {
	for {
		log.Print("waiting for notification")
		event := <-bg.notichan
		log.Println("new notification", event)
		eventJson, err := json.Marshal(event)
		if err != nil {
			log.Printf("workNotifications - err marshalling event \"%s\"\n%s", event, err.Error())
		}

		notify := glwss.WSMsg{Mtype: "game:event", Payload: string(eventJson)}

		worldgamename := strings.Split(event.Gamename, "\uffff")
		worldname := worldgamename[0]
		gamename := worldgamename[1]

		// if the game is a common game or its a win event all players have to be notified
		if gamename == "common" || event.Type == "Win" {
			world := gamelangpb.World{}
			worlds := bg.wm.List()
			for _, world := range worlds {
				if world.GetName() == worldname {
					log.Println("sending world\n", world)
					break
				}
			}
			//world, err := bg.wm.GetWorld(context.Background(), &gamelangpb.World{Name: worldname})
			if err != nil {
				log.Printf("workNotifications - err getting world msg \"%s\"\n%s", worldname, err.Error())
				return
			}
			for _, user := range world.Users {
				go bg.wssserver.Write(user.Name, notify)
			}
		} else {
			bg.wssserver.Write(event.Username, notify)
		}
	}
}

// RenderEndpoints returns the endpoints that are served by the webserver
func (bg *BingoGame) RenderEndpoints() []glws.Endpoint {
	endpoints := []glws.Endpoint{}
	endpoints = append(endpoints, glws.Endpoint{"GET", "/",
		func(ctx echo.Context) error {
			log.Print("/: get")
			ok, user := bg.webserver.CheckSess(ctx)
			if !ok {
				log.Print("/: not found")
				return ctx.Redirect(301, "/login")
			}

			data := struct {
				Username string
				Worlds   []gamelangpb.World
				Items    []string
			}{
				Username: user.Name,
				Worlds: func() []gamelangpb.World {
					worlds := bg.wm.List()
					return worlds
				}(),
				Items: []string{
					"user",
					"world",
				},
			}
			log.Printf("/: user \"%v\" found", user.Name)
			return ctx.Render(http.StatusOK, "index", data)
		}})

	endpoints = append(endpoints, glws.Endpoint{"GET", "/user", func(ctx echo.Context) error {
		log.Print("/user: get")

		log.Print("/user: sending register form")
		return ctx.Render(http.StatusOK, "registerform", "")

	}})

	// POST to /user creates a user
	endpoints = append(endpoints, glws.Endpoint{"POST", "/user", func(ctx echo.Context) error {
		username := ctx.FormValue("username")
		password := ctx.FormValue("password")

		log.Printf("new user request: %v", username)

		newUser := gamelangpb.User{Name: username, Password: []byte(password)}
		id := rand.Uint32()
		name := newUser.GetName()
		_, err := bg.um.Create(id, name, password)
		//user, err := bg.userclient.CreateUser(context.Background(), &newUser)
		if err != nil {
			return ctx.String(501, "user creation failed")
		}

		bg.webserver.NewSession(newUser, ctx)

		log.Printf("new user created: %v", username)
		return ctx.Redirect(302, "/")
	}})

	endpoints = append(endpoints, glws.Endpoint{"GET", "/login", func(ctx echo.Context) error {
		log.Print("/login: get")
		// check if the login page was called with failed parameter (set by /login POST)
		failedBefore := false
		if value := ctx.FormValue("failed"); value == "1" {
			failedBefore = true
		}

		ok, user := bg.webserver.CheckSess(ctx)
		if ok {
			// logged in
			log.Printf("/login: user %v logged in", user.Name)
			return ctx.String(http.StatusOK, "hello, "+user.Name+"\nyou are logged in.")
		}
		// not logged in
		log.Print("/login: not logged in")
		return ctx.Render(http.StatusOK, "loginform", failedBefore)

	}})

	endpoints = append(endpoints, glws.Endpoint{"POST", "/login", func(ctx echo.Context) error {
		log.Print("/login: post")
		username := ctx.FormValue("username")
		password := ctx.FormValue("password")

		// if the session is already set, the user is already logged in
		ok, _ := bg.webserver.CheckSess(ctx)
		if ok {
			return ctx.String(http.StatusOK, "you are already logged in.")
		}

		log.Printf("user login attempt: %v", username)
		//checkuser := gamelangpb.User{Name: username, Password: []byte(password)}
		user, ok := bg.um.Check(username, password) //bg.userclient.CheckUser(context.Background(), &checkuser)
		if ok {                                     //err == nil {
			bg.webserver.NewSession(user, ctx)
			log.Printf("user login suceeded: %v", username)
			return ctx.String(http.StatusOK, "hello, "+username+"\nyou are now logged in.")
		}
		log.Printf("user login failed: %v\n%s", username)
		return ctx.Redirect(302, "/login?failed=1")
	}})

	endpoints = append(endpoints, glws.Endpoint{"GET", "/admin", func(ctx echo.Context) error {
		log.Print("/admin: get")

		ok, user := bg.webserver.CheckSess(ctx)
		if !ok {
			log.Print("/admin: not logged in")
			return ctx.String(http.StatusUnauthorized, "not logged in")
		}

		log.Println("/admin:", user.Name)
		if user.Admin != true && user.Name != "admin" {
			log.Println("/admin: user is not an admin", user.Name)
			return ctx.String(http.StatusUnauthorized, "you are not an admin")
		}

		data := struct {
			Users    []gamelangpb.User
			Sessions map[string]gamelangpb.User
			Worlds   []gamelangpb.World
		}{
			Users: func() []gamelangpb.User {
				users := bg.um.List()
				return users
			}(),
			Sessions: bg.webserver.ListSess(),
			Worlds: func() []gamelangpb.World {
				worlds := bg.wm.List()
				return worlds
			}(),
		}

		err := ctx.Render(http.StatusOK, "admin", data)
		if err != nil {
			log.Println(err)
		}
		return err
	}})

	// worldbuilder
	// Get to /World returns register form
	endpoints = append(endpoints, glws.Endpoint{"GET", "/world", func(ctx echo.Context) error {
		ok, _ := bg.webserver.CheckSess(ctx)
		if !ok {
			log.Print("/World: not logged in")
			return ctx.String(http.StatusUnauthorized, "not logged in")
		}
		data := struct {
			Worlds []gamelangpb.World
		}{
			Worlds: func() []gamelangpb.World {
				worlds := bg.wm.List()
				return worlds
			}(),
		}
		err := ctx.Render(http.StatusOK, "worldlobby", data)
		if err != nil {
			log.Print(err)
		}
		return err

	}})

	endpoints = append(endpoints, glws.Endpoint{"POST", "/world", func(ctx echo.Context) error {
		ok, _ := bg.webserver.CheckSess(ctx)
		if !ok {
			log.Print("/World: not logged in")
			return ctx.String(http.StatusUnauthorized, "not logged in")
		}
		worldname := ctx.FormValue("worldname")

		world := &gamelangpb.World{Name: worldname}
		lobbygame := CraftLobbygame(fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby"))
		world.Games = []string{lobbygame.Name}
		log.Println("/World: creating world_\n", world)
		id := rand.Uint32()
		world.Id = id
		_, err := bg.wm.Create(*world)
		if err != nil {
			log.Println("error creating world", err)
			return ctx.String(http.StatusInternalServerError, "error creating world")
		}
		err = bg.bingomanager.Create(lobbygame)
		if err != nil {
			log.Println("error creating world", err)
			return ctx.String(http.StatusInternalServerError, "error creating world")
		}

		return ctx.Redirect(http.StatusFound, "/world")
	}})

	endpoints = append(endpoints, glws.Endpoint{"POST", "/world/join", func(ctx echo.Context) error {
		log.Print("join attemt")
		ok, user := bg.webserver.CheckSess(ctx)
		if !ok {
			log.Print("/World/join: not logged in")
			return ctx.String(http.StatusUnauthorized, "not logged in")
		}
		worldname := ctx.FormValue("worldname")
		log.Println("joining", user.Name, "to", worldname)
		joinworld := gamelangpb.World{Name: worldname, Users: []*gamelangpb.User{}}
		joinworld.Users = append(joinworld.Users, &user)
		err := bg.wm.Join(worldname, &user)
		if err != nil {
			log.Printf("error joining %s to %s\n%s", user.Name, worldname, err.Error())
			return ctx.String(501, "join failed")
		}
		log.Println("join of", user.Name, "to", worldname, "complete")
		return ctx.Redirect(302, "/world")
	}})
	return endpoints
}

//RenderHandler renders the endpoints that will be served via websocket
func (bg *BingoGame) RenderHandler() []glwss.Handler {
	handler := []glwss.Handler{}
	handler = append(handler, glwss.Handler{MType: "echo", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		log.Printf("echo from \"%v\" with \"%s\"", user, msg.Payload)
		remsg := glwss.WSMsg{Mtype: "echo", Payload: msg.Payload}
		return remsg, nil
	}})

	handler = append(handler, glwss.Handler{MType: "world:create", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		createWorld := gamelangpb.World{Name: msg.Payload}
		lobbygame := CraftLobbygame(fmt.Sprintf("%s\uffff%s", createWorld.GetName(), "lobby"))
		createWorld.Games = []string{lobbygame.Name}
		id := rand.Uint32()
		createWorld.Id = id
		_, err := bg.wm.Create(*&createWorld)
		if err != nil {
			remsg := glwss.WSMsg{Mtype: "world:create", Payload: "failed"}
			return remsg, err
		}
		err = bg.bingomanager.Create(lobbygame)
		if err != nil {
			remsg := glwss.WSMsg{Mtype: "world:create", Payload: "failed"}
			return remsg, err
		}
		log.Println("world:create created Lobbygame:\n", lobbygame)
		createworldJSON, _ := json.Marshal(createWorld)
		remsg := glwss.WSMsg{Mtype: "world:create", Payload: string(createworldJSON)}
		return remsg, nil
	}})

	handler = append(handler, glwss.Handler{MType: "world:remove", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		err := bg.wm.Remove(msg.Payload)
		if err != nil {
			remsg := glwss.WSMsg{Mtype: "world:remove", Payload: "failed"}
			return remsg, err
		}
		remsg := glwss.WSMsg{Mtype: "world:remove", Payload: "success"}
		return remsg, nil
	}})

	handler = append(handler, glwss.Handler{MType: "world:join", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		worldname := msg.Payload
		err := bg.wm.Join(worldname, &user)
		if err != nil {
			remsg := glwss.WSMsg{Mtype: "world:join", Payload: "failed"}
			return remsg, err
		}
		remsg := glwss.WSMsg{Mtype: "world:join", Payload: "success"}
		return remsg, nil
	}})

	handler = append(handler, glwss.Handler{MType: "world:list", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		log.Print("world:list")
		worldlist := bg.wm.List()
		worldlistJSON, err := json.Marshal(worldlist)
		if err != nil {
			log.Print(err.Error())
			msg := glwss.WSMsg{Mtype: "world:list", Payload: "failed"}
			return msg, err
		}

		log.Print("world:list", "sending list")
		remsg := glwss.WSMsg{Mtype: "world:list", Payload: string(worldlistJSON)}
		return remsg, nil
	}})

	handler = append(handler, glwss.Handler{MType: "game:get", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		log.Println("game:get new get request:\n\"", msg.Payload, "\"")
		var world gamelangpb.World
		worldname := msg.Payload
		worlds := bg.wm.List()
		for _, world = range worlds {
			if world.GetName() == worldname {
				log.Println("sending world\n", world)
				break
			}
		}

		log.Println("game:get got world:\n\"", world, "\"")
		lobbygame := glbingopb.Bingo{}
		commongame := glbingopb.Bingo{}
		usergame := glbingopb.Bingo{}
		for _, game := range world.GetGames() {
			log.Println("searching games", game)
			if game == fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby") {
				log.Println("found lobbygame", game)
				searchGame := glbingopb.Bingo{Name: game}
				lobbygame = bg.bingomanager.Get(searchGame)
			}
			if game == fmt.Sprintf("%s\uffff%s", world.GetName(), "common") {
				log.Println("found commongame", game)
				searchGame := glbingopb.Bingo{Name: game}
				commongame = bg.bingomanager.Get(searchGame)
			}
			if user.Name == strings.Split(game, "\uffff")[1] {
				log.Println("found usergame", game)
				searchGame := glbingopb.Bingo{Name: game}
				usergame = bg.bingomanager.Get(searchGame)
				break
			}
		}

		returngame := glbingopb.Bingo{}
		if usergame.Name == fmt.Sprintf("%s\uffff%s", world.GetName(), user.Name) {
			log.Println("sending usergame", usergame.Name)
			returngame = usergame
		} else if commongame.Name == fmt.Sprintf("%s\uffff%s", world.GetName(), "common") {
			log.Println("sending commongame", commongame.Name)
			returngame = commongame
		} else if lobbygame.Name == fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby") {
			log.Println("sending lobbygame", lobbygame.Name)
			returngame = lobbygame
		} else {
			// if get returned empty game
			returngame = CraftLobbygame(fmt.Sprintf("%s\uffff%s", world.GetName(), "lobby"))
			bg.bingomanager.Create(returngame)
			log.Print("could not find lobbygame, so created one")

		}

		gameJSON, err := json.Marshal(returngame)
		if err != nil {
			remsg := glwss.WSMsg{Mtype: "game:get", Payload: "failed"}
			return remsg, err
		}
		log.Println("game:get sending game:\n", string(gameJSON))
		remsg := glwss.WSMsg{Mtype: "game:get", Payload: string(gameJSON)}
		return remsg, nil

	}})

	handler = append(handler, glwss.Handler{MType: "game:start", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		log.Println("game:start new start request:\n", msg.Payload)
		startgame := glbingopb.Bingo{}
		err := json.Unmarshal([]byte(msg.Payload), &startgame)
		if err != nil {
			log.Printf("game:start - err unmarshalling startgame \"%s\"\n%s", msg, err.Error())
			remsg := glwss.WSMsg{Mtype: "game:start", Payload: "failed"}
			return remsg, err
		}
		searchworld := gamelangpb.World{Name: strings.Split(startgame.Name, "\uffff")[0]}
		startworld, err := bg.wm.GetWorld(context.Background(), &searchworld)
		if err != nil {
			log.Printf("game:start - err unmarshalling startworld \"%s\"\n%s", msg, err.Error())
			remsg := glwss.WSMsg{Mtype: "game:start", Payload: "failed"}
			return remsg, err
		}

		// start game
		for _, option := range startgame.Options {
			if option.Key == "common game" && option.Bool {
				newGame := bg.bingomanager.CraftGame(rand.Uint32(), fmt.Sprintf("%s\uffffcommon", startworld.Name), startgame.Textlist, startgame.Numrows, startgame.Numcols, startgame.Options...)
				bg.bingomanager.Create(newGame)
				addworld := gamelangpb.World{Name: startworld.Name, Games: []string{newGame.Name}}
				bg.wm.AddGameWorld(context.Background(), &addworld)
				jsonGame, err := json.Marshal(newGame)
				if err != nil {
					log.Printf("write: err marshalling game \"%s\"\n%s", msg, err.Error())
				}
				remsg := glwss.WSMsg{Mtype: "game:get", Payload: string(jsonGame)}
				if err != nil {
					log.Printf("write: err marshalling msg \"%s\"\n%s", msg, err.Error())
				}
				for _, user := range startworld.Users {
					go bg.wssserver.Write(user.Name, remsg)
				}
			}
		}

		remsg := glwss.WSMsg{Mtype: "game:start", Payload: "sucess"}
		return remsg, nil
	}})

	handler = append(handler, glwss.Handler{MType: "game:mod", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		modgame := &glbingopb.Bingo{}
		err := json.Unmarshal([]byte(msg.Payload), modgame)
		if err != nil {
			log.Printf("game:mod - err unmarshalling msg \"%s\"\n%s", msg, err.Error())
			remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "failed"}
			return remsg, nil
		}
		bg.bingomanager.Modify(*modgame)
		searchworld := gamelangpb.World{Name: strings.Split(modgame.Name, "\uffff")[0]}
		world, err := bg.wm.GetWorld(context.Background(), &searchworld)
		if err != nil {
			log.Printf("game:mod - err getting world msg \"%s\"\n%s", msg, err.Error())
			remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "failed"}
			return remsg, nil
		}
		notify := glwss.WSMsg{Mtype: "game:get", Payload: msg.Payload}
		for _, user := range world.Users {
			go bg.wssserver.Write(user.Name, notify)
		}
		bg.bingomanager.CheckWin()
		remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "success"}
		return remsg, nil
	}})

	handler = append(handler, glwss.Handler{MType: "game:event", Callback: func(msg glwss.WSMsg, user gamelangpb.User) (glwss.WSMsg, error) {
		newevent := &glbingopb.Event{}
		err := json.Unmarshal([]byte(msg.Payload), newevent)
		if err != nil {
			log.Printf("game:event - err unmarshalling msg \"%s\"\n%s", msg, err.Error())
			remsg := glwss.WSMsg{Mtype: "game:event", Payload: "failed"}
			return remsg, nil
		}
		newevent.Username = user.Name
		log.Println("game:event new event", newevent)

		worldgamename := strings.Split(newevent.Gamename, "\uffff")
		worldname := worldgamename[0]
		gamename := worldgamename[1]
		// if the game is not a common game add all games from that world to neighborgames
		if gamename != "common" {
			world, err := bg.wm.GetWorld(context.Background(), &gamelangpb.World{Name: worldname})
			if err != nil {
				log.Printf("workNotifications - err getting world msg \"%s\"\n%s", worldname, err.Error())
				remsg := glwss.WSMsg{Mtype: "game:mod", Payload: "failed"}
				return remsg, nil
			}
			for _, game := range world.Games {
				if strings.Split(game, "\uffff")[1] != "lobby" {
					newevent.Neighborgames = append(newevent.Neighborgames, game)
				}
			}
		}

		err = bg.bingomanager.HandleEvent(*newevent)
		if err != nil {
			if err.Error() == "game finished" {
				reevent := glbingopb.Event{Type: "Win", Gamename: newevent.Gamename}
				eventJson, err := json.Marshal(reevent)
				if err != nil {
					log.Printf("workNotifications - err marshalling event \"%s\"\n%s", reevent, err.Error())
				}
				notify := glwss.WSMsg{Mtype: "game:event", Payload: string(eventJson)}
				return notify, nil
			}
			return glwss.WSMsg{Mtype: "game:event", Payload: "failed"}, err
		}
		remsg := glwss.WSMsg{Mtype: "game:event", Payload: "success"}
		bg.bingomanager.CheckWin()
		return remsg, nil

	}})

	return handler
}

func CraftLobbygame(name string) glbingopb.Bingo {
	lobbygame := glbingopb.Bingo{Name: name}
	lobbygame.Options = append(lobbygame.Options, &glbingopb.Option{Key: "unique fields", Bool: true, Type: 1})
	lobbygame.Options = append(lobbygame.Options, &glbingopb.Option{Key: "common game", Bool: true, Type: 1})
	lobbygame.Numcols = 5
	lobbygame.Numrows = 5
	lobbygame.Textlist = []string{"word"}
	return lobbygame
}

func main() {
	var config BingoConfig
	err := gcfg.ReadFileInto(&config, "config")
	if err != nil {
		if strings.Count(err.Error(), "no such file or directory") > 0 {
			config = getDefaultConfig()
		} else {
			log.Fatal(err)
		}
	}
	log.Print("config read")
	log.Printf("%+v", config)

	bingogame := BingoGame{}
	bingogame.Init(config)

	bingogame.webserver.InitEndpoints(bingogame.RenderEndpoints())
	go bingogame.webserver.Run()

	bingogame.wssserver = glwss.NewWSServer([]string{}, bingogame.userclient)
	handler := bingogame.RenderHandler()
	keys := ""
	for _, k := range handler {
		keys = fmt.Sprintf("%s\n%v", keys, k)
	}
	log.Println("available MTypes:\n", keys, "\n----")
	bingogame.wssserver.RegisterHandler(handler)

	http.HandleFunc("/ws", bingogame.wssserver.WebsocketHandler)
	log.Println("websocket listening on", fmt.Sprintf("%s:%s", config.WS.ListenIP, config.WS.ListenPort))
	log.Print(http.ListenAndServe(fmt.Sprintf("%s:%s", config.WS.ListenIP, config.WS.ListenPort), nil).Error())
}
