package com.adx.tracking.httpapi;

import com.adx.tracking.baidu.AdxClient;
import com.adx.tracking.baidu.ConversionClient;
import com.adx.tracking.baidu.model.ConversionQuery;
import com.adx.tracking.baidu.model.NormalizedBidResponse;
import com.adx.tracking.baidu.model.TrackingResult;
import com.adx.tracking.conversionsync.ConversionSyncService;
import com.adx.tracking.conversionsync.RetryService;
import com.adx.tracking.storage.RedisHotStore;
import com.adx.tracking.storage.model.BidRecord;
import com.adx.tracking.storage.model.TrackingRecord;
import org.springframework.http.HttpStatus;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.Instant;
import java.util.*;

/**
 * 对应 Go internal/httpapi/router.go 的所有路由。
 * 路由列表：
 *   GET  /health
 *   POST /baidu/adx/request
 *   POST /baidu/adx/report-impression
 *   POST /baidu/adx/report-click
 *   POST /baidu/conversions/query
 *   GET  /media/kuaishou/impression
 *   GET  /media/kuaishou/click
 *   POST /jobs/kuaishou/conversions/sync
 *   POST /jobs/kuaishou/callbacks/retry
 */
@RestController
public class AdxController {

    private final AdxClient adxClient;
    private final ConversionClient conversionClient;
    private final RedisHotStore hotStore;
    private final ConversionSyncService conversionSyncer;   // nullable
    private final RetryService retryService;                // nullable
    private final MediaPlacement kuaishouPlacement;         // nullable

    public AdxController(AdxClient adxClient,
                         ConversionClient conversionClient,
                         RedisHotStore hotStore,
                         @Nullable ConversionSyncService conversionSyncer,
                         @Nullable RetryService retryService,
                         @Nullable MediaPlacement kuaishouPlacement) {
        this.adxClient         = adxClient;
        this.conversionClient  = conversionClient;
        this.hotStore          = hotStore;
        this.conversionSyncer  = conversionSyncer;
        this.retryService      = retryService;
        this.kuaishouPlacement = kuaishouPlacement;
    }

    // ─── /health ────────────────────────────────────────────────────────────

    @GetMapping("/health")
    public Map<String, Boolean> health() {
        return Map.of("ok", true);
    }

    // ─── POST /baidu/adx/request ─────────────────────────────────────────────

    @PostMapping("/baidu/adx/request")
    public NormalizedBidResponse baiduAdxRequest(@RequestBody Map<String, Object> body) throws Exception {
        NormalizedBidResponse response = adxClient.requestBid(body);
        if (response != null && response.getQk() != null && !response.getQk().isBlank()) {
            BidRecord record = bidRecordFromResponse(response, mediaParamsFromBody(body));
            hotStore.recordBid(record);
        }
        return response;
    }

    // ─── POST /baidu/adx/report-impression ───────────────────────────────────

    @PostMapping("/baidu/adx/report-impression")
    public Map<String, Object> baiduAdxReportImpression(@RequestBody Map<String, Object> body) throws Exception {
        String qk = (String) body.get("qk");
        @SuppressWarnings("unchecked")
        List<String> showUrls = (List<String>) body.getOrDefault("showUrls", Collections.emptyList());
        long price = toLong(body.get("price"));

        List<TrackingResult> results = adxClient.reportImpression(showUrls, price);
        recordTrackingResults(qk, "impression", results);
        return Map.of("data", results);
    }

    // ─── POST /baidu/adx/report-click ────────────────────────────────────────

    @PostMapping("/baidu/adx/report-click")
    public Map<String, Object> baiduAdxReportClick(@RequestBody Map<String, Object> body) throws Exception {
        String qk = (String) body.get("qk");
        @SuppressWarnings("unchecked")
        List<String> clickUrls = (List<String>) body.getOrDefault("clickUrls", Collections.emptyList());

        List<TrackingResult> results = adxClient.reportClick(clickUrls);
        recordTrackingResults(qk, "click", results);
        return Map.of("data", results);
    }

    // ─── POST /baidu/conversions/query ───────────────────────────────────────

    @PostMapping("/baidu/conversions/query")
    public Object baiduConversionsQuery(@RequestBody ConversionQuery query) throws Exception {
        return conversionClient.queryPayments(query);
    }

    // ─── GET /media/kuaishou/impression ──────────────────────────────────────

    @GetMapping("/media/kuaishou/impression")
    public Map<String, Object> kuaishouImpression(HttpServletRequest request) throws Exception {
        Map<String, String> params = flatQueryParams(request);
        String traceId = mediaTraceId(params);
        if (traceId == null || traceId.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing media trace id");
        }
        if (kuaishouPlacement == null) {
            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "kuaishou placement not configured");
        }

        Map<String, Object> bidRequest = mediaBidRequest(request, "kuaishou", traceId, params, kuaishouPlacement);
        NormalizedBidResponse response = adxClient.requestBid(bidRequest);
        if (response == null) {
            throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "empty baidu ADX response");
        }

        BidRecord record = bidRecordFromMediaResponse(response, "kuaishou", traceId, params);
        hotStore.recordBid(record);

        List<TrackingResult> trackingResults = Collections.emptyList();
        if (response.getBid() != null && response.getBid().getShowUrls() != null
                && !response.getBid().getShowUrls().isEmpty()) {
            trackingResults = adxClient.reportImpression(
                    response.getBid().getShowUrls(), response.getBid().getPrice());
            recordTrackingResults(response.getQk(), "impression", trackingResults);
        }

        return Map.of("qk", nvl(response.getQk()), "traceId", traceId, "tracking", trackingResults);
    }

    // ─── GET /media/kuaishou/click ────────────────────────────────────────────

    @GetMapping("/media/kuaishou/click")
    public Object kuaishouClick(HttpServletRequest request, HttpServletResponse response)
            throws Exception {
        if (kuaishouPlacement == null) {
            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE, "kuaishou placement not configured");
        }

        Map<String, String> params = flatQueryParams(request);
        String traceId = mediaTraceId(params);
        if (traceId == null || traceId.isBlank()) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "missing media trace id");
        }

        // 先从 Redis 按 mediaTrace 查找已有 bid
        BidRecord record = hotStore.findBidByMediaTrace("kuaishou", traceId);
        if (record == null) {
            // 未找到，临时发起竞价请求
            Map<String, Object> bidRequest = mediaBidRequest(request, "kuaishou", traceId, params, kuaishouPlacement);
            NormalizedBidResponse resp = adxClient.requestBid(bidRequest);
            if (resp == null) {
                throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "empty baidu ADX response");
            }
            BidRecord newRecord = bidRecordFromMediaResponse(resp, "kuaishou", traceId, params);
            hotStore.recordBid(newRecord);
            record = newRecord;
        }

        List<TrackingResult> trackingResults = adxClient.reportClick(
                record.getClickUrls() != null ? record.getClickUrls() : Collections.emptyList());
        recordTrackingResults(record.getQk(), "click", trackingResults);

        // 重定向到 landing page 或 app store link
        String destination = record.getAppStoreLink() != null && !record.getAppStoreLink().isBlank()
                ? record.getAppStoreLink() : record.getLandingPage();
        if (destination != null && !destination.isBlank()) {
            response.sendRedirect(destination);
            return null;
        }

        return Map.of("qk", nvl(record.getQk()), "traceId", traceId, "tracking", trackingResults);
    }

    // ─── POST /jobs/kuaishou/conversions/sync ─────────────────────────────────

    @PostMapping("/jobs/kuaishou/conversions/sync")
    public Object kuaishouConversionsSync(@RequestBody ConversionQuery query) throws Exception {
        if (conversionSyncer == null) {
            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
                    "kuaishou conversion syncer is not configured");
        }
        return conversionSyncer.syncKuaishouConversions(query);
    }

    // ─── POST /jobs/kuaishou/callbacks/retry ──────────────────────────────────

    @PostMapping("/jobs/kuaishou/callbacks/retry")
    public Object kuaishouCallbacksRetry(@RequestBody Map<String, Object> body) {
        if (retryService == null) {
            throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
                    "kuaishou callback retryer is not configured");
        }
        int limit = toInt(body.get("limit"));
        return retryService.retryKuaishouCallbacks(limit);
    }

    // ─── 私有工具方法（对应 Go router.go 中的辅助函数）─────────────────────────

    private BidRecord bidRecordFromResponse(NormalizedBidResponse response,
                                             Map<String, String> mediaParams) {
        BidRecord r = new BidRecord();
        r.setQk(response.getQk());
        r.setReqId(response.getReqId());
        r.setBidId(response.getBidId());
        r.setMediaParams(mediaParams);
        r.setCreatedAt(Instant.now());
        if (response.getBid() != null) {
            r.setCreativeId(response.getBid().getCrid());
            r.setImpId(response.getBid().getImpId());
            r.setTagId(response.getBid().getTagId());
            r.setPrice(response.getBid().getPrice());
            r.setShowUrls(response.getBid().getShowUrls());
            r.setClickUrls(response.getBid().getClickUrls());
            r.setLandingPage(response.getBid().getLandingPage());
            r.setAppStoreLink(response.getBid().getAppStoreLink());
            if (response.getBid().getAdm() != null) {
                r.setPackageName(response.getBid().getAdm().getPackageName());
            }
        }
        return r;
    }

    private BidRecord bidRecordFromMediaResponse(NormalizedBidResponse response,
                                                  String media, String traceId,
                                                  Map<String, String> mediaParams) {
        BidRecord r = bidRecordFromResponse(response, mediaParams);
        r.setMedia(media);
        r.setMediaTraceId(traceId);
        return r;
    }

    @SuppressWarnings("unchecked")
    private Map<String, String> mediaParamsFromBody(Map<String, Object> body) {
        Object raw = body.get("mediaParams");
        if (!(raw instanceof Map)) return null;
        Map<Object, Object> rawMap = (Map<Object, Object>) raw;
        Map<String, String> params = new HashMap<>();
        rawMap.forEach((k, v) -> params.put(String.valueOf(k), String.valueOf(v)));
        return params.isEmpty() ? null : params;
    }

    private Map<String, String> flatQueryParams(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();
        request.getParameterMap().forEach((k, v) -> {
            if (v != null && v.length > 0) params.put(k, v[0]);
        });
        return params;
    }

    private String mediaTraceId(Map<String, String> params) {
        for (String key : List.of("trace_id", "click_id", "callback", "request_id",
                "event_id", "oaid", "imei", "idfa", "caid")) {
            String v = params.get(key);
            if (v != null && !v.isBlank()) return v;
        }
        return null;
    }

    private Map<String, Object> mediaBidRequest(HttpServletRequest request, String media,
                                                 String traceId, Map<String, String> params,
                                                 MediaPlacement placement) {
        MediaPlacement.Resolved resolved = placement.resolve(params);
        List<Integer> actionTypes = placement.getActionTypes();
        if (actionTypes == null || actionTypes.isEmpty()) actionTypes = List.of(1);

        params.put("baidu_app_id", resolved.appId());
        params.put("baidu_tag_id", resolved.tagId());
        if (resolved.platform() != null && !resolved.platform().isBlank()) {
            params.put("baidu_platform", resolved.platform());
        }

        Map<String, Object> imp = new LinkedHashMap<>();
        imp.put("id", "1");
        imp.put("appId", resolved.appId());
        imp.put("tagId", resolved.tagId());
        imp.put("secure", 1);
        imp.put("adType", 0);
        imp.put("assets", defaultBaiduAssets());
        imp.put("actionType", actionTypes);
        imp.put("maxCount", 1);
        if (placement.getBidFloor() > 0) imp.put("bidFloor", placement.getBidFloor());

        Map<String, Object> device = new LinkedHashMap<>();
        // 对应 Go gin c.ClientIP()：默认信任反向代理，优先解析 X-Forwarded-For（取第一个非空 IP）、
        // 再 X-Real-IP，最后回退到 RemoteAddr。生产环境必经反向代理，
        // 直接使用 request.getRemoteAddr() 会得到代理 IP（如 127.0.0.1 / 内网 LB IP），
        // 导致百度 ADX 拿到错误的客户端 IP。
        device.put("ip", clientIp(request));
        device.put("ua", AdxClient.BAIDU_USER_AGENT);
        device.put("deviceType", 1);
        int os = baiduOsForPlatform(resolved.platform());
        if (os != 0) device.put("os", os);
        Map<String, Object> uid = baiduUidFromParams(params);
        if (!uid.isEmpty()) device.put("uid", uid);

        Map<String, Object> bidReq = new LinkedHashMap<>();
        bidReq.put("mediaId",     placement.getBaiduMediaId());
        // 对应 Go: fmt.Sprintf("%s-%d", traceID, time.Now().UnixNano())
        // 必须是纳秒精度，毫秒或 nanoTime() 都会使 reqId 与 Go 实现不一致
        Instant now = Instant.now();
        long nanos = now.getEpochSecond() * 1_000_000_000L + now.getNano();
        bidReq.put("reqId",       traceId + "-" + nanos);
        bidReq.put("imp",         List.of(imp));
        bidReq.put("device",      device);
        bidReq.put("media",       media);
        bidReq.put("traceId",     traceId);
        bidReq.put("mediaParams", params);
        return bidReq;
    }

    private List<Map<String, Integer>> defaultBaiduAssets() {
        return List.of(
            Map.of("templateId", 1, "ratio", 2),
            Map.of("templateId", 1, "ratio", 7),
            Map.of("templateId", 2, "ratio", 1),
            Map.of("templateId", 2, "ratio", 4),
            Map.of("templateId", 3, "ratio", 2),
            Map.of("templateId", 4, "ratio", 4),
            Map.of("templateId", 4, "ratio", 5)
        );
    }

    private int baiduOsForPlatform(String platform) {
        if (platform == null) return 0;
        return switch (platform) {
            case "ios"     -> 1;
            case "android" -> 2;
            default        -> 0;
        };
    }

    private Map<String, Object> baiduUidFromParams(Map<String, String> p) {
        Map<String, Object> uid = new LinkedHashMap<>();
        putUid(uid, "did",      firstParam(p, "did", "imei"));
        putUid(uid, "didMd5",   firstParam(p, "did_md5", "didMd5", "imei_md5", "imeiMd5"));
        putUid(uid, "dpid",     firstParam(p, "dpid", "android_id", "androidId"));
        putUid(uid, "dpidMd5",  firstParam(p, "dpid_md5", "dpidMd5", "android_id_md5", "androidIdMd5"));
        putUid(uid, "idfa",     p.get("idfa"));
        putUid(uid, "idfaMd5",  firstParam(p, "idfa_md5", "idfaMd5"));
        putUid(uid, "oaid",     p.get("oaid"));
        putUid(uid, "oaidMd5",  firstParam(p, "oaid_md5", "oaidMd5"));
        return uid;
    }

    private void putUid(Map<String, Object> uid, String key, String value) {
        if (value != null && !value.isBlank()) uid.put(key, value);
    }

    private String firstParam(Map<String, String> p, String... keys) {
        for (String k : keys) {
            String v = p.get(k);
            if (v != null && !v.isBlank()) return v.trim();
        }
        return null;
    }

    private void recordTrackingResults(String qk, String kind, List<TrackingResult> results) {
        if (hotStore == null || results == null) return;
        for (TrackingResult r : results) {
            TrackingRecord record = new TrackingRecord();
            record.setQk(qk);
            record.setKind(kind);
            record.setUrl(r.getUrl());
            record.setStatus(r.getStatus());
            record.setOk(r.isOk());
            record.setCreatedAt(Instant.now());
            hotStore.recordTracking(record);
        }
    }

    /**
     * 对应 Go gin Context.ClientIP() 的默认行为。
     * 解析顺序：X-Forwarded-For 第一个非空 IP → X-Real-IP → RemoteAddr。
     */
    static String clientIp(HttpServletRequest request) {
        String xff = request.getHeader("X-Forwarded-For");
        if (xff != null && !xff.isBlank()) {
            // X-Forwarded-For 格式可能为 "client, proxy1, proxy2"，取第一个
            for (String part : xff.split(",")) {
                String ip = part.trim();
                if (!ip.isEmpty()) return ip;
            }
        }
        String xri = request.getHeader("X-Real-IP");
        if (xri != null && !xri.isBlank()) {
            return xri.trim();
        }
        return request.getRemoteAddr();
    }

    private static long toLong(Object v) {
        if (v == null) return 0L;
        if (v instanceof Number n) return n.longValue();
        try { return Long.parseLong(v.toString()); } catch (NumberFormatException e) { return 0L; }
    }

    private static int toInt(Object v) {
        if (v == null) return 0;
        if (v instanceof Number n) return n.intValue();
        try { return Integer.parseInt(v.toString()); } catch (NumberFormatException e) { return 0; }
    }

    private static String nvl(String s) { return s != null ? s : ""; }
}
