/*
 * 自然写教学资源管理与内容分发系统软件 V1.0
 * service/CdnService.java - CDN分发与缓存管理服务
 */
package com.writech.resource.service;

import java.util.*;
import java.util.logging.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

/**
 * CDN分发与缓存管理服务
 *
 * 负责教学资源的CDN加速分发，包括：
 * - 签名URL生成（防盗链）
 * - CDN缓存预热与刷新
 * - 资源分发策略管理
 * - 下载流量统计
 */
public class CdnService {

    private static final Logger logger =
        Logger.getLogger(CdnService.class.getName());

    // ============================================================
    //  CDN配置
    // ============================================================

    /** CDN域名 */
    private static final String CDN_DOMAIN = "https://cdn.writech.com";

    /** CDN签名密钥 */
    private String cdnSignKey;

    /** 签名URL默认有效期（秒） */
    private static final int DEFAULT_EXPIRE_SECONDS = 1800;

    /** Referer白名单 */
    private static final Set<String> REFERER_WHITELIST = new HashSet<>(Arrays.asList(
        "*.writech.com",
        "localhost",
        "127.0.0.1"
    ));

    /** CDN缓存策略（按资源类型配置TTL） */
    private static final Map<String, Integer> CACHE_TTL_MAP = new HashMap<>();
    static {
        CACHE_TTL_MAP.put("pdf", 86400 * 30);       // PDF资源缓存30天
        CACHE_TTL_MAP.put("image", 86400 * 90);      // 图片缓存90天
        CACHE_TTL_MAP.put("video", 86400 * 7);       // 视频缓存7天
        CACHE_TTL_MAP.put("template", 86400 * 30);   // 模板缓存30天
        CACHE_TTL_MAP.put("dotcode", 86400 * 365);   // 点阵码缓存1年（不变内容）
    }

    public CdnService(String signKey) {
        this.cdnSignKey = signKey;
        logger.info("CDN服务初始化: domain=" + CDN_DOMAIN);
    }

    // ============================================================
    //  签名URL生成（防盗链核心）
    // ============================================================

    /**
     * 生成CDN签名下载URL
     *
     * 签名算法（TypeA鉴权）：
     * 1. 计算签名原文：path-timestamp-rand-uid
     * 2. HMAC-SHA256(原文, 密钥)
     * 3. 拼接签名URL：domain/path?auth_key=timestamp-rand-uid-signature
     *
     * @param objectKey OSS对象Key
     * @param expireSeconds 有效期（秒）
     * @return 签名后的CDN下载URL
     */
    public String generateSignedUrl(String objectKey, int expireSeconds) {
        if (expireSeconds <= 0) {
            expireSeconds = DEFAULT_EXPIRE_SECONDS;
        }

        long timestamp = System.currentTimeMillis() / 1000 + expireSeconds;
        String rand = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
        String uid = "0";  // 用户标识（可选）
        String path = "/" + objectKey;

        // 签名原文
        String signContent = String.format("%s-%d-%s-%s", path, timestamp, rand, uid);

        // HMAC-SHA256计算签名
        String signature = hmacSha256(signContent, cdnSignKey);

        // 拼接签名URL
        String authKey = String.format("%d-%s-%s-%s", timestamp, rand, uid, signature);
        String signedUrl = String.format("%s%s?auth_key=%s", CDN_DOMAIN, path, authKey);

        logger.info(String.format(
            "生成签名URL: key=%s, expire=%ds", objectKey, expireSeconds
        ));

        return signedUrl;
    }

    /**
     * 验证签名URL是否有效
     *
     * @param url 待验证的URL
     * @return 验证结果
     */
    public boolean verifySignedUrl(String url) {
        try {
            // 解析auth_key参数
            String authKey = extractParam(url, "auth_key");
            if (authKey == null) return false;

            String[] parts = authKey.split("-", 4);
            if (parts.length != 4) return false;

            long timestamp = Long.parseLong(parts[0]);
            String rand = parts[1];
            String uid = parts[2];
            String receivedSignature = parts[3];

            // 检查是否过期
            if (System.currentTimeMillis() / 1000 > timestamp) {
                return false;
            }

            // 重新计算签名对比
            String path = extractPath(url);
            String signContent = String.format("%s-%d-%s-%s", path, timestamp, rand, uid);
            String expectedSignature = hmacSha256(signContent, cdnSignKey);

            return expectedSignature.equals(receivedSignature);
        } catch (Exception e) {
            logger.warning("签名验证异常: " + e.getMessage());
            return false;
        }
    }

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

            // 转换为十六进制字符串
            StringBuilder sb = new StringBuilder();
            for (byte b : hash) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            throw new RuntimeException("HMAC-SHA256计算失败", e);
        }
    }

    // ============================================================
    //  CDN缓存管理
    // ============================================================

    /**
     * 预热CDN缓存
     *
     * 将指定资源推送到CDN所有边缘节点，确保用户首次访问也能快速响应。
     * 通常在资源审核通过后触发预热。
     *
     * @param objectKeys 要预热的资源Key列表
     */
    public void preheatResources(List<String> objectKeys) {
        logger.info(String.format("CDN缓存预热: %d个资源", objectKeys.size()));

        List<String> urls = new ArrayList<>();
        for (String key : objectKeys) {
            urls.add(CDN_DOMAIN + "/" + key);
        }

        // 调用CDN API预热
        // PushObjectCacheRequest request = new PushObjectCacheRequest();
        // request.setObjectPath(String.join("\n", urls));
        // cdnClient.pushObjectCache(request);

        logger.info("CDN预热任务已提交");
    }

    /**
     * 刷新CDN缓存
     *
     * 资源更新或删除后，需要刷新CDN缓存使旧版本失效。
     *
     * @param objectKeys 要刷新的资源Key列表
     */
    public void refreshCache(List<String> objectKeys) {
        logger.info(String.format("CDN缓存刷新: %d个资源", objectKeys.size()));

        List<String> urls = new ArrayList<>();
        for (String key : objectKeys) {
            urls.add(CDN_DOMAIN + "/" + key);
        }

        // 调用CDN API刷新
        // RefreshObjectCachesRequest request = new RefreshObjectCachesRequest();
        // request.setObjectPath(String.join("\n", urls));
        // cdnClient.refreshObjectCaches(request);

        logger.info("CDN刷新任务已提交");
    }

    /**
     * 刷新目录缓存（用于整个类别的批量更新）
     */
    public void refreshDirectoryCache(String directoryPath) {
        logger.info("CDN目录缓存刷新: " + directoryPath);
        // RefreshObjectCachesRequest request = new RefreshObjectCachesRequest();
        // request.setObjectPath(CDN_DOMAIN + "/" + directoryPath);
        // request.setObjectType("Directory");
        // cdnClient.refreshObjectCaches(request);
    }

    // ============================================================
    //  Referer防盗链校验
    // ============================================================

    /**
     * 校验请求Referer是否在白名单中
     *
     * @param referer 请求头中的Referer
     * @return 是否允许访问
     */
    public boolean validateReferer(String referer) {
        if (referer == null || referer.isEmpty()) {
            return false;  // 空Referer拒绝
        }

        for (String pattern : REFERER_WHITELIST) {
            if (pattern.startsWith("*.")) {
                // 通配符匹配
                String domain = pattern.substring(2);
                if (referer.contains(domain)) {
                    return true;
                }
            } else {
                if (referer.contains(pattern)) {
                    return true;
                }
            }
        }

        logger.warning("Referer校验失败: " + referer);
        return false;
    }

    // ============================================================
    //  流量统计
    // ============================================================

    /**
     * 记录资源下载事件（异步写入ClickHouse）
     *
     * @param resourceId 资源ID
     * @param userId 下载用户ID
     * @param terminal 终端类型（pad/pc/mobile/board）
     * @param fileSize 文件大小（字节）
     */
    public void recordDownloadEvent(
        String resourceId,
        String userId,
        String terminal,
        long fileSize
    ) {
        // 异步写入ClickHouse使用统计表
        // Map<String, Object> event = new HashMap<>();
        // event.put("resource_id", resourceId);
        // event.put("user_id", userId);
        // event.put("terminal", terminal);
        // event.put("file_size", fileSize);
        // event.put("download_at", new Date());
        // event.put("cdn_node", getCdnNodeId());

        // clickhouseClient.insert("usage_stat", event);
    }

    /**
     * 查询资源下载统计
     */
    public Map<String, Object> getDownloadStats(
        String resourceId, String startDate, String endDate
    ) {
        // 从ClickHouse查询聚合统计
        // SELECT count() as downloads, sum(file_size) as total_bytes,
        //        uniq(user_id) as unique_users
        // FROM usage_stat
        // WHERE resource_id = ? AND download_at BETWEEN ? AND ?

        Map<String, Object> stats = new HashMap<>();
        stats.put("resource_id", resourceId);
        stats.put("total_downloads", 0);
        stats.put("total_bytes", 0L);
        stats.put("unique_users", 0);
        stats.put("by_terminal", new HashMap<>());
        stats.put("daily_trend", new ArrayList<>());
        return stats;
    }

    // ============================================================
    //  辅助方法
    // ============================================================

    /** 从URL中提取指定参数 */
    private String extractParam(String url, String paramName) {
        int start = url.indexOf(paramName + "=");
        if (start < 0) return null;
        start += paramName.length() + 1;
        int end = url.indexOf("&", start);
        return end > 0 ? url.substring(start, end) : url.substring(start);
    }

    /** 从URL中提取路径部分 */
    private String extractPath(String url) {
        int start = url.indexOf("/", url.indexOf("//") + 2);
        int end = url.indexOf("?");
        return end > 0 ? url.substring(start, end) : url.substring(start);
    }
}
