// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/service/auth/refresh_token.go
package auth

import (
	"context"
	"fmt"
	"time"

	"github.com/gocql/gocql"
	"go.uber.org/zap"

	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config"
	uc_user "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/usecase/user"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/auditlog"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/hash"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/security/jwt"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/storage/cache/cassandracache"
	"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/transaction"
)

type RefreshTokenRequestDTO struct {
	RefreshToken string `json:"value"`
}

type RefreshTokenResponseDTO struct {
	Message                string `json:"message"`
	AccessToken            string `json:"access_token"`
	RefreshToken           string `json:"refresh_token"`
	AccessTokenExpiryDate  string `json:"access_token_expiry_date"`
	RefreshTokenExpiryDate string `json:"refresh_token_expiry_date"`
	Username               string `json:"username"`
}

type RefreshTokenService interface {
	Execute(ctx context.Context, req *RefreshTokenRequestDTO) (*RefreshTokenResponseDTO, error)
}

type refreshTokenServiceImpl struct {
	config         *config.Config
	logger         *zap.Logger
	auditLogger    auditlog.AuditLogger
	cache          cassandracache.CassandraCacher
	jwtProvider    jwt.JWTProvider
	userGetByIDUC  uc_user.UserGetByIDUseCase
}

func NewRefreshTokenService(
	config *config.Config,
	logger *zap.Logger,
	auditLogger auditlog.AuditLogger,
	cache cassandracache.CassandraCacher,
	jwtProvider jwt.JWTProvider,
	userGetByIDUC uc_user.UserGetByIDUseCase,
) RefreshTokenService {
	return &refreshTokenServiceImpl{
		config:        config,
		logger:        logger.Named("RefreshTokenService"),
		auditLogger:   auditLogger,
		cache:         cache,
		jwtProvider:   jwtProvider,
		userGetByIDUC: userGetByIDUC,
	}
}

func (s *refreshTokenServiceImpl) Execute(ctx context.Context, req *RefreshTokenRequestDTO) (*RefreshTokenResponseDTO, error) {
	// Create SAGA for token refresh workflow
	saga := transaction.NewSaga("refresh-token", s.logger)

	s.logger.Info("starting token refresh")

	// Step 1: Validate refresh token JWT
	userID, err := s.jwtProvider.ProcessJWTToken(req.RefreshToken)
	if err != nil {
		s.logger.Warn("Invalid refresh token JWT", zap.Error(err))
		return nil, fmt.Errorf("invalid refresh token")
	}

	// Step 2: Check if refresh token exists in cache
	// SECURITY: Hash refresh token to match how it was stored (prevents token leakage via cache keys)
	refreshTokenHash := hash.HashToken(req.RefreshToken)
	refreshKey := fmt.Sprintf("refresh:%s", refreshTokenHash)
	cachedUserID, err := s.cache.Get(ctx, refreshKey)
	if err != nil || cachedUserID == nil {
		s.logger.Warn("Refresh token not found in cache", zap.String("user_id", userID))
		return nil, fmt.Errorf("refresh token not found or expired")
	}

	// Step 3: Verify user IDs match
	if string(cachedUserID) != userID {
		s.logger.Warn("User ID mismatch", zap.String("jwt_user_id", userID), zap.String("cached_user_id", string(cachedUserID)))
		return nil, fmt.Errorf("invalid refresh token")
	}

	// Step 4: Generate new token pair (token rotation for security)
	newAccessToken, accessExpiry, newRefreshToken, refreshExpiry, err := s.jwtProvider.GenerateJWTTokenPair(
		userID,
		s.config.JWT.AccessTokenDuration,
		s.config.JWT.RefreshTokenDuration,
	)
	if err != nil {
		s.logger.Error("Failed to generate new tokens", zap.Error(err))
		return nil, fmt.Errorf("failed to generate new tokens")
	}

	// Step 5: Store NEW refresh token FIRST (compensate: delete new token)
	// CRITICAL: Store new token before deleting old token to prevent lockout
	// SECURITY: Hash new refresh token to prevent token leakage via cache key inspection
	newRefreshTokenHash := hash.HashToken(newRefreshToken)
	newRefreshKey := fmt.Sprintf("refresh:%s", newRefreshTokenHash)
	if err := s.cache.SetWithExpiry(ctx, newRefreshKey, []byte(userID), s.config.JWT.RefreshTokenDuration); err != nil {
		s.logger.Error("Failed to store new refresh token", zap.Error(err))
		return nil, fmt.Errorf("failed to store new refresh token")
	}

	// Register compensation: if deletion of old token fails, delete new token
	newRefreshKeyCaptured := newRefreshKey
	saga.AddCompensation(func(ctx context.Context) error {
		s.logger.Info("compensating: deleting new refresh token",
			zap.String("new_refresh_key", newRefreshKeyCaptured))
		return s.cache.Delete(ctx, newRefreshKeyCaptured)
	})

	// Step 6: Delete old refresh token from cache (compensate: restore old token)
	oldRefreshKeyCaptured := refreshKey
	oldUserIDCaptured := userID
	if err := s.cache.Delete(ctx, refreshKey); err != nil {
		s.logger.Error("Failed to delete old refresh token",
			zap.String("refresh_key", refreshKey),
			zap.Error(err))

		// Trigger compensation: Delete new token (restore consistency)
		saga.Rollback(ctx)
		return nil, fmt.Errorf("failed to delete old refresh token: %w", err)
	}

	// Register compensation: restore old token with reduced TTL (1 hour grace period)
	saga.AddCompensation(func(ctx context.Context) error {
		s.logger.Info("compensating: restoring old refresh token",
			zap.String("old_refresh_key", oldRefreshKeyCaptured))
		// Restore with reduced TTL (1 hour) to allow user retry without long-lived old token
		return s.cache.SetWithExpiry(ctx, oldRefreshKeyCaptured, []byte(oldUserIDCaptured), 1*time.Hour)
	})

	// Step 7: Get user to retrieve username/email (read-only, no compensation needed)
	userUUID, err := gocql.ParseUUID(userID)
	if err != nil {
		s.logger.Error("Invalid user ID", zap.Error(err))
		// No rollback needed for UUID parsing error (tokens already rotated successfully)
		return nil, fmt.Errorf("invalid user ID")
	}

	user, err := s.userGetByIDUC.Execute(ctx, userUUID)
	if err != nil || user == nil {
		s.logger.Error("User not found", zap.String("user_id", userID), zap.Error(err))
		// No rollback needed for user lookup error (tokens already rotated successfully)
		return nil, fmt.Errorf("user not found")
	}

	s.logger.Info("Token refreshed successfully",
		zap.String("user_id", userID),
		zap.String("new_refresh_token", newRefreshToken[:16]+"...")) // Log prefix only for security

	// Audit log token refresh
	s.auditLogger.LogAuth(ctx, auditlog.EventTypeTokenRefresh, auditlog.OutcomeSuccess,
		"", "", map[string]string{
			"user_id": userID,
		})

	return &RefreshTokenResponseDTO{
		Message:                "Token refreshed successfully",
		AccessToken:            newAccessToken,
		RefreshToken:           newRefreshToken,
		AccessTokenExpiryDate:  accessExpiry.Format(time.RFC3339),
		RefreshTokenExpiryDate: refreshExpiry.Format(time.RFC3339),
		Username:               user.Email,
	}, nil
}
