# Comment Restoration from Original File

When comments are lost during docx editing (e.g., paragraph clear operations that remove commentRangeStart/End/Reference elements), restore them from the original file.

## Scenario
- Original file has N comments (e.g., Alice×2, 法务, 杜律 = 4 comments, ids 0-3)
- Edited file lost some/all original comments and may have added new ones (e.g., 华诚-Z comment id=0, Alice id=2)
- Goal: merge all comments — original ones preserved + new ones added, with non-conflicting IDs

## Recovery Technique

### Step 1: Extract original comments
```python
WNS = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'

z_orig = zipfile.ZipFile('original.docx')
with z_orig.open('word/comments.xml') as f:
    ctree_orig = etree.parse(f)
orig_comments = []
for c in ctree_orig.getroot().findall(f'{WNS}comment'):
    orig_comments.append({
        'id': c.get(f'{WNS}id'),
        'author': c.get(f'{WNS}author'),
        'date': c.get(f'{WNS}date'),
        'text': ''.join(t.text for t in c.iter(f'{WNS}t') if t.text),
        'element': copy.deepcopy(c)
    })
z_orig.close()
```

### Step 2: Identify which comments survived in the edited file
```python
z_edit = zipfile.ZipFile('edited.docx')
with z_edit.open('word/comments.xml') as f:
    ctree_edit = etree.parse(f)
edit_comment_ids = set()
for c in ctree_edit.getroot().findall(f'{WNS}comment'):
    edit_comment_ids.add(c.get(f'{WNS}id'))
```

### Step 3: Find new comments (non-original authors)
```python
new_comments = []
for c in ctree_edit.getroot().findall(f'{WNS}comment'):
    if c.get(f'{WNS}author') not in [oc['author'] for oc in orig_comments]:
        new_comments.append({
            'old_id': c.get(f'{WNS}id'),
            'author': c.get(f'{WNS}author'),
            'element': copy.deepcopy(c)
        })
```

### Step 4: Rebuild comments.xml with all comments
Assign non-conflicting IDs:
- Original comments keep their original IDs (0, 1, 2, 3)
- New comments get IDs starting from max(original_ids) + 1

```python
new_comments_xml = etree.Element(f'{WNS}comments')
new_comments_xml.set('xmlns:w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main')
# ... add other namespaces as needed

max_id = max(int(oc['id']) for oc in orig_comments)

# Add original comments
for oc in orig_comments:
    new_comments_xml.append(oc['element'])

# Add new comments with renumbered IDs
for nc in new_comments:
    max_id += 1
    nc['new_id'] = str(max_id)
    nc['element'].set(f'{WNS}id', nc['new_id'])
    new_comments_xml.append(nc['element'])
```

### Step 5: Update document.xml comment references
For each new comment, find its commentRangeStart, commentRangeEnd, and commentReference in document.xml and update the ID from old to new:

```python
for nc in new_comments:
    old_id = nc['old_id']
    new_id = nc['new_id']
    
    # Update commentRangeStart
    for elem in root.iter(f'{WNS}commentRangeStart'):
        if elem.get(f'{WNS}id') == old_id:
            elem.set(f'{WNS}id', new_id)
    
    # Update commentRangeEnd
    for elem in root.iter(f'{WNS}commentRangeEnd'):
        if elem.get(f'{WNS}id') == old_id:
            elem.set(f'{WNS}id', new_id)
    
    # Update commentReference (inside w:r)
    for elem in root.iter(f'{WNS}commentReference'):
        if elem.get(f'{WNS}id') == old_id:
            elem.set(f'{WNS}id', new_id)
```

### Step 6: Write back to docx
```python
z_out = zipfile.ZipFile('output.docx', 'w')
# Copy all files from edited.docx except comments.xml and document.xml
for item in z_edit.namelist():
    if item not in ('word/comments.xml', 'word/document.xml'):
        z_out.writestr(item, z_edit.read(item))

# Write updated comments.xml
z_out.writestr('word/comments.xml', 
    etree.tostring(new_comments_xml, encoding='UTF-8', xml_declaration=True, standalone=True))

# Write updated document.xml
z_out.writestr('word/document.xml',
    etree.tostring(tree, encoding='UTF-8', xml_declaration=True, standalone=True))

z_edit.close()
z_out.close()
```

## Verification
```python
z = zipfile.ZipFile('output.docx')
with z.open('word/comments.xml') as f:
    ctree = etree.parse(f)
for c in ctree.getroot().findall(f'{WNS}comment'):
    print(f"  id={c.get(f'{WNS}id')} author={c.get(f'{WNS}author')}: {text[:80]}")

# Check all IDs referenced in document.xml exist in comments.xml
content = z.read('word/document.xml').decode('utf-8')
doc_ids = set(re.findall(r'commentRangeStart[^>]*w:id="(\d+)"', content))
doc_ids |= set(re.findall(r'commentRangeEnd[^>]*w:id="(\d+)"', content))
doc_ids |= set(re.findall(r'commentReference[^>]*w:id="(\d+)"', content))
comment_ids = set(c.get(f'{WNS}id') for c in ctree.getroot().findall(f'{WNS}comment'))
assert doc_ids == comment_ids, f"ID mismatch: doc={doc_ids} comments={comment_ids}"
```

## Key Pitfall: Comment Text Extraction
When extracting comment text for comparison, comments may have nested `<w:p>` elements (multi-paragraph comments). Use `.iter()` not `.findall()` to get all text nodes.

## Empirical Case (2026-07-01 反委托代发工资协议)
- Original: 4 comments (Alice id=0, Alice id=1, 法务 id=2, 杜律 id=3)
- v1_doro_updated: 2 comments (华诚-Z id=0, Alice id=2) — lost Alice id=0/1, 法务, 杜律
- Final: 5 comments (Alice id=0, Alice id=1, 法务 id=2, 杜律 id=3, 华诚-Z id=4)
- 华诚-Z's comment was id=0 in v1_doro_updated, renumbered to id=4 in final
- All commentRangeStart/End/Reference IDs updated in document.xml accordingly
