/*
Pagerino web application designed for management and various
views of devices and system-related data.
Author: Olek
*/
package main

import (
	"log"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	api_common "pagerino-web/pager_api/common"
	api_user "pagerino-web/pager_api/user"

	"github.com/joho/godotenv"

	"github.com/golang-jwt/jwt/v5"
	"golang.org/x/crypto/bcrypt"

	"net/http"

	"github.com/gin-gonic/gin"
)

// App objects and data needed througout the program
type AppData struct {
	ExitSig       chan os.Signal
	ApiUserClient api_user.UserServiceClient
	Timeout       time.Duration
}

func (data *AppData) FindEnv(name string) string {
	val := os.Getenv(name)
	if val == "" {
		log.Println("Missing config value for: " + name)
		data.ExitSig <- os.Interrupt
		log.Fatalln("Shutting down...")
	}

	return val
}

// === Server Render Data ===

// Basic User information
type UserData struct {
	Name       string
	ID         int64
	Authorized bool
}

// Webpage metadata
type PageMeta struct {
	Title       string
	Description string
	Icon        string
}

// === Utilities ===

// Shortcut to get split up URL from *gin.Context
func SplitPath(ctx *gin.Context) []string {
	return strings.Split(ctx.Request.URL.EscapedPath(), "/")
}

// === Helpers ===

// GenerateToken creates a signed JWT for a user
func GenerateToken(username string) (string, error) {
	expiration, err := time.ParseDuration(os.Getenv("SESSION_DURATION"))
	if err != nil {
		return "", err
	}

	now := time.Now()

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
		Subject:   username,
		ExpiresAt: jwt.NewNumericDate(now.Add(expiration)),
		IssuedAt:  jwt.NewNumericDate(now),
		Issuer:    os.Getenv("APP_NAME"),
	})
	return token.SignedString(os.Getenv("JWT_SECRET"))
}

// CheckAuth aborts with 401 if JWT is invalid or not present
func CheckAuth(ctx *gin.Context) {
	auth_header := ctx.GetHeader("Authorization")
	if auth_header == "" { // No token -> login
		ctx.Redirect(http.StatusTemporaryRedirect, "/login")
		ctx.Abort()
		return
	}

	// Expect "Bearer <token>"
	parts := strings.Split(auth_header, " ")
	if len(parts) != 2 || parts[0] != "Bearer" {
		ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid token format"})
		return
	}

	token_str := parts[1]
	claims := &jwt.RegisteredClaims{}

	token, err := jwt.ParseWithClaims(token_str, claims, func(token *jwt.Token) (any, error) {
		return os.Getenv("JWT_SECRET"), nil
	})

	if err != nil {
		ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Could not parse token"})
		log.Println("Failed to parse authentication token: " + token_str)
		return
	}

	if !token.Valid { // Token expired -> login
		//ctx.Redirect(http.StatusTemporaryRedirect, "/login")
		ctx.Status(200)
		ctx.Abort()
		return
	}

	// Store username in context for handlers/templates
	ctx.Set("username", claims.Subject)
}

// === Handlers ===

// loginHandler is a handler for POST forms in "/login"
func loginPost(ctx *gin.Context, app_data *AppData) {
	username := ctx.PostForm("username")
	password := ctx.PostForm("password")

	db_pass, err := app_data.ApiUserClient.GetPassword(ctx,
		&api_common.StrIndex{Id: &api_common.StrIndex_Name{Name: username}},
	)
	if err != nil {
		ctx.AbortWithStatusJSON(
			http.StatusInternalServerError,
			gin.H{"error": "Could not fetch user password"},
		)
		log.Println("Failed to fetch user password from DB")
		return
	}

	err = bcrypt.CompareHashAndPassword([]byte(db_pass.GetPassword()), []byte(password))
	if err != nil {
		ctx.AbortWithStatusJSON(
			http.StatusBadRequest,
			gin.H{"error": "User password does not match"},
		)
		return
	}

	token, err := GenerateToken(username)
	if err != nil {
		log.Println(" JWT generation fail")
		ctx.AbortWithStatusJSON(
			http.StatusInternalServerError,
			gin.H{"error": "Could not generate token"},
		)
		log.Println("Failed to generate user authentication token")
		return
	}

	ctx.Header("Authorization", "Bearer "+token)
	ctx.Status(http.StatusAccepted)
}

func main() {
	// = Exit signal
	Data := AppData{
		ExitSig: make(chan os.Signal),
	}
	signal.Notify(Data.ExitSig, syscall.SIGINT, syscall.SIGTERM)

	// = Load configs
	godotenv.Load("./config/.env")

	// = Setup logger
	log.SetFlags(log.Lmsgprefix | log.LUTC | log.Lshortfile)
	log.SetPrefix(Data.FindEnv("LOG_PREFIX") + " ")

	// === Router
	router := gin.Default()

	// Load assets and HTML templates
	router.Static("/static", "./layouts/static")
	router.LoadHTMLGlob("./layouts/**/*.tmpl")

	HOME_NAME := os.Getenv("HOME_NAME")

	// Redirect to home
	if HOME_NAME != "" {
		router.GET("/", func(ctx *gin.Context) {
			ctx.Redirect(http.StatusPermanentRedirect, "/"+HOME_NAME)
		})
	}

	// = Load timeout env var
	tmout, err := time.ParseDuration(Data.FindEnv("TIMEOUT"))
	if err != nil {
		log.Println("Format misconfiguration: TIMEOUT")
		return
	}

	Data.Timeout = tmout

	// == User API connection
	// dial_opts := []grpc.DialOption{
	// 	//grpc.WithBlock(),
	// 	grpc.WithInsecure(),
	// }
	// grpc_client, err := grpc.Dial(
	// 	"app_server:"+Data.FindEnv("SERVER_API_PORT"),
	// 	dial_opts...,
	// )
	// if err != nil {
	// 	log.Println("Failed to connect to gRPC API: " + err.Error())
	// 	return
	// }
	// Data.ApiUserClient = api_user.NewUserServiceClient(grpc_client)

	// Create auth group
	auth_group := router.Group("/", CheckAuth)

	// Starting home page
	auth_group.GET("/"+HOME_NAME, func(ctx *gin.Context) {
		var title string
		if HOME_NAME == "" {
			title = "Home"
		} else {
			title = strings.ToTitle(HOME_NAME)
		}

		ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
			"meta": PageMeta{
				Title:       title,
				Description: "Pagerino system home page",
				//Icon:        "",
			},
			"Page": "home",
		})
	})

	// Login page (escape auth handler)
	router.GET("/login", func(ctx *gin.Context) {
		ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
			"meta": PageMeta{
				Title:       "Login",
				Description: "Pagerino system login page",
				//Icon:        "",
			},
			"Page": "login",
		})
	})

	// Register page (escape auth handler)
	router.GET("/register", func(ctx *gin.Context) {
		ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
			"meta": PageMeta{
				Title:       "Register",
				Description: "Pagerino system register page",
				//Icon:        "",
			},
			"Page": "register",
		})
	})

	// Login attempt handler
	router.POST("/login", func(ctx *gin.Context) {
		loginPost(ctx, &Data)
	})

	// Run server in another thread for graceful shutdown
	go func() {
		if err := router.Run(":" + Data.FindEnv("WEB_PORT")); err != nil && err != http.ErrServerClosed {
			log.Println("HTTP Server error: " + err.Error())
		}
		log.Println("HTTP server shutdown.")

		Data.ExitSig <- syscall.SIGINT
	}()

	// = Graceful shutdown
	<-Data.ExitSig
	log.Println("Shutting down...")
}
