package com.gameplatform.server.service.external;

import com.gameplatform.server.annotation.RepeatCall;
import com.gameplatform.server.model.dto.device.DeviceStatusResponse;
import com.gameplatform.server.service.device.DeviceStatusService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

import java.time.Duration;

@Service
public class ScriptClient {
    private static final Logger log = LoggerFactory.getLogger(ScriptClient.class);

    private final WebClient webClient;
    private final String baseUrl;
    private final String apiBaseUrl;
    private final String appBaseUrl;
    private final DeviceStatusService deviceStatusService;
    private final ApplicationEventPublisher eventPublisher;

    public ScriptClient(
            @Value("${script.base-url}") String baseUrl,
            @Value("${script.api-base-url}") String apiBaseUrl,
            @Value("${script.connect-timeout-ms:3000}") int connectTimeoutMs,
            @Value("${script.read-timeout-ms:5000}") int readTimeoutMs,
            @Value("${app.base-url}") String appBaseUrl,
            DeviceStatusService deviceStatusService,
            ApplicationEventPublisher eventPublisher
    ) {
        this.baseUrl = baseUrl;
        this.apiBaseUrl = apiBaseUrl;
        this.appBaseUrl = appBaseUrl;
        this.deviceStatusService = deviceStatusService;
        this.eventPublisher = eventPublisher;
        this.webClient = WebClient.builder()
                .baseUrl(baseUrl)
                .exchangeStrategies(ExchangeStrategies.builder()
                        .codecs(cfg -> cfg.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
                        .build())
                .build();
        if (log.isDebugEnabled()) {
            log.debug("ScriptClient initialized baseUrl={}, apiBaseUrl={}, connectTimeoutMs={}, readTimeoutMs={}", 
                    this.baseUrl, this.apiBaseUrl, connectTimeoutMs, readTimeoutMs);
        }
    }

    public Mono<byte[]> getQrPng(String path) {
        // path example: /{codeNo}/二维码.png
        return webClient.get()
                .uri(path)
                .accept(MediaType.IMAGE_PNG)
                .retrieve()
                .bodyToMono(byte[].class)
                .timeout(Duration.ofSeconds(5))
                .retry(3)  // 失败时重试3次
                .doOnError(e -> log.warn("ScriptClient.getQrPng error path={} err={}", path, e.toString()));
    }

    /**
     * 通用图片获取方法
     * @param path 图片路径，如 /{codeNo}/首次主页.png
     * @return 图片数据
     */
    public Mono<byte[]> getImagePng(String path) {
        log.debug("获取图片: path={}", path);
        return webClient.get()
                .uri(path)
                .accept(MediaType.IMAGE_PNG)
                .retrieve()
                .bodyToMono(byte[].class)
                .timeout(Duration.ofSeconds(10))
                .retry(3)  // 失败时重试3次
                .doOnSuccess(data -> log.debug("获取图片成功: path={}, 数据大小={}字节", path, data != null ? data.length : 0))
                .doOnError(e -> log.warn("获取图片失败: path={}, error={}", path, e.toString()));
    }

    public Mono<String> getText(String path) {
        return webClient.get()
                .uri(path)
                .accept(MediaType.TEXT_PLAIN)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(5))
                .retry(3)  // 失败时重试3次
                .doOnError(e -> log.warn("ScriptClient.getText error path={} err={}", path, e.toString()));
    }

    /**
     * 检查空闲设备（返回原始字符串）
     */
    public Mono<String> checkAvailableDevice() {
        return webClient.get()
                .uri(apiBaseUrl + "/yijianwan_netfile/readAllMsg?文件名=判断分数")
                .accept(MediaType.ALL)  // 先别卡死在 JSON
                .exchangeToMono(resp -> {
                    HttpStatus sc = resp.statusCode();
                    return resp.bodyToMono(String.class)
                            .defaultIfEmpty("")
                            .map(body -> {
                                log.debug("status={}, headers={}, body={}", sc.value(), resp.headers().asHttpHeaders(), body);
                                if (sc.is2xxSuccessful()) return body;
                                throw new IllegalStateException("HTTP " + sc.value() + " - " + body);
                            });
                })
                .timeout(Duration.ofSeconds(10))
                // 仅在 5xx 或 IO 问题时重试；4xx/超时不重试，避免凑满 40s
                .retryWhen(Retry.fixedDelay(2, Duration.ofMillis(500))
                        .filter(ex -> {
                            if (ex instanceof java.io.IOException) return true;
                            if (ex instanceof IllegalStateException ise) {
                                String m = ise.getMessage();
                                return m != null && m.startsWith("HTTP 5");
                            }
                            return false;
                        })
                        .onRetryExhaustedThrow((spec, signal) -> signal.failure()))
                .doOnError(e -> log.warn("checkAvailableDevice failed: {}", e.toString()));
    }
    
    /**
     * 检查空闲设备（解析后的结构化数据）
     */
    public Mono<DeviceStatusResponse> checkAvailableDeviceStatus() {
        long tStart = System.currentTimeMillis();

        return checkAvailableDevice()
                .map(jsonResponse -> {
                    long tHttpEnd = System.currentTimeMillis();
                    long tParseStart = tHttpEnd;
                    DeviceStatusResponse parsed = deviceStatusService.parseDeviceStatus(jsonResponse);
                    long tParseEnd = System.currentTimeMillis();
                    log.debug("checkAvailableDeviceStatus jsonResponse: {}", jsonResponse);
                    // 在对象里临时塞入测量数据不可取，这里通过日志输出
                    if (log.isInfoEnabled()) {
                        long httpCost = tHttpEnd - tStart;
                        long parseCost = tParseEnd - tParseStart;
                        log.info("checkAvailableDeviceStatus stage cost: http={} ms, parse={} ms", httpCost, parseCost);
                    }
                    return parsed;
                })
                .doOnSuccess(deviceStatus -> {
                    log.info("设备状态检查完成: 总设备数={}, 空闲设备数={}", 
                            deviceStatus.getTotalDevices(), deviceStatus.getAvailableCount());
                    if (deviceStatus.getAvailableCount() > 0) {
                        log.info("空闲设备列表: {}", deviceStatus.getAvailableDevices());
                    }

                    long tEventStart = System.currentTimeMillis();
                    try {
                        eventPublisher.publishEvent(new com.gameplatform.server.event.DeviceStatusUpdatedEvent(this, deviceStatus));
                    } catch (Exception e) {
                        log.error("发布设备状态更新事件时发生异常", e);
                    } finally {
                        long tEventEnd = System.currentTimeMillis();
                        long total = tEventEnd - tStart;
                        long eventCost = tEventEnd - tEventStart;
                        if (log.isInfoEnabled()) {
                            log.info("checkAvailableDeviceStatus stage cost: event={} ms, total={} ms", eventCost, total);
                        }
                    }
                })
                .doOnError(e -> log.error("设备状态解析失败: {}", e.getMessage(), e));
    }

    /**
     * 选区操作 - 使用空闲设备编号
     * @param deviceId 空闲设备编号 (如 f1, ss9)
     * @param region 区域参数 (Q或V)
     * @return 选区结果
     */
    @RepeatCall(times = 3, description = "选区操作")
    public Mono<String> selectRegion(String deviceId, String region) {
        if (deviceId == null || region == null) {
            log.warn("选区操作失败: 设备={}, 区域={}", deviceId, region);
            return Mono.just("选区操作失败");
        }
        if (!"Q".equals(region) && !"V".equals(region)) {
            log.warn("选区操作失败: 区域={} 必须是Q或V", region);
            return Mono.just("选区操作失败");
        }
        // 构建选区URL，使用设备编号作为参数名，区域作为参数值
        // 示例: {apiBaseUrl}/yijianwan_netfile/saveMsg?文件名=判断系统&f1=Q
        String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断系统&%s=%s", deviceId, region);
        log.info("选区操作: 设备={}, 区域={}, url={}", deviceId, region, url);
        
        return webClient.get()  // 根据您的curl示例，这应该是GET请求
                .uri(url)
                .accept(MediaType.TEXT_PLAIN)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(10))
                .doOnSuccess(result -> {
                    log.info("选区操作成功: 设备={}, 区域={}, 结果={}", deviceId, region, result);
                })
                .doOnError(e -> {
                    log.error("选区操作失败: 设备={}, 区域={}, 错误={}", deviceId, region, e.toString());
                });
    }



    /**
     * 判断刷新接口 - 统一管理刷新判断逻辑
     */
    @RepeatCall(times = 1, description = "判断刷新")
    public Mono<String> refresh(String machineId) {
        String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断刷新&%s=刷新", machineId); 
        log.info("调用判断刷新接口: {}", url);
        return webClient.get()
                .uri(url)
                .accept(MediaType.TEXT_PLAIN)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(10))
                .doOnSuccess(result -> log.info("判断刷新接口调用成功: result={}", result))
                .doOnError(e -> log.warn("判断刷新接口调用失败: {}", e.toString()));
    }

    /**
     * 检查设备是否已上号 - 根据您提供的API示例
     * URL格式: {apiBaseUrl}/yijianwan_netfile/readMsg?文件名=判断上号&对象名=f1
     * 返回: "未上号" 或 其他状态
     * 
     * @param deviceId 设备编号 (真实设备编号，如 f1, ss9)
     * @return 上号状态文本
     */
    public Mono<String> checkLoginStatus(String deviceId) {
        String url = String.format(apiBaseUrl + "/yijianwan_netfile/readMsg?文件名=判断上号&对象名=%s", deviceId);
        log.debug("检查设备上号状态: 设备={}, url={}", deviceId, url);
        return webClient.get()
                .uri(url)
                .accept(MediaType.TEXT_PLAIN)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(10))
                .retry(3)  // 失败时重试3次
                .doOnSuccess(result -> log.debug("检查设备上号状态成功: 设备={}, 状态={}", deviceId, result))
                .doOnError(e -> log.warn("检查设备上号状态失败: 设备={}, 错误={}", deviceId, e.toString()));
    }

    /**
     * 获取二维码URL（带时间戳防缓存）- 直接链接到脚本服务器
     */
    public String getQrCodeUrl(String codeNo) {
        long timestamp = System.currentTimeMillis();
        return String.format("%s/%s/二维码.png?t=%d", baseUrl, codeNo, timestamp);
    }
    
    /**
     * 获取资源基础URL - 统一管理所有资源链接
     * @param deviceId 设备编号
     * @return 资源基础URL，如 {baseUrl}/f1/
     */
    public String getAssetsBaseUrl(String deviceId) {
        return String.format("%s/%s/", baseUrl, deviceId);
    }
    
    /**
     * 获取特定资源的完整URL
     * @param deviceId 设备编号
     * @param resourceName 资源名称，如 "首次主页.png"
     * @return 完整的资源URL
     */
    public String getResourceUrl(String deviceId, String resourceName) {
        return String.format("%s/%s/%s", baseUrl, deviceId, resourceName);
    }
    
    /**
     * 获取各种游戏图片的URL映射 - 统一管理所有游戏相关图片链接
     * @param deviceId 设备编号
     * @return 包含所有图片URL的映射
     */
    public java.util.Map<String, String> getGameImageUrls(String deviceId) {
        java.util.Map<String, String> imageUrls = new java.util.HashMap<>();
        String baseUrl = getAssetsBaseUrl(deviceId);
        
        imageUrls.put("qrCode", baseUrl + "二维码.png");
        imageUrls.put("homepage", baseUrl + "首次主页.png");
        imageUrls.put("firstReward", baseUrl + "首次赏金.png");
        imageUrls.put("midReward", baseUrl + "中途赏金.png");
        imageUrls.put("endReward", baseUrl + "结束赏金.png");
        
        return imageUrls;
    }
    
    /**
     * 获取代理二维码URL（通过本服务器代理，使用代理code隐藏真实设备编号）
     */
    public String getProxyQrCodeUrl(String proxyCode) {
        long timestamp = System.currentTimeMillis();
        return String.format("%s/api/link/image/%s/qr.png?t=%d", appBaseUrl, proxyCode, timestamp);
    }
    
    /**
     * 获取真实设备的二维码数据
     * @param deviceId 真实设备编号 (如 f1, ss9)
     * @return 二维码图片数据
     */
    public Mono<byte[]> getDeviceQrCode(String deviceId) {
        String path = String.format("/%s/二维码.png", deviceId);
        log.debug("获取设备二维码: 设备={}, path={}", deviceId, path);
        
        return webClient.get()
                .uri(path)
                .accept(MediaType.IMAGE_PNG)
                .retrieve()
                .bodyToMono(byte[].class)
                .timeout(Duration.ofSeconds(10))
                .retry(3)  // 失败时重试3次
                .doOnSuccess(data -> {
                    log.debug("获取设备二维码成功: 设备={}, 数据大小={}字节", deviceId, data.length);
                })
                .doOnError(e -> {
                    log.error("获取设备二维码失败: 设备={}, 错误={}", deviceId, e.toString());
                });
    }

    /**
     * 获取目标分数
     */
    public Mono<String> getTargetScore(String machineId) {
        String url = String.format(apiBaseUrl + "/yijianwan_netfile/readMsg?文件名=判断分数&对象名=%s", machineId);
        log.debug("获取目标分数: machineId={}, url={}", machineId, url);
        return webClient.get()
                .uri(url)
                .accept(MediaType.TEXT_PLAIN)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(5))
                .retry(3)  // 失败时重试3次
                .doOnSuccess(result -> log.debug("获取目标分数成功: machineId={}, result={}", machineId, result))
                .doOnError(e -> log.warn("获取目标分数失败: machineId={}, error={}", machineId, e.toString()));
    }



    /**
     * 保存总次数（使用f4参数格式）
     * @param times 总次数
     * @return 保存结果
     */
    @RepeatCall(times = 3, description = "保存总次数")
    public Mono<String> saveTotalTimes(String meachainId,int times) {
        String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=总次数&%s=%d", meachainId,times);
        log.info("保存刷副本的次数的url: {}", url);
        log.info("开始调用保存总次数接口: times={}, url={}", times, url);
        return webClient.get()
                .uri(url)
                .accept(MediaType.TEXT_PLAIN)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(10))
                .doOnSuccess(result -> log.info("保存总次数接口调用成功: url={}, result={}", url, result))
                .doOnError(e -> log.warn("保存总次数接口调用失败: times={}, error={}", times, e.toString()));
    }

    /**
     *  TODO
     * 获取特定设备的状态信息（用于检查是否完成游戏）
     * @param machineId 设备ID
     * @return 设备状态信息的Map，包含f0(点数)和f1(状态)等信息
     */
    public Mono<java.util.Map<String, Object>> getDeviceStatus(String machineId) {
        String url = apiBaseUrl + "/yijianwan_netfile/readAllMsg?文件名=判断分数";
        log.debug("获取设备状态: 设备={}, url={}", machineId, url);
        
        return webClient.get()
                .uri(url)
                .accept(MediaType.APPLICATION_JSON)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(10))
                .retry(3)  // 失败时重试3次
                .map(jsonResponse -> {
                    // 解析JSON响应，提取指定设备的状态信息
                    return deviceStatusService.parseDeviceStatusForMachine(jsonResponse, machineId);
                })
                .doOnSuccess(deviceStatus -> {
                    log.debug("获取设备状态成功: 设备={}, 状态={}", machineId, deviceStatus);
                })
                .doOnError(e -> {
                    log.warn("获取设备状态失败: 设备={}, 错误={}", machineId, e.toString());
                });
    }

    /**
     * 调用退单接口
     * @param machineId 真实设备编号 (如 f1, ss9)
     * @return 退单操作结果
     */
    @RepeatCall(times = 3, description = "退单操作")
    public Mono<String> refundOrder(String machineId) {
        String url = String.format(apiBaseUrl + "/yijianwan_netfile/saveMsg?文件名=判断退单&%s=T", machineId);
        log.info("调用退单接口: 设备={}, url={}", machineId, url);
        
        return webClient.get()
                .uri(url)
                .accept(MediaType.TEXT_PLAIN)
                .retrieve()
                .bodyToMono(String.class)
                .timeout(Duration.ofSeconds(10))
                .doOnSuccess(result -> {
                    log.info("退单接口调用成功: 设备={}, 结果={}", machineId, result);
                })
                .doOnError(e -> {
                    log.error("退单接口调用失败: 设备={}, 错误={}", machineId, e.toString());
                });
    }
}


