package com.honey.honey.service;

import com.honey.honey.model.Session;
import com.honey.honey.model.UserA;
import com.honey.honey.repository.SessionRepository;
import com.honey.honey.repository.UserARepository;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Base64;
import java.util.List;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class SessionService {

    private final SessionRepository sessionRepository;
    private final UserARepository userARepository;
    private static final int SESSION_TTL_HOURS = 1; // 1 hour
    private static final SecureRandom secureRandom = new SecureRandom();

    /**
     * -- GETTER --
     *  Gets max active sessions per user.
     */
    @Getter
    @Value("${app.session.max-active-per-user:5}")
    private int maxActiveSessionsPerUser;

    /**
     * Creates a new session for a user.
     * Enforces MAX_ACTIVE_SESSIONS_PER_USER by deleting oldest sessions (active or expired) if limit exceeded.
     * Returns the raw session ID (to be sent to frontend) and stores the hash in DB.
     */
    @Transactional
    public String createSession(UserA user) {
        LocalDateTime now = LocalDateTime.now();
        
        // Generate cryptographically random session ID
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        String sessionId = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);

        // Hash the session ID for storage
        String sessionIdHash = hashSessionId(sessionId);

        // Calculate expiration
        LocalDateTime expiresAt = now.plusHours(SESSION_TTL_HOURS);

        // Enforce max active sessions per user
        enforceMaxActiveSessions(user.getId(), now);

        // Create and save session
        Session session = Session.builder()
                .sessionIdHash(sessionIdHash)
                .userId(user.getId())
                .createdAt(now)
                .expiresAt(expiresAt)
                .build();

        sessionRepository.save(session);

        log.debug("Session created: userId={}", user.getId());
        return sessionId;
    }
    
    /**
     * Enforces MAX_ACTIVE_SESSIONS_PER_USER by deleting oldest sessions (active or expired) if limit exceeded.
     * Counts ALL sessions regardless of expiration status.
     */
    private void enforceMaxActiveSessions(Integer userId, LocalDateTime now) {
        // Count ALL sessions for the user (active + expired)
        long totalCount = sessionRepository.countByUserId(userId);
        
        if (totalCount >= maxActiveSessionsPerUser) {
            // Calculate how many to delete to stay under limit
            // If user has 5 sessions and limit is 5, we need to delete 1 to make room for the new one
            int toDelete = (int) (totalCount - maxActiveSessionsPerUser + 1);
            
            // Get oldest sessions (active or expired, ordered by createdAt ASC)
            List<Session> oldestSessions = sessionRepository.findOldestSessionsByUserId(
                    userId, 
                    PageRequest.of(0, toDelete)
            );
            
            // Delete oldest sessions
            if (!oldestSessions.isEmpty()) {
                sessionRepository.deleteAll(oldestSessions);
                log.debug("Deleted {} oldest session(s) for userId={} (limit: {})", 
                        oldestSessions.size(), userId, maxActiveSessionsPerUser);
            }
        }
    }

    /**
     * Validates a session ID and returns the associated user.
     * Returns empty if session is invalid or expired.
     */
    @Transactional(readOnly = true)
    public Optional<UserA> getUserBySession(String sessionId) {
        if (sessionId == null || sessionId.isBlank()) {
            return Optional.empty();
        }

        String sessionIdHash = hashSessionId(sessionId);
        Optional<Session> sessionOpt = sessionRepository.findBySessionIdHash(sessionIdHash);

        if (sessionOpt.isEmpty()) {
            return Optional.empty();
        }

        Session session = sessionOpt.get();

        if (session.isExpired()) {
            // Optionally delete expired session
            sessionRepository.delete(session);
            return Optional.empty();
        }

        // Load user by ID
        return userARepository.findById(session.getUserId());
    }

    /**
     * Invalidates a session (logout).
     */
    @Transactional
    public void invalidateSession(String sessionId) {
        if (sessionId == null || sessionId.isBlank()) {
            return;
        }

        String sessionIdHash = hashSessionId(sessionId);
        sessionRepository.deleteBySessionIdHash(sessionIdHash);
        log.debug("Session invalidated: userId={}", maskSessionId(sessionId));
    }

    /**
     * Hashes a session ID using SHA-256.
     */
    private String hashSessionId(String sessionId) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(sessionId.getBytes(StandardCharsets.UTF_8));
            return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
        } catch (Exception e) {
            log.error("Failed to hash session ID", e);
            throw new RuntimeException("Failed to hash session ID", e);
        }
    }

    /**
     * Masks session ID for logging (security).
     */
    private String maskSessionId(String sessionId) {
        if (sessionId == null || sessionId.length() < 8) {
            return "***";
        }
        return sessionId.substring(0, 4) + "***" + sessionId.substring(sessionId.length() - 4);
    }

    /**
     * Gets session TTL in seconds.
     */
    public int getSessionTtlSeconds() {
        return SESSION_TTL_HOURS * 3600;
    }

}

