package com.honey.honey.service;

import com.honey.honey.dto.WithdrawalApiRequest;
import com.honey.honey.dto.WithdrawalApiResponse;
import com.honey.honey.dto.WithdrawalInfoApiResponse;
import com.honey.honey.dto.WithdrawalMethodDetailsDto;
import com.honey.honey.dto.WithdrawalMethodsApiResponse;
import com.honey.honey.dto.WithdrawalMethodsDto;
import com.honey.honey.model.CryptoWithdrawalMethod;
import com.honey.honey.model.UserA;
import com.honey.honey.repository.CryptoWithdrawalMethodRepository;
import com.honey.honey.repository.UserARepository;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.honey.honey.util.IpUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
public class CryptoWithdrawalService {

    private static final String WITHDRAWAL_METHODS_PATH = "api/v1/withdrawal-methods";
    private static final String WITHDRAWAL_PATH = "api/v1/withdrawal";
    private static final String WITHDRAWALS_INFO_PATH = "api/v1/withdrawals-info";
    private static final BigDecimal DEFAULT_MIN_WITHDRAWAL = new BigDecimal("0.10");

    @Value("${app.crypto-api.base-url:https://spin-passim.tech/}")
    private String baseUrl;

    @Value("${app.crypto-api.api-key:}")
    private String apiKey;

    private final RestTemplate restTemplate = new RestTemplate();
    private final ObjectMapper objectMapper;
    private final CryptoWithdrawalMethodRepository methodRepository;
    private final UserARepository userARepository;

    /** In-memory lock: one withdrawal per user at a time to prevent double-submit / spam. */
    private final ConcurrentHashMap<Integer, Object> withdrawalInProgress = new ConcurrentHashMap<>();

    /**
     * Tries to acquire the withdrawal lock for the user. Returns false if user already has a withdrawal in progress.
     */
    public boolean tryAcquireWithdrawal(Integer userId) {
        return withdrawalInProgress.putIfAbsent(userId, Boolean.TRUE) == null;
    }

    /**
     * Releases the withdrawal lock for the user. Must be called in finally after API call.
     */
    public void releaseWithdrawal(Integer userId) {
        withdrawalInProgress.remove(userId);
    }

    /**
     * Calls external API POST api/v1/withdrawal. Does not create payout or deduct balance.
     * Caller must hold the withdrawal lock and release it in finally.
     *
     * @param manualPay when true, adds manual_pay=1 to the request (for users who completed referral 50 or 100)
     * @return response body on HTTP 200
     * @throws org.springframework.web.client.HttpStatusCodeException on non-2xx
     */
    public WithdrawalApiResponse postWithdrawal(Integer userId, Integer pid, String wallet, Double amountUsd, boolean manualPay) {
        UserA user = userARepository.findById(userId)
                .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId));

        String userIp = formatIpForCryptoApi(user.getIp());
        WithdrawalApiRequest.UserData userData = WithdrawalApiRequest.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();

        WithdrawalApiRequest.WithdrawalApiRequestBuilder requestBuilder = WithdrawalApiRequest.builder()
                .pid(pid)
                .userId(userId)
                .wallet(wallet)
                .amountUsd(amountUsd)
                .userData(userData);
        if (manualPay) {
            requestBuilder.manualPay(1);
        }
        WithdrawalApiRequest request = requestBuilder.build();

        String url = baseUrl.endsWith("/") ? baseUrl + WITHDRAWAL_PATH : baseUrl + "/" + WITHDRAWAL_PATH;
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        if (apiKey != null && !apiKey.isEmpty()) {
            headers.set("API-KEY", apiKey);
        }
        HttpEntity<WithdrawalApiRequest> entity = new HttpEntity<>(request, headers);
        if (log.isDebugEnabled()) {
            log.debug("Crypto API request: POST {} body={}", url, toJson(request));
        }
        try {
            ResponseEntity<WithdrawalApiResponse> response = restTemplate.exchange(url, HttpMethod.POST, entity, WithdrawalApiResponse.class);
            WithdrawalApiResponse body = response.getBody();
            if (log.isDebugEnabled()) {
                log.debug("Crypto API response: POST {} status={} body={}", url, response.getStatusCode(), toJson(body));
            }
            return body;
        } catch (HttpStatusCodeException e) {
            if (log.isDebugEnabled()) {
                log.debug("Crypto API error response: POST {} status={} body={}", url, e.getStatusCode(), e.getResponseBodyAsString());
            }
            throw e;
        }
    }

    /**
     * Calls external API GET api/v1/withdrawals-info/{payment_id}.
     * Returns empty if request fails or result has no payment_list / empty list.
     */
    public Optional<WithdrawalInfoApiResponse> getWithdrawalInfo(int paymentId) {
        String path = WITHDRAWALS_INFO_PATH + "/" + paymentId;
        String url = baseUrl.endsWith("/") ? baseUrl + path : baseUrl + "/" + path;
        HttpHeaders headers = new HttpHeaders();
        if (apiKey != null && !apiKey.isEmpty()) {
            headers.set("API-KEY", apiKey);
        }
        try {
            log.debug("Crypto API request: GET {}", url);
            ResponseEntity<WithdrawalInfoApiResponse> response = restTemplate.exchange(
                    url, HttpMethod.GET, new HttpEntity<>(headers), WithdrawalInfoApiResponse.class);
            WithdrawalInfoApiResponse responseBody = response.getBody();
            log.debug("Crypto API response: GET {} status={} body={}", url, response.getStatusCode(), toJson(responseBody));
            return Optional.ofNullable(responseBody);
        } catch (HttpStatusCodeException e) {
            log.debug("Crypto API error response: GET {} status={} body={}", url, e.getStatusCode(), e.getResponseBodyAsString());
            log.warn("Withdrawal info API error: paymentId={}, status={}", paymentId, e.getStatusCode());
            return Optional.empty();
        }
    }

    private String toJson(Object o) {
        if (o == null) return "null";
        try {
            return objectMapper.writeValueAsString(o);
        } catch (JsonProcessingException e) {
            return "<" + e.getMessage() + ">";
        }
    }

    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";
    }

    /**
     * Fetches from external API and returns details for one method by pid (rate_usd, misha_fee_usd).
     * Called when user opens Payout Confirmation screen.
     */
    public Optional<WithdrawalMethodDetailsDto> getWithdrawalMethodDetails(int pid) {
        String url = baseUrl.endsWith("/") ? baseUrl + WITHDRAWAL_METHODS_PATH : baseUrl + "/" + WITHDRAWAL_METHODS_PATH;
        HttpHeaders headers = new HttpHeaders();
        if (apiKey != null && !apiKey.isEmpty()) {
            headers.set("API-KEY", apiKey);
        }
        HttpEntity<Void> entity = new HttpEntity<>(headers);

        try {
            if (log.isDebugEnabled()) {
                log.debug("Crypto API request: GET {} (pid={})", url, pid);
            }
            ResponseEntity<WithdrawalMethodsApiResponse> response = restTemplate.exchange(url, HttpMethod.GET, entity, WithdrawalMethodsApiResponse.class);
            WithdrawalMethodsApiResponse body = response.getBody();
            if (log.isDebugEnabled()) {
                log.debug("Crypto API response: GET {} status={} body={}", url, response.getStatusCode(), toJson(body));
            }
            if (body == null || body.getResult() == null || body.getResult().getActiveMethods() == null) {
                return Optional.empty();
            }
            if (body.getRequestInfo() != null && body.getRequestInfo().getErrorCode() != null && body.getRequestInfo().getErrorCode() != 0) {
                log.warn("Withdrawal method details: API error pid={}: {}", pid, body.getRequestInfo().getErrorMessage());
                return Optional.empty();
            }
            return body.getResult().getActiveMethods().stream()
                    .filter(m -> pid == (m.getPid() != null ? m.getPid() : 0))
                    .findFirst()
                    .map(m -> {
                        BigDecimal rateUsd = parseBigDecimal(m.getRateUsd());
                        BigDecimal mishaFeeUsd = m.getMishaFeeUsd() != null ? BigDecimal.valueOf(m.getMishaFeeUsd()) : BigDecimal.ZERO;
                        return WithdrawalMethodDetailsDto.builder()
                                .pid(m.getPid())
                                .name(m.getName())
                                .ticker(m.getTicker())
                                .rateUsd(rateUsd != null ? rateUsd : BigDecimal.ZERO)
                                .mishaFeeUsd(mishaFeeUsd)
                                .build();
                    });
        } catch (HttpStatusCodeException e) {
            if (log.isDebugEnabled()) {
                log.debug("Crypto API error response: GET {} status={} body={}", url, e.getStatusCode(), e.getResponseBodyAsString());
            }
            log.warn("Withdrawal method details pid={}: failed to fetch: {}", pid, e.getMessage());
            return Optional.empty();
        } catch (Exception e) {
            log.warn("Withdrawal method details pid={}: failed to fetch: {}", pid, e.getMessage());
            return Optional.empty();
        }
    }

    private static BigDecimal parseBigDecimal(String s) {
        if (s == null || s.isEmpty()) return null;
        try {
            return new BigDecimal(s.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }

    /**
     * Returns withdrawal methods from DB only (no external API call).
     * Called when user opens Payout screen.
     */
    @Transactional(readOnly = true)
    public WithdrawalMethodsDto getWithdrawalMethodsFromDb() {
        List<CryptoWithdrawalMethod> methods = methodRepository.findAllByOrderByPidAsc();
        List<WithdrawalMethodsDto.WithdrawalMethodItemDto> list = methods.stream()
                .map(m -> WithdrawalMethodsDto.WithdrawalMethodItemDto.builder()
                        .pid(m.getPid())
                        .name(m.getName())
                        .network(m.getNetwork())
                        .iconId(m.getIconId())
                        .minWithdrawal(m.getMinWithdrawal())
                        .build())
                .collect(Collectors.toList());
        return WithdrawalMethodsDto.builder()
                .methods(list)
                .build();
    }

    /**
     * Runs every 30 minutes. Fetches from external API and overwrites crypto_withdrawal_methods (no hash check).
     */
    @Scheduled(cron = "0 */30 * * * ?") // Every 30 minutes at second 0
    @Transactional
    public void syncFromExternal() {
        String url = baseUrl.endsWith("/") ? baseUrl + WITHDRAWAL_METHODS_PATH : baseUrl + "/" + WITHDRAWAL_METHODS_PATH;
        HttpHeaders headers = new HttpHeaders();
        if (apiKey != null && !apiKey.isEmpty()) {
            headers.set("API-KEY", apiKey);
        }
        HttpEntity<Void> entity = new HttpEntity<>(headers);

        ResponseEntity<WithdrawalMethodsApiResponse> response;
        try {
            if (log.isDebugEnabled()) {
                log.debug("Crypto API request: GET {} (sync withdrawal methods)", url);
            }
            response = restTemplate.exchange(url, HttpMethod.GET, entity, WithdrawalMethodsApiResponse.class);
            if (log.isDebugEnabled()) {
                log.debug("Crypto API response: GET {} status={} body={}", url, response.getStatusCode(), toJson(response.getBody()));
            }
        } catch (HttpStatusCodeException e) {
            if (log.isDebugEnabled()) {
                log.debug("Crypto API error response: GET {} status={} body={}", url, e.getStatusCode(), e.getResponseBodyAsString());
            }
            log.warn("Crypto withdrawal sync (30 min): failed to fetch from external API: {}", e.getMessage());
            return;
        } catch (Exception e) {
            log.warn("Crypto withdrawal sync (30 min): failed to fetch from external API: {}", e.getMessage());
            return;
        }

        WithdrawalMethodsApiResponse body = response.getBody();
        if (body == null || body.getResult() == null || body.getResult().getActiveMethods() == null) {
            return;
        }
        if (body.getRequestInfo() != null && body.getRequestInfo().getErrorCode() != null && body.getRequestInfo().getErrorCode() != 0) {
            log.warn("Crypto withdrawal sync (30 min): API error: {}", body.getRequestInfo().getErrorMessage());
            return;
        }

        List<WithdrawalMethodsApiResponse.ActiveMethod> active = body.getResult().getActiveMethods();

        methodRepository.deleteAllInBatch();
        Instant now = Instant.now();
        Set<Integer> pidsSeen = new HashSet<>();
        for (WithdrawalMethodsApiResponse.ActiveMethod m : active) {
            if (m.getPid() == null) continue;
            if (!pidsSeen.add(m.getPid())) continue;
            String name = m.getTicker() != null ? m.getTicker() : "";
            String network = m.getName() != null ? m.getName() : "";
            String iconId = m.getIconId() != null ? m.getIconId().trim() : "";
            CryptoWithdrawalMethod method = CryptoWithdrawalMethod.builder()
                    .pid(m.getPid())
                    .name(name)
                    .network(network)
                    .iconId(iconId)
                    .minWithdrawal(DEFAULT_MIN_WITHDRAWAL)
                    .updatedAt(now)
                    .build();
            methodRepository.save(method);
        }

        log.debug("Crypto withdrawal sync (30 min): updated {} methods", active.size());
    }
}
