package com.honey.honey.service;

import com.honey.honey.dto.ReferralDto;
import com.honey.honey.dto.ReferralProjection;
import com.honey.honey.model.UserA;
import com.honey.honey.model.UserB;
import com.honey.honey.model.UserD;
import com.honey.honey.repository.UserARepository;
import com.honey.honey.repository.UserBRepository;
import com.honey.honey.repository.UserDRepository;
import com.honey.honey.util.IpUtils;
import com.honey.honey.util.TimeProvider;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Service for user management with sharded tables.
 * Handles registration, login, and referral system.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserARepository userARepository;
    private final UserBRepository userBRepository;
    private final AvatarService avatarService;
    private final UserDRepository userDRepository;
    private final CountryCodeService countryCodeService;
    private final LocalizationService localizationService;
    
    /**
     * Gets user balance.
     */
    public Long getUserBalance(Integer userId) {
        return userBRepository.findById(userId)
                .map(UserB::getBalanceA)
                .orElse(null);
    }

    /**
     * Gets or creates a user based on Telegram data.
     * Updates user data on each login.
     * Handles referral system if start parameter is present (only for bot registration).
     * 
     * @param tgUserData Parsed Telegram data (contains "user" map and optional "start" string for bot registration)
     * @param request HTTP request for IP extraction
     * @return UserA entity
     */
    @Transactional
    public UserA getOrCreateUser(Map<String, Object> tgUserData, HttpServletRequest request) {
        // Extract user data and start parameter (only used for bot registration, not WebApp)
        @SuppressWarnings("unchecked")
        Map<String, Object> tgUser = (Map<String, Object>) tgUserData.get("user");
        String start = (String) tgUserData.get("start");
        Long telegramId = ((Number) tgUser.get("id")).longValue();
        String firstName = (String) tgUser.get("first_name");
        String lastName = (String) tgUser.get("last_name");
        String username = (String) tgUser.get("username");
        Boolean isPremium = (Boolean) tgUser.get("is_premium");
        String languageCode = (String) tgUser.get("language_code");
        
        // Build screen_name from first_name and last_name
        String screenName = buildScreenName(firstName, lastName);
        
        // device_code: language_code from initData (uppercase), max 5 chars (DB column limit).
        // Telegram may send BCP 47 codes like "en-US", "zh-Hans" which exceed VARCHAR(5).
        String deviceCode = normalizeDeviceCode(languageCode);
        
        // Get client IP and convert to bytes
        String clientIp = IpUtils.getClientIp(request);
        byte[] ipBytes = IpUtils.ipToBytes(clientIp);
        
        // Determine if this is a bot registration (bot registration always has start parameter set, even if null)
        // WebApp initData never has start parameter (it's always missing, not null)
        boolean isBotRegistration = tgUserData.containsKey("start");
        
        // Get country code from IP
        // For bot registration, IP will be bot server's IP (incorrect), so use "XX" for new users
        // For existing users, country_code is preserved anyway
        String countryCode;
        if (isBotRegistration) {
            // Bot registration: can't determine user's real country from bot server IP
            // Set to "XX" for new users (existing users preserve their original country_code)
            countryCode = "XX";
            log.debug("Bot registration detected: using country_code=XX (cannot determine from bot server IP)");
        } else {
            // WebApp registration: IP is user's real IP, so we can determine country
            countryCode = countryCodeService.getCountryCode(clientIp);
        }
        
        // Get current timestamp
        long nowSeconds = TimeProvider.nowSeconds();
        
        // Check if user exists
        Optional<UserA> existingUserOpt = userARepository.findByTelegramId(telegramId);
        
        if (existingUserOpt.isPresent()) {
            // User exists
            UserA userA = existingUserOpt.get();
            
            // Use the isBotRegistration variable already determined above
            // Bot registration = user opening referral link when already registered (no updates)
            // WebApp login = actual login/update (update fields)
            if (isBotRegistration) {
                // Bot registration: User is already registered, just opening referral link
                // Do NOT update any fields - just return the user as-is
                log.debug("Existing user opening referral link: userId={}, telegramId={}", userA.getId(), telegramId);
                return userA;
            } else {
                // WebApp login: User is actually logging in/updating, so update their data
                log.debug("Updating user data on login: userId={}", userA.getId());
                
                // Update avatar if Telegram file_id changed (only downloads if changed)
                avatarService.updateAvatarIfNeeded(userA, telegramId, tgUser);
                
                // Update user data on login (including country_code from real IP)
                updateUserOnLogin(userA, screenName, username, isPremium, countryCode, deviceCode, languageCode, ipBytes, nowSeconds);
                return userA;
            }
        } else {
            // New user - create in all 3 tables
            log.info("New user registration: telegramId={}, referral={}", telegramId, start != null && !start.isEmpty() ? "yes" : "no");
            try {
                return createNewUser(telegramId, screenName, username, isPremium, languageCode, countryCode, deviceCode, ipBytes, nowSeconds, start, tgUser);
            } catch (Exception e) {
                log.warn("New user creation failed for telegramId={}, possible duplicate or DB error: {}", telegramId, e.getMessage());
                throw e;
            }
        }
    }
    
    // Supported languages for the application
    private static final java.util.Set<String> SUPPORTED_LANGUAGES = java.util.Set.of(
        "EN", "RU", "DE", "IT", "NL", "PL", "FR", "ES", "ID", "TR"
    );
    
    /**
     * Updates user data on login (when session is created via WebApp).
     * Sets language_code only if it's null, empty, or 'XX' (not set yet).
     * 
     * @param userA User to update
     * @param screenName Updated screen name
     * @param username Updated username
     * @param isPremium Updated premium status
     * @param countryCode Country code from user's real IP (WebApp only, not bot)
     * @param deviceCode Device code (always updated)
     * @param languageCode Language code from initData (only set if user's languageCode is null, empty, or 'XX')
     * @param ipBytes IP address bytes
     * @param nowSeconds Current timestamp
     */
    private void updateUserOnLogin(UserA userA, String screenName, String username, Boolean isPremium, 
                                   String countryCode, String deviceCode, String languageCode, 
                                   byte[] ipBytes, long nowSeconds) {
        userA.setScreenName(screenName);
        userA.setTelegramName(username != null ? username : "-");
        userA.setIsPremium(isPremium != null && isPremium ? 1 : 0);
        // Update country_code from user's real IP (WebApp login)
        userA.setCountryCode(countryCode);
        // Update device_code always
        userA.setDeviceCode(deviceCode != null ? deviceCode.toUpperCase() : "XX");
        // Update language_code only if it's null, empty, or 'XX' (not set yet)
        String currentLanguageCode = userA.getLanguageCode();
        if (currentLanguageCode == null || currentLanguageCode.isEmpty() || "XX".equals(currentLanguageCode)) {
            String newLanguageCode = determineLanguageCode(languageCode);
            userA.setLanguageCode(newLanguageCode);
            log.debug("Set language_code on login: userId={}, languageCode={}", userA.getId(), newLanguageCode);
        } else {
            log.debug("Preserved existing language_code on login: userId={}, languageCode={}", userA.getId(), currentLanguageCode);
        }
        userA.setIp(ipBytes);
        userA.setDateLogin((int) nowSeconds);
        userA.setBotActive(true);
        
        userARepository.save(userA);
        log.debug("Updated user data on login: userId={}", userA.getId());
    }
    
    /**
     * Determines the language code to use.
     * If the language from initData is supported, use it. Otherwise, default to EN.
     */
    private String determineLanguageCode(String languageCodeFromInitData) {
        if (languageCodeFromInitData == null || languageCodeFromInitData.isEmpty()) {
            return "EN";
        }
        String upperCode = languageCodeFromInitData.toUpperCase();
        // Check if it's a supported language
        if (SUPPORTED_LANGUAGES.contains(upperCode)) {
            return upperCode;
        }
        // Default to EN if not supported
        return "EN";
    }
    
    /**
     * Updates user's language code (called when user changes language in app header).
     */
    @Transactional
    public void updateLanguageCode(Integer userId, String languageCode) {
        Optional<UserA> userOpt = userARepository.findById(userId);
        if (userOpt.isPresent()) {
            UserA user = userOpt.get();
            user.setLanguageCode(languageCode != null && languageCode.length() == 2 ? languageCode.toUpperCase() : "XX");
            userARepository.save(user);
            log.debug("Updated language_code for userId={}: {}", userId, user.getLanguageCode());
        }
    }
    
    /**
     * Creates a new user in all 3 tables with referral handling.
     */
    private UserA createNewUser(Long telegramId, String screenName, String username, Boolean isPremium,
                               String languageCode, String countryCode, String deviceCode,
                               byte[] ipBytes, long nowSeconds, String start, Map<String, Object> tgUser) {
        
        // Determine language code (use supported language or default to EN)
        String determinedLanguageCode = determineLanguageCode(languageCode);
        
        // Create UserA (avatar will be set after user is created and we have userId)
        UserA userA = UserA.builder()
                .screenName(screenName)
                .telegramId(telegramId)
                .telegramName(username != null ? username : "-")
                .isPremium(isPremium != null && isPremium ? 1 : 0)
                .languageCode(determinedLanguageCode)
                .countryCode(countryCode)
                .deviceCode(deviceCode != null ? deviceCode.toUpperCase() : "XX")
                .ip(ipBytes)
                .dateReg((int) nowSeconds)
                .dateLogin((int) nowSeconds)
                .banned(0)
                .build();
        
        userA = userARepository.save(userA);
        Integer userId = userA.getId();
        
        log.info("New user created: userId={}, telegramId={}", userId, telegramId);
        
        // Update avatar (downloads and saves, stores URL in DB with ?v=timestamp)
        avatarService.updateAvatarIfNeeded(userA, telegramId, tgUser);
        
        // Create UserB with same ID
        // Initial balance_a is 0
        UserB userB = UserB.builder()
                .id(userId)
                .balanceA(0L)
                .balanceB(0L)
                .depositTotal(0L)
                .depositCount(0)
                .withdrawTotal(0L)
                .withdrawCount(0)
                .build();
        userBRepository.save(userB);
        
        // Create UserD with referral handling
        UserD userD = createUserDWithReferral(userId, screenName, start, (int) nowSeconds);
        userDRepository.save(userD);
        
        return userA;
    }
    
    /**
     * Creates UserD entity with referral chain setup.
     * IMPORTANT: This method should ONLY be called for NEW users during initial registration.
     * Referral cannot be changed after user registration.
     * 
     * @param userId New user's ID
     * @param screenName User's screen name (from db_users_a)
     * @param start Referral parameter (from bot registration, not WebApp)
     */
    private UserD createUserDWithReferral(Integer userId, String screenName, String start, int dateReg) {
        log.debug("Creating UserD with referral: userId={}, start={}", userId, start);
        
        // Defensive check: Ensure UserD doesn't already exist (should never happen, but safety check)
        Optional<UserD> existingUserD = userDRepository.findById(userId);
        if (existingUserD.isPresent()) {
            log.error("CRITICAL: Attempted to create UserD for existing userId={}. UserD already exists. Skipping UserD creation.", userId);
            return existingUserD.get(); // Return existing UserD without modification
        }
        
        UserD.UserDBuilder builder = UserD.builder()
                .id(userId)
                .screenName(screenName != null ? screenName : "-")
                .dateReg(dateReg)
                .refererId1(0)
                .refererId2(0)
                .refererId3(0)
                .refererId4(0)
                .refererId5(0)
                .masterId(1) // Default master_id = 1
                .referals1(0)
                .referals2(0)
                .referals3(0)
                .referals4(0)
                .referals5(0)
                .fromReferals1(0L)
                .fromReferals2(0L)
                .fromReferals3(0L)
                .fromReferals4(0L)
                .fromReferals5(0L)
                .toReferer1(0L)
                .toReferer2(0L)
                .toReferer3(0L)
                .toReferer4(0L)
                .toReferer5(0L);
        
        if (start != null && !start.isEmpty()) {
            try {
                Integer refererId = Integer.parseInt(start);
                
                // Input validation: bounds check referral ID
                if (refererId <= 0) {
                    log.warn("Invalid referral ID (non-positive) - userId={}, refererId={}", userId, refererId);
                } else if (refererId > 2147483647) {
                    log.warn("Invalid referral ID (too large) - userId={}, refererId={}", userId, refererId);
                } else if (refererId.equals(userId)) {
                    log.warn("Self-referral attempt blocked - userId={}", userId);
                } else {
                    // Validate referer exists in UserA (main user table)
                    Optional<UserA> refererUserAOpt = userARepository.findById(refererId);
                    if (refererUserAOpt.isEmpty()) {
                        log.warn("Invalid referer (not found) - userId={}, refererId={}", userId, refererId);
                    } else {
                        UserA refererUserA = refererUserAOpt.get();
                        
                        // Check if referer is banned
                        if (refererUserA.getBanned() != null && refererUserA.getBanned() > 0) {
                            log.warn("Banned referer attempt - userId={}, refererId={}", userId, refererId);
                        } else {
                            // Referer is valid, check UserD
                            Optional<UserD> refererUserDOpt = userDRepository.findById(refererId);
                            
                            if (refererUserDOpt.isPresent()) {
                                UserD refererUserD = refererUserDOpt.get();
                                
                                // Set referral chain: shift referer's chain down by 1 level (3-level system)
                                builder.refererId1(refererId)
                                       .masterId(refererUserD.getMasterId())
                                       .refererId2(refererUserD.getRefererId1())
                                       .refererId3(refererUserD.getRefererId2());
                                
                                // Increment referal counts for all 3 levels up the chain
                                setupReferralChain(userId, refererId);
                                log.info("Referral chain set: userId={}, refererId={}", userId, refererId);
                            } else {
                                log.warn("Referer missing UserD record - userId={}, refererId={}", userId, refererId);
                                builder.refererId1(refererId);
                            }
                        }
                    }
                }
            } catch (NumberFormatException e) {
                log.warn("Invalid referral format - userId={}, start='{}'", userId, start);
            }
        }
        
        UserD userD = builder.build();
        log.debug("Created UserD: userId={}, refererId1={}, refererId2={}, refererId3={}",
                userId, userD.getRefererId1(), userD.getRefererId2(), userD.getRefererId3());
        
        return userD;
    }
    
    /**
     * Sets up referral chain and increments referal counts for all 3 levels.
     * Example: If user D registers with referer C, increments:
     * - referals_1 for C
     * - referals_2 for B (C's referer_id_1)
     * - referals_3 for A (B's referer_id_1)
     */
    private void setupReferralChain(Integer newUserId, Integer refererId) {
        log.debug("Setting up referral chain: newUserId={}, refererId={}", newUserId, refererId);
        
        // Level 1: Direct referer
        userDRepository.incrementReferals1(refererId);
        
        Optional<UserD> level1Opt = userDRepository.findById(refererId);
        if (level1Opt.isEmpty()) {
            log.warn("Level 1 referer not found after increment: refererId={}", refererId);
            return;
        }
        
        UserD level1 = level1Opt.get();
        
        // Level 2
        if (level1.getRefererId1() > 0) {
            userDRepository.incrementReferals2(level1.getRefererId1());
            
            Optional<UserD> level2Opt = userDRepository.findById(level1.getRefererId1());
            if (level2Opt.isPresent()) {
                UserD level2 = level2Opt.get();
                
                // Level 3
                if (level2.getRefererId1() > 0) {
                    userDRepository.incrementReferals3(level2.getRefererId1());
                }
            }
        }
    }
    
    /** Max length for device_code column (db_users_a). */
    private static final int DEVICE_CODE_MAX_LENGTH = 5;

    /**
     * Normalizes Telegram language_code for storage in device_code (VARCHAR(5)).
     * Uses primary part of BCP 47 codes (e.g. "en-US" -> "EN") and truncates to 5 chars.
     */
    private static String normalizeDeviceCode(String languageCode) {
        if (languageCode == null || languageCode.isEmpty()) {
            return "XX";
        }
        String primary = languageCode.contains("-") ? languageCode.split("-", 2)[0] : languageCode;
        String normalized = primary.toUpperCase().trim();
        return normalized.length() > DEVICE_CODE_MAX_LENGTH ? normalized.substring(0, DEVICE_CODE_MAX_LENGTH) : normalized;
    }
    
    /**
     * Builds screen_name from first_name and last_name.
     */
    private String buildScreenName(String firstName, String lastName) {
        StringBuilder sb = new StringBuilder();
        if (firstName != null && !firstName.isEmpty()) {
            sb.append(firstName);
        }
        if (lastName != null && !lastName.isEmpty()) {
            if (sb.length() > 0) {
                sb.append(" ");
            }
            sb.append(lastName);
        }
        String result = sb.toString().trim();
        return result.isEmpty() ? "-" : (result.length() > 75 ? result.substring(0, 75) : result);
    }
    
    /**
     * Gets user by ID.
     */
    public Optional<UserA> getUserById(Integer userId) {
        return userARepository.findById(userId);
    }
    
    /**
     * Gets user by Telegram ID.
     */
    public Optional<UserA> getUserByTelegramId(Long telegramId) {
        return userARepository.findByTelegramId(telegramId);
    }
    
    private static final double COMMISSION_DISPLAY_DIVISOR = 1_000_000.0;

    /**
     * Gets referrals for a specific level with pagination.
     * Fetches raw rows (ReferralProjection), maps to API DTO (ReferralDto) with display commission.
     *
     * @param userId The user ID to get referrals for
     * @param level The referral level (1, 2, or 3)
     * @param page Page number (0-indexed)
     * @return Page of ReferralDto (name, commission as display value)
     */
    public Page<ReferralDto> getReferrals(Integer userId, Integer level, Integer page) {
        Pageable pageable = PageRequest.of(page, 10);
        Page<ReferralProjection> projectionPage = switch (level) {
            case 1 -> userDRepository.findReferralsLevel1(userId, pageable);
            case 2 -> userDRepository.findReferralsLevel2(userId, pageable);
            case 3 -> userDRepository.findReferralsLevel3(userId, pageable);
            default -> throw new IllegalArgumentException(
                    localizationService.getMessage("user.error.referralLevelInvalid", String.valueOf(level)));
        };
        List<ReferralDto> content = projectionPage.getContent().stream()
                .map(p -> ReferralDto.builder()
                        .name(p.getName())
                        .commission(p.getCommission() != null ? p.getCommission() / COMMISSION_DISPLAY_DIVISOR : 0.0)
                        .build())
                .collect(Collectors.toList());
        return new PageImpl<>(content, projectionPage.getPageable(), projectionPage.getTotalElements());
    }

    @Transactional
    public void setBotActiveByTelegramId(Long telegramId, boolean active) {
        Optional<UserA> opt = userARepository.findByTelegramId(telegramId);
        if (opt.isEmpty()) {
            log.debug("setBotActiveByTelegramId: no user for telegramId={}", telegramId);
            return;
        }
        UserA u = opt.get();
        if (u.isBotActive() == active) {
            return;
        }
        u.setBotActive(active);
        userARepository.save(u);
        log.debug("setBotActiveByTelegramId: userId={}, telegramId={}, active={}", u.getId(), telegramId, active);
    }

    @Transactional
    public void setBotActiveByUserId(Integer userId, boolean active) {
        userARepository.findById(userId).ifPresent(u -> {
            if (u.isBotActive() == active) {
                return;
            }
            u.setBotActive(active);
            userARepository.save(u);
        });
    }
}

