package server

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"net/http"
	"net/mail"
	"strings"
	"time"

	"github.com/bartered-dev/bartered/backend/internal/auth"
	"github.com/jackc/pgx/v5"
	"golang.org/x/crypto/bcrypt"
)

const (
	sessionCookieName = "bartered_session"
	sessionDuration   = 30 * 24 * time.Hour
)

type authHandler struct {
	store auth.Store
	now   func() time.Time
}

type authRequest struct {
	Email       string `json:"email"`
	Password    string `json:"password"`
	DisplayName string `json:"displayName"`
	Role        string `json:"role"`
}

type authResponse struct {
	User auth.User `json:"user"`
}

type errorResponse struct {
	Error string `json:"error"`
}

func (handler authHandler) register(response http.ResponseWriter, request *http.Request) {
	var input authRequest
	if err := decodeJSON(request, &input); err != nil {
		writeError(response, http.StatusBadRequest, "Некорректный формат запроса")
		return
	}

	input.Email = strings.ToLower(strings.TrimSpace(input.Email))
	input.DisplayName = strings.TrimSpace(input.DisplayName)
	if validationError := validateRegistration(input); validationError != "" {
		writeError(response, http.StatusUnprocessableEntity, validationError)
		return
	}

	passwordHash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
	if err != nil {
		writeError(response, http.StatusInternalServerError, "Не удалось создать аккаунт")
		return
	}

	user, err := handler.store.CreateUser(
		request.Context(),
		input.Email,
		input.DisplayName,
		input.Role,
		string(passwordHash),
	)
	if errors.Is(err, auth.ErrEmailTaken) {
		writeError(response, http.StatusConflict, "Аккаунт с таким email уже существует")
		return
	}
	if err != nil {
		writeError(response, http.StatusInternalServerError, "Не удалось создать аккаунт")
		return
	}

	if err := handler.startSession(response, request, user.ID); err != nil {
		writeError(response, http.StatusInternalServerError, "Не удалось начать сессию")
		return
	}

	writeJSON(response, http.StatusCreated, authResponse{User: user})
}

func (handler authHandler) login(response http.ResponseWriter, request *http.Request) {
	var input authRequest
	if err := decodeJSON(request, &input); err != nil {
		writeError(response, http.StatusBadRequest, "Некорректный формат запроса")
		return
	}

	user, err := handler.store.FindUserByEmail(
		request.Context(),
		strings.ToLower(strings.TrimSpace(input.Email)),
	)
	if errors.Is(err, pgx.ErrNoRows) {
		writeError(response, http.StatusUnauthorized, "Неверный email или пароль")
		return
	}
	if err != nil {
		writeError(response, http.StatusInternalServerError, "Не удалось выполнить вход")
		return
	}
	if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(input.Password)) != nil {
		writeError(response, http.StatusUnauthorized, "Неверный email или пароль")
		return
	}

	if err := handler.startSession(response, request, user.ID); err != nil {
		writeError(response, http.StatusInternalServerError, "Не удалось начать сессию")
		return
	}

	writeJSON(response, http.StatusOK, authResponse{User: user})
}

func (handler authHandler) me(response http.ResponseWriter, request *http.Request) {
	user, err := handler.currentUser(request)
	if errors.Is(err, http.ErrNoCookie) || errors.Is(err, pgx.ErrNoRows) {
		writeError(response, http.StatusUnauthorized, "Требуется авторизация")
		return
	}
	if err != nil {
		writeError(response, http.StatusInternalServerError, "Не удалось проверить сессию")
		return
	}

	writeJSON(response, http.StatusOK, authResponse{User: user})
}

func (handler authHandler) logout(response http.ResponseWriter, request *http.Request) {
	cookie, err := request.Cookie(sessionCookieName)
	if err == nil {
		_ = handler.store.DeleteSession(request.Context(), hashToken(cookie.Value))
	}

	http.SetCookie(response, &http.Cookie{
		Name:     sessionCookieName,
		Value:    "",
		Path:     "/",
		MaxAge:   -1,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
	response.WriteHeader(http.StatusNoContent)
}

func (handler authHandler) startSession(
	response http.ResponseWriter,
	request *http.Request,
	userID string,
) error {
	tokenBytes := make([]byte, 32)
	if _, err := rand.Read(tokenBytes); err != nil {
		return err
	}

	token := base64.RawURLEncoding.EncodeToString(tokenBytes)
	expiresAt := handler.now().Add(sessionDuration)
	if err := handler.store.CreateSession(
		request.Context(),
		hashToken(token),
		userID,
		expiresAt,
	); err != nil {
		return err
	}

	http.SetCookie(response, &http.Cookie{
		Name:     sessionCookieName,
		Value:    token,
		Path:     "/",
		Expires:  expiresAt,
		MaxAge:   int(sessionDuration.Seconds()),
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
		Secure:   request.TLS != nil,
	})
	return nil
}

func (handler authHandler) currentUser(request *http.Request) (auth.User, error) {
	cookie, err := request.Cookie(sessionCookieName)
	if err != nil {
		return auth.User{}, err
	}
	return handler.store.FindUserBySession(request.Context(), hashToken(cookie.Value), handler.now())
}

func hashToken(token string) []byte {
	hash := sha256.Sum256([]byte(token))
	return hash[:]
}

func validateRegistration(input authRequest) string {
	if _, err := mail.ParseAddress(input.Email); err != nil {
		return "Укажите корректный email"
	}
	if len([]rune(input.DisplayName)) < 2 || len([]rune(input.DisplayName)) > 50 {
		return "Имя должно содержать от 2 до 50 символов"
	}
	if len(input.Password) < 8 {
		return "Пароль должен содержать минимум 8 символов"
	}
	if input.Role != "user" && input.Role != "creator" {
		return "Выберите тип аккаунта"
	}
	return ""
}

func decodeJSON(request *http.Request, value any) error {
	decoder := json.NewDecoder(io.LimitReader(request.Body, 1<<20))
	decoder.DisallowUnknownFields()
	return decoder.Decode(value)
}

func writeError(response http.ResponseWriter, status int, message string) {
	writeJSON(response, status, errorResponse{Error: message})
}
