/*
 * 自然写互动课堂应用开发SDK软件 V1.0
 * CloudClient - 云平台API客户端
 *
 * 功能说明：
 * 1. 封装云平台REST API调用（用户认证、作业、笔迹等）
 * 2. JWT + Refresh Token 双令牌自动刷新机制
 * 3. 请求签名与加密（防篡改、防重放）
 * 4. 请求重试与超时控制
 * 5. 笔迹数据批量上传（分片压缩）
 * 6. 文件上传/下载（OSS预签名URL）
 */

package com.writech.sdk.android;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Map;
import java.util.TreeMap;
import java.util.zip.GZIPOutputStream;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * 云平台API客户端
 * 提供统一的HTTP调用封装，支持JWT认证和请求签名
 */
public class CloudClient {

    private static final String TAG = "WritechCloudClient";

    /* 默认请求超时（毫秒） */
    private static final int DEFAULT_CONNECT_TIMEOUT = 10000;
    private static final int DEFAULT_READ_TIMEOUT = 30000;

    /* 最大重试次数 */
    private static final int MAX_RETRY_COUNT = 3;

    /* 笔迹批量上传分片大小（字节） */
    private static final int STROKE_CHUNK_SIZE = 64 * 1024;

    /* ========== 认证令牌管理 ========== */

    private String mBaseUrl;                 /* 云平台API基础URL */
    private String mAccessToken;             /* JWT访问令牌 */
    private String mRefreshToken;            /* 刷新令牌 */
    private long mTokenExpireTime;           /* 令牌过期时间（毫秒时间戳） */
    private String mAppKey;                  /* 应用密钥（用于请求签名） */
    private String mAppSecret;               /* 应用签名密钥 */

    /* 令牌刷新回调 */
    private TokenRefreshCallback mTokenCallback;

    /** 令牌刷新回调接口 */
    public interface TokenRefreshCallback {
        void onTokenRefreshed(String newAccessToken, String newRefreshToken);
        void onTokenRefreshFailed(int errorCode, String message);
    }

    /* ========== 构造与初始化 ========== */

    /**
     * 创建云平台API客户端
     * @param baseUrl    云平台API基础地址（如 https://api.writech.com）
     * @param appKey     SDK应用标识
     * @param appSecret  SDK应用密钥
     */
    public CloudClient(String baseUrl, String appKey, String appSecret) {
        mBaseUrl = baseUrl;
        mAppKey = appKey;
        mAppSecret = appSecret;
    }

    /** 设置认证令牌 */
    public void setTokens(String accessToken, String refreshToken, long expireTime) {
        mAccessToken = accessToken;
        mRefreshToken = refreshToken;
        mTokenExpireTime = expireTime;
    }

    /** 设置令牌刷新回调 */
    public void setTokenRefreshCallback(TokenRefreshCallback callback) {
        mTokenCallback = callback;
    }

    /* ========== 用户认证API ========== */

    /**
     * 用户登录（账号密码方式）
     * @param username  用户名
     * @param password  密码（明文，SDK内部做SHA256后传输）
     * @return JSON响应字符串，包含accessToken和refreshToken
     */
    public String login(String username, String password) throws IOException {
        String passwordHash = sha256(password);
        String body = "{\"username\":\"" + username + "\",\"password\":\"" + passwordHash + "\"}";
        return postJson("/api/v1/auth/login", body);
    }

    /**
     * 刷新访问令牌
     * 在accessToken过期前自动调用，使用refreshToken获取新令牌
     */
    public boolean refreshAccessToken() {
        try {
            String body = "{\"refreshToken\":\"" + mRefreshToken + "\"}";
            String response = postJsonNoAuth("/api/v1/auth/refresh", body);

            /* 解析响应中的新令牌 */
            String newAccess = extractJsonValue(response, "accessToken");
            String newRefresh = extractJsonValue(response, "refreshToken");

            if (newAccess != null && newRefresh != null) {
                mAccessToken = newAccess;
                mRefreshToken = newRefresh;
                /* 默认过期时间30分钟 */
                mTokenExpireTime = System.currentTimeMillis() + 30 * 60 * 1000;

                if (mTokenCallback != null) {
                    mTokenCallback.onTokenRefreshed(newAccess, newRefresh);
                }
                return true;
            }
        } catch (IOException e) {
            if (mTokenCallback != null) {
                mTokenCallback.onTokenRefreshFailed(-1, e.getMessage());
            }
        }
        return false;
    }

    /* ========== 作业管理API ========== */

    /** 获取作业列表 */
    public String getAssignments(String classId, int page, int pageSize) throws IOException {
        String params = "classId=" + classId + "&page=" + page + "&pageSize=" + pageSize;
        return get("/api/v1/assignments?" + params);
    }

    /** 获取作业详情 */
    public String getAssignmentDetail(String assignmentId) throws IOException {
        return get("/api/v1/assignments/" + assignmentId);
    }

    /** 提交作业 */
    public String submitAssignment(String assignmentId, String studentId,
                                    String answerJson) throws IOException {
        String body = "{\"assignmentId\":\"" + assignmentId
                     + "\",\"studentId\":\"" + studentId
                     + "\",\"answers\":" + answerJson + "}";
        return postJson("/api/v1/assignments/submit", body);
    }

    /* ========== 笔迹数据上传API ========== */

    /**
     * 上传笔迹数据（单次）
     * @param studentId   学生ID
     * @param pageId      页面ID
     * @param strokeJson  笔迹JSON数据
     */
    public String uploadStroke(String studentId, String pageId,
                                String strokeJson) throws IOException {
        String body = "{\"studentId\":\"" + studentId
                     + "\",\"pageId\":\"" + pageId
                     + "\",\"strokes\":" + strokeJson + "}";
        return postJson("/api/v1/strokes/upload", body);
    }

    /**
     * 批量上传笔迹数据（大数据量分片压缩）
     * 将笔迹数据按CHUNK_SIZE分片，GZIP压缩后逐片上传
     *
     * @param studentId   学生ID
     * @param strokeBytes 笔迹二进制数据
     * @return 上传成功的分片数
     */
    public int uploadStrokeBatch(String studentId, byte[] strokeBytes) throws IOException {
        /* GZIP压缩原始数据 */
        byte[] compressed = gzipCompress(strokeBytes);

        /* 计算分片数 */
        int totalChunks = (compressed.length + STROKE_CHUNK_SIZE - 1) / STROKE_CHUNK_SIZE;
        int uploadedChunks = 0;

        String uploadId = generateUploadId();

        for (int i = 0; i < totalChunks; i++) {
            int offset = i * STROKE_CHUNK_SIZE;
            int length = Math.min(STROKE_CHUNK_SIZE, compressed.length - offset);
            byte[] chunk = new byte[length];
            System.arraycopy(compressed, offset, chunk, 0, length);

            /* 上传分片 */
            String url = mBaseUrl + "/api/v1/strokes/upload-chunk";
            String boundary = "----WritechBoundary" + System.currentTimeMillis();

            HttpURLConnection conn = createConnection(url, "POST");
            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            addAuthHeaders(conn);

            OutputStream os = conn.getOutputStream();
            /* 写入表单字段 */
            writeMultipartField(os, boundary, "uploadId", uploadId);
            writeMultipartField(os, boundary, "studentId", studentId);
            writeMultipartField(os, boundary, "chunkIndex", String.valueOf(i));
            writeMultipartField(os, boundary, "totalChunks", String.valueOf(totalChunks));
            /* 写入二进制数据块 */
            writeMultipartFile(os, boundary, "data", "chunk_" + i + ".gz", chunk);
            os.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
            os.flush();

            int responseCode = conn.getResponseCode();
            conn.disconnect();

            if (responseCode == 200) {
                uploadedChunks++;
            } else {
                break;
            }
        }

        return uploadedChunks;
    }

    /* ========== Multipart POST (静态方法供OCREngine调用) ========== */

    /**
     * 发送Multipart POST请求
     * @param url        完整URL
     * @param token      Bearer令牌
     * @param imageData  图像二进制数据
     * @param strokeData 笔迹数据
     * @param targetChar 目标字符
     * @param timeoutMs  超时毫秒数
     * @return 响应JSON字符串
     */
    public static String postMultipart(String url, String token, byte[] imageData,
                                        byte[] strokeData, String targetChar,
                                        int timeoutMs) throws IOException {
        HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
        conn.setRequestMethod("POST");
        conn.setConnectTimeout(timeoutMs);
        conn.setReadTimeout(timeoutMs);
        conn.setDoOutput(true);

        if (token != null) {
            conn.setRequestProperty("Authorization", "Bearer " + token);
        }

        String boundary = "----WritechBound" + System.nanoTime();
        conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);

        OutputStream os = conn.getOutputStream();
        if (imageData != null) {
            writeMultipartFile(os, boundary, "image", "stroke.png", imageData);
        }
        if (strokeData != null) {
            writeMultipartFile(os, boundary, "strokes", "strokes.bin", strokeData);
        }
        if (targetChar != null) {
            writeMultipartField(os, boundary, "targetChar", targetChar);
        }
        os.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
        os.flush();

        String response = readResponse(conn);
        conn.disconnect();
        return response;
    }

    /* ========== HTTP基础方法 ========== */

    /** GET请求 */
    public String get(String path) throws IOException {
        return executeWithRetry("GET", path, null);
    }

    /** POST JSON请求（带认证） */
    public String postJson(String path, String jsonBody) throws IOException {
        return executeWithRetry("POST", path, jsonBody);
    }

    /** POST JSON请求（无认证，用于登录/刷新令牌） */
    private String postJsonNoAuth(String path, String body) throws IOException {
        String url = mBaseUrl + path;
        HttpURLConnection conn = createConnection(url, "POST");
        conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
        conn.setDoOutput(true);

        OutputStream os = conn.getOutputStream();
        os.write(body.getBytes(StandardCharsets.UTF_8));
        os.flush();

        String response = readResponse(conn);
        conn.disconnect();
        return response;
    }

    /** 带重试和令牌自动刷新的HTTP请求执行 */
    private String executeWithRetry(String method, String path, String body) throws IOException {
        int retryCount = 0;
        IOException lastException = null;

        while (retryCount < MAX_RETRY_COUNT) {
            try {
                /* 检查令牌是否即将过期（提前5分钟刷新） */
                if (mTokenExpireTime > 0 &&
                    System.currentTimeMillis() > mTokenExpireTime - 5 * 60 * 1000) {
                    refreshAccessToken();
                }

                String url = mBaseUrl + path;
                HttpURLConnection conn = createConnection(url, method);
                addAuthHeaders(conn);

                if ("POST".equals(method) && body != null) {
                    conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
                    conn.setDoOutput(true);
                    OutputStream os = conn.getOutputStream();
                    os.write(body.getBytes(StandardCharsets.UTF_8));
                    os.flush();
                }

                int responseCode = conn.getResponseCode();

                /* 401未授权，尝试刷新令牌后重试 */
                if (responseCode == 401 && retryCount == 0) {
                    conn.disconnect();
                    if (refreshAccessToken()) {
                        retryCount++;
                        continue;
                    }
                }

                String response = readResponse(conn);
                conn.disconnect();
                return response;

            } catch (IOException e) {
                lastException = e;
                retryCount++;
                /* 指数退避重试间隔 */
                try {
                    Thread.sleep(1000L * retryCount);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                }
            }
        }

        throw lastException != null ? lastException : new IOException("请求失败，已重试" + MAX_RETRY_COUNT + "次");
    }

    /* ========== 请求签名 ========== */

    /** 添加认证和签名请求头 */
    private void addAuthHeaders(HttpURLConnection conn) {
        if (mAccessToken != null) {
            conn.setRequestProperty("Authorization", "Bearer " + mAccessToken);
        }

        /* 添加请求签名头（防篡改） */
        String timestamp = String.valueOf(System.currentTimeMillis());
        String nonce = generateNonce();
        String signData = mAppKey + timestamp + nonce;
        String signature = hmacSha256(signData, mAppSecret);

        conn.setRequestProperty("X-App-Key", mAppKey);
        conn.setRequestProperty("X-Timestamp", timestamp);
        conn.setRequestProperty("X-Nonce", nonce);
        conn.setRequestProperty("X-Signature", signature);
    }

    /* ========== 工具方法 ========== */

    /** 创建HTTP连接 */
    private HttpURLConnection createConnection(String urlStr, String method) throws IOException {
        URL url = new URL(urlStr);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT);
        conn.setReadTimeout(DEFAULT_READ_TIMEOUT);
        conn.setRequestProperty("User-Agent", "WritechSDK/1.0");
        conn.setRequestProperty("Accept", "application/json");
        return conn;
    }

    /** 读取HTTP响应 */
    private static String readResponse(HttpURLConnection conn) throws IOException {
        InputStream is;
        try {
            is = conn.getInputStream();
        } catch (IOException e) {
            is = conn.getErrorStream();
            if (is == null) throw e;
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int len;
        while ((len = is.read(buffer)) != -1) {
            baos.write(buffer, 0, len);
        }
        is.close();
        return baos.toString("UTF-8");
    }

    /** GZIP压缩 */
    private byte[] gzipCompress(byte[] data) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        GZIPOutputStream gzos = new GZIPOutputStream(baos);
        gzos.write(data);
        gzos.finish();
        gzos.close();
        return baos.toByteArray();
    }

    /** 写入Multipart文本字段 */
    private static void writeMultipartField(OutputStream os, String boundary,
                                             String name, String value) throws IOException {
        String field = "--" + boundary + "\r\n"
                     + "Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n"
                     + value + "\r\n";
        os.write(field.getBytes(StandardCharsets.UTF_8));
    }

    /** 写入Multipart文件字段 */
    private static void writeMultipartFile(OutputStream os, String boundary,
                                            String name, String filename,
                                            byte[] data) throws IOException {
        String header = "--" + boundary + "\r\n"
                      + "Content-Disposition: form-data; name=\"" + name
                      + "\"; filename=\"" + filename + "\"\r\n"
                      + "Content-Type: application/octet-stream\r\n\r\n";
        os.write(header.getBytes(StandardCharsets.UTF_8));
        os.write(data);
        os.write("\r\n".getBytes(StandardCharsets.UTF_8));
    }

    /** SHA-256哈希 */
    private String sha256(String input) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash);
        } catch (Exception e) {
            return input;
        }
    }

    /** HMAC-SHA256签名 */
    private String hmacSha256(String data, String key) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash);
        } catch (Exception e) {
            return "";
        }
    }

    /** 字节数组转十六进制字符串 */
    private String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    /** 生成随机Nonce */
    private String generateNonce() {
        return Long.toHexString(System.nanoTime()) + Long.toHexString((long)(Math.random() * Long.MAX_VALUE));
    }

    /** 生成上传ID */
    private String generateUploadId() {
        return "upload_" + System.currentTimeMillis() + "_" + (int)(Math.random() * 10000);
    }

    /** 从JSON中提取字段值（简化解析） */
    private String extractJsonValue(String json, String key) {
        if (json == null) return null;
        String searchKey = "\"" + key + "\"";
        int idx = json.indexOf(searchKey);
        if (idx < 0) return null;
        int start = json.indexOf("\"", idx + searchKey.length() + 1) + 1;
        int end = json.indexOf("\"", start);
        if (start > 0 && end > start) {
            return json.substring(start, end);
        }
        return null;
    }
}
