package com.ruoyi.app.utils.linepay;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;

/** LINE Pay Online API v4 client. */
@Component
public class LinePayClient {
    private static final Logger log = LoggerFactory.getLogger(LinePayClient.class);

    static final String REQUEST_URI = "/v4/payments/request";
    static final String RETRIEVE_URI = "/v4/payments";
    static final String OFFLINE_PAY_URI = "/v4/payments/oneTimeKeys/pay";

    private final LinePayProperties properties;
    private final LinePaySigner signer;
    private final LinePayHttpTransport transport;
    private final Supplier<String> nonceSupplier;

    @Autowired
    public LinePayClient(LinePayProperties properties, LinePaySigner signer,
                         LinePayHttpTransport transport) {
        this(properties, signer, transport, () -> UUID.randomUUID().toString());
    }

    LinePayClient(LinePayProperties properties, LinePaySigner signer,
                  LinePayHttpTransport transport, Supplier<String> nonceSupplier) {
        this.properties = properties;
        this.signer = signer;
        this.transport = transport;
        this.nonceSupplier = nonceSupplier;
    }

    public LinePayResponse request(LinePayCredential credential, LinePayRequest request) throws Exception {
        validateRequest(request);
        JSONObject product = new JSONObject();
        product.put("name", request.productName());
        product.put("quantity", 1);
        product.put("price", request.amount());
        JSONObject packageValue = new JSONObject();
        packageValue.put("id", request.ddId());
        packageValue.put("amount", request.amount());
        packageValue.put("products", new JSONArray().fluentAdd(product));

        JSONObject redirectUrls = new JSONObject();
        redirectUrls.put("confirmUrl", require(properties.getConfirmUrl(), "confirmUrl"));
        redirectUrls.put("cancelUrl", require(properties.getCancelUrl(), "cancelUrl"));

        JSONObject payment = new JSONObject();
        payment.put("capture", true);
        JSONObject options = new JSONObject();
        options.put("payment", payment);

        JSONObject body = new JSONObject();
        body.put("amount", request.amount());
        body.put("currency", request.currency());
        body.put("orderId", request.lineOrderId());
        body.put("packages", new JSONArray().fluentAdd(packageValue));
        body.put("redirectUrls", redirectUrls);
        body.put("options", options);
        return execute("POST", REQUEST_URI, null, body.toJSONString(), credential, 10_000);
    }

    public LinePayResponse check(LinePayCredential credential, String transactionId) throws Exception {
        String uri = "/v4/payments/requests/" + pathSegment(transactionId) + "/check";
        return execute("GET", uri, null, "", credential, 20_000);
    }

    public LinePayResponse confirm(LinePayCredential credential, String transactionId,
                                   int amount, String currency) throws Exception {
        if (amount <= 0) {
            throw new IllegalArgumentException("amount must be positive");
        }
        JSONObject body = new JSONObject();
        body.put("amount", amount);
        body.put("currency", require(currency, "currency"));
        return execute("POST", "/v4/payments/" + pathSegment(transactionId) + "/confirm",
                null, body.toJSONString(), credential, 40_000);
    }

    public LinePayResponse retrieveByOrderId(LinePayCredential credential, String lineOrderId)
            throws Exception {
        String query = "orderId=" + URLEncoder.encode(require(lineOrderId, "lineOrderId"),
                StandardCharsets.UTF_8);
        return execute("GET", RETRIEVE_URI, query, "", credential, 20_000);
    }

    public LinePayResponse retrieveByTransactionId(LinePayCredential credential, String transactionId)
            throws Exception {
        String query = "transactionId=" + URLEncoder.encode(require(transactionId, "transactionId"),
                StandardCharsets.UTF_8);
        return execute("GET", RETRIEVE_URI, query, "", credential, 20_000);
    }

    public LinePayResponse refundFull(LinePayCredential credential, String transactionId) throws Exception {
        return execute("POST", "/v4/payments/" + pathSegment(transactionId) + "/refund",
                null, "{}", credential, 20_000);
    }

    public LinePayResponse payOffline(LinePayCredential credential, LinePayOfflineRequest request)
            throws Exception {
        validateOfflineRequest(request);
        JSONObject product = new JSONObject();
        product.put("name", request.productName());
        product.put("quantity", 1);
        product.put("price", request.amount());
        JSONObject packageValue = new JSONObject();
        packageValue.put("id", request.ddId());
        packageValue.put("amount", request.amount());
        packageValue.put("products", new JSONArray().fluentAdd(product));

        JSONObject body = new JSONObject();
        body.put("amount", request.amount());
        body.put("currency", request.currency());
        body.put("orderId", request.lineOrderId());
        body.put("oneTimeKey", request.oneTimeKey());
        body.put("packages", new JSONArray().fluentAdd(packageValue));
        return execute("POST", OFFLINE_PAY_URI, null, body.toJSONString(), credential, 40_000);
    }

    public LinePayResponse checkOffline(LinePayCredential credential, String lineOrderId)
            throws Exception {
        String uri = "/v4/payments/orders/" + pathSegment(lineOrderId) + "/check";
        return execute("GET", uri, null, "", credential, 20_000);
    }

    public LinePayResponse refundOfflineFull(LinePayCredential credential, String lineOrderId)
            throws Exception {
        String uri = "/v4/payments/orders/" + pathSegment(lineOrderId) + "/refund";
        return execute("POST", uri, null, "{}", credential, 20_000);
    }

    private LinePayResponse execute(String method, String uri, String query, String body,
                                    LinePayCredential credential, int readTimeoutMs) throws Exception {
        validateCredential(credential);
        String nonce = require(nonceSupplier.get(), "nonce");
        String signedContent = "GET".equals(method) ? (query == null ? "" : query) : body;
        Map<String, String> headers = new LinkedHashMap<>();
        headers.put("X-LINE-ChannelId", credential.channelId());
        headers.put("X-LINE-Authorization-Nonce", nonce);
        headers.put("X-LINE-Authorization", signer.sign(
                credential.channelSecret(), uri, signedContent, nonce));
        headers.put("Content-Type", "application/json");
        String url = baseUrl() + uri + (query == null || query.isEmpty() ? "" : "?" + query);
        // 请求体可能含 oneTimeKey、请求头含 Channel 签名，两者都不得写日志；只记方法/URI/耗时/网关返回码
        long started = System.currentTimeMillis();
        log.info("LINE Pay gateway request, method={}, uri={}", method, uri);
        LinePayHttpResponse httpResponse;
        try {
            httpResponse = transport.execute(method, url, body, headers, readTimeoutMs);
        } catch (Exception transportError) {
            // 失败单行留痕；完整堆栈由上层 Service 的 catch 记录，避免双份堆栈刷屏
            log.warn("LINE Pay gateway request failed, method={}, uri={}, costMs={}, error={}",
                    method, uri, System.currentTimeMillis() - started, transportError.toString());
            throw transportError;
        }
        JSONObject json;
        try {
            json = JSON.parseObject(httpResponse.body());
        } catch (RuntimeException parseError) {
            throw new IllegalStateException("LINE Pay returned invalid JSON", parseError);
        }
        if (json == null) {
            throw new IllegalStateException("LINE Pay returned an empty response");
        }
        Object infoValue = json.get("info");
        JSONObject info = infoValue instanceof JSONObject ? (JSONObject) infoValue : null;
        String transactionId = info == null ? null : info.getString("transactionId");
        log.info("LINE Pay gateway response, method={}, uri={}, httpStatus={}, returnCode={}, returnMessage={}, costMs={}",
                method, uri, httpResponse.statusCode(), json.getString("returnCode"),
                json.getString("returnMessage"), System.currentTimeMillis() - started);
        return new LinePayResponse(httpResponse.statusCode(), json.getString("returnCode"),
                json.getString("returnMessage"), transactionId, info, httpResponse.body());
    }

    private String baseUrl() {
        String baseUrl = require(properties.getBaseUrl(), "baseUrl");
        if (!("https://sandbox-api-pay.line.me".equals(baseUrl)
                || "https://api-pay.line.me".equals(baseUrl))) {
            throw new IllegalStateException("Unsupported LINE Pay base URL");
        }
        return baseUrl;
    }

    private static void validateCredential(LinePayCredential credential) {
        if (credential == null) {
            throw new IllegalArgumentException("credential is required");
        }
        require(credential.channelId(), "channelId");
        require(credential.channelSecret(), "channelSecret");
    }

    private static void validateRequest(LinePayRequest request) {
        if (request == null || request.amount() <= 0) {
            throw new IllegalArgumentException("valid request is required");
        }
        require(request.lineOrderId(), "lineOrderId");
        require(request.ddId(), "ddId");
        require(request.productName(), "productName");
        require(request.currency(), "currency");
    }

    private static void validateOfflineRequest(LinePayOfflineRequest request) {
        if (request == null || request.amount() <= 0) {
            throw new IllegalArgumentException("valid offline request is required");
        }
        require(request.lineOrderId(), "lineOrderId");
        require(request.ddId(), "ddId");
        require(request.productName(), "productName");
        require(request.currency(), "currency");
        if (request.oneTimeKey() == null || !request.oneTimeKey().matches("\\d{18}")) {
            throw new IllegalArgumentException("oneTimeKey must contain 18 digits");
        }
    }

    private static String pathSegment(String value) {
        return URLEncoder.encode(require(value, "path segment"), StandardCharsets.UTF_8)
                .replace("+", "%20");
    }

    private static String require(String value, String name) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalArgumentException(name + " is required");
        }
        return value;
    }
}
