package main

import (
	"encoding/base64"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"
	"github.com/lithammer/shortuuid/v3"
)

func (app *appContext) webAuth() gin.HandlerFunc {
	return app.authenticate
}

// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
func CreateToken(userId, jfId string) (string, string, error) {
	var token, refresh string
	claims := jwt.MapClaims{
		"valid": true,
		"id":    userId,
		"exp":   strconv.FormatInt(time.Now().Add(time.Minute*20).Unix(), 10),
		"jfid":  jfId,
		"type":  "bearer",
	}

	tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
	if err != nil {
		return "", "", err
	}
	claims["exp"] = strconv.FormatInt(time.Now().Add(time.Hour*24).Unix(), 10)
	claims["type"] = "refresh"
	tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
	if err != nil {
		return "", "", err
	}
	return token, refresh, nil
}

func respond(code int, message string, gc *gin.Context) {
	resp := map[string]string{}
	if code == 200 || code == 204 {
		resp["response"] = message
	} else {
		resp["error"] = message
	}
	gc.JSON(code, resp)
	gc.Abort()
}

// Check header for token
func (app *appContext) authenticate(gc *gin.Context) {
	header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
	if header[0] != "Basic" {
		app.debug.Println("Invalid authentication header")
		respond(401, "Unauthorized", gc)
		return
	}
	auth, _ := base64.StdEncoding.DecodeString(header[1])
	creds := strings.SplitN(string(auth), ":", 2)
	token, err := jwt.Parse(creds[0], checkToken)
	if err != nil {
		app.debug.Printf("Auth denied: %s", err)
		respond(401, "Unauthorized", gc)
		return
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
	if err != nil {
		app.debug.Printf("Auth denied: %s", err)
		respond(401, "Unauthorized", gc)
		return
	}
	expiry := time.Unix(expiryUnix, 0)
	if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
		app.debug.Printf("Auth denied: Invalid token")
		respond(401, "Unauthorized", gc)
		return
	}
	userID := claims["id"].(string)
	jfID := claims["jfid"].(string)
	match := false
	for _, user := range app.users {
		if user.UserID == userID {
			match = true
			break
		}
	}
	if !match {
		app.debug.Printf("Couldn't find user ID \"%s\"", userID)
		respond(401, "Unauthorized", gc)
		return
	}
	gc.Set("jfId", jfID)
	gc.Set("userId", userID)
	app.debug.Println("Auth succeeded")
	gc.Next()
}

func checkToken(token *jwt.Token) (interface{}, error) {
	if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
		return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"])
	}
	return []byte(os.Getenv("JFA_SECRET")), nil
}

// getToken checks the header for a username and password, as well as checking the refresh cookie.
func (app *appContext) getToken(gc *gin.Context) {
	app.info.Println("Token requested (login attempt)")
	header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
	auth, _ := base64.StdEncoding.DecodeString(header[1])
	creds := strings.SplitN(string(auth), ":", 2)
	// check cookie first
	var userID, jfID string
	valid := false
	noLogin := false
	checkLogin := func() {
		if creds[0] == "" || creds[1] == "" {
			app.debug.Println("Auth denied: blank username/password")
			respond(401, "Unauthorized", gc)
			return
		}
		match := false
		for _, user := range app.users {
			if user.Username == creds[0] && user.Password == creds[1] {
				match = true
				app.debug.Println("Found existing user")
				userID = user.UserID
				break
			}
		}
		if !app.jellyfinLogin && !match {
			app.info.Println("Auth denied: Invalid username/password")
			respond(401, "Unauthorized", gc)
			return
		}
		if !match {
			var status int
			var err error
			var user map[string]interface{}
			user, status, err = app.authJf.authenticate(creds[0], creds[1])
			if status != 200 || err != nil {
				if status == 401 || status == 400 {
					app.info.Println("Auth denied: Invalid username/password (Jellyfin)")
					respond(401, "Unauthorized", gc)
					return
				}
				app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
				respond(500, "Jellyfin error", gc)
				return
			}
			jfID = user["Id"].(string)
			if app.config.Section("ui").Key("admin_only").MustBool(true) {
				if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) {
					app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0])
					respond(401, "Unauthorized", gc)
					return
				}
			}
			// New users are only added when using jellyfinLogin.
			userID = shortuuid.New()
			newUser := User{
				UserID: userID,
			}
			app.debug.Printf("Token generated for user \"%s\"", creds[0])
			app.users = append(app.users, newUser)
		}
		valid = true
	}
	checkCookie := func() {
		cookie, err := gc.Cookie("refresh")
		if err == nil && cookie != "" {
			for _, token := range app.invalidTokens {
				if cookie == token {
					if creds[0] == "" || creds[1] == "" {
						app.debug.Println("getToken denied: Invalid refresh token and no username/password provided")
						respond(401, "Unauthorized", gc)
						noLogin = true
						return
					}
					app.debug.Println("getToken: Invalid token but username/password provided")
					return
				}
			}
			token, err := jwt.Parse(cookie, checkToken)
			if err != nil {
				if creds[0] == "" || creds[1] == "" {
					app.debug.Println("getToken denied: Invalid refresh token and no username/password provided")
					respond(401, "Unauthorized", gc)
					noLogin = true
					return
				}
				app.debug.Println("getToken: Invalid token but username/password provided")
				return
			}
			claims, ok := token.Claims.(jwt.MapClaims)
			expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64)
			if err != nil {
				if creds[0] == "" || creds[1] == "" {
					app.debug.Printf("getToken denied: Invalid token (%s) and no username/password provided", err)
					respond(401, "Unauthorized", gc)
					noLogin = true
					return
				}
				app.debug.Printf("getToken: Invalid token (%s) but username/password provided", err)
				return
			}
			expiry := time.Unix(expiryUnix, 0)
			if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
				if creds[0] == "" || creds[1] == "" {
					app.debug.Printf("getToken denied: Invalid token (%s) and no username/password provided", err)
					respond(401, "Unauthorized", gc)
					noLogin = true
					return
				}
				app.debug.Printf("getToken: Invalid token (%s) but username/password provided", err)
				return
			}
			userID = claims["id"].(string)
			jfID = claims["jfid"].(string)
			valid = true
		}
	}
	checkCookie()
	if !valid && !noLogin {
		checkLogin()
	}
	if valid {
		token, refresh, err := CreateToken(userID, jfID)
		if err != nil {
			app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
			respond(500, "Couldn't generate token", gc)
			return
		}
		gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true)
		gc.JSON(200, map[string]string{"token": token})
	} else {
		gc.AbortWithStatus(401)
	}
}
