#!/usr/bin/env python3
"""
contract_preprocess.py — 合同预处理：检测并切割非审查图片内容

用途：在workflow审查前，检测合同末尾的纯图片附件（如招标公告截图、中标通知书等），
     切割出来保存，审查完后再还原。

判断逻辑：
1. 扫描文件结构：文字段落数 vs 图片段落数
2. 全文/大部分是图片（扫描件合同）→ 不切割，标记需OCR
3. 正文文字+末尾图片附件 → 切割末尾图片区域
4. 切割点：从最后一个"纯文字附件"结束后，到第一个"纯图片附件"开始

输出：
- {basename}_stripped.docx — 去掉图片附件的版本（供workflow处理）
- {basename}_cutdata.json — 切割信息（供还原用）
"""

import zipfile, json, os, sys, re
from lxml import etree

W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
A_NS = 'http://schemas.openxmlformats.org/drawingml/2006/main'


def analyze_contract(docx_path):
    """Analyze contract structure, return analysis dict"""
    with zipfile.ZipFile(docx_path) as z:
        doc = etree.fromstring(z.read('word/document.xml'))
        media_files = {n: z.getinfo(n).file_size for n in z.namelist() if n.startswith('word/media/')}

    body = doc.find(f'{{{W}}}body')
    paras = body.findall(f'{{{W}}}p')

    paragraphs = []
    total_text_chars = 0
    total_img_paras = 0

    for i, p in enumerate(paras):
        texts = p.findall(f'.//{{{W}}}t')
        text = ''.join(t.text or '' for t in texts).strip()

        has_img = any('drawing' in (e.tag if isinstance(e.tag, str) else '') for e in p.iter())

        blips = list(p.iter(f'{{{A_NS}}}blip'))
        img_rids = [b.get(f'{{{R_NS}}}embed', '') for b in blips]

        total_text_chars += len(text)
        if has_img:
            total_img_paras += 1

        paragraphs.append({
            'idx': i,
            'text': text,
            'text_len': len(text),
            'has_img': has_img,
            'img_rids': img_rids,
            'is_appendix_heading': bool(re.match(r'^附件[一二三四五六七八九十\d]+[：:、]', text)),
        })

    return {
        'total_paras': len(paras),
        'total_text_chars': total_text_chars,
        'total_img_paras': total_img_paras,
        'media_files': media_files,
        'total_media_bytes': sum(media_files.values()),
        'paragraphs': paragraphs,
    }


def detect_cut_zone(analysis):
    """Detect if there's a tail image zone to cut."""
    paras = analysis['paragraphs']
    total = analysis['total_paras']

    text_paras = sum(1 for p in paras if p['text_len'] > 0 and not p['has_img'])
    img_paras = analysis['total_img_paras']

    if text_paras == 0 and img_paras > 0:
        return {'action': 'ocr', 'reason': '全文无文字段落，疑似扫描件合同'}

    if img_paras == 0:
        return None

    img_ratio = img_paras / max(1, text_paras + img_paras)
    if img_ratio > 0.5:
        return {'action': 'ocr', 'reason': f'图片段落占比{img_ratio:.0%}，疑似扫描件合同'}

    # Find tail image zones
    image_zones = []
    i = 0
    while i < total:
        p = paras[i]
        if p['is_appendix_heading']:
            zone_start = i
            zone_has_images = False
            zone_has_text_content = False
            j = i + 1

            while j < total:
                next_p = paras[j]
                if next_p['is_appendix_heading']:
                    break
                if next_p['has_img']:
                    zone_has_images = True
                if next_p['text_len'] > 20 and not next_p['has_img']:
                    zone_has_text_content = True
                j += 1

            image_zones.append({
                'start_idx': zone_start,
                'end_idx': j - 1,
                'heading': p['text'],
                'has_images': zone_has_images,
                'has_text': zone_has_text_content,
                'is_image_only': zone_has_images and not zone_has_text_content,
            })
            i = j
        else:
            i += 1

    # Find consecutive image-only appendices at the tail
    tail_cut_zones = []
    for zone in reversed(image_zones):
        if zone['is_image_only']:
            tail_cut_zones.insert(0, zone)
        else:
            break

    if not tail_cut_zones:
        return None

    cut_start = tail_cut_zones[0]['start_idx']
    cut_headings = [z['heading'] for z in tail_cut_zones]

    return {
        'action': 'cut',
        'cut_start_idx': cut_start,
        'cut_end_idx': total - 1,
        'cut_headings': cut_headings,
        'reason': f'末尾{len(tail_cut_zones)}个附件为纯图片：{", ".join(cut_headings)}',
    }


def preprocess_contract(docx_path, output_dir=None):
    """Main entry: analyze and optionally strip tail images."""
    if output_dir is None:
        output_dir = os.path.dirname(docx_path) or '.'

    basename = os.path.splitext(os.path.basename(docx_path))[0]

    analysis = analyze_contract(docx_path)
    cut_info = detect_cut_zone(analysis)

    print(f"\n=== 合同预处理分析 ===")
    print(f"文件: {os.path.basename(docx_path)}")
    print(f"段落数: {analysis['total_paras']}")
    print(f"文字字符: {analysis['total_text_chars']}")
    print(f"图片段落: {analysis['total_img_paras']}")
    print(f"媒体文件: {len(analysis['media_files'])} ({analysis['total_media_bytes']:,} bytes)")

    if cut_info is None:
        print(f"结论: 无需切割")
        return {'action': 'none', 'analysis': analysis}

    if cut_info['action'] == 'ocr':
        print(f"结论: {cut_info['reason']}，需OCR处理")
        return {'action': 'ocr', 'reason': cut_info['reason'], 'analysis': analysis}

    cut_start = cut_info['cut_start_idx']
    print(f"结论: 需切割 — {cut_info['reason']}")
    print(f"切割点: 段落 #{cut_start}")

    with zipfile.ZipFile(docx_path) as z:
        doc = etree.fromstring(z.read('word/document.xml'))
        all_files = {}
        for name in z.namelist():
            all_files[name] = z.read(name)

    body = doc.find(f'{{{W}}}body')
    paras = body.findall(f'{{{W}}}p')

    cut_paras_xml = []
    for i in range(cut_start, len(paras)):
        cut_paras_xml.append(etree.tostring(paras[i], encoding='unicode'))

    for i in range(len(paras) - 1, cut_start - 1, -1):
        body.remove(paras[i])

    cut_rids = set()
    for p_info in analysis['paragraphs'][cut_start:]:
        cut_rids.update(p_info['img_rids'])

    rels_xml = all_files.get('word/_rels/document.xml.rels', b'')
    if isinstance(rels_xml, bytes):
        rels_xml = rels_xml.decode()
    rid_to_media = {}
    for m in re.finditer(r'Id="(rId\d+)"[^/]*Target="(media/[^"]+)"', rels_xml):
        rid_to_media[m.group(1)] = f'word/{m.group(2)}'

    cut_media = {}
    for rid in cut_rids:
        media_path = rid_to_media.get(rid)
        if media_path and media_path in all_files:
            cut_media[media_path] = len(all_files[media_path])

    stripped_path = os.path.join(output_dir, f'{basename}_stripped.docx')
    all_files['word/document.xml'] = etree.tostring(doc, xml_declaration=True, encoding='UTF-8', standalone=True)

    with zipfile.ZipFile(stripped_path, 'w', zipfile.ZIP_DEFLATED) as zout:
        for name, data in all_files.items():
            zout.writestr(name, data)

    cutdata = {
        'original_file': os.path.basename(docx_path),
        'cut_start_idx': cut_start,
        'total_paras_original': len(paras) + len(cut_paras_xml),
        'cut_paragraphs_xml': cut_paras_xml,
        'cut_headings': cut_info['cut_headings'],
        'cut_media_files': list(cut_media.keys()),
        'reason': cut_info['reason'],
    }

    cutdata_path = os.path.join(output_dir, f'{basename}_cutdata.json')
    with open(cutdata_path, 'w', encoding='utf-8') as f:
        json.dump(cutdata, f, ensure_ascii=False, indent=2)

    stripped_size = os.path.getsize(stripped_path)
    original_size = os.path.getsize(docx_path)

    print(f"\n输出:")
    print(f"  stripped: {stripped_path} ({stripped_size:,} bytes)")
    print(f"  cutdata:  {cutdata_path}")
    print(f"  大小变化: {original_size:,} → {stripped_size:,} bytes ({stripped_size/original_size:.0%})")

    return {
        'action': 'cut',
        'stripped_path': stripped_path,
        'cutdata_path': cutdata_path,
        'cut_info': cut_info,
        'analysis': analysis,
    }


def restore_contract(reviewed_path, cutdata_path, output_path):
    """Restore cut content back into the reviewed file."""
    with open(cutdata_path, 'r', encoding='utf-8') as f:
        cutdata = json.load(f)

    with zipfile.ZipFile(reviewed_path) as z:
        doc = etree.fromstring(z.read('word/document.xml'))
        all_files = {}
        for name in z.namelist():
            all_files[name] = z.read(name)

    body = doc.find(f'{{{W}}}body')
    sect_pr = body.find(f'{{{W}}}sectPr')

    for para_xml in cutdata['cut_paragraphs_xml']:
        para_elem = etree.fromstring(para_xml)
        if sect_pr is not None:
            sect_pr.addprevious(para_elem)
        else:
            body.append(para_elem)

    original_dir = os.path.dirname(cutdata_path)
    original_name = cutdata['original_file']
    original_path = os.path.join(original_dir, original_name)

    if os.path.exists(original_path):
        with zipfile.ZipFile(original_path) as z_orig:
            for media_file in cutdata.get('cut_media_files', []):
                if media_file not in all_files and media_file in z_orig.namelist():
                    all_files[media_file] = z_orig.read(media_file)
                    print(f"  还原媒体文件: {media_file}")

    all_files['word/document.xml'] = etree.tostring(doc, xml_declaration=True, encoding='UTF-8', standalone=True)

    with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zout:
        for name, data in all_files.items():
            zout.writestr(name, data)

    restored_size = os.path.getsize(output_path)
    print(f"\n=== 合同还原完成 ===")
    print(f"还原文件: {output_path} ({restored_size:,} bytes)")
    print(f"还原段落: {len(cutdata['cut_paragraphs_xml'])} 个")
    print(f"还原附件: {', '.join(cutdata['cut_headings'])}")

    return output_path


if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage:")
        print("  预处理: python contract_preprocess.py preprocess <input.docx> [output_dir]")
        print("  还原:   python contract_preprocess.py restore <reviewed.docx> <cutdata.json> <output.docx>")
        sys.exit(1)

    action = sys.argv[1]

    if action == 'preprocess':
        docx_path = sys.argv[2]
        output_dir = sys.argv[3] if len(sys.argv) > 3 else None
        result = preprocess_contract(docx_path, output_dir)
        print(f"\nResult: {json.dumps({k: v for k, v in result.items() if k != 'analysis'}, ensure_ascii=False, indent=2)}")

    elif action == 'restore':
        reviewed_path = sys.argv[2]
        cutdata_path = sys.argv[3]
        output_path = sys.argv[4]
        restore_contract(reviewed_path, cutdata_path, output_path)

    else:
        print(f"Unknown action: {action}")
        sys.exit(1)
