package com.honey.honey.service;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.honey.honey.config.TelegramProperties;
import com.honey.honey.dto.CreatePaymentRequest;
import com.honey.honey.dto.DepositAddressApiRequest;
import com.honey.honey.dto.DepositAddressResponse;
import com.honey.honey.dto.DepositAddressResultDto;
import com.honey.honey.dto.PaymentInvoiceResponse;
import com.honey.honey.dto.PaymentWebhookRequest;
import com.honey.honey.model.CryptoDepositMethod;
import com.honey.honey.model.Payment;
import com.honey.honey.model.UserA;
import com.honey.honey.model.UserB;
import com.honey.honey.model.UserD;
import com.honey.honey.repository.CryptoDepositMethodRepository;
import com.honey.honey.repository.PaymentRepository;
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.TelegramTokenRedactor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;

/**
 * Service for handling Telegram Stars payment operations.
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final UserARepository userARepository;
    private final UserBRepository userBRepository;
    private final UserDRepository userDRepository;
    private final CryptoDepositMethodRepository cryptoDepositMethodRepository;
    private final TelegramProperties telegramProperties;
    private final TransactionService transactionService;
    private final LocalizationService localizationService;
    private final CryptoDepositService cryptoDepositService;
    private final RestTemplate restTemplate = new RestTemplate();

    // Conversion rate: 1 Star = 9 Tickets, so in bigint: 1 Star = 9,000,000 (legacy)
    private static final long STARS_TO_TICKETS_MULTIPLIER = 9_000_000L;

    // USD stored as decimal (e.g. 1.25). Tickets in DB: 1 ticket = 1_000_000; 1 USD = 1000 tickets -> ticketsAmount = round(usdAmount * 1_000_000_000) (legacy Stars)
    private static final long TICKETS_DB_UNITS_PER_USD = 1_000_000_000L;

    // Honey app: game balance (in-game currency). 1 USD = 10_000 display units -> 1 USD = 10_000_000_000 in DB.
    private static final long GAME_BALANCE_DB_UNITS_PER_USD = 10_000_000_000L;
    private static final double FIRST_DEPOSIT_BONUS_MULTIPLIER = 1.2; // 20% bonus on first deposit

    private static final double MIN_USD = 3.0;
    private static final double MAX_USD = 20_000.0;

    // Minimum and maximum stars amounts (legacy)
    private static final int MIN_STARS = 50;
    private static final int MAX_STARS = 100000;

    /** When false, Telegram Stars payment webhooks are ignored (no balance credited). Invoice creation for Stars is already rejected. */
    private static final boolean TELEGRAM_STARS_PAYMENTS_ENABLED = false;

    /** Referral commission as fraction of base deposit (no bonus). Level 1 = referer_1, level 2 = referer_2, level 3 = referer_3. */
    private static final double REFERRER_LEVEL1_PERCENT = 0.12;
    private static final double REFERRER_LEVEL2_PERCENT = 0.06;
    private static final double REFERRER_LEVEL3_PERCENT = 0.02;

    /**
     * Creates a payment invoice for the user.
     * Validates the stars amount and creates a pending payment record.
     * 
     * @param userId The user ID
     * @param request The payment request with stars amount
     * @return Payment invoice response with order ID and amounts
     */
    @Transactional
    public PaymentInvoiceResponse createPaymentInvoice(Integer userId, CreatePaymentRequest request) {
        Double usdAmountDouble = request.getUsdAmount();
        Integer starsAmount = request.getStarsAmount();

        if (usdAmountDouble != null) {
            validateUsd(usdAmountDouble);
            BigDecimal usdAmount = BigDecimal.valueOf(usdAmountDouble).setScale(2, RoundingMode.UNNECESSARY);
            long playBalance = usdToPlayBalance(usdAmountDouble);

            String orderId = generateOrderId(userId);
            Payment payment = Payment.builder()
                    .userId(userId)
                    .orderId(orderId)
                    .starsAmount(0)
                    .usdAmount(usdAmount)
                    .ticketsAmount(0L)
                    .playBalance(playBalance)
                    .status(Payment.PaymentStatus.PENDING)
                    .build();
            paymentRepository.save(payment);
            log.info("Payment invoice created (crypto): orderId={}, userId={}, usdAmount={}, playBalance={}", orderId, userId, usdAmount, playBalance);

            return PaymentInvoiceResponse.builder()
                    .invoiceId(orderId)
                    .invoiceUrl(null)
                    .starsAmount(null)
                    .usdAmount(usdAmountDouble)
                    .ticketsAmount(null)
                    .playBalance(playBalance)
                    .build();
        }

        // Legacy payment flow no longer supported (crypto-only)
        throw new IllegalArgumentException(localizationService.getMessage("payment.error.legacyNotSupported"));
    }

    /**
     * Creates an invoice link via Telegram Bot API for Stars payment.
     * The invoice link can be used with tg.openInvoice() in the Mini App.
     * Disabled when {@link #TELEGRAM_STARS_PAYMENTS_ENABLED} is false.
     */
    private String createInvoiceLink(String orderId, Integer starsAmount) {
        if (!TELEGRAM_STARS_PAYMENTS_ENABLED) {
            throw new IllegalStateException(localizationService.getMessage("payment.error.legacyNotSupported"));
        }
        String botToken = telegramProperties.getBotToken();
        if (botToken == null || botToken.isEmpty()) {
            log.error("Bot token is not configured");
            throw new IllegalStateException(localizationService.getMessage("payment.error.botTokenNotConfigured"));
        }

        String url = "https://api.telegram.org/bot" + botToken + "/createInvoiceLink";
        // Do not log url or any string that may contain the bot token (use TelegramTokenRedactor if needed).

        // Build request body
        var requestBody = new java.util.HashMap<String, Object>();
        requestBody.put("title", "Purchase Tickets");
        requestBody.put("description", String.format("Purchase %d Stars to receive tickets", starsAmount));
        requestBody.put("payload", orderId); // Order ID as payload
        requestBody.put("provider_token", ""); // Empty for Stars
        requestBody.put("currency", "XTR"); // XTR is the currency code for Stars
        requestBody.put("prices", java.util.List.of(
                java.util.Map.of("label", starsAmount + " Stars", "amount", starsAmount)
        ));

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);

        try {
            log.debug("Creating invoice link: orderId={}, starsAmount={}", orderId, starsAmount);
            ResponseEntity<TelegramCreateInvoiceLinkResponse> response = restTemplate.postForEntity(
                    url,
                    entity,
                    TelegramCreateInvoiceLinkResponse.class
            );

            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
                String invoiceUrl = response.getBody().getResult();
                log.debug("Invoice link created: orderId={}", orderId);
                return invoiceUrl;
            } else {
                log.error("Failed to create invoice link: orderId={}, status={}", orderId, response.getStatusCode());
                throw new IllegalStateException(localizationService.getMessage("payment.error.failedToCreateInvoice"));
            }
        } catch (Exception e) {
            log.error("Error creating invoice link: orderId={}, error={}", orderId, TelegramTokenRedactor.redact(e.getMessage()), e);
            throw new IllegalStateException(localizationService.getMessage("payment.error.failedToCreateInvoice") + ": " + e.getMessage(), e);
        }
    }

    /**
     * Processes a payment webhook from Telegram bot.
     * Validates the payment and credits the user's balance.
     * Ensures idempotency by checking if payment was already processed.
     * 
     * @param request The webhook request with payment details
     * @return true if payment was successfully processed, false if already processed
     */
    @Transactional
    public boolean processPaymentWebhook(PaymentWebhookRequest request) {
        if (!TELEGRAM_STARS_PAYMENTS_ENABLED) {
            log.warn("Telegram Stars payment webhook ignored (Stars payments disabled): orderId={}", request.getOrderId());
            return false;
        }

        String orderId = request.getOrderId();
        Long telegramUserId = request.getTelegramUserId();
        Integer starsAmount = request.getStarsAmount();

        // Find payment by order ID
        Payment payment = paymentRepository.findByOrderId(orderId)
                .orElseThrow(() -> {
                    log.info("Payment webhook rejected: orderId={}, reason=paymentNotFound", orderId);
                    return new IllegalArgumentException(localizationService.getMessage("payment.error.paymentNotFound", orderId));
                });

        log.info("Payment webhook processing: orderId={}, paymentUserId={}, currentStatus={}, starsAmount={}",
                orderId, payment.getUserId(), payment.getStatus(), starsAmount);

        // Check if payment was already processed (idempotency)
        if (payment.getStatus() == Payment.PaymentStatus.COMPLETED) {
            log.warn("Payment already processed: orderId={}", orderId);
            return false; // Already processed
        }

        // Validate payment details: Look up user by Telegram ID and compare internal user IDs
        UserA user = userARepository.findByTelegramId(telegramUserId)
                .orElseThrow(() -> {
                    log.info("Payment webhook rejected: orderId={}, reason=userNotFound, telegramUserId={}", orderId, telegramUserId);
                    return new IllegalArgumentException(localizationService.getMessage("payment.error.userNotFound", String.valueOf(telegramUserId)));
                });
        
        if (!payment.getUserId().equals(user.getId())) {
            log.info("Payment webhook rejected: orderId={}, reason=userIdMismatch, paymentUserId={}, telegramUserId={} -> userId={}",
                    orderId, payment.getUserId(), telegramUserId, user.getId());
            throw new IllegalArgumentException(localizationService.getMessage("payment.error.userIdMismatch", 
                    String.valueOf(payment.getUserId()), String.valueOf(user.getId())));
        }

        if (!payment.getStarsAmount().equals(starsAmount)) {
            log.info("Payment webhook rejected: orderId={}, reason=starsAmountMismatch, expected={}, received={}",
                    orderId, payment.getStarsAmount(), starsAmount);
            throw new IllegalArgumentException(localizationService.getMessage("payment.error.starsAmountMismatch", 
                    String.valueOf(payment.getStarsAmount()), String.valueOf(starsAmount)));
        }

        // Update payment status
        payment.setStatus(Payment.PaymentStatus.COMPLETED);
        payment.setTelegramPaymentChargeId(request.getTelegramPaymentChargeId());
        payment.setTelegramProviderPaymentChargeId(request.getTelegramProviderPaymentChargeId());
        payment.setCompletedAt(Instant.now());
        paymentRepository.save(payment);

        // Credit user balance
        UserB userB = userBRepository.findById(payment.getUserId())
                .orElseThrow(() -> new IllegalStateException(localizationService.getMessage("user.error.balanceNotFound")));

        Long ticketsAmount = payment.getTicketsAmount();
        userB.setBalanceA(userB.getBalanceA() + ticketsAmount);
        
        // Update deposit statistics
        userB.setDepositTotal(userB.getDepositTotal() + ticketsAmount);
        userB.setDepositCount(userB.getDepositCount() + 1);

        userBRepository.save(userB);

        // Create transaction record
        try {
            transactionService.createDepositTransaction(payment.getUserId(), ticketsAmount);
        } catch (Exception e) {
            log.error("Error creating deposit transaction: userId={}, amount={}", payment.getUserId(), ticketsAmount, e);
            // Continue even if transaction record creation fails
        }

        log.info("Payment completed: orderId={}, userId={}, ticketsAmount={}", 
                orderId, payment.getUserId(), ticketsAmount);

        return true; // Successfully processed
    }

    /**
     * Requests a crypto deposit address from the external API. Does not create any payment record.
     * Used when user selects a payment method on Payment Options screen.
     *
     * @param userId   current user id (from db_users_a)
     * @param pid      deposit method PID from deposit-methods
     * @param usdAmount USD as decimal, e.g. 3.25
     * @return result with address, amountCoins, name, network, psId for Payment Confirmation screen
     */
    public DepositAddressResultDto requestCryptoDepositAddress(Integer userId, Integer pid, Double usdAmount) {
        if (pid == null) {
            throw new IllegalArgumentException(localizationService.getMessage("payment.error.invalidPid"));
        }
        if (usdAmount == null) {
            throw new IllegalArgumentException(localizationService.getMessage("payment.error.usdRange", "3", "20000"));
        }
        validateUsd(usdAmount);

        UserA user = userARepository.findById(userId)
                .orElseThrow(() -> new IllegalArgumentException(localizationService.getMessage("user.error.notFound")));

        String userIp = formatIpForCryptoApi(user.getIp());
        DepositAddressApiRequest.UserData userData = DepositAddressApiRequest.UserData.builder()
                .internalId(user.getId())
                .screenName(user.getScreenName() != null ? user.getScreenName() : "")
                .tgUsername(user.getTelegramName() != null ? user.getTelegramName() : "")
                .tgId(user.getTelegramId() != null ? String.valueOf(user.getTelegramId()) : "")
                .countryCode(user.getCountryCode() != null ? user.getCountryCode() : "XX")
                .deviceCode(user.getDeviceCode() != null ? user.getDeviceCode() : "XX")
                .languageCode(user.getLanguageCode() != null ? user.getLanguageCode() : "XX")
                .userIp(userIp != null ? userIp : "0.0.0.0")
                .build();

        DepositAddressApiRequest apiRequest = DepositAddressApiRequest.builder()
                .pid(pid)
                .amountUsd(usdAmount)
                .userData(userData)
                .build();

        DepositAddressResponse apiResponse = cryptoDepositService.postDepositAddress(apiRequest);
        if (apiResponse == null || apiResponse.getResult() == null) {
            throw new IllegalStateException(localizationService.getMessage("payment.error.depositAddressFailed"));
        }
        if (apiResponse.getRequestInfo() != null && apiResponse.getRequestInfo().getErrorCode() != null
                && apiResponse.getRequestInfo().getErrorCode() != 0) {
            String msg = apiResponse.getRequestInfo().getErrorMessage() != null
                    ? apiResponse.getRequestInfo().getErrorMessage()
                    : localizationService.getMessage("payment.error.depositAddressFailed");
            throw new IllegalStateException(msg);
        }

        DepositAddressResponse.Result result = apiResponse.getResult();

        log.debug("Crypto deposit address requested: userId={}, psId={}, usdAmount={}", userId, result.getPsId(), usdAmount);

        int pidForLookup = result.getPsId() != null ? result.getPsId() : pid;
        Double minAmount = cryptoDepositMethodRepository.findByPid(pidForLookup)
                .map(CryptoDepositMethod::getMinDepositSum)
                .map(BigDecimal::doubleValue)
                .orElse(null);

        return DepositAddressResultDto.builder()
                .address(result.getAddress())
                .amountCoins(result.getAmountCoins())
                .name(result.getName())
                .network(result.getNetwork())
                .psId(result.getPsId())
                .minAmount(minAmount)
                .build();
    }

    /**
     * Format stored IP (varbinary 16) for crypto API. IPv6 -> "0.0.0.0" per spec.
     */
    private static String formatIpForCryptoApi(byte[] ipBytes) {
        if (ipBytes == null || ipBytes.length == 0) {
            return "0.0.0.0";
        }
        if (ipBytes.length == 16) {
            return "0.0.0.0";
        }
        String s = IpUtils.bytesToIp(ipBytes);
        return s != null ? s : "0.0.0.0";
    }

    /**
     * Processes a deposit completion from 3rd party webhook (e.g. crypto payment provider).
     * Creates a COMPLETED payment with playBalance, credits balanceB, updates deposit stats, creates DEPOSIT transaction.
     * First deposit gets 20% bonus (playBalance = usd * 12_000_000_000).
     *
     * @param userId             internal user id (db_users_a.id)
     * @param usdAmount          USD as decimal, e.g. 1.45 (3rd party sends as number)
     */
    @Transactional
    public void processExternalDepositCompletion(Integer userId, Double usdAmount) {
        if (userId == null) {
            throw new IllegalArgumentException("user_id is required");
        }
        if (usdAmount == null) {
            throw new IllegalArgumentException(localizationService.getMessage("payment.error.usdRange", "3", "20000"));
        }
        validateUsd(usdAmount);

        String orderId = "ext-deposit-" + userId + "-" + UUID.randomUUID().toString();

        if (!userARepository.existsById(userId)) {
            throw new IllegalArgumentException(localizationService.getMessage("user.error.notFound"));
        }
        UserB userB = userBRepository.findById(userId)
                .orElseThrow(() -> new IllegalArgumentException(localizationService.getMessage("user.error.balanceNotFound")));

        boolean firstDeposit = (userB.getDepositCount() == null || userB.getDepositCount() == 0);
        long basePlayBalance = usdToPlayBalance(usdAmount); // USD * 10_000_000_000 (no bonus)
        long playBalance = firstDeposit
                ? Math.round(usdAmount * GAME_BALANCE_DB_UNITS_PER_USD * FIRST_DEPOSIT_BONUS_MULTIPLIER)
                : basePlayBalance;

        BigDecimal usdAmountBd = BigDecimal.valueOf(usdAmount).setScale(2, RoundingMode.HALF_UP);
        Instant now = Instant.now();

        Payment payment = Payment.builder()
                .userId(userId)
                .orderId(orderId)
                .starsAmount(0)
                .usdAmount(usdAmountBd)
                .ticketsAmount(0L)
                .playBalance(playBalance)
                .status(Payment.PaymentStatus.COMPLETED)
                .completedAt(now)
                .ftd(firstDeposit)
                .build();
        paymentRepository.save(payment);

        userB.setBalanceB(userB.getBalanceB() + playBalance);
        // depositTotal = sum of base amounts only (no bonus), so we add basePlayBalance
        userB.setDepositTotal(userB.getDepositTotal() + basePlayBalance);
        userB.setDepositCount(userB.getDepositCount() + 1);
        userBRepository.save(userB);

        try {
            transactionService.createDepositTransaction(userId, playBalance);
        } catch (Exception e) {
            log.error("Error creating deposit transaction: userId={}, amount={}", userId, playBalance, e);
        }

        // Referral commissions: % of base deposit (no bonus), credited to referer balanceB and tracked in UserD
        applyReferralCommissions(userId, basePlayBalance);

        log.info("External deposit completed: orderId={}, userId={}, usdAmount={}, playBalance={}, firstDeposit={}", orderId, userId, usdAmountBd, playBalance, firstDeposit);
    }

    /**
     * Applies referral commissions on deposit: referer_1 gets REFERRER_LEVEL1_PERCENT, referer_2 gets REFERRER_LEVEL2_PERCENT, referer_3 gets REFERRER_LEVEL3_PERCENT of base play balance (no bonus).
     * Updates: referer's balanceB, depositor's to_referer_N, referer's from_referals_N.
     */
    private void applyReferralCommissions(Integer depositorUserId, long basePlayBalance) {
        if (basePlayBalance <= 0) return;
        UserD depositorUserD = userDRepository.findById(depositorUserId).orElse(null);
        if (depositorUserD == null) return;

        long commission1 = Math.round(basePlayBalance * REFERRER_LEVEL1_PERCENT);
        long commission2 = Math.round(basePlayBalance * REFERRER_LEVEL2_PERCENT);
        long commission3 = Math.round(basePlayBalance * REFERRER_LEVEL3_PERCENT);

        applyReferralLevel(depositorUserD, depositorUserId, 1, depositorUserD.getRefererId1(), commission1,
                UserD::getToReferer1, UserD::setToReferer1, UserD::getFromReferals1, UserD::setFromReferals1);
        applyReferralLevel(depositorUserD, depositorUserId, 2, depositorUserD.getRefererId2(), commission2,
                UserD::getToReferer2, UserD::setToReferer2, UserD::getFromReferals2, UserD::setFromReferals2);
        applyReferralLevel(depositorUserD, depositorUserId, 3, depositorUserD.getRefererId3(), commission3,
                UserD::getToReferer3, UserD::setToReferer3, UserD::getFromReferals3, UserD::setFromReferals3);

        userDRepository.save(depositorUserD);
    }

    private void applyReferralLevel(UserD depositorUserD, Integer depositorUserId, int level, Integer refererId, long commission,
                                    java.util.function.Function<UserD, Long> getToReferer, java.util.function.BiConsumer<UserD, Long> setToReferer,
                                    java.util.function.Function<UserD, Long> getFromReferals, java.util.function.BiConsumer<UserD, Long> setFromReferals) {
        if (refererId == null || refererId <= 0 || commission <= 0) return;

        setToReferer.accept(depositorUserD, (getToReferer.apply(depositorUserD) != null ? getToReferer.apply(depositorUserD) : 0L) + commission);

        userBRepository.findById(refererId).ifPresent(refererB -> {
            refererB.setBalanceB(refererB.getBalanceB() + commission);
            userBRepository.save(refererB);
        });
        userDRepository.findById(refererId).ifPresent(refererD -> {
            setFromReferals.accept(refererD, (getFromReferals.apply(refererD) != null ? getFromReferals.apply(refererD) : 0L) + commission);
            userDRepository.save(refererD);
        });

        log.debug("Referral commission applied: depositor={}, refererLevel={}, refererId={}, commission={}", depositorUserId, level, refererId, commission);
    }

    /** USD range 3–20000 and at most 2 decimal places. */
    private void validateUsd(double usdAmount) {
        if (usdAmount < MIN_USD || usdAmount > MAX_USD) {
            throw new IllegalArgumentException(localizationService.getMessage("payment.error.usdRange", "3", "20000"));
        }
        double rounded = Math.round(usdAmount * 100) / 100.0;
        if (Math.abs(usdAmount - rounded) > 1e-9) {
            throw new IllegalArgumentException(localizationService.getMessage("payment.error.usdMaxTwoDecimals"));
        }
    }

    /** Converts USD (e.g. 1.45) to tickets amount in DB. 1 USD = 1000 tickets; 1 ticket = 1_000_000 in DB. (Legacy Stars) */
    private long usdToTicketsAmount(double usdAmount) {
        return Math.round(usdAmount * TICKETS_DB_UNITS_PER_USD);
    }

    /** Converts USD to play balance in DB. 1 USD = 10_000_000_000. */
    private long usdToPlayBalance(double usdAmount) {
        return Math.round(usdAmount * GAME_BALANCE_DB_UNITS_PER_USD);
    }

    /**
     * Marks a payment as cancelled (e.g., user cancelled in Telegram UI).
     * 
     * @param orderId The order ID
     */
    @Transactional
    public void cancelPayment(String orderId) {
        Payment payment = paymentRepository.findByOrderId(orderId)
                .orElseThrow(() -> new IllegalArgumentException(localizationService.getMessage("payment.error.paymentNotFound", orderId)));

        if (payment.getStatus() == Payment.PaymentStatus.COMPLETED) {
            log.warn("Cannot cancel completed payment: orderId={}", orderId);
            return;
        }

        payment.setStatus(Payment.PaymentStatus.CANCELLED);
        paymentRepository.save(payment);

        log.info("Payment cancelled: orderId={}, userId={}", orderId, payment.getUserId());
    }

    /**
     * Generates a unique order ID for the payment.
     * Format: payment-{userId}-{uuid}
     */
    private String generateOrderId(Integer userId) {
        return "payment-" + userId + "-" + UUID.randomUUID().toString();
    }

    // Response DTOs for Telegram API
    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class TelegramCreateInvoiceLinkResponse {
        @JsonProperty("ok")
        private Boolean ok;
        
        @JsonProperty("result")
        private String result;
    }
}

