package handlers

import (
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	"github.com/tapper/backend/database"
	"github.com/tapper/backend/models"
	"github.com/tapper/backend/services"
)

type registerRequest struct {
	Username string `json:"username"`
	Email    string `json:"email"`
	Password string `json:"password"`
}

type loginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type refreshRequest struct {
	RefreshToken string `json:"refresh_token"`
}

func authResponse(user models.User) (fiber.Map, error) {
	return issueAuthTokens(database.DB, user)
}

func issueAuthTokens(db *gorm.DB, user models.User) (fiber.Map, error) {
	steamID := ""
	if user.SteamID != nil {
		steamID = *user.SteamID
	}

	accessToken, err := services.GenerateAccessToken(user.ID, user.Username, steamID, user.Role, user.Tier)
	if err != nil {
		return nil, err
	}

	refreshToken, refreshHash, err := services.GenerateRefreshToken()
	if err != nil {
		return nil, err
	}

	if err := db.Create(&models.RefreshToken{
		UserID:    user.ID,
		TokenHash: refreshHash,
		ExpiresAt: time.Now().UTC().Add(services.RefreshTokenTTL),
	}).Error; err != nil {
		return nil, err
	}

	return fiber.Map{
		"access_token":       accessToken,
		"refresh_token":      refreshToken,
		"token":              accessToken,
		"token_type":         "Bearer",
		"expires_in":         int(services.AccessTokenTTL().Seconds()),
		"refresh_expires_in": int(services.RefreshTokenTTL.Seconds()),
		"user":               user.ToPublicProfile(),
	}, nil
}

func Register(ownerEmail string) fiber.Handler {
	return func(c *fiber.Ctx) error {
		var req registerRequest
		if err := c.BodyParser(&req); err != nil {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
				"error": "Invalid request body",
			})
		}

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

		if len(req.Username) < 3 || len(req.Username) > 32 {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
				"error": "Username must be between 3 and 32 characters",
			})
		}
		if !isValidUsername(req.Username) {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
				"error": "Username may only contain letters, numbers, underscores, and hyphens",
			})
		}
		if !isValidEmail(req.Email) {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
				"error": "Invalid email address",
			})
		}
		if len(req.Password) < 8 {
			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
				"error": "Password must be at least 8 characters",
			})
		}

		var existing models.User
		if database.DB.Where("username = ?", req.Username).First(&existing).Error == nil {
			return c.Status(fiber.StatusConflict).JSON(fiber.Map{
				"error": "Username already taken",
			})
		}
		if database.DB.Where("email = ?", req.Email).First(&existing).Error == nil {
			return c.Status(fiber.StatusConflict).JSON(fiber.Map{
				"error": "Email already registered",
			})
		}

		hash, err := services.HashPassword(req.Password)
		if err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
				"error": "Failed to process password",
			})
		}

		role := "user"
		if ownerEmail != "" && req.Email == strings.ToLower(ownerEmail) {
			role = "owner"
		}

		user := models.User{
			Username:     req.Username,
			Email:        req.Email,
			PasswordHash: hash,
			Role:         role,
			Tier:         "normal",
			MMR:          1000,
		}

		if err := database.DB.Create(&user).Error; err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
				"error": "Failed to create user",
			})
		}

		resp, err := authResponse(user)
		if err != nil {
			return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
				"error": "Failed to issue tokens",
			})
		}

		return c.Status(fiber.StatusCreated).JSON(resp)
	}
}

func Login(c *fiber.Ctx) error {
	var req loginRequest
	if err := c.BodyParser(&req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid request body",
		})
	}

	req.Email = strings.TrimSpace(strings.ToLower(req.Email))
	if req.Email == "" || req.Password == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Email and password are required",
		})
	}

	var user models.User
	if err := database.DB.Where("email = ?", req.Email).First(&user).Error; err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"error": "Invalid email or password",
		})
	}

	if !services.CheckPassword(req.Password, user.PasswordHash) {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"error": "Invalid email or password",
		})
	}

	ban := services.GetActiveBan(user.ID)
	if ban != nil && ban.Type == "full" {
		resp := fiber.Map{
			"error":  "Your account has been banned",
			"reason": ban.Reason,
			"type":   ban.Type,
		}
		if ban.ExpiresAt != nil {
			resp["expires_at"] = ban.ExpiresAt
		}
		return c.Status(fiber.StatusForbidden).JSON(resp)
	}

	resp, err := authResponse(user)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": "Failed to issue tokens",
		})
	}

	return c.JSON(resp)
}

// IssueSSEToken returns a short-lived token (purpose=sse) that EventSource
// passes via query string. Full JWTs are rejected by the middleware when
// they arrive that way, so this endpoint is the only way to authenticate SSE.
func IssueSSEToken(c *fiber.Ctx) error {
	claims, ok := c.Locals("claims").(*services.JWTClaims)
	if !ok {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"error": "Invalid token claims",
		})
	}

	token, err := services.GenerateSSEToken(claims.UserID, claims.Username, claims.SteamID, claims.Role, claims.Tier)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": "Failed to generate SSE token",
		})
	}

	return c.JSON(fiber.Map{
		"token":      token,
		"expires_in": int(services.SSETokenTTL.Seconds()),
	})
}

func Refresh(c *fiber.Ctx) error {
	var req refreshRequest
	if err := c.BodyParser(&req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid request body",
		})
	}
	req.RefreshToken = strings.TrimSpace(req.RefreshToken)

	if req.RefreshToken == "" {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
			"error": "Missing refresh token",
		})
	}

	hash := services.HashRefreshToken(req.RefreshToken)
	now := time.Now().UTC()

	var resp fiber.Map

	err := database.DB.Transaction(func(tx *gorm.DB) error {
		var stored models.RefreshToken
		if err := tx.
			Clauses(clause.Locking{Strength: "UPDATE"}).
			Preload("User").
			Where("token_hash = ? AND revoked_at IS NULL AND expires_at > ?", hash, now).
			First(&stored).Error; err != nil {
			return err
		}

		ban := services.GetActiveBan(stored.UserID)
		if ban != nil && ban.Type == "full" {
			return fiber.NewError(fiber.StatusForbidden, "Your account has been banned")
		}

		stored.RevokedAt = &now
		if err := tx.Save(&stored).Error; err != nil {
			return err
		}

		nextResp, err := issueAuthTokens(tx, stored.User)
		if err != nil {
			return err
		}
		resp = nextResp
		return nil
	})
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
				"error": "Invalid or expired refresh token",
			})
		}
		if fiberErr, ok := err.(*fiber.Error); ok {
			return c.Status(fiberErr.Code).JSON(fiber.Map{
				"error": fiberErr.Message,
			})
		}
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": "Failed to refresh token",
		})
	}

	return c.JSON(resp)
}

func Logout(c *fiber.Ctx) error {
	var req refreshRequest
	if err := c.BodyParser(&req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "Invalid request body",
		})
	}
	req.RefreshToken = strings.TrimSpace(req.RefreshToken)
	if req.RefreshToken == "" {
		return c.SendStatus(fiber.StatusNoContent)
	}

	hash := services.HashRefreshToken(req.RefreshToken)
	now := time.Now().UTC()
	if err := database.DB.
		Model(&models.RefreshToken{}).
		Where("token_hash = ? AND revoked_at IS NULL", hash).
		Update("revoked_at", &now).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": "Failed to revoke refresh token",
		})
	}

	return c.SendStatus(fiber.StatusNoContent)
}

func Health(c *fiber.Ctx) error {
	hostname := services.GetSystemMetrics().Hostname
	return c.JSON(fiber.Map{
		"status":   "ok",
		"instance": hostname,
		"time":     time.Now().UTC(),
	})
}

func isValidUsername(s string) bool {
	for _, r := range s {
		if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' || r == '-') {
			return false
		}
	}
	return true
}

func isValidEmail(s string) bool {
	at := strings.IndexByte(s, '@')
	if at < 1 {
		return false
	}
	dot := strings.LastIndexByte(s[at:], '.')
	return dot > 1 && dot < len(s[at:])-1
}
