package auth

import (
	"crypto/rand"
	"encoding/hex"
	"fmt"
	"net/http"
	"time"

	"github.com/labstack/echo/v4"
	"gorm.io/gorm"
)

const (
	sessionDuration         = 30 * 24 * time.Hour // 30 days
	sessionIDBytes          = 32                  // 32 bytes = 64 hex chars
	sessionCookie           = "session"
	sessionRotationInterval = 1 * time.Hour
)

// CreateSession creates a new session for the given user, returning the
// plaintext token (64-char hex string). The stored session ID is the
// HMAC-SHA256 hash of the token.
func CreateSession(db *gorm.DB, userID, hmacSecret string) (string, error) {
	b := make([]byte, sessionIDBytes)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("failed to generate session ID: %w", err)
	}

	plaintext := hex.EncodeToString(b)
	hash := HashAPIKey(plaintext, hmacSecret)

	now := time.Now()
	session := Session{
		ID:        hash,
		UserID:    userID,
		ExpiresAt: now.Add(sessionDuration),
		RotatedAt: now,
	}

	if err := db.Create(&session).Error; err != nil {
		return "", fmt.Errorf("failed to create session: %w", err)
	}

	return plaintext, nil
}

// ValidateSession hashes the plaintext token and looks up the session.
// Returns the associated user and session, or (nil, nil) if not found/expired.
func ValidateSession(db *gorm.DB, token, hmacSecret string) (*User, *Session) {
	hash := HashAPIKey(token, hmacSecret)

	var session Session
	if err := db.Preload("User").Where("id = ? AND expires_at > ?", hash, time.Now()).First(&session).Error; err != nil {
		return nil, nil
	}
	if session.User.Status != StatusActive {
		return nil, nil
	}
	return &session.User, &session
}

// DeleteSession removes a session by hashing the plaintext token.
func DeleteSession(db *gorm.DB, token, hmacSecret string) error {
	hash := HashAPIKey(token, hmacSecret)
	return db.Where("id = ?", hash).Delete(&Session{}).Error
}

// CleanExpiredSessions removes all sessions that have passed their expiry time.
func CleanExpiredSessions(db *gorm.DB) error {
	return db.Where("expires_at < ?", time.Now()).Delete(&Session{}).Error
}

// DeleteUserSessions removes all sessions for the given user.
func DeleteUserSessions(db *gorm.DB, userID string) error {
	return db.Where("user_id = ?", userID).Delete(&Session{}).Error
}

// RotateSession creates a new session for the same user, deletes the old one,
// and returns the new plaintext token.
func RotateSession(db *gorm.DB, oldSession *Session, hmacSecret string) (string, error) {
	b := make([]byte, sessionIDBytes)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("failed to generate session ID: %w", err)
	}

	plaintext := hex.EncodeToString(b)
	hash := HashAPIKey(plaintext, hmacSecret)

	now := time.Now()
	newSession := Session{
		ID:        hash,
		UserID:    oldSession.UserID,
		ExpiresAt: oldSession.ExpiresAt,
		RotatedAt: now,
	}

	err := db.Transaction(func(tx *gorm.DB) error {
		if err := tx.Create(&newSession).Error; err != nil {
			return err
		}
		return tx.Where("id = ?", oldSession.ID).Delete(&Session{}).Error
	})
	if err != nil {
		return "", fmt.Errorf("failed to rotate session: %w", err)
	}

	return plaintext, nil
}

// MaybeRotateSession checks if the session should be rotated and does so if needed.
// Called from the auth middleware after successful cookie-based authentication.
func MaybeRotateSession(c echo.Context, db *gorm.DB, session *Session, hmacSecret string) {
	if session == nil {
		return
	}

	rotatedAt := session.RotatedAt
	if rotatedAt.IsZero() {
		rotatedAt = session.CreatedAt
	}

	if time.Since(rotatedAt) < sessionRotationInterval {
		return
	}

	newToken, err := RotateSession(db, session, hmacSecret)
	if err != nil {
		// Rotation failure is non-fatal; the old session remains valid
		return
	}

	SetSessionCookie(c, newToken)
}

// isSecure returns true when the request arrived over HTTPS, either directly
// or via a reverse proxy that sets X-Forwarded-Proto.
func isSecure(c echo.Context) bool {
	return c.Scheme() == "https"
}

// SetSessionCookie sets the session cookie on the response.
func SetSessionCookie(c echo.Context, sessionID string) {
	cookie := &http.Cookie{
		Name:     sessionCookie,
		Value:    sessionID,
		Path:     "/",
		HttpOnly: true,
		Secure:   isSecure(c),
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int(sessionDuration.Seconds()),
	}
	c.SetCookie(cookie)
}

// SetTokenCookie sets an httpOnly "token" cookie for legacy API key auth.
func SetTokenCookie(c echo.Context, token string) {
	cookie := &http.Cookie{
		Name:     "token",
		Value:    token,
		Path:     "/",
		HttpOnly: true,
		Secure:   isSecure(c),
		SameSite: http.SameSiteLaxMode,
		MaxAge:   int(sessionDuration.Seconds()),
	}
	c.SetCookie(cookie)
}

// ClearSessionCookie clears the session cookie.
func ClearSessionCookie(c echo.Context) {
	cookie := &http.Cookie{
		Name:     sessionCookie,
		Value:    "",
		Path:     "/",
		HttpOnly: true,
		Secure:   isSecure(c),
		SameSite: http.SameSiteLaxMode,
		MaxAge:   -1,
	}
	c.SetCookie(cookie)
}
