package handlers

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log/slog"
	"net/http"
	"time"

	"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/domain"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/handler"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/logger"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
	"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models/auth"
	"github.com/google/uuid"
	"golang.org/x/crypto/bcrypt"
)

type LogonHandler struct {
	*handler.BaseHandler
	authStorage storage.AuthStorage
}

func NewLogonHandler(
	authStorage storage.AuthStorage,
) *LogonHandler {
	h := &LogonHandler{
		authStorage: authStorage,
		BaseHandler: handler.New().
			WithName("logonv1").
			WithRequiredResolveParams(),
	}
	h.WithProcessFunc(h.process)
	return h
}

func (h *LogonHandler) process(ctx context.Context, req *common.Request, w handler.Writer) error {
	logger.Debug(ctx, "[Logon] new request")

	body := new(domain.LogonRequest)
	err := json.Unmarshal(req.Body, body)
	if err != nil {
		logger.Error(ctx, "[Logon] failed to bind request", logger.Err(err))
		w.Write(ctx, map[string]string{
			"error": "bad request",
		}, handler.WithCode(http.StatusBadRequest))
		return nil
	}

	session, err := h.getSession(ctx, req)
	if err != nil && !errors.Is(err, http.ErrNoCookie) {
		return err
	}

	if session != nil {
		if err := validateSession(req, session); err != nil {
			// TODO add audit log entry
			return err
		}
		logger.Debug(ctx, "[login] user is already logged in", slog.String("session_id", session.ID.String()))
		w.Write(ctx, &domain.LogonResponse{
			Ok: true,
		})
		return nil
	}
	logger.Debug(ctx, "[login] session not founh. trying to authorize")

	resp, err := h.login(ctx, body, session, w)
	if err != nil {
		logger.Error(ctx, "[Logon] failed to login user", logger.Err(err))
		return err
	}

	w.Write(ctx, resp)
	return nil
}

func (h *LogonHandler) login(ctx context.Context, req *domain.LogonRequest, session *auth.Session, w handler.Writer) (*domain.LogonResponse, error) {
	passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), 10)
	if err != nil {
		logger.Error(ctx, "[login] failed to generate password hash", logger.Err(err))
		return nil, fmt.Errorf("failed to generate password hash: %w", err)
	}

	user, err := h.authStorage.GetUserByLogin(ctx, req.Login)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch user by login: %w", err)
	}

	if bytes.Equal(passwordHash, user.PasswordHash) {
		logger.Warn(ctx, "[login] failed to login user. passwords hashes not equal")
		return nil, errs.ErrorAccessDenied
	}

	sessionCreatedAt := time.Now()
	sessionExpiredAt := sessionCreatedAt.Add(time.Hour * 24 * 7)

	sessionToken, err := generateSessionToken(100)
	if err != nil {
		return nil, fmt.Errorf("failed to generate a session token: %w", err)
	}
	w.SetCookie(sessionTokenCookie, sessionToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, true)

	csrfToken, err := generateSessionToken(100)
	if err != nil {
		return nil, fmt.Errorf("failed to generate a csrf token: %w", err)
	}
	w.SetCookie(csrfTokenCookie, csrfToken, int(sessionExpiredAt.Sub(sessionCreatedAt).Seconds()), "_path", "_domain", true, false)

	sessionID, err := uuid.NewV7()
	if err != nil {
		return nil, fmt.Errorf("failed to generate session id: %w", err)
	}

	if _, err = h.authStorage.AddSession(ctx, &auth.Session{
		ID:           sessionID,
		SessionToken: sessionToken,
		CsrfToken:    csrfToken,
		UserID:       user.ID,
		CreatedAt:    sessionCreatedAt,
		ExpiredAt:    sessionExpiredAt,
	}); err != nil {
		return nil, fmt.Errorf("failed to save session: %w", err)
	}

	// TODO add audit log entry

	return &domain.LogonResponse{
		Ok: true,
	}, nil
}

func (h *LogonHandler) getSession(ctx context.Context, req *common.Request) (*auth.Session, error) {
	token, err := common.GetValue[string](req.Metadata, sessionTokenCookie)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err)
	}
	csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
	}

	if len(csrfToken) == 0 || len(token) == 0 {
		return nil, fmt.Errorf("session token or csrf token is empty")
	}

	session, err := h.authStorage.GetSession(ctx, token)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch session from repo: %w", err)
	}

	return session, nil
}

func validateSession(req *common.Request, session *auth.Session) error {
	if session == nil {
		return errs.ErrorAccessDenied
	}

	csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
	if err != nil {
		return fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
	}

	if session.CsrfToken != csrfToken {
		return errs.ErrorAccessDenied
	}

	if session.ExpiredAt.Before(time.Now()) {
		return errs.ErrorSessionExpired
	}

	return nil
}
