package auth

import (
	"errors"
	"os"
	"strings"
	"system-altrak/internal/dto"
	actModule "system-altrak/internal/modules/activity"
	"system-altrak/pkg/utils"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/gofiber/fiber/v2"
	zap "go.uber.org/zap"
)

type Handler struct {
	service      AuthService
	validator    *validator.Validate
	activity     actModule.ActivityService
	loginLimiter LoginAttemptLimiter
}

type LoginAttemptLimiter interface {
	Allow(key string, max int, window time.Duration) (bool, time.Duration, error)
	RecordFailure(key string, window time.Duration) error
	Reset(key string) error
}

const (
	accessTokenCookieName  = "access_token"
	refreshTokenCookieName = "refresh_token"
	accessTokenTTL         = 15 * time.Minute
	refreshTokenTTL        = 7 * 24 * time.Hour
	loginIPAttemptLimit    = 5
	loginIPAttemptWindow   = 5 * time.Minute
	loginUserAttemptLimit  = 5
	loginUserAttemptWindow = 15 * time.Minute
)

func setAuthCookies(c *fiber.Ctx, accessToken, refreshToken string, persistent bool) {
	isProd := os.Getenv("ENV") == "production"
	secure := c.Secure() || isProd
	accessCookie := fiber.Cookie{
		Name:     accessTokenCookieName,
		Value:    accessToken,
		Path:     "/",
		HTTPOnly: true,
		Secure:   secure,
		SameSite: "Lax",
	}
	refreshCookie := fiber.Cookie{
		Name:     refreshTokenCookieName,
		Value:    refreshToken,
		Path:     "/",
		HTTPOnly: true,
		Secure:   secure,
		SameSite: "Lax",
	}

	if persistent {
		accessCookie.Expires = time.Now().Add(accessTokenTTL)
		refreshCookie.Expires = time.Now().Add(refreshTokenTTL)
	}

	c.Cookie(&accessCookie)
	c.Cookie(&refreshCookie)
}

func clearAuthCookies(c *fiber.Ctx) {
	isProd := os.Getenv("ENV") == "production"
	secure := c.Secure() || isProd
	expired := time.Unix(0, 0)

	c.Cookie(&fiber.Cookie{
		Name:     accessTokenCookieName,
		Value:    "",
		Path:     "/",
		Expires:  expired,
		MaxAge:   -1,
		HTTPOnly: true,
		Secure:   secure,
		SameSite: "Strict",
	})

	c.Cookie(&fiber.Cookie{
		Name:     refreshTokenCookieName,
		Value:    "",
		Path:     "/",
		Expires:  expired,
		MaxAge:   -1,
		HTTPOnly: true,
		Secure:   secure,
		SameSite: "Strict",
	})
}

func NewHandler(s AuthService, a actModule.ActivityService, limiter ...LoginAttemptLimiter) *Handler {
	var loginLimiter LoginAttemptLimiter
	if len(limiter) > 0 {
		loginLimiter = limiter[0]
	}

	return &Handler{
		service:      s,
		validator:    validator.New(),
		activity:     a,
		loginLimiter: loginLimiter,
	}
}

func (h *Handler) Login(c *fiber.Ctx) error {
	var req dto.LoginRequest
	if err := c.BodyParser(&req); err != nil {
		return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid input format")
	}

	req.Username = strings.TrimSpace(req.Username)

	// Use new detailed validation
	result := utils.ValidateStructDetailed(req)
	if !result.IsValid {
		return utils.ValidationErrorResponse(c, "Validation failed", result.Errors)
	}

	if err := h.checkLoginAttempts(c, req.Username); err != nil {
		return err
	}

	user, at, rt, err := h.service.Login(req.Username, req.Password, req.RememberMe)
	if err != nil {
		if h.activity != nil {
			_ = h.activity.Log(0, req.Username, "unknown", "AUTH", "LOGIN_FAILED", err.Error(), c.IP(), c.Get("User-Agent"), 0)
		}
		
		// Log detailed error for debugging
		utils.Error("Login service failure", zap.Error(err), zap.String("username", req.Username))

		if errors.Is(err, ErrInvalidCredentials) {
			if recordErr := h.recordLoginFailure(c, req.Username); recordErr != nil {
				utils.Error("Failed to record login failure", zap.Error(recordErr))
				return utils.InternalErrorResponse(c, "Gagal memproses login")
			}
			return utils.UnauthorizedResponse(c, "Username atau password salah")
		}
		return utils.InternalErrorResponse(c, "Gagal memproses login: " + err.Error())
	}

	setAuthCookies(c, at, rt, req.RememberMe)
	h.resetLoginAttempts(c, req.Username)

	if h.activity != nil {
		_ = h.activity.Log(user.ID, user.Username, user.Role, "authentication", "LOGIN", "Credential validation successful", c.IP(), c.Get("User-Agent"), user.BranchID)
	}

	return utils.SuccessResponse(c, "Login successful", dto.LoginResponse{
		User: user,
	})
}

func (h *Handler) checkLoginAttempts(c *fiber.Ctx, username string) error {
	if h.loginLimiter == nil {
		return nil
	}

	for _, rule := range h.loginAttemptRules(c, username) {
		allowed, _, err := h.loginLimiter.Allow(rule.key, rule.limit, rule.window)
		if err != nil {
			utils.Error("Login attempt check failed", zap.Error(err), zap.String("key", rule.key))
			return utils.InternalErrorResponse(c, "Gagal memproses login: " + err.Error())
		}
		if !allowed {
			return utils.ErrorResponse(c, fiber.StatusTooManyRequests, "Terlalu banyak percobaan login. Silakan coba lagi nanti.")
		}
	}

	return nil
}

func (h *Handler) recordLoginFailure(c *fiber.Ctx, username string) error {
	if h.loginLimiter == nil {
		return nil
	}

	for _, rule := range h.loginAttemptRules(c, username) {
		if err := h.loginLimiter.RecordFailure(rule.key, rule.window); err != nil {
			return err
		}
	}

	return nil
}

func (h *Handler) resetLoginAttempts(c *fiber.Ctx, username string) {
	if h.loginLimiter == nil {
		return
	}

	for _, rule := range h.loginAttemptRules(c, username) {
		_ = h.loginLimiter.Reset(rule.key)
	}
}

type loginAttemptRule struct {
	key    string
	limit  int
	window time.Duration
}

func (h *Handler) loginAttemptRules(c *fiber.Ctx, username string) []loginAttemptRule {
	rules := []loginAttemptRule{{
		key:    loginAttemptKey("ip:", c.IP()),
		limit:  loginIPAttemptLimit,
		window: loginIPAttemptWindow,
	}}

	if normalized := strings.ToLower(strings.TrimSpace(username)); normalized != "" {
		rules = append(rules, loginAttemptRule{
			key:    loginAttemptKey("user:", normalized),
			limit:  loginUserAttemptLimit,
			window: loginUserAttemptWindow,
		})
	}

	return rules
}

func loginAttemptKey(prefix, value string) string {
	return prefix + strings.ToLower(strings.TrimSpace(value))
}

func (h *Handler) Refresh(c *fiber.Ctx) error {
	var req dto.RefreshRequest
	if len(c.Body()) > 0 {
		if err := c.BodyParser(&req); err != nil {
			return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid input format")
		}
	}

	req.RefreshToken = strings.TrimSpace(req.RefreshToken)
	if req.RefreshToken == "" {
		req.RefreshToken = strings.TrimSpace(c.Cookies(refreshTokenCookieName))
	}

	if req.RefreshToken == "" {
		return utils.UnauthorizedResponse(c, "Refresh token diperlukan")
	}

	at, rt, persistent, err := h.service.Refresh(req.RefreshToken)
	if err != nil {
		return utils.UnauthorizedResponse(c, err.Error())
	}

	setAuthCookies(c, at, rt, persistent)

	return utils.SuccessResponse(c, "Token refreshed", fiber.Map{"refreshed": true})
}

func (h *Handler) Session(c *fiber.Ctx) error {
	// Extract user ID from JWT claims
	userID, ok := c.Locals("user_id").(uint)
	if !ok || userID == 0 {
		// No valid user ID present – treat as unauthorized
		return utils.UnauthorizedResponse(c, "Sesi tidak valid atau telah kedaluwarsa")
	}

	// Retrieve user session from the service
	user, err := h.service.GetSession(userID)
	if err != nil {
		if errors.Is(err, ErrInvalidSession) {
			// Session is invalid or user inactive – clear cookies and respond
			clearAuthCookies(c)
			return utils.UnauthorizedResponse(c, "Sesi tidak valid atau telah kedaluwarsa")
		}
		// Unexpected error – internal server error
		return utils.InternalErrorResponse(c, "Gagal memuat sesi pengguna")
	}

	return utils.SuccessResponse(c, "Session active", fiber.Map{"user": user})
}


func (h *Handler) Logout(c *fiber.Ctx) error {
	var req struct {
		RefreshToken string `json:"refresh_token" validate:"omitempty"`
	}
	if len(c.Body()) > 0 {
		if err := c.BodyParser(&req); err != nil {
			return utils.ErrorResponse(c, fiber.StatusBadRequest, "Invalid input format")
		}
	}

	req.RefreshToken = strings.TrimSpace(req.RefreshToken)
	if req.RefreshToken == "" {
		req.RefreshToken = strings.TrimSpace(c.Cookies(refreshTokenCookieName))
	}

	result := utils.ValidateStructDetailed(req)
	if !result.IsValid {
		return utils.ValidationErrorResponse(c, "Validation failed", result.Errors)
	}

	accessToken, _ := c.Locals("access_token").(string)

	if err := h.service.Logout(accessToken, req.RefreshToken); err != nil {
		if errors.Is(err, ErrInvalidAccessToken) ||
			errors.Is(err, ErrInvalidRefreshToken) ||
			errors.Is(err, ErrUnableToDetermineUser) {
			clearAuthCookies(c)
			return utils.UnauthorizedResponse(c, err.Error())
		}
		return utils.InternalErrorResponse(c, err.Error())
	}

	if h.activity != nil {
		if userID, ok := c.Locals("user_id").(uint); ok && userID > 0 {
			if user, err := h.service.GetSession(userID); err == nil && user != nil {
				_ = h.activity.Log(user.ID, user.Username, user.Role, "authentication", "LOGOUT", "Session terminated successfully", c.IP(), c.Get("User-Agent"), user.BranchID)
			}
		}
	}

	clearAuthCookies(c)

	return utils.SuccessResponse(c, "Logged out successfully", nil)
}
