package handlers

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
	"golang.org/x/crypto/bcrypt"

	"xtablo-backend/internal/web/views"

	"github.com/a-h/templ"
)

const sessionCookieName = "xtablo_session"
const sessionLifetime = 30 * 24 * time.Hour

var ErrUserNotFound = errors.New("user not found")
var ErrUserAlreadyExists = errors.New("user already exists")
var ErrSessionNotFound = errors.New("session not found")

type AuthRepository interface {
	CreateAuthUser(ctx context.Context, input CreateAuthUserInput) (uuid.UUID, error)
	GetAuthUserByEmail(ctx context.Context, email string) (AuthUser, error)
	GetPublicUserByID(ctx context.Context, id uuid.UUID) (PublicUser, error)
	CreateSession(ctx context.Context, token string, userID uuid.UUID, expiresAt time.Time) error
	GetSessionByToken(ctx context.Context, token string) (Session, error)
	DeleteSessionByToken(ctx context.Context, token string) error
	CreateTablo(ctx context.Context, input CreateTabloInput) (TabloRecord, error)
	UpdateTablo(ctx context.Context, input UpdateTabloInput) error
	ListTablos(ctx context.Context, input ListTablosInput) ([]TabloRecord, error)
	SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error
}

type CreateAuthUserInput struct {
	Email             string
	EncryptedPassword string
	DisplayName       string
}

type AuthUser struct {
	ID                uuid.UUID
	Email             string
	EncryptedPassword string
	CreatedAt         time.Time
	UpdatedAt         time.Time
}

type PublicUser struct {
	ID          uuid.UUID
	Email       string
	DisplayName string
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

type Session struct {
	Token     string
	UserID    uuid.UUID
	CreatedAt time.Time
	UpdatedAt time.Time
	ExpiresAt time.Time
}

type AuthHandler struct {
	repo AuthRepository
}

func NewAuthHandler(repo AuthRepository) *AuthHandler {
	return &AuthHandler{repo: repo}
}

func (h *AuthHandler) GetHome() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		user, ok := h.authenticatedUser(r.Context(), r)
		if !ok {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}

		tablos, err := h.repo.ListTablos(context.Background(), ListTablosInput{
			OwnerID: user.ID,
		})
		if err != nil {
			http.Error(w, "failed to load projects", http.StatusInternalServerError)
			return
		}

		projects := views.OverviewProjectsFromTablos(tablos)

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		content := views.OverviewMainContent(user.DisplayName, user.Email, projects)
		var renderErr error
		if isHXRequest(r) {
			renderErr = views.DashboardContentSwap("/", tablos, content).Render(r.Context(), w)
		} else {
			renderErr = views.DashboardPage("/", tablos, content).Render(r.Context(), w)
		}
		if renderErr != nil {
			http.Error(w, "failed to render app page", http.StatusInternalServerError)
		}
	}
}

func (h *AuthHandler) GetTasksPage() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.renderTasksPage(w, r)
	}
}

func (h *AuthHandler) GetTablosPage() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		h.renderTablosPage(w, r)
	}
}

func (h *AuthHandler) GetPlanningPage() http.HandlerFunc {
	return h.renderAppPage("/planning", func(user PublicUser) templ.Component {
		return views.PlanningMainContent()
	})
}

func (h *AuthHandler) GetChatPage() http.HandlerFunc {
	return h.renderAppPage("/chat", func(user PublicUser) templ.Component {
		return views.ChatMainContent()
	})
}

func (h *AuthHandler) GetFilesPage() http.HandlerFunc {
	return h.renderAppPage("/files", func(user PublicUser) templ.Component {
		return views.FilesMainContent()
	})
}

func (h *AuthHandler) GetFeedbackPage() http.HandlerFunc {
	return h.renderAppPage("/feedback", func(user PublicUser) templ.Component {
		return views.FeedbackMainContent()
	})
}

func (h *AuthHandler) GetNotFound() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		user, ok := h.authenticatedUser(r.Context(), r)
		if !ok {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}

		tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
			OwnerID: user.ID,
		})
		if err != nil {
			http.Error(w, "failed to load projects", http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.WriteHeader(http.StatusNotFound)
		content := views.NotFoundContent(user.DisplayName)
		var renderErr error
		if isHXRequest(r) {
			renderErr = views.DashboardContentSwap("", tablos, content).Render(r.Context(), w)
		} else {
			renderErr = views.DashboardPage("", tablos, content).Render(r.Context(), w)
		}
		if renderErr != nil {
			http.Error(w, "failed to render not found page", http.StatusInternalServerError)
		}
	}
}

func (h *AuthHandler) renderAppPage(activePath string, content func(user PublicUser) templ.Component) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		user, ok := h.authenticatedUser(r.Context(), r)
		if !ok {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}

		tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{
			OwnerID: user.ID,
		})
		if err != nil {
			http.Error(w, "failed to load projects", http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		pageContent := content(user)
		var renderErr error
		if isHXRequest(r) {
			renderErr = views.DashboardContentSwap(activePath, tablos, pageContent).Render(r.Context(), w)
		} else {
			renderErr = views.DashboardPage(activePath, tablos, pageContent).Render(r.Context(), w)
		}
		if renderErr != nil {
			http.Error(w, "failed to render app page", http.StatusInternalServerError)
		}
	}
}

func (h *AuthHandler) authenticatedUser(ctx context.Context, r *http.Request) (PublicUser, bool) {
	userID, ok := h.currentUserID(ctx, r)
	if !ok {
		return PublicUser{}, false
	}

	user, err := h.repo.GetPublicUserByID(ctx, userID)
	if err != nil {
		return PublicUser{}, false
	}
	return user, true
}

func (h *AuthHandler) PostLogout() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var email string
		if userID, ok := h.currentUserID(r.Context(), r); ok {
			if user, err := h.repo.GetPublicUserByID(r.Context(), userID); err == nil {
				email = user.Email
			}
		}

		if cookie, err := r.Cookie(sessionCookieName); err == nil && cookie.Value != "" {
			_ = h.repo.DeleteSessionByToken(r.Context(), cookie.Value)
			logStoreMutation("delete_session", email, cookie.Value, 0, 0)
		}

		http.SetCookie(w, &http.Cookie{
			Name:     sessionCookieName,
			Value:    "",
			Path:     "/",
			MaxAge:   -1,
			HttpOnly: true,
			SameSite: http.SameSiteLaxMode,
		})
		http.Redirect(w, r, "/login", http.StatusSeeOther)
	}
}

func (h *AuthHandler) GetLoginPage() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if _, ok := h.currentUserID(r.Context(), r); ok {
			http.Redirect(w, r, "/", http.StatusSeeOther)
			return
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		if err := views.LoginPage().Render(r.Context(), w); err != nil {
			http.Error(w, "failed to render login page", http.StatusInternalServerError)
		}
	}
}

func (h *AuthHandler) GetSignupPage() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if _, ok := h.currentUserID(r.Context(), r); ok {
			http.Redirect(w, r, "/", http.StatusSeeOther)
			return
		}

		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		if err := views.SignupPage().Render(r.Context(), w); err != nil {
			http.Error(w, "failed to render signup page", http.StatusInternalServerError)
		}
	}
}

func (h *AuthHandler) PostLogin() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, "invalid form payload", http.StatusBadRequest)
			return
		}

		email := normalizeEmail(r.FormValue("email"))
		password := strings.TrimSpace(r.FormValue("password"))

		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		switch {
		case email == "" || password == "":
			w.WriteHeader(http.StatusUnprocessableEntity)
			_ = views.AuthStatus("error", "Veuillez renseigner votre email et votre mot de passe.").Render(r.Context(), w)
			return
		}

		authUser, err := h.repo.GetAuthUserByEmail(r.Context(), email)
		if err != nil || bcrypt.CompareHashAndPassword([]byte(authUser.EncryptedPassword), []byte(password)) != nil {
			w.WriteHeader(http.StatusUnauthorized)
			_ = views.AuthStatus("error", "Identifiants invalides. Essayez demo@xtablo.com / xtablo-demo.").Render(r.Context(), w)
			return
		}

		h.setSession(r.Context(), w, authUser.ID, authUser.Email)
		w.Header().Set("HX-Redirect", "/")
		_ = views.AuthStatus("success", "Connexion réussie.").Render(r.Context(), w)
	}
}

func (h *AuthHandler) PostSignup() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if err := r.ParseForm(); err != nil {
			http.Error(w, "invalid form payload", http.StatusBadRequest)
			return
		}

		email := normalizeEmail(r.FormValue("email"))
		password := strings.TrimSpace(r.FormValue("password"))

		w.Header().Set("Content-Type", "text/html; charset=utf-8")

		switch {
		case email == "" || password == "":
			w.WriteHeader(http.StatusUnprocessableEntity)
			_ = views.AuthStatus("error", "Veuillez renseigner votre email et choisir un mot de passe.").Render(r.Context(), w)
			return
		}

		if _, err := h.repo.GetAuthUserByEmail(r.Context(), email); err == nil {
			w.WriteHeader(http.StatusConflict)
			_ = views.AuthStatus("error", "Un compte existe déjà avec cet email.").Render(r.Context(), w)
			return
		} else if !errors.Is(err, ErrUserNotFound) {
			http.Error(w, "failed to check existing user", http.StatusInternalServerError)
			return
		}

		passwordHash, err := hashPassword(password)
		if err != nil {
			http.Error(w, "failed to hash password", http.StatusInternalServerError)
			return
		}

		displayName := displayNameFromEmail(email)
		userID, err := h.repo.CreateAuthUser(r.Context(), CreateAuthUserInput{
			Email:             email,
			EncryptedPassword: passwordHash,
			DisplayName:       displayName,
		})
		if err != nil {
			if errors.Is(err, ErrUserAlreadyExists) {
				w.WriteHeader(http.StatusConflict)
				_ = views.AuthStatus("error", "Un compte existe déjà avec cet email.").Render(r.Context(), w)
				return
			}
			log.Err(err).Msg("failed to create user")
			w.WriteHeader(http.StatusInternalServerError)
			_ = views.AuthStatus("error", "Un problème est survenu.").Render(r.Context(), w)
			return
		}

		h.setSession(r.Context(), w, userID, email)
		w.Header().Set("HX-Redirect", "/")
		_ = views.AuthStatus("success", "Compte créé.").Render(r.Context(), w)
	}
}

func (h *AuthHandler) currentUserID(ctx context.Context, r *http.Request) (uuid.UUID, bool) {
	cookie, err := r.Cookie(sessionCookieName)
	if err != nil || cookie.Value == "" {
		return uuid.Nil, false
	}
	session, err := h.repo.GetSessionByToken(ctx, cookie.Value)
	if err != nil {
		return uuid.Nil, false
	}
	if session.ExpiresAt.Before(time.Now().UTC()) {
		_ = h.repo.DeleteSessionByToken(ctx, cookie.Value)
		return uuid.Nil, false
	}
	return session.UserID, true
}

func (h *AuthHandler) setSession(ctx context.Context, w http.ResponseWriter, userID uuid.UUID, email string) {
	sessionID := randomToken(32)
	expiresAt := time.Now().UTC().Add(sessionLifetime)
	if err := h.repo.CreateSession(ctx, sessionID, userID, expiresAt); err != nil {
		panic(err)
	}
	logStoreMutation("create_session", email, sessionID, 0, 0)
	http.SetCookie(w, &http.Cookie{
		Name:     sessionCookieName,
		Value:    sessionID,
		Path:     "/",
		Expires:  expiresAt,
		HttpOnly: true,
		SameSite: http.SameSiteLaxMode,
	})
}

func normalizeEmail(email string) string {
	return strings.ToLower(strings.TrimSpace(email))
}

func displayNameFromEmail(email string) string {
	email = normalizeEmail(email)
	if email == "" {
		return ""
	}
	return strings.Split(email, "@")[0]
}

func hashPassword(password string) (string, error) {
	passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
	if err != nil {
		return "", err
	}
	return string(passwordHash), nil
}

func randomToken(size int) string {
	buf := make([]byte, size)
	if _, err := rand.Read(buf); err != nil {
		panic(err)
	}
	return hex.EncodeToString(buf)
}

func logStoreMutation(action string, email string, sessionID string, usersCount int, sessionsCount int) {
	event := log.Info().
		Str("component", "auth_store").
		Str("action", action).
		Int("users_count", usersCount).
		Int("sessions_count", sessionsCount)

	if email != "" {
		event = event.Str("email", email)
	}
	if sessionID != "" {
		event = event.Str("session_id", sessionID)
	}

	event.Msg("auth store mutated")
}

func isHXRequest(r *http.Request) bool {
	return r.Header.Get("HX-Request") == "true"
}
