#!/usr/bin/env python3
"""U89 跨语言契约客户端：只用 Python 标准库实现同一份 JSON 契约。

用法（由 Java 测试通过 ProcessBuilder 调用，也可以手工运行）：

    python3 device_client.py --contract contract-fixture.json --mode encode --command '{"commandId":"c-1",...}'
    python3 device_client.py --contract contract-fixture.json --mode decode --json '<command json>'
    python3 device_client.py --contract contract-fixture.json --mode ack --command '{"...":...}' --success true

输出统一是单行 JSON：{"ok":true,"digest":"..."} 或 {"ok":false,"error":"..."}。
digest 与 Java CommandService.digest 的规范串一致：
commandId|deviceId|type|payload|createdAt|expireAt，ISO-8601 UTC（Z 结尾），SHA-256。
"""
import argparse
import hashlib
import json
import sys
from datetime import datetime, timezone

SCHEMA_VERSION = 1
REQUIRED = ("commandId", "deviceId", "type", "payload", "createdAt", "expireAt")


def parse_time(value):
    # 契约要求 UTC；带 Z 或 +00:00 都接受，但统一转成 Z 结尾的 UTC。
    text = value.replace("Z", "+00:00")
    parsed = datetime.fromisoformat(text)
    if parsed.tzinfo is None:
        raise ValueError("time must carry an explicit UTC offset")
    return parsed.astimezone(timezone.utc)


def format_time(moment):
    return moment.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"


def canonical_digest(command):
    joined = "|".join([
        command["commandId"], command["deviceId"], command["type"], command["payload"],
        format_time(parse_time(command["createdAt"])), format_time(parse_time(command["expireAt"])),
    ])
    return hashlib.sha256(joined.encode("utf-8")).hexdigest()


def validate(command):
    if command.get("schemaVersion") != SCHEMA_VERSION:
        raise ValueError("unsupported schemaVersion")
    for field in REQUIRED:
        if not isinstance(command.get(field), str) or not command[field]:
            raise ValueError("missing required field " + field)
    created = parse_time(command["createdAt"])
    expire = parse_time(command["expireAt"])
    if expire < created:
        raise ValueError("expireAt must not precede createdAt")
    return created, expire


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--contract", required=True)
    parser.add_argument("--mode", required=True, choices=["encode", "decode", "ack"])
    parser.add_argument("--command", default=None)
    parser.add_argument("--json", default=None)
    parser.add_argument("--success", default="true")
    args = parser.parse_args()
    with open(args.contract, encoding="utf-8") as handle:
        fixture = json.load(handle)
    if fixture.get("schema") != "iot-command-contract-v1":
        raise SystemExit("fixture schema mismatch")
    if args.mode == "encode":
        command = json.loads(args.command)
        validate(command)
        envelope = {"schemaVersion": SCHEMA_VERSION, **{k: command[k] for k in REQUIRED}}
        print(json.dumps({"ok": True, "encoded": envelope, "digest": canonical_digest(command)},
                         ensure_ascii=False, sort_keys=True))
        return
    if args.mode == "decode":
        command = json.loads(args.json)
        created, expire = validate(command)
        print(json.dumps({"ok": True, "createdAtUtc": format_time(created),
                          "expireAtUtc": format_time(expire), "digest": canonical_digest(command)},
                         ensure_ascii=False, sort_keys=True))
        return
    command = json.loads(args.command)
    validate(command)
    ack = {"schemaVersion": SCHEMA_VERSION, "commandId": command["commandId"],
           "deviceId": command["deviceId"], "success": args.success.lower() == "true",
           "reported": {"speed": 0, "position": "home"}}
    print(json.dumps({"ok": True, "ack": ack}, ensure_ascii=False, sort_keys=True))


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:  # noqa: BLE001 - 统一以 JSON 报错，便于 Java 侧断言
        print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, sort_keys=True))
        sys.exit(1)
