#!/usr/bin/env python3
"""RC46 selective movement rebuild.

Purpose:
- Stop trusting old sheet/pose names as exercise truth.
- Apply the first batch of AI-physio movement decisions to sequence runtime data.
- Selectively activate MediaPipe runtime landmarks only where confidence and movement risk allow it.
- Keep risky/special exercises gated with explicit review metadata rather than pretending they are solved.
"""
from __future__ import annotations
import json
from copy import deepcopy
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / 'assets' / 'data' / 'image-admin'
SEQ_PATH = DATA / 'pose-atlas-exercise-sequences.json'
PACKS_PATH = DATA / 'pose-atlas-packs.json'
AUDIT_PATH = DATA / 'rc45_ai_physio_first_pass.json'
OUT_REPORT = ROOT / 'qa_screenshots' / 'rc46_selective_movement_rebuild_report.json'
OUT_DATA = DATA / 'rc46_selective_movement_rebuild.json'
BACKUP_SEQ = DATA / 'pose-atlas-exercise-sequences.rc45-before-rc46.backup.json'
BACKUP_PACKS = DATA / 'pose-atlas-packs.rc45-before-rc46.backup.json'

RC46_VERSION = 'v97-rc46-selective-movement-rebuild'
NOW = datetime.now(timezone.utc).isoformat(timespec='seconds')


def load_json(path: Path):
    return json.loads(path.read_text(encoding='utf-8'))


def save_json(path: Path, data):
    path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')


def limb(anchor: str, *, role: str, confidence: float = 0.99, source: str = 'rc46-ai-physio-first-pass') -> dict:
    return {
        'schema': 'rc46-movement-intent-v1',
        'primaryAnchor': anchor,
        'anchorSource': source,
        'movementRole': role,
        'confidence': confidence,
        'linkedToSpriteLandmarks': True,
        'coordinateSpace': 'sprite-percent-unflipped',
        'runtimeTransformPolicy': 'skeleton and sprite share the same anchor + flip transform',
        'note': 'Exercise truth and limb binding are defined by movement standard, not by atlas pose/sheet name.'
    }


def tag_sequence(seq: dict, status: str, decision: str, *, risk: str = 'moderate', runtime_gate: str = 'selective-active'):
    seq['rc46MovementRebuild'] = {
        'version': RC46_VERSION,
        'status': status,
        'decision': decision,
        'riskTier': risk,
        'source': 'rc45_ai_physio_first_pass.json',
        'poseNameAuthority': False,
        'exerciseTruthAuthority': 'movement standard + reviewed skeleton anchors',
        'updatedAt': NOW,
        'runtimeGate': runtime_gate
    }
    return seq


def rewrite_steps(seq: dict, step_specs: list[dict]):
    old = seq.get('poses', [])
    out = []
    for i, spec in enumerate(step_specs):
        base = deepcopy(old[spec.get('oldIndex', i)] if spec.get('oldIndex', i) < len(old) else old[-1])
        base.update({k: v for k, v in spec.items() if k != 'oldIndex'})
        base['limbIntent'] = limb(base.get('anchorLandmark', 'footCenter'), role=base.get('rc46Role', base.get('phase','keyframe')))
        base['rc46Frame'] = {
            'order': i + 1,
            'phase': base.get('phase'),
            'contact': base.get('contact'),
            'anchorLandmark': base.get('anchorLandmark'),
            'physioIntent': base.get('rc46Role', ''),
            'reviewNote': base.get('rc46ReviewNote', '')
        }
        out.append(base)
    seq['poses'] = out
    seq['bindingSchema'] = 'rc46-movement-intent-v1'
    return seq


def rebuild_sequences(sequences: dict):
    changes = []

    # 1) Critical bug test case: lateral line hops.
    # Old mapping locked left/right foot independently. RC46 makes the foot-pair the truth.
    if 'lateral-line-hops' in sequences:
        s = sequences['lateral-line-hops']
        rewrite_steps(s, [
            {'oldIndex': 0, 'phase': 'ready-left-foot-pair', 'pathT': 0.00, 'contact': 'grounded', 'anchorLandmark': 'footCenter', 'rc46Role': 'bilateral foot pair locked on left side of line', 'rc46ReviewNote': 'Feet move as a pair; do not choose a random single foot.'},
            {'oldIndex': 1, 'phase': 'takeoff-foot-pair', 'pathT': 0.18, 'contact': 'takeoff', 'anchorLandmark': 'footCenter', 'rc46Role': 'two-foot takeoff from line side', 'rc46ReviewNote': 'Skeletal feet and sprite feet stay linked as a pair.'},
            {'oldIndex': 2, 'phase': 'airborne-foot-pair', 'pathT': 0.50, 'contact': 'airborne', 'anchorLandmark': 'footCenter', 'rc46Role': 'compact airborne hop across line', 'rc46ReviewNote': 'No left/right foot swap allowed in mid-air.'},
            {'oldIndex': 3, 'phase': 'land-right-foot-pair', 'pathT': 0.82, 'contact': 'land/reset', 'anchorLandmark': 'footCenter', 'rc46Role': 'two-foot landing on other side', 'rc46ReviewNote': 'Land through the foot pair, not through visual right foot only.'},
            {'oldIndex': 4, 'phase': 'reset-right-foot-pair', 'pathT': 1.00, 'contact': 'grounded', 'anchorLandmark': 'footCenter', 'rc46Role': 'stable reset on other side', 'rc46ReviewNote': 'End state must still keep foot-pair identity.'},
        ])
        s['motionProfile'].update({'family': 'jump_arc_lateral', 'anchor': 'foot-pair-lock', 'x0': 42, 'x1': 58, 'arcHeight': 10, 'settle': 1.6, 'loopMs': 1500, 'direction': 'right', 'ghostCount': 0, 'ghostMode': 'none', 'debug': True})
        s['layout'].update({'floorY': 84, 'baseStandingHeightPct': 50})
        tag_sequence(s, 'selective-rebuilt-active', 'Foot-pair anchor replaces old single-foot lock. This is the first real movement-truth rebuild.', risk='moderate')
        changes.append({'exerciseKey': 'lateral-line-hops', 'action': 'rebuilt foot-pair anchor sequence', 'runtimeImpact': 'active sequence data changed'})

    # 2) Bar work: lock bar/hands and treat as keyframes, not free animation.
    for key, label in [('pull-up','Pull-up'), ('chin-up','Chin-up')]:
        if key in sequences:
            s = sequences[key]
            phases = ['dead-hang', 'active-hang', 'mid-pull', 'top-hold', 'controlled-lower']
            pts = [0.00, 0.18, 0.46, 0.68, 1.00]
            specs = []
            for i, (ph, t) in enumerate(zip(phases, pts)):
                specs.append({'oldIndex': i, 'phase': ph, 'pathT': t, 'contact': 'bar-anchor', 'facing': 'front', 'anchorLandmark': 'barTop', 'rc46Role': 'hands locked to bar / body changes by keyframe only', 'rc46ReviewNote': 'Do not animate free body travel; source sprite/bar are top-pinned.'})
            rewrite_steps(s, specs)
            s['motionProfile'].update({'family': 'fixed_bar_sequence', 'anchor': 'bar-hand-lock', 'x0': 50, 'x1': 50, 'barY': 3, 'loopMs': 2800, 'direction': 'neutral', 'flipX': False, 'forceFlipX': False, 'ghostCount': 0, 'ghostMode': 'none', 'debug': True})
            tag_sequence(s, 'special-handling-active', f'{label}: locked bar keyframe sequence. No free glide.', risk='moderate-low')
            changes.append({'exerciseKey': key, 'action': 'bar/hands locked keyframe sequence', 'runtimeImpact': 'active sequence data tightened'})

    # 3) Buddy hamstring curl: lock knee pivot, do not trust MediaPipe candidate for low-conf partner sprites.
    if 'buddy-hamstring-curls' in sequences:
        s = sequences['buddy-hamstring-curls']
        rewrite_steps(s, [
            {'oldIndex': 0, 'phase': 'kneel-tall-knee-pivot', 'pathT': 0.00, 'contact': 'fixed-pivot', 'facing': 'neutral', 'anchorLandmark': 'kneeCenter', 'rc46Role': 'knees fixed / hips tall', 'rc46ReviewNote': 'Partner/anchor not MediaPipe-driven.'},
            {'oldIndex': 1, 'phase': 'eccentric-lower', 'pathT': 0.34, 'contact': 'fixed-pivot', 'facing': 'neutral', 'anchorLandmark': 'kneeCenter', 'rc46Role': 'controlled lower around knee pivot', 'rc46ReviewNote': 'Do not hinge from hips like a good morning.'},
            {'oldIndex': 2, 'phase': 'lowest-controlled', 'pathT': 0.68, 'contact': 'fixed-pivot', 'facing': 'neutral', 'anchorLandmark': 'kneeCenter', 'rc46Role': 'straight line knee-hip-shoulder as far as controlled', 'rc46ReviewNote': 'Knee pivot remains fixed.'},
            {'oldIndex': 3, 'phase': 'return-knee-pivot', 'pathT': 1.00, 'contact': 'fixed-pivot', 'facing': 'neutral', 'anchorLandmark': 'kneeCenter', 'rc46Role': 'return via hamstrings / same knee anchor', 'rc46ReviewNote': 'Use alpha/manual knee lock; MediaPipe candidate is too weak here.'},
        ])
        s['motionProfile'].update({'family': 'fixed_knee_pivot_sequence', 'anchor': 'knee-pivot-lock', 'x0': 50, 'x1': 50, 'loopMs': 3300, 'direction': 'neutral', 'flipX': False, 'forceFlipX': False, 'ghostCount': 0, 'ghostMode': 'none', 'debug': True})
        tag_sequence(s, 'special-handling-active-manual-landmarks', 'Knee pivot is authoritative; MediaPipe candidate remains disabled because partner/floor sprites scored low.', risk='moderate')
        changes.append({'exerciseKey': 'buddy-hamstring-curls', 'action': 'manual knee pivot lock tightened', 'runtimeImpact': 'active sequence data tightened'})

    # 4) Box work: prop-aware metadata; do not activate pose 07 MediaPipe because confidence is poor.
    for key in ['seated-box-jump', 'box-jump']:
        if key in sequences:
            s = sequences[key]
            phases = ['box-start', 'load', 'airborne', 'box-landing'] if key == 'seated-box-jump' else ['floor-ready', 'takeoff', 'airborne', 'box-landing']
            pts = [0.00, 0.18, 0.52, 0.88]
            contacts = ['grounded', 'takeoff', 'airborne', 'land/reset']
            specs = []
            for i in range(min(4, len(s.get('poses',[])))):
                specs.append({'oldIndex': i, 'phase': phases[i], 'pathT': pts[i], 'contact': contacts[i], 'anchorLandmark': 'footCenter', 'rc46Role': 'foot pair + box prop relationship', 'rc46ReviewNote': 'Box prop must remain visually consistent; low confidence frame kept manual.'})
            rewrite_steps(s, specs)
            s['motionProfile'].update({'family': 'jump_arc_forward', 'anchor': 'box-foot-pair', 'x0': 32 if key == 'seated-box-jump' else 30, 'x1': 62, 'arcHeight': 13 if key == 'seated-box-jump' else 15, 'settle': 1.5, 'loopMs': 1750 if key == 'seated-box-jump' else 1600, 'ghostCount': 0, 'ghostMode': 'none', 'debug': True})
            tag_sequence(s, 'prop-aware-active-manual-review', 'Box jump phases are explicit, but prop/box binding still needs visual review before MediaPipe activation.', risk='moderate-high', runtime_gate='active-with-manual-prop-review')
            changes.append({'exerciseKey': key, 'action': 'box prop phases/foot-pair anchors added; MediaPipe not activated for weak frame', 'runtimeImpact': 'active sequence data tightened'})

    # 5) Medball slam stays active but receives object/hand binding requirements.
    if 'med-ball-slam' in sequences:
        s = sequences['med-ball-slam']
        for i, p in enumerate(s.get('poses', [])):
            p['anchorLandmark'] = 'footCenter'
            p['contact'] = p.get('contact') or 'grounded'
            p['phase'] = p.get('phase') or f'slam-kf-{i+1:02d}'
            p['limbIntent'] = limb('footCenter', role='feet planted while hands/ball path changes')
            p['rc46Frame'] = {'order': i+1, 'phase': p['phase'], 'contact': 'grounded', 'anchorLandmark': 'footCenter', 'physioIntent': 'ball must stay associated with hands during slam/pickup'}
        s['motionProfile'].update({'family': 'hinge_slam_lift', 'anchor': 'feet-with-ball-hand-binding', 'x0': 50, 'x1': 50, 'loopMs': 2250, 'ghostCount': 0, 'ghostMode': 'none', 'debug': True})
        tag_sequence(s, 'object-binding-active-review', 'Medball slam can stay active, but ball must be visually checked against hand path.', risk='moderate')
        changes.append({'exerciseKey': 'med-ball-slam', 'action': 'object/hand binding metadata added', 'runtimeImpact': 'active sequence data annotated'})

    # 6) Chest pass and rotational throw must not pretend medball-slam frames are valid.
    for key in ['chest-pass', 'rotational-throw']:
        if key in sequences:
            s = sequences[key]
            s['rc46DoNotTrustCurrentImages'] = True
            s['rc46ReviewHold'] = {
                'reason': 'Current mapping reuses medball-slam candidates. Movement standard requires a different throw pattern.',
                'needed': 'true chest/rotation sequence with ball prop path and hand release/receive phases',
                'runtimeRecommendation': 'do not activate MediaPipe candidate; use as placeholder only until new candidate sequence exists'
            }
            for i, p in enumerate(s.get('poses', [])):
                p['anchorLandmark'] = 'footCenter'
                p['limbIntent'] = limb('footCenter', role='placeholder only / not exercise truth', confidence=0.25, source='rc46-review-hold')
                p['rc46Frame'] = {'order': i+1, 'phase': p.get('phase') or f'placeholder-{i+1:02d}', 'contact': p.get('contact') or 'grounded', 'anchorLandmark': 'footCenter', 'physioIntent': 'review hold: wrong movement family'}
            tag_sequence(s, 'blocked-needs-new-sequence', 'Hold from selective activation. Current image sequence is not the exercise.', risk='high', runtime_gate='review-hold')
            changes.append({'exerciseKey': key, 'action': 'blocked from selective activation; current images marked placeholder', 'runtimeImpact': 'metadata only, active runtime still shows placeholder if used'})

    # 7) Footwork split: carioca and lateral power shuffle should not share a generic foot rule.
    if 'carioca-shuffle' in sequences:
        s = sequences['carioca-shuffle']
        phases = ['ready', 'front-crossover', 'open-step', 'rear-crossover', 'reset']
        for i, p in enumerate(s.get('poses', [])):
            p['phase'] = phases[i] if i < len(phases) else f'carioca-{i+1}'
            p['pathT'] = i / max(1, len(s.get('poses', []))-1)
            p['contact'] = 'grounded'
            p['anchorLandmark'] = 'dominantContactFoot'
            p['limbIntent'] = limb('dominantContactFoot', role='crossover gait foot contact')
        tag_sequence(s, 'manual-review-annotated', 'Carioca keeps crossover foot logic; do not merge with lateral shuffle.', risk='moderate', runtime_gate='active-with-footwork-review')
        changes.append({'exerciseKey': 'carioca-shuffle', 'action': 'crossover phases annotated', 'runtimeImpact': 'active sequence metadata tightened'})

    if 'lateral-power-shuffle' in sequences:
        s = sequences['lateral-power-shuffle']
        for i, p in enumerate(s.get('poses', [])):
            p['phase'] = ['athletic-ready','push','slide','catch','push-return','reset'][i] if i < 6 else f'shuffle-{i+1}'
            p['pathT'] = i / max(1, len(s.get('poses', []))-1)
            p['contact'] = 'grounded'
            p['anchorLandmark'] = 'footCenter'
            p['limbIntent'] = limb('footCenter', role='non-crossover lateral foot pair / base of support')
        tag_sequence(s, 'manual-review-annotated', 'Lateral power shuffle uses non-crossover foot-pair/base-of-support logic.', risk='moderate', runtime_gate='active-with-footwork-review')
        changes.append({'exerciseKey': 'lateral-power-shuffle', 'action': 'non-crossover foot-pair phases annotated', 'runtimeImpact': 'active sequence metadata tightened'})

    # 8) Mountain climber active leg semantics, but no broad MediaPipe activation yet.
    if 'mountain-climber' in sequences:
        s = sequences['mountain-climber']
        phases = ['plank-support-left-leg-drive', 'switch', 'plank-support-right-leg-drive']
        for i, p in enumerate(s.get('poses', [])):
            p['phase'] = phases[i] if i < len(phases) else f'mountain-climber-{i+1}'
            p['pathT'] = [0, .5, 1][i] if i < 3 else i / max(1, len(s.get('poses', []))-1)
            p['contact'] = 'hands/feet-ground'
            p['anchorLandmark'] = 'supportCenter'
            p['limbIntent'] = limb('supportCenter', role='hands/feet support with explicit alternating active leg')
        tag_sequence(s, 'needs-leg-phase-QA-annotated', 'Active-leg semantics added; still needs visual QA before candidate activation.', risk='moderate-high', runtime_gate='active-with-leg-review')
        changes.append({'exerciseKey': 'mountain-climber', 'action': 'active leg phases annotated', 'runtimeImpact': 'active sequence metadata tightened'})

    return changes


def min_conf(candidate: dict, names: list[str]) -> float:
    vals = []
    for n in names:
        v = candidate.get(n)
        if not isinstance(v, dict):
            return 0.0
        vals.append(float(v.get('confidence', v.get('visibility', 0)) or 0))
    return min(vals) if vals else 0.0


def activate_mediapipe_landmarks(packs: dict, sequences: dict):
    # Only activate in exercises where the movement audit says geometry can help now.
    activation_rules = {
        'lateral-line-hops': {'threshold': 0.68, 'required': ['leftFoot','rightFoot','footCenter','hipCenter','kneeCenter','shoulderCenter']},
        'bodyweight-squat': {'threshold': 0.82, 'required': ['leftFoot','rightFoot','footCenter','hipCenter','kneeCenter','shoulderCenter']},
        'hamstring-bridge': {'threshold': 0.66, 'required': ['leftFoot','rightFoot','footCenter','hipCenter','shoulderCenter']},
        'pull-up': {'threshold': 0.64, 'required': ['leftHand','rightHand','handCenter','shoulderCenter','hipCenter']},
        'chin-up': {'threshold': 0.64, 'required': ['leftHand','rightHand','handCenter','shoulderCenter','hipCenter']},
    }
    selected_pose_sources: dict[str, set[str]] = {}
    for ex, rule in activation_rules.items():
        seq = sequences.get(ex, {})
        for p in seq.get('poses', []):
            selected_pose_sources.setdefault(p.get('poseKey'), set()).add(ex)

    activated = []
    skipped = []
    for entry in packs.get('poses', []):
        pose_key = entry.get('poseKey')
        if pose_key not in selected_pose_sources:
            continue
        exs = sorted(selected_pose_sources[pose_key])
        # Use the lowest applicable threshold when a pose is shared by multiple safe exercises.
        thresholds = [activation_rules[e]['threshold'] for e in exs if e in activation_rules]
        required_names = sorted(set(sum((activation_rules[e]['required'] for e in exs if e in activation_rules), [])))
        threshold = min(thresholds) if thresholds else 1.0
        for aid, athlete in (entry.get('athletes') or {}).items():
            sk = (((athlete or {}).get('metrics') or {}).get('skeleton') or {})
            cand = sk.get('runtimeLandmarksMediapipeCandidate')
            if not cand:
                skipped.append({'poseKey': pose_key, 'athleteId': aid, 'reason': 'no-mediapipe-candidate'})
                continue
            score = min_conf(cand, required_names)
            if score >= threshold:
                if 'runtimeLandmarksAlphaFallback' not in sk:
                    sk['runtimeLandmarksAlphaFallback'] = deepcopy(sk.get('runtimeLandmarks', {}))
                next_lm = deepcopy(cand)
                # Preserve barTop if the alpha/manual data has it; MediaPipe cannot see the bar.
                old_lm = sk.get('runtimeLandmarks', {}) or {}
                for extra in ['barTop']:
                    if extra in old_lm and extra not in next_lm:
                        next_lm[extra] = deepcopy(old_lm[extra])
                for lm in next_lm.values():
                    if isinstance(lm, dict):
                        lm['activatedBy'] = RC46_VERSION
                sk['runtimeLandmarks'] = next_lm
                sk['runtimeLandmarkSource'] = 'mediapipe-rc46-selective-active'
                sk['quality'] = 'mediapipe-selected-active-with-alpha-fallback'
                sk.setdefault('mediaPipe', {})['runtimeActivatedAt'] = NOW
                sk.setdefault('mediaPipe', {})['runtimeActivationPolicy'] = 'selective safe exercises only; special/low confidence motions stay manual'
                activated.append({'poseKey': pose_key, 'athleteId': aid, 'exercises': exs, 'minRequiredConfidence': round(score, 3), 'threshold': threshold})
            else:
                skipped.append({'poseKey': pose_key, 'athleteId': aid, 'exercises': exs, 'minRequiredConfidence': round(score, 3), 'threshold': threshold, 'reason': 'below-threshold'})
    packs['version'] = RC46_VERSION
    packs['mediaPipeBridge'] = packs.get('mediaPipeBridge', {}) | {
        'rc46SelectiveRuntimeActivation': {
            'status': 'partial-active',
            'activatedCount': len(activated),
            'skippedCount': len(skipped),
            'policy': 'Only high-confidence, low/moderate-risk exercise poses use MediaPipe runtime landmarks. Special props, partner frames, and weak detections remain alpha/manual.',
            'updatedAt': NOW
        }
    }
    return activated, skipped


def main():
    seq_data = load_json(SEQ_PATH)
    packs = load_json(PACKS_PATH)
    audit = load_json(AUDIT_PATH)
    if not BACKUP_SEQ.exists(): save_json(BACKUP_SEQ, seq_data)
    if not BACKUP_PACKS.exists(): save_json(BACKUP_PACKS, packs)

    sequences = seq_data.get('sequences', {})
    changes = rebuild_sequences(sequences)
    activated, skipped = activate_mediapipe_landmarks(packs, sequences)

    seq_data['version'] = RC46_VERSION
    seq_data['rc46SelectiveMovementRebuild'] = {
        'version': RC46_VERSION,
        'createdAt': NOW,
        'sourceAuditVersion': audit.get('version'),
        'runtimePolicy': {
            'poseNamesAuthoritative': False,
            'exerciseTruthLayer': 'rc45_ai_physio_first_pass + rc46 movement rules',
            'specialExercisesNotAutoActivated': ['buddy-hamstring-curls','box-jump','seated-box-jump','chest-pass','rotational-throw'],
            'selectiveMediaPipeRuntimeActivation': True,
            'safeActivationExercises': ['lateral-line-hops','bodyweight-squat','hamstring-bridge','pull-up','chin-up']
        }
    }
    seq_data['limbIntentSchema'] = 'rc46-movement-intent-v1'

    report = {
        'version': RC46_VERSION,
        'createdAt': NOW,
        'summary': {
            'sequenceChanges': len(changes),
            'mediapipeRuntimeLandmarkActivations': len(activated),
            'mediapipeRuntimeLandmarkSkipped': len(skipped),
            'runtimeChanged': True,
            'imagesGenerated': False,
            'principle': 'movement truth over pose/sheet names'
        },
        'changes': changes,
        'activatedMediapipeRuntimeLandmarks': activated,
        'skippedMediapipeRuntimeLandmarks': skipped[:200],
        'notes': [
            'Lateral-line-hops is the first real rebuild: foot-pair anchor replaces left/right single foot locking.',
            'Bar exercises are fixed keyframes with barTop lock; no free glide.',
            'Buddy hamstring curl stays manual/knee-pivot because MediaPipe confidence is poor on partner frames.',
            'Chest-pass and rotational-throw are marked as wrong-family placeholders and blocked from selective activation.',
            'Box exercises received explicit prop/phase data but are not MediaPipe-activated due weak frame confidence.'
        ]
    }
    movement_data = {
        'version': RC46_VERSION,
        'createdAt': NOW,
        'status': 'selective runtime rebuild + review gates',
        'report': report['summary'],
        'exerciseDecisions': {c['exerciseKey']: c for c in changes},
        'activationPolicy': seq_data['rc46SelectiveMovementRebuild']['runtimePolicy']
    }
    save_json(SEQ_PATH, seq_data)
    save_json(PACKS_PATH, packs)
    save_json(OUT_REPORT, report)
    save_json(OUT_DATA, movement_data)
    print(json.dumps(report['summary'], indent=2, ensure_ascii=False))

if __name__ == '__main__':
    main()
