package com.honey.honey.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.Adler32;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.honey.honey.config.TelegramProperties;
import com.honey.honey.model.UserA;
import com.honey.honey.repository.UserARepository;

/**
 * Service for handling user avatar download, processing, and storage.
 * Stores avatar URL and last_telegram_file_id in database to avoid unnecessary downloads.
 * URLs include ?v=timestamp parameter for cache busting (forces refresh within 1h session lifetime).
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AvatarService {

    private final TelegramProperties telegramProperties;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final UserARepository userARepository;

    @Value("${app.avatar.storage-path:./data/avatars}")
    private String storagePath;
    
    // Normalized absolute storage path (computed once on initialization)
    private Path normalizedStoragePath;

    @Value("${app.avatar.public-base-url:}")
    private String publicBaseUrl;

    @Value("${app.avatar.max-size-bytes:2097152}") // 2MB default
    private long maxSizeBytes;

    @Value("${app.avatar.max-dimension:110}") // 110x110 default
    private int maxDimension;

    @Value("${app.avatar.cache-ttl-minutes:5}") // 5 minutes default
    private int cacheTtlMinutes;

    private final HttpClient httpClient = HttpClient.newBuilder()
            .followRedirects(HttpClient.Redirect.NORMAL)
            .build();
    
    // In-memory cache for avatar URLs (userId -> CacheEntry with URL and timestamp)
    private final Map<Integer, CacheEntry> avatarUrlCache = new ConcurrentHashMap<>();
    
    // Store last downloaded content type for avatar type detection
    private volatile String lastDownloadedContentType = null;
    
    /**
     * Cache entry with timestamp for TTL-based expiration.
     */
    private static class CacheEntry {
        final String avatarUrl;
        final long timestamp;
        
        CacheEntry(String avatarUrl) {
            this.avatarUrl = avatarUrl;
            this.timestamp = System.currentTimeMillis();
        }
        
        boolean isExpired(int ttlMinutes) {
            return (System.currentTimeMillis() - timestamp) > (ttlMinutes * 60 * 1000L);
        }
    }
    
    /**
     * Initializes the normalized storage path after dependency injection.
     */
    @PostConstruct
    public void init() {
        normalizedStoragePath = Paths.get(storagePath).toAbsolutePath().normalize();
        log.debug("Avatar storage path initialized: {}", normalizedStoragePath);
    }

    /**
     * Updates user avatar if the Telegram file_id has changed.
     * Only downloads and processes avatar if last_telegram_file_id differs from current file_id.
     * Stores avatar URL with ?v=timestamp parameter in database for cache busting.
     * 
     * @param user UserA entity (will be updated with new avatar_url and last_telegram_file_id)
     * @param telegramUserId Telegram user ID (from initData.user.id)
     * @param tgUser Telegram user data map (from initData) - used for fallback to photo_url
     * @return true if avatar was updated, false if no update needed or failed
     */
    public boolean updateAvatarIfNeeded(UserA user, Long telegramUserId, Map<String, Object> tgUser) {
        try {
            String currentFileId = null;
            byte[] imageData = null;
            boolean isSvg = false;
            
            // Step 1: Try to get user profile photos from Telegram Bot API
            String fileId = getProfilePhotoFileId(telegramUserId);
            if (fileId != null && !fileId.isEmpty()) {
                currentFileId = fileId;
                
                // Check if file_id changed (skip download if unchanged)
                if (fileId.equals(user.getLastTelegramFileId())) {
                    return false;
                }
                
                // Step 2: Get file path from Telegram
                String telegramFilePath = getTelegramFilePath(fileId);
                if (telegramFilePath != null && !telegramFilePath.isEmpty()) {
                    // Step 3: Download the actual image from Telegram
                    imageData = downloadTelegramFile(telegramFilePath);
                }
            }
            
            // Fallback: If Bot API failed (user restricted access or no photos), try photo_url from initData
            if (imageData == null || imageData.length == 0) {
                String photoUrl = extractAvatarUrl(tgUser);
                if (photoUrl != null && !photoUrl.isEmpty()) {
                    imageData = downloadAvatar(photoUrl);
                    String contentTypeFromDownload = getLastDownloadedContentType();
                    if (imageData != null && imageData.length > 0) {
                        // Check if it's an SVG - use Content-Type header first, then URL extension, then content check
                        String lowerUrl = photoUrl.toLowerCase();
                        boolean urlSuggestsSvg = lowerUrl.endsWith(".svg");
                        boolean contentTypeSuggestsSvg = contentTypeFromDownload != null && 
                                (contentTypeFromDownload.toLowerCase().contains("svg") || contentTypeFromDownload.toLowerCase().contains("xml"));
                        boolean contentSuggestsSvg = imageData.length > 4 && 
                                new String(imageData, 0, Math.min(100, imageData.length), java.nio.charset.StandardCharsets.UTF_8).trim().startsWith("<svg");
                        
                        // Prioritize Content-Type header over URL extension
                        if (contentTypeSuggestsSvg) {
                            isSvg = true;
                        } else if (urlSuggestsSvg && contentSuggestsSvg) {
                            isSvg = true;
                        } else if (urlSuggestsSvg && !contentTypeSuggestsSvg) {
                            // URL says .svg but Content-Type says otherwise - trust Content-Type
                            isSvg = false;
                            log.warn("URL suggests SVG but Content-Type indicates otherwise for userId={}: url={}, contentType={}, treating as binary image", 
                                    user.getId(), photoUrl, contentTypeFromDownload);
                        } else {
                            isSvg = false;
                        }
                    } else {
                        log.warn("Failed to download avatar from photo_url for userId={}, photoUrl={}", user.getId(), photoUrl);
                    }
                } else {
                    log.warn("No photo_url found in initData for userId={}", user.getId());
                }
            }
            
            if (imageData == null || imageData.length == 0) {
                log.warn("Failed to download avatar for userId={} (both Bot API and photo_url failed)", user.getId());
                return false;
            }

            // Validate size
            if (imageData.length > maxSizeBytes) {
                log.warn("Avatar too large for userId={}: {} bytes (max: {})", user.getId(), imageData.length, maxSizeBytes);
                return false;
            }

            // Determine file extension: .svg for fallback avatars, .png for Bot API avatars
            String fileExtension = isSvg ? "svg" : "png";
            
            // Calculate path components once (includes hash calculation) - reused for both file path and URL
            PathComponents pathComponents = calculatePathComponents(user.getId(), fileExtension);
            
            // Calculate file path using sharding with appropriate extension
            Path filePath = calculateFilePath(pathComponents);
            
            // Create directories if they don't exist
            Path parentDir = filePath.getParent();
            try {
                Files.createDirectories(parentDir);
            } catch (Exception e) {
                log.error("Failed to create parent directory for userId={}, parentDir={}: {}", 
                        user.getId(), parentDir, e.getMessage(), e);
                return false;
            }

            // Save file: SVG directly, PNG after processing
            if (isSvg) {
                // Save SVG directly
                try {
                    Files.write(filePath, imageData);
                } catch (Exception e) {
                    log.error("Exception while writing SVG file for userId={}, filePath={}: {}", 
                            user.getId(), filePath, e.getMessage(), e);
                    return false;
                }
            } else {
                // Process and save PNG (resize if needed)
                BufferedImage image = processImage(imageData);
                if (image == null) {
                    log.warn("Failed to process image for userId={}, imageData size={} bytes", user.getId(), imageData.length);
                    return false;
                }
                
                File outputFile = filePath.toFile();
                boolean written = ImageIO.write(image, "png", outputFile);
                if (!written) {
                    log.warn("ImageIO.write() returned false for userId={}, filePath={}", user.getId(), filePath);
                }
            }

            // Build public URL with ?v=timestamp parameter for cache busting (reuses pathComponents)
            String avatarUrl = buildPublicUrlWithTimestamp(pathComponents);
            
            // Update user entity with new avatar URL and file_id
            user.setAvatarUrl(avatarUrl);
            user.setLastTelegramFileId(currentFileId);
            userARepository.save(user);
            
            // Invalidate cache when avatar is updated
            invalidateCache(user.getId());
            
            return true;

        } catch (Exception e) {
            log.error("Error updating avatar for userId={}: {}", user.getId(), e.getMessage(), e);
            return false;
        }
    }
    
    /**
     * Gets the public URL for a user's avatar from database.
     * Returns null if user has no avatar URL stored.
     * 
     * @param userId User ID
     * @return Avatar URL from database, or null if not found
     */
    public String getAvatarUrl(Integer userId) {
        if (userId == null) {
            return null;
        }
        
        return userARepository.findById(userId)
                .map(UserA::getAvatarUrl)
                .orElse(null);
    }

    /**
     * Gets avatar URLs for multiple users in a single database query (optimized for bulk operations).
     * Returns a map of userId -> avatarUrl. Users without avatars will have null values in the map.
     * Uses cache to avoid database queries for recently fetched avatars.
     * 
     * @param userIds List of user IDs
     * @return Map of userId to avatarUrl
     */
    public Map<Integer, String> getAvatarUrls(List<Integer> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return Collections.emptyMap();
        }
        
        Map<Integer, String> avatarUrlMap = new HashMap<>();
        List<Integer> uncachedUserIds = new ArrayList<>();
        
        // Check cache first
        for (Integer userId : userIds) {
            CacheEntry cached = avatarUrlCache.get(userId);
            if (cached != null && !cached.isExpired(cacheTtlMinutes)) {
                avatarUrlMap.put(userId, cached.avatarUrl);
                // Cache hit
            } else {
                uncachedUserIds.add(userId);
            }
        }
        
        // Fetch uncached users from database in one query
        if (!uncachedUserIds.isEmpty()) {
            List<UserA> users = userARepository.findAllById(uncachedUserIds);
            
            for (UserA user : users) {
                String avatarUrl = user.getAvatarUrl();
                avatarUrlMap.put(user.getId(), avatarUrl);
                
                // Update cache
                if (avatarUrl != null) {
                    avatarUrlCache.put(user.getId(), new CacheEntry(avatarUrl));
                } else {
                    // Cache null for a shorter time (1 minute)
                    avatarUrlCache.put(user.getId(), new CacheEntry(null));
                }
            }
            
            // Ensure all requested userIds are in the map (even if null)
            for (Integer userId : uncachedUserIds) {
                avatarUrlMap.putIfAbsent(userId, null);
            }
            
            // Bulk fetch completed
        }
        
        return avatarUrlMap;
    }
    
    /**
     * Invalidates cache entry for a user (called when avatar is updated).
     * 
     * @param userId User ID
     */
    public void invalidateCache(Integer userId) {
        if (userId != null) {
            avatarUrlCache.remove(userId);
            // Cache invalidated
        }
    }

    /**
     * Gets user profile photos from Telegram Bot API and returns the file_id of the largest photo.
     * 
     * @param telegramUserId Telegram user ID
     * @return file_id of the largest profile photo, or null if no photos found
     */
    private String getProfilePhotoFileId(Long telegramUserId) {
        try {
            String botToken = telegramProperties.getBotToken();
            if (botToken == null || botToken.isEmpty()) {
                log.error("Bot token is not configured for avatar fetching");
                return null;
            }
            
            String apiUrl = String.format("https://api.telegram.org/bot%s/getUserProfilePhotos?user_id=%d&limit=1", 
                    botToken, telegramUserId);
            
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(apiUrl))
                    .GET()
                    .timeout(java.time.Duration.ofSeconds(10))
                    .build();
            
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            
            if (response.statusCode() != 200) {
                log.warn("Failed to get profile photos from Telegram: HTTP {}", response.statusCode());
                return null;
            }
            
            // Parse JSON response
            JsonNode jsonResponse = objectMapper.readTree(response.body());
            
            if (!jsonResponse.get("ok").asBoolean()) {
                String description = jsonResponse.has("description") ? jsonResponse.get("description").asText() : "Unknown error";
                log.warn("Telegram API returned error when getting profile photos: {}, telegramUserId={}", 
                        description, telegramUserId);
                return null;
            }
            
            JsonNode result = jsonResponse.get("result");
            int totalCount = result.get("total_count").asInt();
            
            if (totalCount == 0) {
                return null;
            }
            
            // Get the first photo (photos[0])
            JsonNode photos = result.get("photos");
            if (!photos.isArray() || photos.size() == 0) {
                log.warn("Photos array is empty or invalid for telegramUserId={}", telegramUserId);
                return null;
            }
            
            JsonNode firstPhoto = photos.get(0);
            if (!firstPhoto.isArray() || firstPhoto.size() == 0) {
                log.warn("First photo array is empty or invalid for telegramUserId={}", telegramUserId);
                return null;
            }
            
            // Get the last element (largest resolution)
            JsonNode largestPhoto = firstPhoto.get(firstPhoto.size() - 1);
            String fileId = largestPhoto.get("file_id").asText();
            
            return fileId;
            
        } catch (Exception e) {
            log.error("Error fetching profile photos for telegramUserId={}: {}", telegramUserId, e.getMessage(), e);
            return null;
        }
    }
    
    /**
     * Gets file path from Telegram using file_id.
     * 
     * @param fileId Telegram file_id
     * @return file_path from Telegram, or null if failed
     */
    private String getTelegramFilePath(String fileId) {
        try {
            String botToken = telegramProperties.getBotToken();
            String apiUrl = String.format("https://api.telegram.org/bot%s/getFile?file_id=%s", 
                    botToken, fileId);
            
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(apiUrl))
                    .GET()
                    .timeout(java.time.Duration.ofSeconds(10))
                    .build();
            
            HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
            
            if (response.statusCode() != 200) {
                log.warn("Failed to get file path from Telegram: HTTP {}", response.statusCode());
                return null;
            }
            
            // Parse JSON response
            JsonNode jsonResponse = objectMapper.readTree(response.body());
            
            if (!jsonResponse.get("ok").asBoolean()) {
                String description = jsonResponse.has("description") ? jsonResponse.get("description").asText() : "Unknown error";
                log.warn("Telegram API returned error when getting file path: {}", description);
                return null;
            }
            
            String filePath = jsonResponse.get("result").get("file_path").asText();
            
            return filePath;
            
        } catch (Exception e) {
            log.error("Error getting file path for fileId={}: {}", fileId, e.getMessage(), e);
            return null;
        }
    }
    
    /**
     * Downloads file from Telegram file server.
     * 
     * @param filePath Telegram file path (e.g., "photos/file_123.jpg")
     * @return File content as bytes, or null if failed
     */
    private byte[] downloadTelegramFile(String filePath) {
        try {
            String botToken = telegramProperties.getBotToken();
            String downloadUrl = String.format("https://api.telegram.org/file/bot%s/%s", 
                    botToken, filePath);
            
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(downloadUrl))
                    .GET()
                    .timeout(java.time.Duration.ofSeconds(30)) // Longer timeout for file downloads
                    .build();
            
            HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
            
            if (response.statusCode() == 200) {
                return response.body();
            } else {
                log.warn("Failed to download file from Telegram: HTTP {}", response.statusCode());
                return null;
            }
        } catch (Exception e) {
            log.error("Error downloading file from Telegram: {}", e.getMessage(), e);
            return null;
        }
    }

    /**
     * Extracts avatar URL from Telegram user data (fallback method).
     * Telegram provides photo_url in the user object (usually SVG with initials).
     */
    private String extractAvatarUrl(Map<String, Object> tgUser) {
        if (tgUser == null) {
            return null;
        }
        Object photoUrl = tgUser.get("photo_url");
        if (photoUrl != null) {
            return photoUrl.toString();
        }
        return null;
    }

    /**
     * Downloads avatar image from URL (fallback method for photo_url).
     */
    private byte[] downloadAvatar(String url) {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                    .uri(URI.create(url))
                    .GET()
                    .timeout(java.time.Duration.ofSeconds(10))
                    .build();

            HttpResponse<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
            
            // Store Content-Type for later use
            String contentType = response.headers().firstValue("Content-Type").orElse("");
            lastDownloadedContentType = contentType;
            
            if (response.statusCode() == 200) {
                return response.body();
            } else {
                log.warn("Failed to download avatar from URL: HTTP {}, url={}", response.statusCode(), url);
                lastDownloadedContentType = null;
                return null;
            }
        } catch (Exception e) {
            log.error("Error downloading avatar from {}: {}", url, e.getMessage(), e);
            lastDownloadedContentType = null;
            return null;
        }
    }
    
    /**
     * Gets the Content-Type of the last downloaded avatar (used for type detection).
     * @return Content-Type header value, or null if not available
     */
    private String getLastDownloadedContentType() {
        return lastDownloadedContentType;
    }

    /**
     * Processes image: validates format and resizes if needed.
     * Only used for PNG images from Telegram Bot API.
     */
    private BufferedImage processImage(byte[] imageData) {
        try {
            // Read image from bytes
            BufferedImage image = ImageIO.read(new java.io.ByteArrayInputStream(imageData));
            if (image == null) {
                log.warn("Failed to read image data - unsupported format (might be SVG or corrupted file)");
                return null;
            }

            // Resize if image is too large
            int width = image.getWidth();
            int height = image.getHeight();
            
            if (width > maxDimension || height > maxDimension) {
                // Calculate new dimensions maintaining aspect ratio
                double scale = Math.min((double) maxDimension / width, (double) maxDimension / height);
                int newWidth = (int) (width * scale);
                int newHeight = (int) (height * scale);
                
                log.debug("Resizing image from {}x{} to {}x{}", width, height, newWidth, newHeight);
                
                // Use high-quality scaling
                BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB);
                Graphics2D g = resized.createGraphics();
                g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
                g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g.drawImage(image, 0, 0, newWidth, newHeight, null);
                g.dispose();
                
                return resized;
            }

            // Convert to ARGB if needed (for PNG support)
            if (image.getType() != BufferedImage.TYPE_INT_ARGB) {
                BufferedImage converted = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
                Graphics2D g = converted.createGraphics();
                g.drawImage(image, 0, 0, null);
                g.dispose();
                return converted;
            }

            return image;

        } catch (IOException e) {
            log.error("Error processing image: {}", e.getMessage(), e);
            return null;
        }
    }

    /**
     * Path components for avatar file storage.
     * Contains all calculated values needed for both file path and URL generation.
     */
    private static class PathComponents {
        final String digit2;
        final String digit1;
        final String digit0;
        final String hash;
        final String filename;
        
        PathComponents(String digit2, String digit1, String digit0, String hash, String filename) {
            this.digit2 = digit2;
            this.digit1 = digit1;
            this.digit0 = digit0;
            this.hash = hash;
            this.filename = filename;
        }
    }
    
    /**
     * Calculates path components (folder digits, hash, filename) for a user ID.
     * This is done once and reused for both file path and URL generation.
     * 
     * @param userId User ID
     * @param extension File extension ("png" or "svg")
     * @return PathComponents with all calculated values
     */
    private PathComponents calculatePathComponents(Integer userId, String extension) {
        // Zero-pad user ID to 8 digits
        String paddedId = String.format("%08d", userId);
        
        // Extract last 3 digits for folder hierarchy
        String digit2 = String.valueOf(paddedId.charAt(5)); // 6th digit (0-indexed)
        String digit1 = String.valueOf(paddedId.charAt(6)); // 7th digit
        String digit0 = String.valueOf(paddedId.charAt(7)); // 8th digit (last)
        
        // Hash the entire 8-digit padded ID for filename
        String hash = hashString(paddedId);
        String filename = hash + "." + extension;
        
        return new PathComponents(digit2, digit1, digit0, hash, filename);
    }
    
    /**
     * Calculates file path using sharding logic from pre-calculated path components.
     * 
     * @param components Pre-calculated path components
     * @return File path for storing the avatar
     */
    private Path calculateFilePath(PathComponents components) {
        // Build path using normalized storage path
        return normalizedStoragePath
                .resolve(components.digit2)
                .resolve(components.digit1)
                .resolve(components.digit0)
                .resolve(components.filename);
    }

    /**
     * Hashes a string to an 8-character hex string using Adler32.
     * Adler32 is a fast checksum algorithm that produces a 32-bit hash value.
     * 
     * @param input Input string to hash
     * @return 8-character hexadecimal hash string
     */
    private String hashString(String input) {
        if (input == null || input.isEmpty()) {
            return "00000000";
        }
        
        Adler32 adler32 = new Adler32();
        byte[] bytes = input.getBytes(java.nio.charset.StandardCharsets.UTF_8);
        adler32.update(bytes);
        
        long hashValue = adler32.getValue();
        return String.format("%08x", hashValue);
    }

    /**
     * Builds public URL for the avatar with ?v=timestamp parameter for cache busting.
     * Format: {publicBaseUrl}/avatars/{digit2}/{digit1}/{digit0}/{hash}.{ext}?v={timestamp}
     * If publicBaseUrl is empty, returns relative URL starting with /avatars
     * 
     * Uses pre-calculated path components to avoid duplicate hash calculation.
     * 
     * @param components Pre-calculated path components
     * @return Public URL with timestamp parameter
     */
    private String buildPublicUrlWithTimestamp(PathComponents components) {
        // Build relative path
        String relativePath = String.format("/avatars/%s/%s/%s/%s", 
                components.digit2, components.digit1, components.digit0, components.filename);
        
        // Add timestamp parameter for cache busting (forces refresh within 1h session lifetime)
        long timestamp = System.currentTimeMillis() / 1000; // Unix timestamp in seconds
        String pathWithTimestamp = relativePath + "?v=" + timestamp;
        
        // If publicBaseUrl is set, prepend it; otherwise return relative URL
        if (publicBaseUrl != null && !publicBaseUrl.isEmpty()) {
            String baseUrl = publicBaseUrl.endsWith("/") 
                    ? publicBaseUrl.substring(0, publicBaseUrl.length() - 1) 
                    : publicBaseUrl;
            return baseUrl + pathWithTimestamp;
        }
        
        // Return relative URL (browser will resolve it relative to current origin)
        return pathWithTimestamp;
    }
}
