package study.iot.command;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 设备影子文档：desired（云端期望）与 reported（设备上报）两个分区，加一个单调递增的 version。
 *
 * <p>影子是文档而不是消息队列：每次更新带 expectedVersion 做乐观并发，版本不匹配即冲突，
 * 不会用 last-writer-wins 悄悄丢更新。delta 是 desired 与 reported 的差集投影，不是第三份存储。</p>
 */
public final class Shadow {

    public static final ObjectMapper MAPPER = new ObjectMapper();
    private static final DateTimeFormatter TIME = DateTimeFormatter.ISO_INSTANT;

    private final String deviceId;
    private final Map<String, Object> desired = new LinkedHashMap<>();
    private final Map<String, Object> reported = new LinkedHashMap<>();
    private long version;
    private Instant updatedAt;
    private boolean deleted;

    public Shadow(String deviceId) {
        this.deviceId = deviceId;
        this.updatedAt = Instant.now();
    }

    public synchronized long version() {
        return version;
    }

    public synchronized boolean deleted() {
        return deleted;
    }

    public synchronized Map<String, Object> desired() {
        return Map.copyOf(desired);
    }

    public synchronized Map<String, Object> reported() {
        return Map.copyOf(reported);
    }

    /**
     * 更新 desired 分区。expectedVersion 必须是当前版本（首次更新传 0）。
     *
     * @throws VersionConflictException 版本不匹配或影子已删除
     */
    public synchronized long updateDesired(Map<String, Object> patch, long expectedVersion) {
        if (deleted) {
            throw new VersionConflictException("shadow deleted");
        }
        if (expectedVersion != version) {
            throw new VersionConflictException("expected " + expectedVersion + " but was " + version);
        }
        desired.putAll(patch);
        version++;
        updatedAt = Instant.now();
        return version;
    }

    /** 设备上报：只写 reported 分区，同样走版本检查。 */
    public synchronized long report(Map<String, Object> patch, long expectedVersion) {
        if (deleted) {
            throw new VersionConflictException("shadow deleted");
        }
        if (expectedVersion != version) {
            throw new VersionConflictException("expected " + expectedVersion + " but was " + version);
        }
        reported.putAll(patch);
        version++;
        updatedAt = Instant.now();
        return version;
    }

    /** 待执行意图：desired 中与 reported 不同的键。 */
    public synchronized Map<String, Object> delta() {
        Map<String, Object> delta = new LinkedHashMap<>();
        for (var entry : desired.entrySet()) {
            if (!Objects.equals(reported.get(entry.getKey()), entry.getValue())) {
                delta.put(entry.getKey(), entry.getValue());
            }
        }
        return delta;
    }

    /** 标记删除：之后所有更新都以冲突拒绝，历史版本仍可查。 */
    public synchronized long delete(long expectedVersion) {
        if (expectedVersion != version) {
            throw new VersionConflictException("expected " + expectedVersion + " but was " + version);
        }
        deleted = true;
        version++;
        updatedAt = Instant.now();
        return version;
    }

    public synchronized ObjectNode toJson() {
        ObjectNode root = MAPPER.createObjectNode();
        root.put("deviceId", deviceId);
        root.put("version", version);
        root.put("updatedAt", TIME.format(updatedAt));
        root.put("deleted", deleted);
        root.set("desired", MAPPER.valueToTree(desired));
        root.set("reported", MAPPER.valueToTree(reported));
        root.set("delta", MAPPER.valueToTree(delta()));
        return root;
    }

    public static Shadow fromJson(JsonNode node) {
        Shadow shadow = new Shadow(node.path("deviceId").asText());
        node.path("desired").fields().forEachRemaining(e ->
                shadow.desired.put(e.getKey(), MAPPER.convertValue(e.getValue(), Object.class)));
        node.path("reported").fields().forEachRemaining(e ->
                shadow.reported.put(e.getKey(), MAPPER.convertValue(e.getValue(), Object.class)));
        shadow.version = node.path("version").asLong();
        if (node.hasNonNull("updatedAt")) {
            shadow.updatedAt = Instant.parse(node.get("updatedAt").asText());
        }
        shadow.deleted = node.path("deleted").asBoolean(false);
        return shadow;
    }

    public static final class VersionConflictException extends RuntimeException {
        public VersionConflictException(String message) {
            super(message);
        }
    }
}
