package main

import (
	"fmt"
	"net/http"
	"strconv"

	"github.com/alexedwards/argon2id"
	"github.com/google/uuid"
	"github.com/jackc/pgerrcode"
	"github.com/jackc/pgx/v5"
	"github.com/labstack/echo/v5"
	"github.com/zoriya/kyoo/keibi/dbc"
	. "github.com/zoriya/kyoo/keibi/models"
)

func MapDbUser(user *dbc.User) User {
	return User{
		Pk:          user.Pk,
		Id:          user.Id,
		Username:    user.Username,
		Email:       user.Email,
		HasPassword: user.Password != nil,
		CreatedDate: user.CreatedDate,
		LastSeen:    user.LastSeen,
		Claims:      user.Claims,
		Oidc:        nil,
	}
}

// @Summary      List all users
// @Description  List all users existing in this instance.
// @Tags         users
// @Accept       json
// @Produce      json
// @Security     Jwt[users.read]
// @Param        after   query      string  false  "used for pagination."
// @Success      200  {object}  Page[User]
// @Failure      422  {object}  KError "Invalid after id"
// @Router       /users [get]
func (h *Handler) ListUsers(c *echo.Context) error {
	ctx := c.Request().Context()

	err := CheckPermissions(c, []string{"users.read"})
	if err != nil {
		return err
	}

	limit := int32(20)
	id := c.Param("after")

	if id == "" {
		users, err := h.db.GetAllUsers(ctx, limit)
		if err != nil {
			return err
		}

		ret := make([]User, 0, len(users))
		for _, user := range users {
			u := MapDbUser(&user.User)
			u.Oidc = user.Oidc
			ret = append(ret, u)
		}
		return c.JSON(200, NewPage(ret, c.Request().URL, limit))
	} else {
		pk, err := strconv.Atoi(id)
		if err != nil {
			return echo.NewHTTPError(http.StatusUnprocessableEntity, "Invalid `after` parameter")
		}
		users, err := h.db.GetAllUsersAfter(ctx, dbc.GetAllUsersAfterParams{
			Limit:   limit,
			AfterPk: int32(pk),
		})
		if err != nil {
			return err
		}

		ret := make([]User, 0, len(users))
		for _, user := range users {
			u := MapDbUser(&user.User)
			u.Oidc = user.Oidc
			ret = append(ret, u)
		}
		return c.JSON(200, NewPage(ret, c.Request().URL, limit))
	}
}

// @Summary      Get user
// @Description  Get informations about a user from it's id
// @Tags         users
// @Produce      json
// @Security     Jwt[users.read]
// @Param        id   path      string    true  "The id of the user" Format(uuid)
// @Success      200  {object}  User
// @Failure      404  {object}  KError "No user with the given id found"
// @Failure      422  {object}  KError "Invalid id (not a uuid)"
// @Router /users/{id} [get]
func (h *Handler) GetUser(c *echo.Context) error {
	ctx := c.Request().Context()
	err := CheckPermissions(c, []string{"users.read"})
	if err != nil {
		return err
	}

	id := c.Param("id")
	uid, err := uuid.Parse(id)
	dbuser, err := h.db.GetUser(ctx, dbc.GetUserParams{
		UseId:    err == nil,
		Id:       uid,
		Username: id,
	})
	if err == pgx.ErrNoRows {
		return echo.NewHTTPError(404, fmt.Sprintf("No user found with id or username: '%s'.", id))
	} else if err != nil {
		return err
	}

	ret := MapDbUser(&dbuser.User)
	ret.Oidc = dbuser.Oidc
	return c.JSON(200, ret)
}

// @Summary      Get me
// @Description  Get informations about the currently connected user
// @Tags         users
// @Produce      json
// @Security     Jwt
// @Success      200  {object}  User
// @Failure      401  {object}  KError "Missing jwt token"
// @Failure      403  {object}  KError "Invalid jwt token (or expired)"
// @Router /users/me [get]
func (h *Handler) GetMe(c *echo.Context) error {
	ctx := c.Request().Context()
	id, err := GetCurrentUserId(c)
	if err != nil {
		return err
	}
	dbuser, err := h.db.GetUser(ctx, dbc.GetUserParams{
		UseId: true,
		Id:    id,
	})
	if err == pgx.ErrNoRows {
		return c.JSON(403, "Invalid jwt token, couldn't find user.")
	} else if err != nil {
		return err
	}

	ret := MapDbUser(&dbuser.User)
	ret.Oidc = dbuser.Oidc
	return c.JSON(200, ret)
}

// @Summary      Register
// @Description  Register as a new user and open a session for it
// @Tags         users
// @Accept       json
// @Produce      json
// @Param        device   query   string         false  "The device the created session will be used on"  Example(android)
// @Param        user     body    RegisterDto  false  "Registration informations"
// @Success      201  {object}  SessionWToken
// @Success      409  {object}  KError "Duplicated email or username"
// @Failure      403  {object}  KError "Registrations are disabled"
// @Failure      422  {object}  KError "Invalid register body"
// @Router /users [post]
func (h *Handler) Register(c *echo.Context) error {
	if h.config.DisableRegistration {
		return echo.NewHTTPError(http.StatusForbidden, "Registrations are disabled")
	}

	ctx := c.Request().Context()
	var req RegisterDto
	err := c.Bind(&req)
	if err != nil {
		return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
	}
	if err = c.Validate(&req); err != nil {
		return err
	}

	pass, err := argon2id.CreateHash(req.Password, argon2id.DefaultParams)
	if err != nil {
		return err
	}

	duser, err := h.db.CreateUser(ctx, dbc.CreateUserParams{
		Username:    req.Username,
		Email:       req.Email,
		Password:    &pass,
		Claims:      h.config.DefaultClaims,
		FirstClaims: h.config.FirstUserClaims,
	})
	if ErrIs(err, pgerrcode.UniqueViolation) {
		return echo.NewHTTPError(409, "Email or username already taken")
	} else if err != nil {
		return err
	}
	user := MapDbUser(&duser)
	return h.createSession(c, &user)
}

// @Summary      Delete user
// @Description  Delete an account and all it's sessions.
// @Tags         users
// @Accept       json
// @Produce      json
// @Security     Jwt[users.delete]
// @Param        id   path      string  false  "User id of the user to delete" Format(uuid)
// @Success      200  {object}  User
// @Failure      404  {object}  KError "Invalid user id"
// @Failure      422  {object}  KError "Invalid id format"
// @Router /users/{id} [delete]
func (h *Handler) DeleteUser(c *echo.Context) error {
	ctx := c.Request().Context()
	err := CheckPermissions(c, []string{"users.delete"})
	if err != nil {
		return err
	}

	uid, err := uuid.Parse(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(422, "Invalid id given: not an uuid")
	}

	ret, err := h.db.DeleteUser(ctx, uid)
	if err == pgx.ErrNoRows {
		return echo.NewHTTPError(404, "No user found with given id")
	} else if err != nil {
		return err
	}
	return c.JSON(200, MapDbUser(&ret))
}

// @Summary      Delete self
// @Description  Delete your account and all your sessions
// @Tags         users
// @Accept       json
// @Produce      json
// @Security     Jwt
// @Success      200  {object}  User
// @Router /users/me [delete]
func (h *Handler) DeleteSelf(c *echo.Context) error {
	ctx := c.Request().Context()
	uid, err := GetCurrentUserId(c)
	if err != nil {
		return err
	}

	ret, err := h.db.DeleteUser(ctx, uid)
	if err == pgx.ErrNoRows {
		return echo.NewHTTPError(403, "Invalid token, user already deleted.")
	} else if err != nil {
		return err
	}
	return c.JSON(200, MapDbUser(&ret))
}

// @Summary      Edit self
// @Description  Edit your account's info
// @Tags         users
// @Accept       json
// @Produce      json
// @Security     Jwt
// @Param        user     body  EditUserDto  false  "Edited user info"
// @Success      200  {object}  User
// @Success      403  {object}  KError  "You can't edit a protected claim"
// @Success      422  {object}  KError  "Invalid body"
// @Router /users/me [patch]
func (h *Handler) EditSelf(c *echo.Context) error {
	ctx := c.Request().Context()
	var req EditUserDto
	err := c.Bind(&req)
	if err != nil {
		return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
	}
	if err = c.Validate(&req); err != nil {
		return err
	}

	for _, key := range h.config.ProtectedClaims {
		if _, contains := req.Claims[key]; contains {
			return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("Can't edit protected claim: '%s'.", key))
		}
	}

	uid, err := GetCurrentUserId(c)
	if err != nil {
		return err
	}

	ret, err := h.db.UpdateUser(ctx, dbc.UpdateUserParams{
		Id:       uid,
		Username: req.Username,
		Email:    req.Email,
		Claims:   req.Claims,
	})
	if err == pgx.ErrNoRows {
		return echo.NewHTTPError(http.StatusNotFound, "Invalid token, user not found.")
	} else if err != nil {
		return err
	}

	return c.JSON(200, MapDbUser(&ret))
}

// @Summary      Edit user
// @Description  Edit an account info or permissions
// @Tags         users
// @Accept       json
// @Produce      json
// @Security     Jwt[users.write]
// @Param        id       path  string  false  "User id of the user to edit" Format(uuid)
// @Param        user     body  EditUserDto  false  "Edited user info"
// @Success      200  {object}  User
// @Success      403  {object}  KError  "You don't have permissions to edit another account"
// @Success      422  {object}  KError  "Invalid body"
// @Router /users/{id} [patch]
func (h *Handler) EditUser(c *echo.Context) error {
	ctx := c.Request().Context()
	err := CheckPermissions(c, []string{"users.write"})
	if err != nil {
		return err
	}

	uid, err := uuid.Parse(c.Param("id"))
	if err != nil {
		return echo.NewHTTPError(400, "Invalid id given: not an uuid")
	}

	var req EditUserDto
	err = c.Bind(&req)
	if err != nil {
		return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
	}
	if err = c.Validate(&req); err != nil {
		return err
	}

	ret, err := h.db.UpdateUser(ctx, dbc.UpdateUserParams{
		Id:       uid,
		Username: req.Username,
		Email:    req.Email,
		Claims:   req.Claims,
	})
	if err == pgx.ErrNoRows {
		return echo.NewHTTPError(http.StatusNotFound, "Invalid user id, user not found")
	} else if err != nil {
		return err
	}

	return c.JSON(200, MapDbUser(&ret))
}

// @Summary      Edit password
// @Description  Edit your password
// @Tags         users
// @Accept       json
// @Produce      json
// @Security     Jwt
// @Param        invalidate  query  bool  false  "Invalidate other sessions" default(true)
// @Param        user     body  EditPasswordDto  false  "New password"
// @Success      204
// @Success      422  {object}  KError  "Invalid body"
// @Router /users/me/password [patch]
func (h *Handler) ChangePassword(c *echo.Context) error {
	ctx := c.Request().Context()
	uid, err := GetCurrentUserId(c)
	if err != nil {
		return err
	}
	user, err := h.db.GetUser(ctx, dbc.GetUserParams{
		UseId: true,
		Id:    uid,
	})

	sid, err := GetCurrentSessionId(c)
	if err != nil {
		return err
	}

	var req EditPasswordDto
	err = c.Bind(&req)
	if err != nil {
		return echo.NewHTTPError(http.StatusUnprocessableEntity, err.Error())
	}
	if err = c.Validate(&req); err != nil {
		return err
	}

	if user.User.Password != nil {
		if req.OldPassword == nil {
			return echo.NewHTTPError(http.StatusUnprocessableEntity, "Missing old password")
		}
		match, err := argon2id.ComparePasswordAndHash(
			*req.OldPassword,
			*user.User.Password,
		)
		if err != nil {
			return err
		}
		if !match {
			return echo.NewHTTPError(http.StatusForbidden, "Invalid password")
		}
	}

	pass, err := argon2id.CreateHash(req.NewPassword, argon2id.DefaultParams)
	if err != nil {
		return err
	}
	_, err = h.db.UpdateUser(ctx, dbc.UpdateUserParams{
		Id:       uid,
		Password: &pass,
	})
	if err != nil {
		return err
	}

	err = h.db.ClearOtherSessions(ctx, dbc.ClearOtherSessionsParams{
		SessionId: sid,
		UserId:    uid,
	})
	if err != nil {
		return err
	}

	return c.NoContent(http.StatusNoContent)
}
