package api

import (
	"crypto/md5"
	"fmt"
	"net/http"
	"strings"
	"time"

	argon2 "github.com/alexedwards/argon2id"
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
	"reichard.io/antholume/database"
	"reichard.io/antholume/utils"
)

// Authorization Data
type authData struct {
	UserName string
	IsAdmin  bool
	AuthHash string
}

// KOSync API Auth Headers
type authKOHeader struct {
	AuthUser string `header:"x-auth-user"`
	AuthKey  string `header:"x-auth-key"`
}

func (api *API) authorizeCredentials(username string, password string) (auth *authData) {
	user, err := api.db.Queries.GetUser(api.db.Ctx, username)
	if err != nil {
		return
	}

	if match, err := argon2.ComparePasswordAndHash(password, *user.Pass); err != nil || !match {
		return
	}

	// Update auth cache
	api.userAuthCache[user.ID] = *user.AuthHash

	return &authData{
		UserName: user.ID,
		IsAdmin:  user.Admin,
		AuthHash: *user.AuthHash,
	}
}

func (api *API) authKOMiddleware(c *gin.Context) {
	session := sessions.Default(c)

	// Check Session First
	if auth, ok := api.getSession(session); ok {
		c.Set("Authorization", auth)
		c.Header("Cache-Control", "private")
		c.Next()
		return
	}

	// Session Failed -> Check Headers (Allowed on API for KOSync Compatibility)

	var rHeader authKOHeader
	if err := c.ShouldBindHeader(&rHeader); err != nil {
		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Incorrect Headers"})
		return
	}
	if rHeader.AuthUser == "" || rHeader.AuthKey == "" {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
		return
	}

	authData := api.authorizeCredentials(rHeader.AuthUser, rHeader.AuthKey)
	if authData == nil {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
		return
	}

	if err := api.setSession(session, *authData); err != nil {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
		return
	}

	c.Set("Authorization", *authData)
	c.Header("Cache-Control", "private")
	c.Next()
}

func (api *API) authOPDSMiddleware(c *gin.Context) {
	c.Header("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)

	user, rawPassword, hasAuth := c.Request.BasicAuth()

	// Validate Auth Fields
	if !hasAuth || user == "" || rawPassword == "" {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization Headers"})
		return
	}

	// Validate Auth
	password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
	authData := api.authorizeCredentials(user, password)
	if authData == nil {
		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
		return
	}

	c.Set("Authorization", *authData)
	c.Header("Cache-Control", "private")
	c.Next()
}

func (api *API) authWebAppMiddleware(c *gin.Context) {
	session := sessions.Default(c)

	// Check Session
	if auth, ok := api.getSession(session); ok {
		c.Set("Authorization", auth)
		c.Header("Cache-Control", "private")
		c.Next()
		return
	}

	c.Redirect(http.StatusFound, "/login")
	c.Abort()
}

func (api *API) authAdminWebAppMiddleware(c *gin.Context) {
	if data, _ := c.Get("Authorization"); data != nil {
		auth := data.(authData)
		if auth.IsAdmin {
			c.Next()
			return
		}
	}

	appErrorPage(c, http.StatusUnauthorized, "Admin Permissions Required")
	c.Abort()
}

func (api *API) appAuthLogin(c *gin.Context) {
	templateVars, _ := api.getBaseTemplateVars("login", c)

	username := strings.TrimSpace(c.PostForm("username"))
	rawPassword := strings.TrimSpace(c.PostForm("password"))

	if username == "" || rawPassword == "" {
		templateVars["Error"] = "Invalid Credentials"
		c.HTML(http.StatusUnauthorized, "page/login", templateVars)
		return
	}

	// MD5 - KOSync Compatiblity
	password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))
	authData := api.authorizeCredentials(username, password)
	if authData == nil {
		templateVars["Error"] = "Invalid Credentials"
		c.HTML(http.StatusUnauthorized, "page/login", templateVars)
		return
	}

	// Set Session
	session := sessions.Default(c)
	if err := api.setSession(session, *authData); err != nil {
		templateVars["Error"] = "Invalid Credentials"
		c.HTML(http.StatusUnauthorized, "page/login", templateVars)
		return
	}

	c.Header("Cache-Control", "private")
	c.Redirect(http.StatusFound, "/")
}

func (api *API) appAuthRegister(c *gin.Context) {
	if !api.cfg.RegistrationEnabled {
		appErrorPage(c, http.StatusUnauthorized, "Nice try. Registration is disabled.")
		return
	}

	templateVars, _ := api.getBaseTemplateVars("login", c)
	templateVars["Register"] = true

	username := strings.TrimSpace(c.PostForm("username"))
	rawPassword := strings.TrimSpace(c.PostForm("password"))

	if username == "" || rawPassword == "" {
		templateVars["Error"] = "Invalid User or Password"
		c.HTML(http.StatusBadRequest, "page/login", templateVars)
		return
	}
	password := fmt.Sprintf("%x", md5.Sum([]byte(rawPassword)))

	hashedPassword, err := argon2.CreateHash(password, argon2.DefaultParams)
	if err != nil {
		templateVars["Error"] = "Registration Disabled or User Already Exists"
		c.HTML(http.StatusBadRequest, "page/login", templateVars)
		return
	}

	// Generate auth hash
	rawAuthHash, err := utils.GenerateToken(64)
	if err != nil {
		log.Error("Failed to generate user token: ", err)
		templateVars["Error"] = "Failed to Create User"
		c.HTML(http.StatusBadRequest, "page/login", templateVars)
		return
	}

	// Get current users
	currentUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
	if err != nil {
		log.Error("Failed to check all users: ", err)
		templateVars["Error"] = "Failed to Create User"
		c.HTML(http.StatusBadRequest, "page/login", templateVars)
		return
	}

	// Determine if we should be admin
	isAdmin := false
	if len(currentUsers) == 0 {
		isAdmin = true
	}

	// Create user in DB
	authHash := fmt.Sprintf("%x", rawAuthHash)
	if rows, err := api.db.Queries.CreateUser(api.db.Ctx, database.CreateUserParams{
		ID:       username,
		Pass:     &hashedPassword,
		AuthHash: &authHash,
		Admin:    isAdmin,
	}); err != nil {
		log.Error("CreateUser DB Error:", err)
		templateVars["Error"] = "Registration Disabled or User Already Exists"
		c.HTML(http.StatusBadRequest, "page/login", templateVars)
		return
	} else if rows == 0 {
		log.Warn("User Already Exists:", username)
		templateVars["Error"] = "Registration Disabled or User Already Exists"
		c.HTML(http.StatusBadRequest, "page/login", templateVars)
		return
	}

	// Get user
	user, err := api.db.Queries.GetUser(api.db.Ctx, username)
	if err != nil {
		log.Error("GetUser DB Error:", err)
		templateVars["Error"] = "Registration Disabled or User Already Exists"
		c.HTML(http.StatusBadRequest, "page/login", templateVars)
		return
	}

	// Set session
	auth := authData{
		UserName: user.ID,
		IsAdmin:  user.Admin,
		AuthHash: *user.AuthHash,
	}
	session := sessions.Default(c)
	if err := api.setSession(session, auth); err != nil {
		appErrorPage(c, http.StatusUnauthorized, "Unauthorized.")
		return
	}

	c.Header("Cache-Control", "private")
	c.Redirect(http.StatusFound, "/")
}

func (api *API) appAuthLogout(c *gin.Context) {
	session := sessions.Default(c)
	session.Clear()
	if err := session.Save(); err != nil {
		log.Error("unable to save session")
	}

	c.Redirect(http.StatusFound, "/login")
}

func (api *API) koAuthRegister(c *gin.Context) {
	if !api.cfg.RegistrationEnabled {
		c.AbortWithStatus(http.StatusConflict)
		return
	}

	var rUser requestUser
	if err := c.ShouldBindJSON(&rUser); err != nil {
		log.Error("Invalid JSON Bind")
		apiErrorPage(c, http.StatusBadRequest, "Invalid User Data")
		return
	}

	if rUser.Username == "" || rUser.Password == "" {
		log.Error("Invalid User - Empty Username or Password")
		apiErrorPage(c, http.StatusBadRequest, "Invalid User Data")
		return
	}

	// Generate password hash
	hashedPassword, err := argon2.CreateHash(rUser.Password, argon2.DefaultParams)
	if err != nil {
		log.Error("Argon2 Hash Failure:", err)
		apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
		return
	}

	// Generate auth hash
	rawAuthHash, err := utils.GenerateToken(64)
	if err != nil {
		log.Error("Failed to generate user token: ", err)
		apiErrorPage(c, http.StatusBadRequest, "Unknown Error")
		return
	}

	// Get current users
	currentUsers, err := api.db.Queries.GetUsers(api.db.Ctx)
	if err != nil {
		log.Error("Failed to check all users: ", err)
		apiErrorPage(c, http.StatusBadRequest, "Failed to Create User")
		return
	}

	// Determine if we should be admin
	isAdmin := false
	if len(currentUsers) == 0 {
		isAdmin = true
	}

	// Create user
	authHash := fmt.Sprintf("%x", rawAuthHash)
	if rows, err := api.db.Queries.CreateUser(api.db.Ctx, database.CreateUserParams{
		ID:       rUser.Username,
		Pass:     &hashedPassword,
		AuthHash: &authHash,
		Admin:    isAdmin,
	}); err != nil {
		log.Error("CreateUser DB Error:", err)
		apiErrorPage(c, http.StatusBadRequest, "Invalid User Data")
		return
	} else if rows == 0 {
		log.Error("User Already Exists:", rUser.Username)
		apiErrorPage(c, http.StatusBadRequest, "User Already Exists")
		return
	}

	c.JSON(http.StatusCreated, gin.H{
		"username": rUser.Username,
	})
}

func (api *API) getSession(session sessions.Session) (auth authData, ok bool) {
	// Get Session
	authorizedUser := session.Get("authorizedUser")
	isAdmin := session.Get("isAdmin")
	expiresAt := session.Get("expiresAt")
	authHash := session.Get("authHash")
	if authorizedUser == nil || isAdmin == nil || expiresAt == nil || authHash == nil {
		return
	}

	// Create Auth Object
	auth = authData{
		UserName: authorizedUser.(string),
		IsAdmin:  isAdmin.(bool),
		AuthHash: authHash.(string),
	}

	// Validate Auth Hash
	correctAuthHash, err := api.getUserAuthHash(auth.UserName)
	if err != nil || correctAuthHash != auth.AuthHash {
		return
	}

	// Refresh
	if expiresAt.(int64)-time.Now().Unix() < 60*60*24 {
		log.Info("Refreshing Session")
		if err := api.setSession(session, auth); err != nil {
			log.Error("unable to get session")
			return
		}
	}

	// Authorized
	return auth, true
}

func (api *API) setSession(session sessions.Session, auth authData) error {
	// Set Session Cookie
	session.Set("authorizedUser", auth.UserName)
	session.Set("isAdmin", auth.IsAdmin)
	session.Set("expiresAt", time.Now().Unix()+(60*60*24*7))
	session.Set("authHash", auth.AuthHash)

	return session.Save()
}

func (api *API) getUserAuthHash(username string) (string, error) {
	// Return Cache
	if api.userAuthCache[username] != "" {
		return api.userAuthCache[username], nil
	}

	// Get DB
	user, err := api.db.Queries.GetUser(api.db.Ctx, username)
	if err != nil {
		log.Error("GetUser DB Error:", err)
		return "", err
	}

	// Update Cache
	api.userAuthCache[username] = *user.AuthHash

	return api.userAuthCache[username], nil
}

func (api *API) rotateAllAuthHashes() error {
	// Do Transaction
	tx, err := api.db.DB.Begin()
	if err != nil {
		log.Error("Transaction Begin DB Error: ", err)
		return err
	}

	// Defer & Start Transaction
	defer func() {
		if err := tx.Rollback(); err != nil {
			log.Error("DB Rollback Error:", err)
		}
	}()
	qtx := api.db.Queries.WithTx(tx)

	users, err := qtx.GetUsers(api.db.Ctx)
	if err != nil {
		return err
	}

	// Update Users
	newAuthHashCache := make(map[string]string, 0)
	for _, user := range users {
		// Generate Auth Hash
		rawAuthHash, err := utils.GenerateToken(64)
		if err != nil {
			return err
		}

		// Update User
		authHash := fmt.Sprintf("%x", rawAuthHash)
		if _, err = qtx.UpdateUser(api.db.Ctx, database.UpdateUserParams{
			UserID:   user.ID,
			AuthHash: &authHash,
			Admin:    user.Admin,
		}); err != nil {
			return err
		}

		// Save New Hash Cache
		newAuthHashCache[user.ID] = fmt.Sprintf("%x", rawAuthHash)
	}

	// Commit Transaction
	if err := tx.Commit(); err != nil {
		log.Error("Transaction Commit DB Error: ", err)
		return err
	}

	// Transaction Succeeded -> Update Cache
	for user, hash := range newAuthHashCache {
		api.userAuthCache[user] = hash
	}

	return nil
}