package com.gzzm.lobster.api.admin;

import com.gzzm.lobster.audit.AuditService;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.SkillScope;
import com.gzzm.lobster.config.LobsterConfig;
import com.gzzm.lobster.identity.AdminGuard;
import com.gzzm.lobster.identity.UserContext;
import com.gzzm.lobster.skill.SkillAssetService;
import com.gzzm.lobster.skill.SkillDefinition;
import com.gzzm.lobster.skill.SkillDefinitionDao;
import com.gzzm.lobster.storage.FileSystemContentStore;
import com.gzzm.platform.commons.Tools;
import net.cyan.arachne.HttpMethod;
import net.cyan.arachne.annotation.Service;
import net.cyan.commons.util.InputFile;
import net.cyan.nest.annotation.Inject;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * AdminSkillBundleApi —— Skill 资产包（tar.gz）上传 / 版本回滚 / 启用禁用.
 *
 * <p>P0 覆盖 upload + §7.3 8 项解压安全；P1 增加 {@code rollback} 与独立的启用/禁用端点
 * （显式挂 bundle 维度；SkillDefinition 本身的 enabled 继续由 AdminSkillApi 管）.
 */
@Service
public class AdminSkillBundleApi {

    @Inject private SkillDefinitionDao skillDao;
    @Inject private FileSystemContentStore contentStore;
    @Inject private SkillAssetService skillAssetService;
    @Inject private AuditService auditService;

    /** multipart 文件注入点 */
    private InputFile bundleFile;
    public InputFile getBundleFile() { return bundleFile; }
    public void setBundleFile(InputFile bundleFile) { this.bundleFile = bundleFile; }

    /**
     * 上传 bundle —— 覆盖式：新 bundle 替换旧 ref，skill.version +1.
     */
    @Service(url = "/ai/api/admin/skills/{$0}/bundle/upload", method = HttpMethod.post)
    public Map<String, Object> upload(String skillId) throws Exception {
        UserContext admin = AdminGuard.requireAdmin();
        if (skillId == null || skillId.isEmpty()) {
            throw new LobsterException("admin.bundle.invalid", "skillId required");
        }
        if (bundleFile == null) {
            throw new LobsterException("admin.bundle.invalid", "bundleFile required");
        }
        SkillDefinition skill = skillDao.getSkill(skillId);
        if (skill == null) {
            throw new LobsterException("admin.bundle.skill_not_found", "Skill not found: " + skillId);
        }
        rejectIfSystem(skill);

        long max = 60L * 1024 * 1024; // 60MB hard cap for upload stream
        byte[] bytes = readAll(bundleFile.getInputStream(), max);

        Set<String> allowedPkgs = loadInstalledPackages();
        SkillAssetService.ValidationResult v = skillAssetService.validateBundle(bytes, allowedPkgs);
        if (!v.ok) {
            auditBundle(admin, skillId, "upload_rejected",
                    v.code + ": " + v.message);
            throw new LobsterException(v.code, v.message);
        }

        String newRef = contentStore.writeBinary("skill-bundle", admin.getUserId(), bytes, "tgz");
        String oldRef = skill.getAssetBundleRef();
        skill.setAssetBundleRef(newRef);
        int newVer = (skill.getVersion() == null ? 0 : skill.getVersion()) + 1;
        skill.setVersion(newVer);
        skill.setUpdateTime(new Date());
        skillDao.save(skill);
        // 清解压缓存，避免老版本残留
        skillAssetService.invalidate(skillId);
        if (oldRef != null && !oldRef.equals(newRef)) {
            try { /* 保留旧 ref，支持 rollback —— 不删除 */ } catch (Throwable ignore) { /* ignore */ }
        }
        auditBundle(admin, skillId, "upload_ok", "newRef=" + newRef + ", version=" + newVer);

        Map<String, Object> out = new LinkedHashMap<>();
        out.put("skillId", skillId);
        out.put("version", newVer);
        out.put("bundleRef", newRef);
        out.put("sizeBytes", bytes.length);
        out.put("fileCount", v.fileCount);
        out.put("totalBytes", v.totalBytes);
        return out;
    }

    /**
     * 回滚到指定 bundleRef —— P1：需要调用方显式提供旧 ref（通常从 ContentStore 审计里找）.
     */
    @Service(url = "/ai/api/admin/skills/{$0}/bundle/rollback", method = HttpMethod.post)
    public Map<String, Object> rollback(String skillId, String bundleRef) throws Exception {
        UserContext admin = AdminGuard.requireAdmin();
        if (skillId == null || bundleRef == null || bundleRef.isEmpty()) {
            throw new LobsterException("admin.bundle.invalid", "skillId/bundleRef required");
        }
        SkillDefinition skill = skillDao.getSkill(skillId);
        if (skill == null) {
            throw new LobsterException("admin.bundle.skill_not_found", "Skill not found: " + skillId);
        }
        rejectIfSystem(skill);
        if (!contentStore.exists(bundleRef)) {
            throw new LobsterException("admin.bundle.ref_not_found",
                    "bundleRef not in ContentStore: " + bundleRef);
        }
        // 重新校验旧 bundle —— 防止旧包里埋了已被当前白名单排除的 pip 包
        byte[] bytes = contentStore.readBinary(bundleRef);
        SkillAssetService.ValidationResult v = skillAssetService.validateBundle(bytes, loadInstalledPackages());
        if (!v.ok) {
            auditBundle(admin, skillId, "rollback_rejected", v.code + ": " + v.message);
            throw new LobsterException(v.code, v.message);
        }
        skill.setAssetBundleRef(bundleRef);
        int newVer = (skill.getVersion() == null ? 0 : skill.getVersion()) + 1;
        skill.setVersion(newVer);
        skill.setUpdateTime(new Date());
        skillDao.save(skill);
        skillAssetService.invalidate(skillId);
        auditBundle(admin, skillId, "rollback_ok",
                "to=" + bundleRef + ", version=" + newVer);

        Map<String, Object> out = new LinkedHashMap<>();
        out.put("skillId", skillId);
        out.put("version", newVer);
        out.put("bundleRef", bundleRef);
        return out;
    }

    /**
     * 启用/禁用 bundle —— P1：仅控制 SkillDefinition.enabled；保留 assetBundleRef 以便复启.
     */
    @Service(url = "/ai/api/admin/skills/{$0}/bundle/enabled", method = HttpMethod.post)
    public Map<String, Object> setEnabled(String skillId, Boolean enabled) throws Exception {
        UserContext admin = AdminGuard.requireAdmin();
        SkillDefinition skill = skillDao.getSkill(skillId);
        if (skill == null) {
            throw new LobsterException("admin.bundle.skill_not_found", "Skill not found: " + skillId);
        }
        boolean target = enabled == null ? !Boolean.TRUE.equals(skill.getEnabled()) : enabled;
        skill.setEnabled(target);
        skill.setUpdateTime(new Date());
        skillDao.save(skill);
        if (!target) skillAssetService.invalidate(skillId);
        auditBundle(admin, skillId, target ? "enabled" : "disabled", null);

        Map<String, Object> out = new LinkedHashMap<>();
        out.put("skillId", skillId);
        out.put("enabled", target);
        return out;
    }

    // ---------- helpers ----------

    /**
     * 系统 skill（fs:// 挂文件系统）不允许经 Admin API 覆盖上传或回滚——
     * 启动时 SystemSkillLoader 会按文件重新对齐，导致两边打架.
     * 系统 skill 的启用/禁用仍可用 {@link #setEnabled(String, Boolean)}.
     */
    private static void rejectIfSystem(SkillDefinition skill) {
        if (skill.getScope() == SkillScope.system) {
            throw new LobsterException("admin.bundle.system_readonly",
                    "System skill '" + skill.getSkillId() + "' is file-backed; "
                            + "bundle upload/rollback is not allowed. Edit the files on disk and restart.");
        }
        String ref = skill.getAssetBundleRef();
        if (ref != null && ref.startsWith("fs://")) {
            throw new LobsterException("admin.bundle.system_readonly",
                    "Skill '" + skill.getSkillId() + "' is bound to filesystem (fs://); "
                            + "bundle upload/rollback is not allowed.");
        }
    }

    private void auditBundle(UserContext admin, String skillId, String action, String extra) {
        try {
            Map<String, Object> detail = new LinkedHashMap<>();
            if (extra != null) detail.put("detail", extra);
            auditService.record(admin, null, null, "skill_bundle." + action,
                    "skill", skillId, "ok", detail);
        } catch (Throwable t) {
            try { Tools.log("[AdminSkillBundleApi] audit failed", t); } catch (Throwable ignore) { /* ignore */ }
        }
    }

    /**
     * 读沙箱镜像已安装 pip 包白名单。路径由 {@link LobsterConfig#getSandboxInstalledPackagesFile()}
     * 指定（默认 {@code WEB-INF/sandbox-installed-packages.json}），绝对路径或
     * 相对 catalina.base / user.dir 解析.
     * 读不到返回 null —— {@link SkillAssetService#validateBundle} 会跳过 pip 校验.
     */
    private Set<String> loadInstalledPackages() {
        String raw = LobsterConfig.getSandboxInstalledPackagesFile();
        if (raw == null || raw.isEmpty()) return null;
        java.nio.file.Path file = java.nio.file.Paths.get(raw);
        if (!file.isAbsolute()) {
            String base = System.getProperty("catalina.base");
            if (base == null || base.isEmpty()) base = System.getProperty("user.dir", ".");
            file = java.nio.file.Paths.get(base).resolve(raw).toAbsolutePath().normalize();
        }
        if (!java.nio.file.Files.exists(file)) return null;
        try {
            byte[] data = java.nio.file.Files.readAllBytes(file);
            Map<String, Object> m = com.gzzm.lobster.common.JsonUtil.fromJsonToMap(new String(data, StandardCharsets.UTF_8));
            Object arr = m.get("packages");
            if (!(arr instanceof java.util.List)) return null;
            // 同样做 PEP 503 归一化，让 `et_xmlfile` / `typing_extensions` / `pdfminer.six`
            // 等清单里的实际 installed name，与 user bundle 里可能写的 PyPI 规范名（连字符）
            // 对比时能匹配上. 与 SkillAssetService.baseName 保持一致规则.
            Set<String> out = new HashSet<>();
            for (Object o : (java.util.List<?>) arr) {
                if (o == null) continue;
                String name = String.valueOf(o).trim().toLowerCase().replaceAll("[._-]+", "-");
                if (!name.isEmpty()) out.add(name);
            }
            return out;
        } catch (Throwable t) {
            try { Tools.log("[AdminSkillBundleApi] loadInstalledPackages failed: " + file, t); } catch (Throwable ignore) { /* ignore */ }
            return null;
        }
    }

    private static byte[] readAll(InputStream in, long cap) throws Exception {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        byte[] chunk = new byte[16 * 1024];
        int n;
        long total = 0;
        while ((n = in.read(chunk)) > 0) {
            total += n;
            if (total > cap) {
                throw new LobsterException("admin.bundle.too_large",
                        "Bundle exceeds " + cap + " bytes (streamed)");
            }
            buf.write(chunk, 0, n);
        }
        return buf.toByteArray();
    }
}
