package main

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"crypto/rand"
	"database/sql"
	"encoding/base64"
	"encoding/json"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	_ "modernc.org/sqlite"
)

func initUser(channelID string, isModerator bool, isStreamer bool) {
	_, err := conn.Exec("INSERT OR IGNORE INTO users (channelID, points, isModerator, isStreamer) VALUES (?, ?, ?, ?)", channelID, 0, isModerator, isStreamer)
	if err != nil {
		panic("Error inserting user: " + err.Error())
	}
}

func getPoints(channelID string) int64 {
	var points int64
	err := conn.QueryRow("SELECT points FROM users WHERE channelID = ?", channelID).Scan(&points)
	if err != nil {
		panic("Error querying database: " + err.Error())
	}

	return points
}

func updatePoints(channelID string, points int64) {
	_, err := conn.Exec("UPDATE users SET points = ? WHERE channelID = ?", points, channelID)
	if err != nil {
		panic("Error updating user points: " + err.Error())
	}
}

func addPoints(channelID string, points int64) {
	updatePoints(channelID, getPoints(channelID)+points)
}

func minusPoints(channelID string, points int64) {
	updatePoints(channelID, getPoints(channelID)-points)
}

//goland:noinspection GoUnusedFunction
func userIsStreamer(channelID string) bool {
	var isStreamer bool
	err := conn.QueryRow("SELECT isStreamer FROM users WHERE channelID = ?", channelID).Scan(&isStreamer)
	if err != nil {
		panic("Error querying database: " + err.Error())
	}

	return isStreamer
}

func userIsModerator(channelID string) bool {
	var isModerator bool
	err := conn.QueryRow("SELECT isModerator FROM users WHERE channelID = ?", channelID).Scan(&isModerator)
	if err != nil {
		panic("Error querying database: " + err.Error())
	}

	return isModerator
}

type LiveChatMessages struct {
	Items []Items `json:"items"`
}

type Items struct {
	AutorDetails AuthorDetails `json:"authorDetails"`
	Snippet      Snippet       `json:"snippet"`
}

type AuthorDetails struct {
	DisplayName     string `json:"displayName"`
	ChannelID       string `json:"channelId"`
	IsChatOwner     bool   `json:"isChatOwner"`
	IsChatModerator bool   `json:"isChatModerator"`
}

type Snippet struct {
	PublishedAt        string             `json:"publishedAt"`
	TextMessageDetails TextMessageDetails `json:"textMessageDetails"`
}

type TextMessageDetails struct {
	MessageText string `json:"messageText"`
}

func scrape(liveChatID string) {
	for {
		if streaming == true {
			// Check for chat messages
			println("Scary, we are expending a Google API credit (on stream)!")

			response, err := http.Get("https://www.googleapis.com/youtube/v3/liveChat/messages?liveChatId=" + liveChatID + "&part=snippet,authorDetails&key=" + configFile.ApiKey)
			if err != nil {
				println("Error getting chat messages: " + err.Error() + ", trying again in 20 seconds")
				time.Sleep(time.Second * 20)
				continue
			}

			// Read the response
			var responseJSON LiveChatMessages
			err = json.NewDecoder(response.Body).Decode(&responseJSON)
			if err != nil {
				panic("Error decoding JSON: " + err.Error())
			}

			// Check the status code
			if response.StatusCode != 200 {
				println("Error getting chat messages: ", response.Status)
				return
			} else {
				// Iterate through each live chat message
				for _, item := range responseJSON.Items {
					println("Processing message from ", item.AutorDetails.DisplayName)
					println("Message: ", item.Snippet.TextMessageDetails.MessageText)
					// Check if the message starts with !verify
					if strings.HasPrefix(item.Snippet.TextMessageDetails.MessageText, "!link ") {
						println("User ", item.AutorDetails.DisplayName, " is verifying")
						// Get the channel ID
						channelID := item.AutorDetails.ChannelID

						// Check if the user is in pendingSessions
						pendingSession, ok := pendingSessions[strings.TrimPrefix(item.Snippet.TextMessageDetails.MessageText, "!link ")]
						if !ok {
							// This user does not need to be verified
							continue
						}

						// Add the user to the sessions map
						sessions[pendingSession.IP] = channelID
						initUser(channelID, item.AutorDetails.IsChatModerator, item.AutorDetails.IsChatOwner)

						// Call the event callback
						pendingSession.EventCallback()

						// Remove the user from the pendingSessions map
						delete(pendingSessions, strings.TrimPrefix("!verify ", item.Snippet.TextMessageDetails.MessageText))
					} else {
						earliestSentMessage, ok := unclaimedMessages[item.AutorDetails.ChannelID]
						publishedTime, err := time.Parse(time.RFC3339Nano, item.Snippet.PublishedAt)
						if err != nil {
							println("Error parsing time: " + err.Error())
						} else if !ok || publishedTime.Before(earliestSentMessage) {
							if publishedTime.After(streamingSince) {
								println("New message from ", item.AutorDetails.DisplayName)
								unclaimedMessages[item.AutorDetails.ChannelID] = publishedTime
							}
						}
					}
				}

				// Close the response body
				err := response.Body.Close()
				if err != nil {
					println("Error closing response body: " + err.Error())
				}

				// Wait for the rate because Google likes to screw us over
				time.Sleep(time.Second * time.Duration(configFile.Rate))
			}
		} else {
			return
		}
	}
}

func getUserSecondDifference(channelID string) (int64, bool) {
	earliestSentMessage, ok := unclaimedMessages[channelID]
	if !ok {
		return 0, false
	}

	return int64(time.Now().Sub(earliestSentMessage).Seconds()), true
}

func getLiveChatID(videoID string) string {
	response, err := http.Get("https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=" + videoID + "&key=" + configFile.ApiKey)
	if err != nil {
		panic("Error getting live chat ID: " + err.Error())
	}

	var responseJSON map[string]interface{}
	err = json.NewDecoder(response.Body).Decode(&responseJSON)
	if err != nil {
		panic("Error decoding JSON: " + err.Error())
	}

	println(responseJSON)

	liveChatID := responseJSON["items"].([]interface{})[0].(map[string]interface{})["liveStreamingDetails"].(map[string]interface{})["activeLiveChatId"].(string)

	return liveChatID
}

type PendingSession struct {
	IP            string
	EventCallback func()
}

var (
	conn              *sql.DB
	unclaimedMessages = make(map[string]time.Time)
	sessions          = make(map[string]string)
	pendingSessions   = make(map[string]PendingSession)
	streamingSince    time.Time
	configFile        Config
	streaming         bool
	upgrade           = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
		CheckOrigin: func(r *http.Request) bool {
			if r.Header.Get("Origin") == configFile.Origin {
				return true
			} else {
				return false
			}
		},
	}
)

func giveBetPoints(correct int) {
	for better, bet := range bets {
		if bet.answer == correct {
			addPoints(better, bet.amount*stakesMultiplier)
		}
	}

	endBet()
}

func endBet() {
	bettingOpen = false
	question = ""
	possibleAnswers = nil
	timeToBet = 0
	bets = nil
}

func startBet(q string, a []string, d time.Duration) {
	bettingOpen = true
	question = q
	possibleAnswers = make(map[int]string)
	for i, answer := range a {
		possibleAnswers[i] = answer
	}
	timeToBet = d
	bets = make(map[string]Bet)
}

var (
	stakesMultiplier int64
	bettingOpen      bool
	question         string
	possibleAnswers  map[int]string
	timeToBet        time.Duration
	bets             map[string]Bet
)

type Bet struct {
	amount int64
	answer int
}

type Config struct {
	ApiKey     string  `json:"key"`
	Origin     string  `json:"origin"`
	Rate       float64 `json:"rate"`
	Multiplier float64 `json:"multiplier"`
}

func main() {
	// Connect to the database
	var err error
	conn, err = sql.Open("sqlite", "database.db")
	if err != nil {
		panic("Error connecting to database: " + err.Error())
	}

	// Read in config.json
	configBytes, err := os.ReadFile("config.json")
	if err != nil {
		panic("Error reading config.json: " + err.Error())
	}

	// Parse the JSON
	err = json.Unmarshal(configBytes, &configFile)
	if err != nil {
		panic("Error parsing config.json: " + err.Error())
	}

	// Create the user table if it doesn't exist
	_, err = conn.Exec("CREATE TABLE IF NOT EXISTS users (channelID TEXT NOT NULL UNIQUE, points INTEGER NOT NULL, isModerator BOOLEAN NOT NULL DEFAULT FALSE, isStreamer BOOLEAN NOT NULL DEFAULT FALSE)")
	if err != nil {
		panic("Error creating users table: " + err.Error())
	}

	// Add shounic by default
	_, err = conn.Exec("INSERT OR IGNORE INTO users (channelID, points, isModerator, isStreamer) VALUES ('UCHlTEt24Yb4ylJFuWz7hXIw', 0, 1, 1)")
	if err != nil {
		panic("Error inserting shounic: " + err.Error())
	}

	// Set up the router
	gin.SetMode(gin.ReleaseMode)
	router := gin.New()

	// Set up the routes
	router.Static("/static", "./static")
	router.LoadHTMLGlob("./templates/*")

	// Ugh, why do we have to do this legally
	router.GET("/delete", func(c *gin.Context) {
		// Get the user's channel ID
		id, ok := sessions[c.ClientIP()]
		if !ok {
			c.JSON(403, gin.H{"error": "Invalid token"})
			return
		}

		// Delete everything from every map with the user's channel ID
		delete(unclaimedMessages, id)
		_, err := conn.Exec("DELETE FROM users WHERE channelID = ?", id)
		if err != nil {
			panic("Error deleting user: " + err.Error())
		}

		// Delete all access tokens with the user's channel ID
		for key, value := range sessions {
			if value == id {
				delete(sessions, key)
			}
		}

		c.JSON(200, "Data deleted")
	})

	// Link the user's account
	router.GET("/api/link", func(c *gin.Context) {
		// Kick over to a WebSocket
		conn, err := upgrade.Upgrade(c.Writer, c.Request, nil)
		if err != nil {
			println("Error upgrading connection: " + err.Error())
		}

		// Create a new nonce
		nonce := make([]byte, 16)
		_, err = rand.Read(nonce)
		if err != nil {
			println("Error generating nonce: " + err.Error())
		}

		// Base64 encode the nonce
		nonceBase64 := base64.StdEncoding.EncodeToString(nonce)

		// Set a read deadline
		var read bool
		go func() {
			time.Sleep(time.Second * 5)
			if !read {
				err := conn.WriteJSON(gin.H{
					"type":  "error",
					"error": "Did not respond to nonce",
				})
				if err != nil {
					return
				}
			}
		}()

		// Send the nonce
		err = conn.WriteJSON(gin.H{
			"type":  "nonce",
			"nonce": nonceBase64,
		})
		if err != nil {
			return
		}

		_, nonceResponse, err := conn.ReadMessage()
		if err != nil {
			return
		}

		read = true

		jsonResponse := make(map[string]interface{})
		err = json.Unmarshal(nonceResponse, &jsonResponse)
		if err != nil {
			println("Error unmarshalling nonce response: " + err.Error())
		}

		heartbeatStop := make(chan struct{})

		// If the response is ok, add the user to the pendingSessions map and start heartbeats
		if jsonResponse["type"] == "success" {
			pendingSessions[nonceBase64] = PendingSession{
				IP: c.ClientIP(),
				EventCallback: func() {
					// Write back to the WebSocket
					err = conn.WriteJSON(gin.H{"type": "success"})
					if err != nil {
						return
					}

					err = conn.Close()
					if err != nil {
						return
					}

					// Tell the heartbeat loop to stop
					close(heartbeatStop)
				},
			}
		}

		for {
			select {
			case <-heartbeatStop:
				return
			case <-time.After(time.Second * 5):
				err := conn.WriteJSON(gin.H{
					"type": "ping",
				})
				if err != nil {
					return
				}

				var read bool

				go func() {
					time.Sleep(time.Second * 5)
					if !read {
						err := conn.WriteJSON(gin.H{
							"type":  "error",
							"error": "Did not respond to ping",
						})
						if err != nil {
							return
						}
					}
				}()

				_, _, err = conn.ReadMessage()
				if err != nil {
					return
				}

				read = true
			}
		}
	})

	// Set up the betting route
	router.POST("/api/bet", func(c *gin.Context) {
		var data map[string]interface{}
		err := c.BindJSON(&data)
		if err != nil {
			c.JSON(400, gin.H{"error": "Invalid JSON"})
			return
		}

		// Check if betting is open
		if bettingOpen {
			// Get the user's channel ID
			id, ok := sessions[c.ClientIP()]
			if !ok {
				c.JSON(403, gin.H{"error": "Invalid token"})
				return
			}

			// Get the user's bet amount
			amountFloat, ok := data["amount"].(float64)
			if !ok {
				c.JSON(400, gin.H{"error": "Invalid bet"})
				return
			}

			amount := int64(amountFloat)

			// Check if the user has enough points
			if getPoints(id) < amount {
				c.JSON(400, gin.H{"error": "Not enough points"})
				return
			}

			// Get the user's bet answer
			answerFloat, ok := data["answer"].(float64)
			if !ok {
				c.JSON(400, gin.H{"error": "Invalid answer"})
				return
			}

			answer := int(answerFloat)

			// Check if the answer is valid
			if _, ok := possibleAnswers[answer]; !ok {
				c.JSON(400, gin.H{"error": "Invalid answer"})
				return
			}

			// Add the bet to the bets map
			bets[id] = Bet{
				amount: amount,
				answer: answer,
			}

			// Subtract the points from the user
			minusPoints(id, amount)

			c.JSON(200, gin.H{"message": "Bet placed"})
		} else {
			c.JSON(400, gin.H{"error": "Betting is closed"})
		}
	})

	router.POST("/api/startBet", func(c *gin.Context) {
		// Parse the JSON
		var data map[string]interface{}
		err := c.BindJSON(&data)
		if err != nil {
			c.JSON(400, gin.H{"error": "Invalid JSON"})
			return
		}

		// Get the user's channel ID
		id, ok := sessions[c.ClientIP()]
		if !ok {
			c.JSON(403, gin.H{"error": "Invalid token"})
			return
		}

		// Check if the user is a moderator
		if !userIsModerator(id) {
			c.JSON(403, gin.H{"error": "You must be a moderator to start a bet"})
			return
		}

		// Check if betting is open
		if bettingOpen {
			c.JSON(206, gin.H{"error": "There is already a running bet"})
			return
		}

		question := data["question"].(string)
		possibleAnswersRaw := data["possible"].([]interface{})
		var possibleAnswers []string
		for _, answer := range possibleAnswersRaw {
			possibleAnswers = append(possibleAnswers, answer.(string))
		}
		timeToBetFloat := data["timeToBet"].(float64)
		timeToBet := time.Duration(timeToBetFloat) * time.Second

		startBet(question, possibleAnswers, timeToBet)

		c.JSON(200, gin.H{"message": "Bet started"})
	})

	// Define the route for /api/getCurrentBet
	router.GET("/api/getCurrentBet", func(c *gin.Context) {
		if bettingOpen {
			var possibleAnswersList []string
			for _, answer := range possibleAnswers {
				possibleAnswersList = append(possibleAnswersList, answer)
			}
			c.JSON(200, gin.H{
				"question":  question,
				"possible":  possibleAnswersList,
				"timeToBet": timeToBet.Seconds(),
			})
			return
		} else {
			c.JSON(206, gin.H{"error": "There is not a running bet"})
			return
		}
	})

	// Define the route for /api/endBet
	router.POST("/api/endBet", func(c *gin.Context) {
		// Parse the JSON
		var data map[string]interface{}
		err := c.BindJSON(&data)
		if err != nil {
			c.JSON(400, gin.H{"error": "Invalid JSON"})
			return
		}

		// Authenticate the user
		accessToken := c.GetHeader("Authorization")
		if accessToken == "" {
			c.JSON(400, gin.H{"error": "No token provided"})
			return
		}

		// Check if the user is a moderator
		if !userIsModerator(accessToken) {
			c.JSON(403, gin.H{"error": "You must be a moderator to end a bet"})
			return
		}

		correctFloat, ok := data["correct"].(float64)
		if !ok {
			c.JSON(400, gin.H{"error": "Invalid correct answer"})
			return
		}

		correct := int(correctFloat)

		// Give the points to the correct betters
		giveBetPoints(correct)

		c.JSON(200, gin.H{"message": "Bet ended"})
	})

	// Define the route for /api/claimUnclaimedPoints
	router.GET("/api/claimUnclaimedPoints", func(c *gin.Context) {
		// Get the user's channel ID
		id, ok := sessions[c.ClientIP()]
		if !ok {
			c.JSON(403, gin.H{"error": "Invalid token"})
			return
		}

		// Get the user's second difference
		secondDifference, ok := getUserSecondDifference(id)
		if !ok {
			c.JSON(200, gin.H{"points": "0"})
			return
		}

		// Clear the earliest sent message
		delete(unclaimedMessages, id)

		// Issue the points
		addPoints(id, secondDifference*int64(configFile.Multiplier))

		// Return the points
		c.JSON(200, gin.H{"points": strconv.FormatInt(secondDifference, 10)})
	})

	// Define the route for /api/getUnclaimedPoints
	router.GET("/api/getUnclaimedPoints", func(c *gin.Context) {
		// Get the user's channel ID
		id, ok := sessions[c.ClientIP()]
		if !ok {
			c.JSON(403, gin.H{"error": "Invalid token"})
			return
		}

		// Get the user's second difference
		secondDifference, ok := getUserSecondDifference(id)
		if !ok {
			c.JSON(200, gin.H{"points": "0"})
			return
		}

		c.JSON(200, gin.H{"points": strconv.FormatInt(secondDifference, 10)})
	})

	// Define the route for /api/getPoints
	router.GET("/api/getPoints", func(c *gin.Context) {
		// Get the user's channel ID
		id, ok := sessions[c.ClientIP()]
		if !ok {
			c.JSON(403, gin.H{"error": "Invalid token"})
			return
		}

		c.JSON(200, gin.H{"points": getPoints(id)})
	})

	// Define the route for /api/startStream
	router.GET("/api/startStream", func(c *gin.Context) {
		// Parse the JSON
		var data map[string]interface{}
		err := c.BindJSON(&data)
		if err != nil {
			c.JSON(400, gin.H{"error": "Invalid JSON"})
			return
		}

		// Get the user's channel ID
		id, ok := sessions[c.ClientIP()]
		if !ok {
			c.JSON(403, gin.H{"error": "Invalid token"})
			return
		}

		// Check if the user is a streamer
		if !userIsModerator(id) {
			c.JSON(403, gin.H{"error": "You must be a moderator to start a stream"})
			return
		}

		// Set the live status
		streaming = true
		streamingSince = time.Now()
		videoID, ok := data["videoID"].(string)
		if !ok {
			c.JSON(400, gin.H{"error": "Invalid JSON"})
			return
		}

		// Start the chat message checker
		println("Starting chat message checker: ", videoID)

		go scrape(getLiveChatID(videoID))

		// Return 200
		c.JSON(200, gin.H{"message": "Stream started"})
	})

	// Define the route for /api/endStream
	router.GET("/api/endStream", func(c *gin.Context) {
		// Authenticate the user
		accessToken := c.GetHeader("Authorization")
		if accessToken == "" {
			c.JSON(400, gin.H{"error": "No token provided"})
			return
		}

		// Check if the user is a moderator
		if !userIsModerator(accessToken) {
			c.JSON(403, gin.H{"error": "You must be a moderator to end a stream"})
			return
		}

		// Set the live status
		streaming = false

		// Clear all unclaimed points
		for key := range unclaimedMessages {
			delete(unclaimedMessages, key)
		}

		// Clear the bets
		endBet()

		// Return 200
		c.JSON(200, gin.H{"message": "Stream ended"})
	})

	router.GET("/api/loggedIn", func(c *gin.Context) {
		// Get the user's channel ID
		_, ok := sessions[c.ClientIP()]
		if !ok {
			c.JSON(403, gin.H{"loggedIn": false})
			return
		}

		c.JSON(200, gin.H{"loggedIn": true})
	})

	// Now some static routes
	router.GET("/", func(c *gin.Context) {
		c.HTML(200, "index.html", gin.H{})
	})

	router.GET("/admin", func(c *gin.Context) {
		c.HTML(200, "admin.html", gin.H{})
	})

	router.GET("/link", func(c *gin.Context) {
		c.HTML(200, "link.html", gin.H{})
	})

	router.GET("/bet", func(c *gin.Context) {
		c.HTML(200, "bet.html", gin.H{})
	})

	router.GET("/privacy", func(c *gin.Context) {
		c.HTML(200, "privacy.html", gin.H{})
	})

	router.GET("/tos", func(c *gin.Context) {
		c.HTML(200, "tos.html", gin.H{})
	})

	// Start the server
	var address string
	if len(os.Args) < 2 {
		address = ":8080"
	} else {
		address = os.Args[1]
	}

	println("Start server on " + address)

	go func() {
		println("Shounic CMD started")
		for {
			var input string
			println("Enter command: \n" +
				"1. Stop server\n" +
				"2. Start stream\n" +
				"3. End stream\n" +
				"4. Force login\n" +
				"5. Force logout\n" +
				"6. Get user's IP")

			print("> ")
			_, err := fmt.Scanln(&input)
			if err != nil {
				println("Error reading input: " + err.Error())
			}

			switch input {
			case "1":
				println("OK: Stopping server")
				os.Exit(0)
			case "2":
				if !streaming {
					streaming = true
					var videoID string
					print("Enter video ID: ")
					_, err := fmt.Scanln(&videoID)
					if err != nil {
						println("Error reading input: " + err.Error())
					}
					go scrape(getLiveChatID(videoID))
					println("OK: Stream started")
				} else {
					println("ERR: Stream already started")
				}
			case "3":
				streaming = false
				println("OK: Stream ended")
			case "4":
				print("Enter channel ID: ")
				var channelID string
				_, err := fmt.Scanln(&channelID)
				if err != nil {
					println("Error reading input: " + err.Error())
				}

				print("Enter IP: ")
				var ip string
				_, err = fmt.Scanln(&ip)
				if err != nil {
					println("Error reading input: " + err.Error())
				}

				print("Is moderator? (y/n): ")
				var isModerator string
				_, err = fmt.Scanln(&isModerator)
				if err != nil {
					println("Error reading input: " + err.Error())
				}

				print("Is streamer? (y/n): ")
				var isStreamer string
				_, err = fmt.Scanln(&isStreamer)
				if err != nil {
					println("Error reading input: " + err.Error())
				}

				sessions[ip] = channelID
				initUser(channelID, isModerator == "y", isStreamer == "y")
				println("OK: Forced login")
			case "5":
				print("Enter IP: ")
				var ip string
				_, err := fmt.Scanln(&ip)
				if err != nil {
					println("Error reading input: " + err.Error())
				}

				delete(sessions, ip)
				println("OK: Forced logout")
			case "6":
				print("Enter channel ID: ")
				var channelID string
				_, err := fmt.Scanln(&channelID)
				if err != nil {
					println("Error reading input: " + err.Error())
				}

				for key, value := range sessions {
					if value == channelID {
						println("IP: " + key)
					}
				}

				println("OK: Got user's IP")
			default:
				println("ERR: Invalid command")
			}
		}
	}()

	err = router.Run(address)
	if err != nil {
		panic("Error starting server: " + err.Error())
	}
}