package com.ttstd.signaling.service;

import com.ttstd.signaling.model.AccountStatus;
import com.ttstd.signaling.model.AuthPrincipal;
import com.ttstd.signaling.model.DeviceAccount;
import com.ttstd.signaling.model.DeviceAllowlist;
import com.ttstd.signaling.model.DeviceType;
import com.ttstd.signaling.model.PrincipalType;
import com.ttstd.signaling.security.AuthException;
import com.ttstd.signaling.security.JwtService;
import com.ttstd.signaling.security.SecurityProperties;
import com.ttstd.signaling.security.TokenUtils;
import com.ttstd.signaling.store.DeviceStore;
import com.ttstd.signaling.store.NonceStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * 被控端设备身份服务。
 *
 * <p>被控端为系统签名应用、无法登录账号，因此采用「SN + 内置共享密钥 HMAC」完成首次激活，
 * 激活后换取长期 {@code deviceSecret}，再由 deviceSecret 换取短期访问令牌。
 *
 * <p>底层存储通过 {@link DeviceStore}/{@link NonceStore} 抽象，生产环境使用 MySQL/JPA + Redis，
 * 测试/演示使用内存实现，调用方无感知。
 */
@Service
public class DeviceIdentityService {

    private static final Logger logger = LoggerFactory.getLogger(DeviceIdentityService.class);

    private final DeviceStore deviceStore;
    private final NonceStore nonceStore;
    private final SecurityProperties properties;
    private final JwtService jwtService;

    private volatile AccountService.SessionRevocationListener revocationListener;

    public DeviceIdentityService(DeviceStore deviceStore,
                                 NonceStore nonceStore,
                                 SecurityProperties properties,
                                 JwtService jwtService) {
        this.deviceStore = deviceStore;
        this.nonceStore = nonceStore;
        this.properties = properties;
        this.jwtService = jwtService;
    }

    public void setRevocationListener(AccountService.SessionRevocationListener listener) {
        this.revocationListener = listener;
    }

    /**
     * 设备激活结果。deviceSecret 仅此一次返回明文。
     */
    public record ProvisionResult(String deviceUid, String deviceSecret) {
    }

    // ==================== 激活 ====================

    /**
     * 首次激活：校验 SN 白名单、HMAC 签名、时间戳与 nonce，签发 deviceSecret。
     *
     * <p>若设备已激活，则执行重新激活（轮换 deviceSecret 并吊销旧令牌）。
     */
    public ProvisionResult provision(String sn, String model, String nonce, long timestamp, String hmac) {
        if (sn == null || sn.isBlank()) {
            throw AuthException.badRequest("SN 不能为空");
        }
        String secret = properties.getDevice().getProvisionSecret();
        if (secret == null || secret.isBlank()) {
            logger.error("未配置 security.device.provision-secret，拒绝所有设备激活请求");
            throw AuthException.forbidden("设备激活未启用", "provision secret not configured");
        }
        if (nonce == null || nonce.isBlank() || hmac == null || hmac.isBlank()) {
            throw AuthException.badRequest("激活参数不完整");
        }

        long skew = properties.getDevice().getProvisionSkewSeconds();
        long now = Instant.now().getEpochSecond();
        if (Math.abs(now - timestamp) > skew) {
            throw AuthException.forbidden("激活请求已过期", "provision timestamp out of window, sn="
                    + DeviceAccount.maskSn(sn));
        }

        // nonce 唯一性校验（防重放）
        if (!nonceStore.tryReserve(nonce, skew * 2)) {
            throw AuthException.forbidden("激活请求重复", "nonce replayed, sn=" + DeviceAccount.maskSn(sn));
        }

        String expected = hmacHex(secret, sn + "|" + nonce + "|" + timestamp);
        if (!MessageDigest.isEqual(
                expected.getBytes(StandardCharsets.UTF_8),
                hmac.toLowerCase().getBytes(StandardCharsets.UTF_8))) {
            throw AuthException.forbidden("激活签名校验失败",
                    "provision hmac mismatch, sn=" + DeviceAccount.maskSn(sn));
        }

        if (properties.getDevice().isSnAllowlistEnabled() && !deviceStore.isSnAllowed(sn)) {
            throw AuthException.forbidden("设备未授权",
                    "sn not in allowlist: " + DeviceAccount.maskSn(sn));
        }

        String deviceSecret = TokenUtils.randomSecret(32);
        String secretHash = TokenUtils.sha256(deviceSecret);

        DeviceAccount existing = deviceStore.findBySn(sn).orElse(null);
        if (existing != null) {
            if (existing.getStatus() == AccountStatus.BANNED) {
                throw AuthException.forbidden("设备已被禁用",
                        "banned device re-provision attempt: " + existing.maskedSn());
            }
            existing.setSecretHash(secretHash);
            existing.setModel(model);
            existing.setTokenVersion(existing.getTokenVersion() + 1);
            deviceStore.save(existing);
            notifyRevoked(existing.getDeviceUid(), "设备已重新激活，旧凭据失效");
            logger.warn("设备重新激活: uid={} sn={}", existing.getDeviceUid(), existing.maskedSn());
            return new ProvisionResult(existing.getDeviceUid(), deviceSecret);
        }

        String deviceUid = "dev_" + TokenUtils.randomBase62(22);
        DeviceAccount device = new DeviceAccount(deviceUid, sn, secretHash, model);
        deviceStore.save(device);
        logger.info("设备已激活: uid={} sn={} model={}", deviceUid, device.maskedSn(), model);
        return new ProvisionResult(deviceUid, deviceSecret);
    }

    // ==================== 换取令牌 ====================

    public TokenPair issueDeviceToken(String deviceUid, String deviceSecret) {
        DeviceAccount device = deviceUid == null ? null : deviceStore.findByDeviceUid(deviceUid).orElse(null);
        if (device == null || deviceSecret == null) {
            throw AuthException.unauthorized("设备凭据无效");
        }
        if (!TokenUtils.constantTimeEquals(device.getSecretHash(), TokenUtils.sha256(deviceSecret))) {
            throw AuthException.unauthorized("设备凭据无效: " + deviceUid);
        }
        assertDeviceUsable(device);

        Map<String, Object> claims = jwtService.newClaims();
        claims.put("typ", PrincipalType.DEVICE.name());
        claims.put("model", device.getModel());
        String token = jwtService.issue(device.getDeviceUid(), JwtService.PURPOSE_DEVICE,
                properties.getJwt().getDeviceTokenTtlSeconds(),
                device.getTokenVersion(), claims);

        device.setLastOnlineAt(Instant.now());
        deviceStore.save(device);
        return new TokenPair(token, null,
                properties.getJwt().getDeviceTokenTtlSeconds(),
                null, device.getDeviceUid(), device.getModel());
    }

    public AuthPrincipal authenticate(String token) {
        Map<String, Object> claims = jwtService.verify(token, JwtService.PURPOSE_DEVICE);
        if (!PrincipalType.DEVICE.name().equals(JwtService.claimAsString(claims, "typ"))) {
            throw AuthException.unauthorized("令牌主体类型不匹配");
        }
        String deviceUid = JwtService.claimAsString(claims, "sub");
        DeviceAccount device = deviceStore.findByDeviceUid(deviceUid).orElse(null);
        if (device == null) {
            throw AuthException.unauthorized("设备不存在: " + deviceUid);
        }
        long ver = JwtService.claimAsLong(claims, "ver", -1);
        if (ver != device.getTokenVersion()) {
            throw AuthException.unauthorized("设备令牌已失效（凭据版本变更）");
        }
        assertDeviceUsable(device);
        device.setLastOnlineAt(Instant.now());
        deviceStore.save(device);

        return new AuthPrincipal(PrincipalType.DEVICE, deviceUid, deviceUid,
                DeviceType.CONTROLLED, null, device.getModel());
    }

    private void assertDeviceUsable(DeviceAccount device) {
        if (device.getStatus() == AccountStatus.SUSPENDED
                && device.getStatusUntil() != null
                && Instant.now().isAfter(device.getStatusUntil())) {
            device.setStatus(AccountStatus.ACTIVE);
            device.setStatusUntil(null);
            device.setStatusReason(null);
            deviceStore.save(device);
        }
        if (!device.isUsable()) {
            throw AuthException.forbidden("设备已被禁用",
                    "device not usable: " + device.getDeviceUid());
        }
    }

    // ==================== 管理 ====================

    public void disable(String deviceUid, Instant until, String reason) {
        DeviceAccount device = requireDevice(deviceUid);
        device.setStatus(until == null ? AccountStatus.BANNED : AccountStatus.SUSPENDED);
        device.setStatusUntil(until);
        device.setStatusReason(reason);
        device.setTokenVersion(device.getTokenVersion() + 1);
        deviceStore.save(device);
        notifyRevoked(deviceUid, "设备已被禁用" + (reason == null ? "" : "：" + reason));
        logger.info("设备 {} 已被禁用（until={}, reason={}）", deviceUid, until, reason);
    }

    public void enable(String deviceUid) {
        DeviceAccount device = requireDevice(deviceUid);
        device.setStatus(AccountStatus.ACTIVE);
        device.setStatusUntil(null);
        device.setStatusReason(null);
        deviceStore.save(device);
        logger.info("设备 {} 已启用", deviceUid);
    }

    /**
     * 强制设备下线：递增令牌版本使现有令牌立即失效，并断开在线连接。
     *
     * <p>与 {@link #disable} 的区别是不改变账号状态，设备可用新令牌重新上线。
     */
    public void kick(String deviceUid, String reason) {
        DeviceAccount device = requireDevice(deviceUid);
        device.setTokenVersion(device.getTokenVersion() + 1);
        deviceStore.save(device);
        notifyRevoked(deviceUid, reason == null ? "设备已被强制下线" : reason);
        logger.info("设备 {} 已被强制下线：{}", deviceUid, reason);
    }

    /**
     * 删除设备记录，并断开其在线连接。
     */
    public void delete(String deviceUid) {
        DeviceAccount device = requireDevice(deviceUid);
        deviceStore.delete(deviceUid);
        notifyRevoked(deviceUid, "设备已被删除");
        logger.info("设备 {} (sn={}) 已被删除", deviceUid, device.maskedSn());
    }

    public DeviceAccount requireDevice(String deviceUid) {
        return deviceStore.findByDeviceUid(deviceUid)
                .orElseThrow(() -> AuthException.badRequest("设备不存在"));
    }

    public DeviceAccount findByUid(String deviceUid) {
        return deviceUid == null ? null : deviceStore.findByDeviceUid(deviceUid).orElse(null);
    }

    public List<DeviceAccount> listDevices() {
        return new ArrayList<>(deviceStore.listAll());
    }

    /** 批量导入 SN 白名单（持久化到 MySQL）。 */
    public int importAllowlist(List<String> sns) {
        int added = 0;
        String importedBy = "admin";
        for (String sn : sns) {
            if (sn == null || sn.isBlank()) {
                continue;
            }
            String trimmed = sn.trim();
            if (!deviceStore.isSnAllowed(trimmed)) {
                deviceStore.addAllowedSn(new DeviceAllowlist(trimmed, "manual", importedBy));
                added++;
            }
        }
        logger.info("SN 白名单导入 {} 条，当前总数 {}", added, deviceStore.listAllowed().size());
        return added;
    }

    public Set<String> getAllowlist() {
        return Set.copyOf(deviceStore.listAllowed().stream().map(DeviceAllowlist::getSn).toList());
    }

    /** 从 SN 白名单移除，返回是否实际删除。 */
    public boolean removeFromAllowlist(String sn) {
        if (sn == null || sn.isBlank()) {
            throw AuthException.badRequest("SN 不能为空");
        }
        String trimmed = sn.trim();
        if (!deviceStore.isSnAllowed(trimmed)) {
            return false;
        }
        deviceStore.removeAllowedSn(trimmed);
        logger.info("SN 已移出白名单: {}", DeviceAccount.maskSn(trimmed));
        return true;
    }

    private void notifyRevoked(String deviceUid, String reason) {
        AccountService.SessionRevocationListener listener = this.revocationListener;
        if (listener != null) {
            try {
                listener.onRevoked(deviceUid, null, reason);
            } catch (Exception e) {
                logger.warn("下发设备失效通知失败: {}", e.getMessage());
            }
        }
    }

    private static String hmacHex(String key, String data) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
            return HexFormat.of().formatHex(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
        } catch (Exception e) {
            throw new IllegalStateException("HMAC 计算失败", e);
        }
    }
}
