"""Reconcile r03 native CAD bodies with procurement references.

Run with ordinary Python from any directory, after fusion_build.py has finished.
No Fusion imports or CAD mutations. Requires current builder/parameter hashes.
The r02 audit supplies retained descriptions, never r03 body quantities.
"""
from __future__ import annotations

import copy
import csv
import hashlib
import json
from collections import Counter, defaultdict
from pathlib import Path


HERE = Path(__file__).resolve().parent
ROOT = HERE.parents[1]
PREFIX = "v2_robot/rs06_r03/"


def sha(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def build() -> dict:
    old = json.loads((ROOT / "v2_robot/rs06_r02/audit_inventory.json").read_text())
    cad = json.loads((HERE / "cad/cad_checks.json").read_text())
    params = json.loads((HERE / "parameters.json").read_text())
    bracket = json.loads((HERE / "research/stock_brackets.json").read_text())
    tooling = json.loads((HERE / "research/tooling_standard.json").read_text())
    if cad["source_sha256"] != sha(HERE / "fusion_build.py"):
        raise RuntimeError("cad_checks.json does not match the current Fusion builder")
    if cad["parameters_sha256"] != sha(HERE / "parameters.json"):
        raise RuntimeError("cad_checks.json does not match current parameters")
    if cad["revision"] != params["revision"]:
        raise RuntimeError("CAD and parameters revisions differ")

    groups = {g["id"]: copy.deepcopy(g) for g in old["groups"]}
    retained_map = {
        (r["component"], r["body"]): g["id"]
        for g in old["groups"] if g["scope"] == "r01_retained"
        for r in g["body_records"]
    }
    for g in groups.values():
        g["body_records"] = []
        if g["scope"] == "r02_new_chassis":
            g["scope"] = "r03_chassis"
        elif g["scope"] == "r01_retained":
            g["scope"] = "r03_leg_and_reserved"
        g["source"] = (
            PREFIX + "fusion_build.py; " + PREFIX + "parameters.json; "
            + "v2_robot/rs06_r02/audit_inventory.json"
        )
        g["driver_hex_mm"] = None
        g["purchase_unit"] = "piece" if g["installed_quantity_reference"] else None
        g["pack_quantity"] = None
        g["separate_purchase_quantity_reference"] = None
        g["procurement_note"] = "장착 참조 수량이며 구매/제조 확정 수량 아님. 가격·재고·납기 미확인."

    def update(key: str, **values) -> None:
        groups[key].update(values)

    groups["lower_strap_screws"] = groups.pop("gusset_and_lower_strap_screws")
    update("lower_strap_screws", id="lower_strap_screws", name_ko="낮은 상부 스트랩 M5 볼트")
    groups["standard_m5_washers"] = groups.pop("top_strap_flat_washers")
    update("standard_m5_washers", id="standard_m5_washers", name_ko="M5 표준 평와셔")
    groups["stock_corner_brackets"] = groups.pop("lower_flat_gussets")
    update("stock_corner_brackets", id="stock_corner_brackets", name_ko="하부 가로대 기성 코너 브래킷")
    del groups["raised_strap_end_spacers"]
    groups["stock_bracket_screws"] = copy.deepcopy(groups["raised_strap_screws"])
    update("stock_bracket_screws", id="stock_bracket_screws", name_ko="기성 브래킷 동봉 M5 볼트")

    for key in ("longitudinal_profiles", "lower_crossmembers"):
        update(key,
            selection_status="catalogue_size_selected_reference_geometry",
            material="알루미늄; 현 일본 웹 A6005CSS-T5 / 과거 한국 PDF A6N01SS-T5; 실제 공급 재질 확인 필요",
            source=PREFIX + "research/stock_brackets.json; " + PREFIX + "fusion_build.py",
            note=("공식 슬롯 치수 기반 재구성; 필렛/슬롯 경사 상세 일부 단순화, 공급사 STEP 미사용. "
                  "카탈로그 선형 질량 0.50 kg/m; 슬롯 끼움/공차 미검증. "
                  + ("340 mm×4; TPW 양단 M5 깊이15; 상부 Y±145, 하부 Y±125."
                     if key == "longitudinal_profiles" else "230 mm×2; 끝단 탭 없음; X±70.")))
    update("profile_tnuts",
        installed_quantity_reference=24,
        model="HNTT5-5",
        dimensions_and_threads_mm="평면10×10; T4.1/E3.3/목 폭5.8; M5; 나사산 생략",
        material="S10C 상당 / 3가 크로메이트",
        selection_status="catalogue_size_selected_reference_geometry",
        note="브래킷16+상부 스트랩4+트레이4. 공식 단면 재구성; 뒤쪽 평면 폭5.8은 이미지 추정. 선입 방식이며 끝판 조립 전 삽입. 실제 공차/끼움 미검증.",
        source=PREFIX + "research/stock_brackets.json; " + PREFIX + "fusion_build.py",
        separate_purchase_quantity_reference=8,
        procurement_note="24개 장착 중 16개는 HBLFSN5-SET 8세트에 포함, 별도 필요량8개. 판매 포장 단위 미확인.")
    for key, size in (("haa_stator_screws", 2.5), ("profile_end_screws", 3),
                      ("lower_strap_screws", 4), ("raised_strap_screws", 4)):
        update(key, driver_hex_mm=size,
               selection_status="catalogue_size_selected_reference_geometry",
               source=PREFIX + "research/tooling_standard.json; " + PREFIX + "fusion_build.py")
    update("lower_strap_screws", installed_quantity_reference=2,
        note="CB5-8×2; 2 mm 스트랩 위 1 mm 평와셔. 나사산 생략; 체결 토크/풀림/공차 검증 전.")
    update("raised_strap_screws",
        note="CB5-10×2; 각 끝단 아래 WSI-SUS-M5 1 mm 두 장, 머리 아래 한 장. 맞춤 2 mm 스페이서는 제거. 나사산/체결강도 미검증.")
    update("standard_m5_washers", installed_quantity_reference=12,
        dimensions_and_threads_mm="내경5.3 / 외경10 / 두께1; 명목 카탈로그 치수 재구성",
        selection_status="catalogue_size_selected_reference_geometry",
        source=PREFIX + "research/tooling_standard.json; " + PREFIX + "fusion_build.py",
        note="상부 머리 아래4 + 높은 스트랩 두 끝 아래2장씩4 + 트레이 머리 아래4 =12. 공차 누적/접촉면/장기 풀림 미검증.")
    update("tray_screws", model="CB5-14", driver_hex_mm=4,
        dimensions_and_threads_mm="M5×0.8×14; 머리Ø8.5×5; 내육각4",
        domestic_purchase_or_service_url="https://kr.misumi-ec.com/vona2/detail/110100141750/?HissuCode=CB5-14",
        selection_status="catalogue_size_selected_reference_geometry",
        source=PREFIX + "research/tooling_standard.json; " + PREFIX + "fusion_build.py",
        note="트레이7.5+와셔1=명목 그립8.5; 슬롯 진입5.5; 슬롯 바닥 여유0.5. PETG 크리프·공차·완전나사 길이·체결강도 미검증.")
    update("stock_corner_brackets", model="HBLFSN5-SET", installed_quantity_reference=8,
        dimensions_and_threads_mm="HBLFSN5 본체30×30×20; 벽4.5; 돌기높이1; 3×Ø5.3; 일반 장착 홀18 mm 오프셋",
        material="ADC12 알루미늄 다이캐스팅; 브래킷 자체 카탈로그 질량15g",
        domestic_seller="한국미스미",
        domestic_purchase_or_service_url=bracket["selected"]["domestic_purchase_url"],
        selection_status="stock_kit_selected_drawing_reconstruction", purchase_unit="set",
        separate_purchase_quantity_reference=8,
        note="4개 T접합에 2개씩. 도면 재구성 참조이며 공급사 STEP 미사용. 리브2 mm/돌기 폭·외곽은 추정; 주조 공차/접촉 강도 미검증.",
        source=PREFIX + "research/stock_brackets.json; " + PREFIX + "fusion_build.py",
        procurement_note="8세트 주문 참조. 세트당 브래킷1+CBM5-10 2+HNTT5-5 2. 아래 볼트/너트 행은 장착 전수 매핑용으로 구매 중복 금지.")
    update("stock_bracket_screws", model="CBM5-10", installed_quantity_reference=16,
        dimensions_and_threads_mm="M5×0.8×10; 머리Ø8.5×5; 내육각4", driver_hex_mm=4,
        material="SCM435 상당 / 강도등급10.9; 표면처리 실제 주문 사양 확인",
        domestic_purchase_or_service_url=bracket["bolt"]["domestic_purchase_url"],
        selection_status="included_in_selected_stock_kit", separate_purchase_quantity_reference=0,
        source=PREFIX + "research/stock_brackets.json; " + PREFIX + "fusion_build.py",
        note="8개 브래킷에 2개씩. 벽4.5/슬롯 진입5.5/명목 바닥 여유0.5. 실제 나사산·공차·체결력 미검증.",
        procurement_note="16개 모두 HBLFSN5-SET 8세트에 포함. 별도 구매하지 않는 장착 참조 행.")
    update("printed_battery_tray", note="일체형 스탠드오프/체결점4; 출력 외곽240×120×7.5. 인필·서포트·브림·끼움·체결 크리프 미검증; 슬라이싱/실물 출력 미실시.")
    update("integral_knee_shafts",
        dimensions_and_threads_mm="전체 축방향78; Ø12 축69; Ø48×5 플랜지; Ø16×3 숄더; Ø12×4 파일럿; PCD32의4×M4 관통탭 의도, CAD명목Ø4; 다월2×Ø4 간격35",
        note="맞춤 일체형 축. r03 플랜지에 M4 관통탭 설계 의도를 반영했으나 나사산 없음. 판 두께5 mm가 완전 유효나사 길이5 mm를 뜻하지 않음. 가공 공차·체결강도·축단 나사·풀리 결합 미검증.")
    update("root_m4_screw_envelopes", name_ko="하퇴 뿌리 M4 표준 볼트 참조", model="CB4-30", driver_hex_mm=3,
        dimensions_and_threads_mm="M4×0.7×30; 머리Ø7×4; 내육각3; 나사산 없음",
        domestic_purchase_or_service_url="https://kr.misumi-ec.com/vona2/detail/110100141750/?HissuCode=CB4-30",
        selection_status="catalogue_size_selected_reference_geometry",
        source=PREFIX + "research/tooling_standard.json; " + PREFIX + "fusion_build.py",
        note="27 mm 참조 길이를 표준30 mm로 변경. 맞춤 강재 플랜지 명목5 mm 관통탭에 결합; 플랜지 뒤3 mm 돌출. 모따기/불완전나사를 제외한 유효 물림과 강도/풀림 미검증.")
    update("root_drive_dowels",
        source=PREFIX + "research/tooling_standard.json; " + PREFIX + "fusion_build.py",
        note="현재 CAD는 맞춤 Ø4×12 참조. SHEMEKS M11301 ISO2338 SUS304 h8 Ø4×12 국내 후보가 있으나 채택/끼움/유지/전단강도 검증 전. 기성 확정품으로 집계하지 않음.")
    dowel_candidate = next(x for x in tooling["standard_part_candidates"] if x["item"] == "root locating pin")
    groups["root_drive_dowels"]["candidate_domestic_url"] = dowel_candidate["purchase_url"]
    update("foot_m3_screw_envelopes", model="CB3-20", driver_hex_mm=2.5,
        dimensions_and_threads_mm="M3×0.5×20; 머리Ø5.5×3; 내육각2.5; 나사산 없음",
        domestic_purchase_or_service_url="https://kr.misumi-ec.com/vona2/detail/110100141750/?HissuCode=CB3-20",
        selection_status="catalogue_size_selected_reference_geometry",
        source=PREFIX + "research/tooling_standard.json; " + PREFIX + "fusion_build.py",
        note="육각 너트8개와 조합. 실제 완전나사 물림/볼트강도/잠금/반복 조립 검증 전.")
    update("foot_m3_nut_envelopes", name_ko="발 M3 육각 너트 참조", model="NATTO-3-5.5-2.4",
        dimensions_and_threads_mm="M3; 대변5.5/두께2.4; CAD 관통Ø3.3으로 나사산 생략",
        material="철 / 크로메이트; 강도등급 미확인", domestic_seller="한국미스미",
        domestic_purchase_or_service_url="https://kr.misumi-ec.com/vona2/detail/110500137250/?HissuCode=NATTO-3-5.5-2.4",
        selection_status="catalogue_size_selected_reference_geometry", pack_quantity=10,
        source=PREFIX + "research/tooling_standard.json; " + PREFIX + "fusion_build.py",
        procurement_note="8개 장착, 판매 포장10개. 수량8은 8포장이 아님; 1포장으로 명목 수량 충족, 예비/실제 주문 전 재확인.",
        note="기존 원통 참조를 대변5.5 육각으로 수정. 발 컵 AF5.8 포켓으로 회전 저지 의도. 출력 공차·회전방지 강도·나사/잠금 검증 전; ISO4032/DIN934 적합성 단정 안 함.")
    update("printed_foot_cups",
        dimensions_and_threads_mm="구면좌면R20.2; 혀 틈8.6; 2×Ø5.2 슬리브홀 X152/166; M3 너트 대변5.8 포켓 및 일체형 보스 추가",
        note="r03 M3 육각 너트 회전방지 포켓 추가. 출력 끼움/너트 유지/보스 강도 미검증. 탄성 발 접착제와 재료 미선정.")
    update("missing_root_thread_closure", name_ko="미검증: 하퇴 플랜지 M4 관통탭 체결",
        scope="validation_required", model="맞춤 강재 플랜지 M4 관통탭 설계 의도",
        dimensions_and_threads_mm="플랜지 명목 두께5; CAD Ø4; CB4-30 뒤3 돌출",
        missing_from_cad=False, selection_status="nominal_tapped_flange_strength_unvalidated", purchase_unit=None,
        quantity_basis="16개 체결 인터페이스 검토 수; 별도 구매 너트 수가 아님",
        note="r02의 반대편 체결 미설정은 r03 관통탭 설계 의도로 변경. 플랜지 솔리드는 integral_knee_shafts에 이미 집계. 실제 탭 가공/나사산/완전 유효길이/토크/피로강도 검증은 미완.",
        procurement_note="별도 너트 구매행 아님. 맞춤 플랜지의 미완 검증 항목.")
    update("missing_motor_output_bolts",
        domestic_purchase_or_service_url="https://kr.misumi-ec.com/vona2/detail/110100141750/",
        note="12모터×6홀=72개 필요 위치 참조. HAA/HFE/KFE의 다른 스택에 공통 길이를 임의 적용하지 않음. 볼트 길이/SKU/간섭 미선정.")

    # Stable exact identities for unchanged retained geometry, explicit r03 overrides.
    knee_generic = {
        "Body2": "lower_link_backing_plates",
        "Body3": "root_crush_sleeves", "Body5": "root_crush_sleeves",
        "Body7": "root_crush_sleeves", "Body9": "root_crush_sleeves",
        "Body11": "root_drive_dowels", "Body12": "root_drive_dowels",
        "Body13": "knee_retention_nut_envelopes",
        "Body14": "foot_crush_sleeves", "Body17": "foot_crush_sleeves",
    }

    def classify(r: dict) -> str:
        c, b = r["component"], r["body"]
        if c.startswith("Long_profile_"):
            return "longitudinal_profiles"
        if c.startswith("Lower_cross_profile_"):
            return "lower_crossmembers"
        if c == "Chassis_motor_end_plates_5mm":
            return "motor_endplates"
        if c == "HBLFSN5_stock_corner_brackets":
            return "stock_corner_brackets"
        if c == "Upper_X_braces_2mm":
            return "top_x_straps"
        if c == "P2S_PETG_battery_tray":
            return "printed_battery_tray"
        if c == "Chassis_fasteners_and_Tnuts":
            if b.endswith("_HNTT5_5_drawing_reference"):
                return "profile_tnuts"
            if "_WSI_SUS_M5_" in b:
                return "standard_m5_washers"
            for prefix, key in (("HAA_stator_M3x8_", "haa_stator_screws"),
                                ("Profile_end_M5x16_", "profile_end_screws"),
                                ("Stock_bracket_CBM5x10_", "stock_bracket_screws"),
                                ("Top_brace_M5x8_", "lower_strap_screws"),
                                ("Top_brace_M5x10_", "raised_strap_screws"),
                                ("Tray_CB5x14_", "tray_screws")):
                if b.startswith(prefix):
                    return key
            raise RuntimeError("Unknown chassis hardware: " + b)
        if c.endswith("_knee_hardware"):
            if b.startswith("Knee_shoulder_shaft_M4_tapped_flange"):
                return "integral_knee_shafts"
            if b.startswith("Root_CB4_30_"):
                return "root_m4_screw_envelopes"
            if b.startswith("Foot_CB3_20_"):
                return "foot_m3_screw_envelopes"
            if b.startswith("Foot_M3_hex_nut_AF5p5_"):
                return "foot_m3_nut_envelopes"
            if b in knee_generic:
                return knee_generic[b]
            raise RuntimeError("Unknown knee body: " + c + "::" + b)
        if c.endswith("_foot_cup"):
            if b != "Foot_cup_PETG_M3_nut_pockets_r03":
                raise RuntimeError("Foot cup revision changed: " + b)
            return "printed_foot_cups"
        return retained_map[(c, b)]

    records = cad["body_records"]
    seen = set()
    for index, r in enumerate(records):
        identity = (r["component"], r["body"])
        if identity in seen:
            raise RuntimeError("Duplicate CAD body identity: " + str(identity))
        seen.add(identity)
        if not r["is_solid"] or r["volume_mm3"] <= 0:
            raise RuntimeError("Invalid CAD body: " + str(identity))
        groups[classify(r)]["body_records"].append({"record_index": index, **r})

    expected_counts = {g["id"]: g["native_body_count"] for g in old["groups"]}
    for key in ("gusset_and_lower_strap_screws", "top_strap_flat_washers",
                "lower_flat_gussets", "raised_strap_end_spacers"):
        del expected_counts[key]
    expected_counts.update(profile_tnuts=24, lower_strap_screws=2,
                           standard_m5_washers=12, stock_corner_brackets=8,
                           stock_bracket_screws=16)
    for key, g in groups.items():
        g["native_body_count"] = len(g["body_records"])
        if g["native_body_count"] != expected_counts[key]:
            raise RuntimeError(f"Group count differs: {key}: {g['native_body_count']} != {expected_counts[key]}")
        if g["native_body_count"] and key not in ("rs06_motor_assemblies", "battery_reserved", "computer_reserved", "power_board_reserved"):
            if g["installed_quantity_reference"] != g["native_body_count"]:
                raise RuntimeError("Installed count mismatch: " + key)
    if len(records) != cad["body_count"] or len(records) != 286:
        raise RuntimeError("Native revision body count differs from audited 286-body contract")
    scopes = Counter()
    for g in groups.values():
        scopes[g["scope"]] += g["native_body_count"]
    if scopes["r03_chassis"] != 119 or scopes["r03_leg_and_reserved"] != 167:
        raise RuntimeError("Chassis/leg decomposition differs")

    source_paths = [
        "v2_robot/rs06_r02/audit_inventory.json",
        PREFIX + "build_bom.py", PREFIX + "fusion_build.py", PREFIX + "parameters.json",
        PREFIX + "cad/cad_checks.json", PREFIX + "research/stock_brackets.json",
        PREFIX + "research/tooling_standard.json", "v2_leg/link_candidates/fusion_build.py",
        "v2_robot/research/rs06_support_parts.json",
        "v2_robot/research/domestic_motor_sellers.json",
    ]
    frame_screw_ids = ["haa_stator_screws", "profile_end_screws", "lower_strap_screws",
                       "raised_strap_screws", "tray_screws", "stock_bracket_screws"]
    frame_screws = sum(groups[k]["native_body_count"] for k in frame_screw_ids)
    assert frame_screws == 64
    ordered = list(groups.values())
    audit = {
        "revision": cad["revision"], "checked_on": "2026-10-28",
        "scope": "All actual r03 native solids mapped exactly once to procurement/design references; no CAD changed by this audit.",
        "summary": {
            "native_body_count": len(records), "mapped_native_bodies": len(seen),
            "unmapped_native_bodies": 0, "duplicate_native_bodies": 0,
            "r03_chassis_bodies": scopes["r03_chassis"],
            "r03_leg_and_reserved_bodies": scopes["r03_leg_and_reserved"],
            "bom_rows": len(ordered), "motor_complete_units": 12, "motor_reference_solids": 36,
            "reserved_electronics_solids": 3, "chassis_modeled_screws": frame_screws,
            "root_modeled_screws": 16, "foot_modeled_screws": 8, "total_modeled_screw_solids": 88,
            "stock_bracket_kits": 8, "stock_bracket_body_solids": 8,
            "frame_tnut_solids": 24, "standard_flat_washer_solids": 12,
            "unmodeled_stator_screws_reference": 64, "unmodeled_output_screws_reference": 72,
            "supplier_cad_imported": False, "production_ready": False,
        },
        "limitations": [
            "286 CAD solids are not 286 purchased components: 12 motors occupy 36 reference solids and three electronics bodies are reserved spaces.",
            "HBLFSN5-SET 8 sets include 16 CBM5-10 and 16 HNTT5-5; component rows must not be added again as independent purchases.",
            "Supplier STEP was not imported. Drawing-derived brackets/profile/T nuts retain undimensioned approximations; threads are not modeled.",
            "Quantities are installed design needs or interface counts, not sale pack counts. Unknown products, quantities, prices, stocks and lead times stay unconfirmed.",
            "Custom carriers, adapters, shafts and sleeves are not claimed to be stock SKUs; machining service links do not release manufacturing drawings.",
            "64 HFE/KFE stator screws and 72 motor output screws remain unmodeled; root nominal M4 tapped flange is represented but complete thread engagement/strength is unvalidated.",
            "Assembly screw drive sizes 2.5/3/4 mm do not resolve knee retention tools, fabrication tools, electrical tools or tightening torques.",
            "This inventory audit does not replace native interference/tool-access verification or structural/physical testing.",
        ],
        "r02_to_r03_body_delta": {
            "before": 268, "after": 286, "net": 18,
            "stock_brackets_replace_flat_gussets": 4,
            "bracket_fastener_count_increase": 4, "tnut_count_increase": 4,
            "washers_replace_custom_spacers_and_add_tray_washers": 6,
            "leg_body_count_change": 0,
        },
        "procurement_bundle_reconciliation": {
            "HBLFSN5_SET": {"sets": 8, "native_body_solids": 8,
                           "included_CBM5_10": 16, "included_HNTT5_5": 16},
            "HNTT5_5": {"installed": 24, "included_in_sets": 16, "separate_required": 8},
            "NATTO_3_5p5_2p4": {"installed": 8, "seller_pack_quantity": 10,
                                  "minimum_packs_by_count": 1, "ordered": False},
        },
        "root_thread_closure_status": {
            "r02": "opposing nut/tap not selected",
            "r03": "nominal M4 through tap in 5 mm custom steel flange; no separate nut",
            "interfaces": 16, "thread_geometry_modeled": False,
            "nominal_plate_thickness_mm": 5, "effective_complete_thread_length_mm": None,
            "thread_strength_validated": False,
        },
        "standard_pin_candidate_not_adopted": dowel_candidate,
        "source_sha256": {p: sha(ROOT / p) for p in source_paths},
        "cad_source_consistency": {"builder_hash_matches": True, "parameters_hash_matches": True},
        "mapping_method": "Exact component/body identity; explicit r03 hardware names and generic knee-body IDs cross-checked against current lower_hardware creation order. Every expected group count asserted.",
        "groups": ordered,
    }
    fields = [
        "id", "name_ko", "scope", "installed_quantity_reference", "quantity_basis", "native_body_count",
        "model", "dimensions_and_threads_mm", "material", "domestic_seller", "domestic_purchase_or_service_url",
        "selection_status", "supplier_cad_imported", "supplier_fit_qualified", "manufacturing_drawing_released",
        "missing_from_cad", "price_status", "stock_status", "lead_time_status", "source", "note", "actual_body_refs",
        "driver_hex_mm", "purchase_unit", "pack_quantity", "separate_purchase_quantity_reference", "procurement_note",
        "candidate_domestic_url",
    ]
    with (HERE / "assembly_bom.csv").open("w", encoding="utf-8-sig", newline="") as f:
        writer = csv.DictWriter(f, fields, extrasaction="ignore")
        writer.writeheader()
        for g in ordered:
            row = dict(g)
            row["actual_body_refs"] = " | ".join(r["component"] + "::" + r["body"] for r in g["body_records"])
            writer.writerow(row)
    (HERE / "audit_inventory.json").write_text(json.dumps(audit, ensure_ascii=False, indent=2) + "\n")
    return audit["summary"]


if __name__ == "__main__":
    print(json.dumps(build(), ensure_ascii=False, indent=2))
