package router

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	"github.com/golang-jwt/jwt/v5"
	_config "github.com/johannesbuehl/golunteer/backend/pkg/config"
	"github.com/johannesbuehl/golunteer/backend/pkg/db"
	"github.com/johannesbuehl/golunteer/backend/pkg/db/users"
	_logger "github.com/johannesbuehl/golunteer/backend/pkg/logger"
)

var validate *validator.Validate

var logger = _logger.Logger
var config = _config.Config

var app *fiber.App

// general message for REST-responses
type responseMessage struct {
	Status  int
	Message string
	Data    any
}

// answer the client request with the response-message
func (a *Handler) send(c *fiber.Ctx) error {
	// if the status-code is in the error-region, return an error
	if a.Status >= 400 {
		// if available, include the message
		if a.Message != "" {
			return fiber.NewError(a.Status, a.Message)
		} else {
			return fiber.NewError(a.Status)
		}
	} else {
		// if there is data, send it as JSON
		if a.Data != nil {
			c.JSON(a.Data)

			// if there is a message, send it instead
		} else if a.Message != "" {
			c.SendString(a.Message)
		}

		return c.SendStatus(a.Status)
	}
}

type Handler struct {
	C *fiber.Ctx
	UserChecked
	responseMessage
}

func init() {
	validate = validator.New()

	// setup fiber
	app = fiber.New(fiber.Config{
		AppName:               "johannes-pv",
		DisableStartupMessage: true,
	})

	// map with the individual methods
	handleMethods := map[string]func(path string, handlers ...func(*fiber.Ctx) error) fiber.Router{
		"GET":    app.Get,
		"POST":   app.Post,
		"PATCH":  app.Patch,
		"PUT":    app.Put,
		"DELETE": app.Delete,
	}

	// map with the individual registered endpoints
	endpoints := map[string]map[string]func(*Handler){
		"GET": {
			// all events with the task-assignments
			"events/assignments": (*Handler).getEventsAssignments,

			// all events with the availabilities of the individual users
			"events/availabilities": (*Handler).getEventsAvailabilities,

			// all events with the task-assignments and the availability of the current user
			"events/user/assignmentAvailability": (*Handler).getEventUserAssignmentAvailability,

			// events the user has to enter his availability for
			"events/user/pending": (*Handler).getEventsUserPending,

			// number of events the user has to enter his availability for
			"events/user/pending/count": (*Handler).getEventsUserPendingCount,

			"events/user/assigned": (*Handler).getEventsUserAssigned,
			"tasks":                (*Handler).getTasks,          // all available tasks
			"users":                (*Handler).getUsers,          // all users
			"user/tasks":           (*Handler).getUserTasks,      // get all possible tasks for the user
			"availabilities":       (*Handler).getAvailabilities, // all available availabilities
		},
		"POST": {
			"events":         (*Handler).postEvent,        // create an event
			"users":          (*Handler).postUser,         // add an user
			"availabilities": (*Handler).postAvailability, // add an availability
			"tasks":          (*Handler).postTask,         // add a task
		},
		"PATCH": {
			"users":          (*Handler).patchUser,          // modify an user
			"events":         (*Handler).patchEvent,         // modify an event
			"availabilities": (*Handler).patchAvailabilitiy, // modify an availability
			"tasks":          (*Handler).patchTask,          // modify a task
		},
		"PUT": {
			"events/user/availability": (*Handler).putEventUserAvailability, // set or change the users availability for a specific event
			"events/assignments":       (*Handler).putEventAssignment,
			"users/password":           (*Handler).putPassword, // change the password
		},
		"DELETE": {
			"event":              (*Handler).deleteEvent, // remove an event
			"events/assignments": (*Handler).deleteEventAssignment,
			"tasks":              (*Handler).deleteTask,         // remove a task
			"availabilities":     (*Handler).deleteAvailability, // remove an availability
			"users":              (*Handler).deleteUser,         // remove an user
		},
	}

	// handle specific requests special
	app.Get("/api/welcome", handleWelcome)
	app.Post("/api/login", handleLogin)
	app.Get("/api/logout", handleLogout)

	app.Use(func(c *fiber.Ctx) error {
		path := c.Path()

		// if it is for the api, don't try to add the html extension
		if len(path) < 4 || path[1:4] != "dev" {
			// check, wether a html-file exists for the path
			if _, err := os.Stat(filepath.Join("html", path)); errors.Is(err, os.ErrNotExist) {
				htmlPath := path + ".html"

				if _, err := os.Stat(filepath.Join("html", htmlPath)); err == nil {
					c.Path(htmlPath)
				}
			}
		}

		c.Next()

		return nil
	})
	app.Static("/", "html")

	// register the registered endpoints
	for method, handlers := range endpoints {
		for address, handler := range handlers {
			handleMethods[method]("/api/"+address, func(c *fiber.Ctx) error {
				logger.Debug().Msgf("HTTP %s request: %q", c.Method(), c.OriginalURL())

				args := Handler{
					C: c,
				}

				if loggedIn, err := args.checkUser(); err != nil {
					args.Status = fiber.StatusBadRequest

					logger.Error().Msgf("can't check user: %v", err)
				} else if !loggedIn {
					args.Status = fiber.StatusUnauthorized

					logger.Info().Msgf("user not authorized")
				} else {
					handler(&args)
				}

				return args.send(c)
			})
		}
	}
}

func Listen() {
	// start the server
	err := app.Listen(fmt.Sprintf(":%d", config.Server.Port))

	fmt.Println(err)
}

func (args Handler) setSessionCookie(jwt *string) {
	var value string

	if jwt == nil {
		value = args.C.Cookies("session")
	} else {
		value = *jwt
	}

	args.C.Cookie(&fiber.Cookie{
		Name:     "session",
		Value:    value,
		HTTPOnly: true,
		SameSite: "strict",
		MaxAge:   int(config.SessionExpire.Seconds()),
	})
}

// removes the session-coockie from a request
func (args Handler) removeSessionCookie() {
	args.C.Cookie(&fiber.Cookie{
		Name:     "session",
		Value:    "",
		HTTPOnly: true,
		SameSite: "strict",
		Expires:  time.Unix(0, 0),
	})
}

// payload of the JSON webtoken
type JWTPayload struct {
	users.UserName `json:"userName"`
	TokenID        string `json:"tokenID"`
}

// complete JSON webtoken
type JWT struct {
	_config.Payload
	CustomClaims JWTPayload
}

// extracts the json webtoken from the request
//
// @returns (userName, tokenID, error)
func extractJWT(c *fiber.Ctx) (users.UserName, string, error) {
	// get the session-cookie
	cookie := c.Cookies("session")

	token, err := jwt.ParseWithClaims(cookie, &JWT{}, func(token *jwt.Token) (any, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("unexpected JWT signing method: %v", token.Header["alg"])
		}

		return []byte(config.ClientSession.JwtSignature), nil
	})

	if err != nil {
		return "", "", err
	}

	// extract the claims from the JWT
	if claims, ok := token.Claims.(*JWT); ok && token.Valid {
		return claims.CustomClaims.UserName, claims.CustomClaims.TokenID, nil
	} else {
		return "", "", fmt.Errorf("invalid JWT")
	}
}

// user-entry in the database
type userDB struct {
	UserName string `db:"userName"`
	Password []byte `db:"password"`
	Admin    bool   `db:"admin"`
	TokenID  string `db:"tokenID"`
}

type UserChecked struct {
	users.UserName `json:"userName" db:"userName"`
	Admin          bool `json:"admin" db:"admin"`
}

// checks wether the request is from a valid user
func (args *Handler) checkUser() (bool, error) {
	userName, tokenID, err := extractJWT(args.C)

	if err != nil {
		return false, nil
	}

	var dbResult struct {
		TokenID string `db:"tokenID"`
		Admin   bool   `db:"admin"`
	}

	// retrieve the user from the database
	if err := db.DB.Get(&dbResult, "SELECT tokenID, admin FROM USERS WHERE userName = ?", userName); err != nil {
		return false, err

		// if the tokenID is valid, the user is authorized
	} else if dbResult.TokenID != tokenID {
		return false, nil
	} else {
		// reset the expiration of the cookie
		args.setSessionCookie(nil)

		args.UserName = userName
		args.Admin = dbResult.Admin
	}
	return true, nil

}
