"""String-leaf object example: unambiguous row-relative JSON Pointer headers.
python pointer-columns-to-csv.py path-collision.json new-output.csv new-map.json
"""
import csv,json,sys
from pathlib import Path

def unique_object(pairs):
    result={}
    for key,value in pairs:
        if key in result:raise ValueError('duplicate source key: '+repr(key))
        result[key]=value
    return result

def leaves(obj,path=()):
    if not isinstance(obj,dict) or not obj:raise ValueError('nonempty objects required')
    result={}
    for key,value in obj.items():
        child=path+(key,)
        if isinstance(value,dict):result.update(leaves(value,child))
        elif isinstance(value,str):result[child]=value
        else:raise ValueError('this example supports string leaves only')
    return result

def pointer(path):
    return ''.join('/'+token.replace('~','~0').replace('/','~1') for token in path)

def export(source,target,map_target):
    if Path(target).exists() or Path(map_target).exists():raise FileExistsError('use new filenames')
    values=json.loads(Path(source).read_text(encoding='utf-8'),object_pairs_hook=unique_object)
    if not isinstance(values,list) or not values:raise ValueError('nonempty array required')
    flattened=[leaves(row) for row in values]
    paths=sorted(flattened[0])
    if any(set(row)!=set(paths) for row in flattened):raise ValueError('same leaf paths required in every row')
    headers=[pointer(path) for path in paths]
    if len(headers)!=len(set(headers)):raise ValueError('header collision')
    mapping=[{'header':header,'segments':list(path)} for header,path in zip(headers,paths)]
    with Path(target).open('x',encoding='utf-8',newline='') as file:
        writer=csv.writer(file);writer.writerow(headers)
        writer.writerows([[row[path] for path in paths] for row in flattened])
    Path(map_target).write_text(json.dumps(mapping,ensure_ascii=False,indent=2),encoding='utf-8')
    with Path(target).open(encoding='utf-8',newline='') as file:parsed=list(csv.reader(file))
    expected=[headers]+[[row[path] for path in paths] for row in flattened]
    if parsed!=expected:raise ValueError('parsed CSV differs from source mapping')
    dot_names={};collisions=[]
    for path in paths:
        name='.'.join(path)
        if name in dot_names:collisions.append({'dot_header':name,'first':list(dot_names[name]),'second':list(path)})
        else:dot_names[name]=path
    return {'records':len(parsed)-1,'columns':len(headers),'mapping':mapping,'parsed_csv':parsed,
            'dot_join_collisions':collisions,'all_string_leaf_values_match':True}

if __name__=='__main__':print(json.dumps(export(*sys.argv[1:4]),ensure_ascii=False))
