"""NeoLogic / 切戻し・復元・訂正競合の小さな教材。Python 3、追加ライブラリ不要。

実行: python3 business-recovery-examples-20261026.py
架空データだけを使います。外部接続なし。一時フォルダーへの保存・復元後は自動削除します。
実サービス・実機の同期・データベースの性能や同時実行を検証するコードではありません。
"""
import copy
import hashlib
import json
import pathlib
import shutil
import tempfile


def total(orders):
    active = [x for x in orders.values() if x["status"] == "active"]
    return {"all": len(orders), "active": len(active),
            "boxes": sum(x["boxes"] for x in active),
            "yen": sum(x["boxes"] * 1000 for x in active)}


def rollback_example():
    before = {"O1001": {"boxes": 2, "status": "active"},
              "O1002": {"boxes": 3, "status": "active"},
              "O1003": {"boxes": 2, "status": "active"}}
    changes = [
        {"operation": "new-004", "id": "O1004", "boxes": 4, "status": "active"},
        {"operation": "edit-002", "id": "O1002", "boxes": 5, "status": "active"},
        {"operation": "cancel-003", "id": "O1003", "boxes": 2, "status": "cancelled"},
    ]
    target, applied = copy.deepcopy(before), {}

    def replay(events):
        for event in events:
            operation = event["operation"]
            encoded = json.dumps(event, sort_keys=True)
            if operation in applied:
                if applied[operation] != encoded:
                    raise ValueError("same operation, different content")
                continue
            target[event["id"]] = {"boxes": event["boxes"], "status": event["status"]}
            applied[operation] = encoded

    replay(changes)
    expected = {"all": 4, "active": 3, "boxes": 11, "yen": 11000}
    assert total(target) == expected
    assert target["O1002"]["boxes"] == 5 and target["O1003"]["status"] == "cancelled"
    replay(changes)
    assert total(target) == expected
    conflict = False
    try:
        replay([{**changes[0], "boxes": 99}])
    except ValueError:
        conflict = True
    assert conflict and total(target) == expected
    # 新規だけ回収する反例：数量・金額は正解と同じでも、訂正と取消が欠ける。
    new_only = copy.deepcopy(before)
    new_only["O1004"] = {"boxes": 4, "status": "active"}
    assert total(new_only)["boxes"] == expected["boxes"]
    assert total(new_only)["yen"] == expected["yen"]
    assert total(new_only)["active"] != expected["active"]
    return {"backup_only": total(before), "new_only": total(new_only),
            "all_changes": total(target), "replay_kept_same_state": True,
            "changed_content_rejected": conflict}


def restore_example():
    with tempfile.TemporaryDirectory(prefix="neologic-recovery-demo-") as temporary:
        root = pathlib.Path(temporary)
        backup, restored = root / "backup", root / "restored"
        backup.mkdir(); restored.mkdir()
        orders = [{"order": "SAMPLE-001", "yen": 4000, "attachment": "order-001.txt"}]
        attachment = "説明用の注文書。SAMPLE-001 / 4000円\n".encode()
        (backup / "orders.json").write_text(json.dumps(orders))
        (backup / "order-001.txt").write_bytes(attachment)
        expected_hash = hashlib.sha256(attachment).hexdigest()

        def inspect():
            data = json.loads((restored / "orders.json").read_text())
            issues = []
            for order in data:
                f = restored / order["attachment"]
                if not f.is_file(): issues.append("attachment_missing")
                elif hashlib.sha256(f.read_bytes()).hexdigest() != expected_hash:
                    issues.append("attachment_changed")
            return {"orders": len(data), "yen": sum(x["yen"] for x in data), "issues": issues}

        shutil.copy2(backup / "orders.json", restored / "orders.json")
        partial = inspect()
        assert partial == {"orders": 1, "yen": 4000, "issues": ["attachment_missing"]}
        (restored / "order-001.txt").write_text("同じファイル名でも内容が違う")
        changed = inspect()
        assert changed["issues"] == ["attachment_changed"]
        shutil.copy2(backup / "order-001.txt", restored / "order-001.txt")
        complete = inspect()
        assert complete == {"orders": 1, "yen": 4000, "issues": []}
        # 復元後に業務操作できるかの最小見本。実アプリの操作ではない。
        data = json.loads((restored / "orders.json").read_text())
        data.append({"order": "SAMPLE-002", "yen": 3000, "attachment": None})
        (restored / "orders.json").write_text(json.dumps(data))
        assert len(json.loads((restored / "orders.json").read_text())) == 2
        return {"data_only": partial, "same_name_changed_file": changed,
                "with_attachment": complete, "new_order_write_read": True}


def synchronization_example():
    # 端末・通信の代替となるメモリー上の状態遷移。Power Appsの実装ではない。
    server = {"version": 1, "text": "初回記録", "photos": set(), "pending": None}
    a = {"base": 1, "text": "要再点検", "photos": {"P01", "P02"}}
    server.update(version=2, text="再点検不要")  # 先に届いたBの訂正
    if a["base"] != server["version"]:
        server["pending"] = a["text"]
    assert server["text"] == "再点検不要" and server["pending"] == "要再点検"
    conflict = {"current": server["text"], "pending": server["pending"], "decision": "要確認"}
    server.update(version=3, text=a["text"], pending=None)  # 責任者による架空の採用判断
    server["photos"].add("P01")
    one_photo_complete = server["photos"] == a["photos"]
    assert not one_photo_complete
    server["photos"].add("P02")
    server["photos"].add("P02")  # 同じ写真の再送
    assert server["photos"] == {"P01", "P02"}
    return {"conflict": conflict, "one_photo_complete": one_photo_complete,
            "after_second_photo": {"version": server["version"], "photo_count": len(server["photos"])}}


if __name__ == "__main__":
    print(json.dumps({"scope": "synthetic local examples; no external API or device test",
                      "rollback": rollback_example(), "restore": restore_example(),
                      "synchronization": synchronization_example()}, ensure_ascii=False, indent=2))
