import sys
from pathlib import Path
import zipfile
import xml.etree.ElementTree as ET
from docx import Document
from docx.text.paragraph import Paragraph as _Paragraph
from docx.table import Table as _Table
from docx.enum.text import WD_ALIGN_PARAGRAPH
import json

NS_W = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}'
NS_REL = '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}'
NS_A = '{http://schemas.openxmlformats.org/drawingml/2006/main}'
NS_PKG = '{http://schemas.openxmlformats.org/package/2006/relationships}'
NS_VML = '{urn:schemas-microsoft-com:vml}'

def _resolve_target(target: str) -> str:
    t = (target or '').replace('\\','/').lstrip('/')
    if t.startswith('../'):
        return 'word/' + t[3:]
    if t.startswith('word/'):
        return t
    if t.startswith('media/'):
        return 'word/' + t
    return 'word/' + t

def _get_core_properties(doc: Document):
    cp = doc.core_properties
    return {
        'author': cp.author or '',
        'title': cp.title or '',
        'last_modified_by': cp.last_modified_by or '',
        'created': cp.created.isoformat() if cp.created else '',
        'modified': cp.modified.isoformat() if cp.modified else '',
    }

def _get_app_pages(docx_path: Path):
    try:
        with zipfile.ZipFile(str(docx_path), 'r') as z:
            app_xml = z.read('docProps/app.xml')
        root = ET.fromstring(app_xml)
        ns = '{http://schemas.openxmlformats.org/officeDocument/2006/extended-properties}'
        el = root.find('.//' + ns + 'Pages')
        return int(el.text) if el is not None and el.text and el.text.isdigit() else None
    except Exception:
        return None

def _align_str(a):
    if a == WD_ALIGN_PARAGRAPH.CENTER:
        return 'center'
    if a == WD_ALIGN_PARAGRAPH.RIGHT:
        return 'right'
    if a == WD_ALIGN_PARAGRAPH.LEFT:
        return 'left'
    if a == WD_ALIGN_PARAGRAPH.JUSTIFY:
        return 'justify'
    return 'default'

def _len_pt(x):
    try:
        return round(x.pt, 2)
    except Exception:
        return None

def _collect_image_order(docx_path: Path):
    with zipfile.ZipFile(str(docx_path), 'r') as z:
        body_xml = z.read('word/document.xml')
        rels_xml = z.read('word/_rels/document.xml.rels')
    root = ET.fromstring(body_xml)
    rels = ET.fromstring(rels_xml)
    rid_to_target = {}
    rid_to_external = {}
    header_ids = []
    footer_ids = []
    for r in rels.findall('.//' + NS_PKG + 'Relationship'):
        rid = r.attrib.get('Id')
        tgt = r.attrib.get('Target')
        mode = r.attrib.get('TargetMode')
        typ = r.attrib.get('Type')
        if rid and tgt:
            if mode is None or mode != 'External':
                rid_to_target[rid] = tgt
            else:
                rid_to_external[rid] = tgt
            if typ and typ.endswith('/header'):
                header_ids.append((rid, tgt))
            elif typ and typ.endswith('/footer'):
                footer_ids.append((rid, tgt))
    body_order = []
    body_external = []
    # drawingML in body
    for blip in root.findall('.//' + NS_A + 'blip'):
        rid = blip.attrib.get(NS_REL + 'embed')
        if rid and rid in rid_to_target:
            body_order.append((rid, rid_to_target[rid]))
        rlink = blip.attrib.get(NS_REL + 'link')
        if rlink and rlink in rid_to_external:
            body_external.append(('body', rid_to_external[rlink]))
    # VML fallback in body
    for vimg in root.findall('.//' + NS_VML + 'imagedata'):
        rid = vimg.attrib.get(NS_REL + 'id')
        if rid and rid in rid_to_target:
            body_order.append((rid, rid_to_target[rid]))
        rlink = vimg.attrib.get(NS_REL + 'id')
        if rlink and rlink in rid_to_external:
            body_external.append(('body', rid_to_external[rlink]))

    def _collect_from_part(docx_path: Path, part_rel_target: str):
        with zipfile.ZipFile(str(docx_path), 'r') as z:
            part_xml = z.read(_resolve_target(part_rel_target))
            # header1.xml -> word/_rels/header1.xml.rels
            rels_name = 'word/_rels/' + Path(part_rel_target).name + '.rels'
            try:
                rels_xml_local = z.read(rels_name)
            except KeyError:
                rels_xml_local = b'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>'
        root_local = ET.fromstring(part_xml)
        rels_local = ET.fromstring(rels_xml_local)
        rid2t = {}
        rid2ext = {}
        for r in rels_local.findall('.//' + NS_PKG + 'Relationship'):
            rid = r.attrib.get('Id')
            tgt = r.attrib.get('Target')
            mode = r.attrib.get('TargetMode')
            if rid and tgt:
                if mode is None or mode != 'External':
                    rid2t[rid] = tgt
                else:
                    rid2ext[rid] = tgt
        out = []
        out_ext = []
        for blip in root_local.findall('.//' + NS_A + 'blip'):
            rid = blip.attrib.get(NS_REL + 'embed')
            if rid and rid in rid2t:
                out.append((rid, rid2t[rid]))
            rlink = blip.attrib.get(NS_REL + 'link')
            if rlink and rlink in rid2ext:
                out_ext.append(rid2ext[rlink])
        for vimg in root_local.findall('.//' + NS_VML + 'imagedata'):
            rid = vimg.attrib.get(NS_REL + 'id')
            if rid and rid in rid2t:
                out.append((rid, rid2t[rid]))
            if rid and rid in rid2ext:
                out_ext.append(rid2ext[rid])
        return out, out_ext

    header_order = []
    header_external = []
    for _, tgt in header_ids:
        o, oe = _collect_from_part(docx_path, tgt)
        header_order.extend(o)
        header_external.extend(oe)
    footer_order = []
    footer_external = []
    for _, tgt in footer_ids:
        o, oe = _collect_from_part(docx_path, tgt)
        footer_order.extend(o)
        footer_external.extend(oe)
    externals = body_external + [('header', u) for u in header_external] + [('footer', u) for u in footer_external]
    return body_order, header_order, footer_order, externals

def _extract_images_zip(docx_path: Path, out_zip: Path, body_order, header_order, footer_order, externals, positions=None, para_formats=None):
    with zipfile.ZipFile(str(docx_path), 'r') as z, zipfile.ZipFile(str(out_zip), 'w', compression=zipfile.ZIP_DEFLATED) as zout:
        # body images
        for idx, ((_, target)) in enumerate(body_order, start=1):
            inner = _resolve_target(target)
            ext = Path(target).suffix.lower() or '.bin'
            data = z.read(inner)
            name = f'body/image_{idx:03d}{ext}'
            zout.writestr(name, data)
        # header images
        for idx, ((_, target)) in enumerate(header_order, start=1):
            inner = _resolve_target(target)
            ext = Path(target).suffix.lower() or '.bin'
            data = z.read(inner)
            name = f'header/image_{idx:03d}{ext}'
            zout.writestr(name, data)
        # footer images
        for idx, ((_, target)) in enumerate(footer_order, start=1):
            inner = _resolve_target(target)
            ext = Path(target).suffix.lower() or '.bin'
            data = z.read(inner)
            name = f'footer/image_{idx:03d}{ext}'
            zout.writestr(name, data)
        # manifest
        manifest = {
            'body': [f'body/image_{i+1:03d}{Path(t).suffix.lower() or ".bin"}' for i, (_, t) in enumerate(body_order)],
            'header': [f'header/image_{i+1:03d}{Path(t).suffix.lower() or ".bin"}' for i, (_, t) in enumerate(header_order)],
            'footer': [f'footer/image_{i+1:03d}{Path(t).suffix.lower() or ".bin"}' for i, (_, t) in enumerate(footer_order)],
            'external': [{'section': sec, 'url': url} for (sec, url) in externals],
            'positions': positions or [],
            'paragraph_formats': para_formats or [],
        }
        zout.writestr('manifest.json', json.dumps(manifest, ensure_ascii=False, indent=2))

def _paragraph_to_md(p: _Paragraph):
    txt = p.text or ''
    name = (p.style.name if p.style else '') or ''
    if name.startswith('Heading'):
        try:
            lvl = int(name.split()[-1])
        except Exception:
            lvl = 1
        return '#' * max(1, min(6, lvl)) + ' ' + txt.strip()
    if 'List Bullet' in name:
        return '- ' + txt.strip()
    if 'List Number' in name:
        return '1. ' + txt.strip()
    return txt

def _table_to_md(t: _Table):
    rows = [[c.text.replace('\n', ' ').strip() for c in row.cells] for row in t.rows]
    if not rows:
        return []
    header = rows[0]
    body = rows[1:] if len(rows) > 1 else []
    # alignment per column based on first row cells
    aligns = []
    for idx, c in enumerate(t.rows[0].cells):
        try:
            a = c.paragraphs[0].alignment
        except Exception:
            a = None
        if a == WD_ALIGN_PARAGRAPH.CENTER:
            aligns.append(':---:')
        elif a == WD_ALIGN_PARAGRAPH.RIGHT:
            aligns.append('---:')
        elif a == WD_ALIGN_PARAGRAPH.LEFT:
            aligns.append(':---')
        else:
            aligns.append('---')
    out = [
        '| ' + ' | '.join(header) + ' |',
        '| ' + ' | '.join(aligns) + ' |',
    ]
    for r in body:
        out.append('| ' + ' | '.join(r) + ' |')
    return out

def convert_docx_to_md(docx_path: Path, out_md: Path):
    doc = Document(str(docx_path))
    md_lines = []
    # collect images first for indexing and header/footer listing
    body_order, header_order, footer_order, externals = _collect_image_order(docx_path)
    body_rid_to_target = {rid: tgt for rid, tgt in body_order}
    body_rid_to_index = {rid: idx for idx, (rid, _) in enumerate(body_order, start=1)}
    pos_lines = []
    props = _get_core_properties(doc)
    pages = _get_app_pages(docx_path)
    if props.get('title') or props.get('author') or pages is not None:
        md_lines.append('文档属性:')
        if props.get('title'):
            md_lines.append(f'- 标题: {props["title"]}')
        if props.get('author'):
            md_lines.append(f'- 作者: {props["author"]}')
        if pages is not None:
            md_lines.append(f'- 页数: {pages}')
        if props.get('created'):
            md_lines.append(f'- 创建时间: {props["created"]}')
        if props.get('modified'):
            md_lines.append(f'- 修改时间: {props["modified"]}')
    if header_order:
        md_lines.append('页首图片位置:')
        for i, (_, t) in enumerate(header_order, start=1):
            ext = Path(t).suffix.lower() or '.bin'
            md_lines.append(f'- 页首-图{i}: header/image_{i:03d}{ext}')
    if footer_order:
        md_lines.append('页尾图片位置:')
        for i, (_, t) in enumerate(footer_order, start=1):
            ext = Path(t).suffix.lower() or '.bin'
            md_lines.append(f'- 页尾-图{i}: footer/image_{i:03d}{ext}')
    # collect positions of body images by paragraph index
    para_idx = 0
    positions_struct = []
    body = doc.element.body
    for child in body.iterchildren():
        tag = child.tag
        if tag == NS_W + 'p':
            para_idx += 1
            ptext = ''
            try:
                ptext = _Paragraph(child, doc).text or ''
            except Exception:
                ptext = ''
            snippet = ptext.strip().replace('\n',' ')[:30]
            img_seq = 0
            for blip in child.findall('.//' + NS_A + 'blip'):
                rid = blip.attrib.get(NS_REL + 'embed')
                if rid and rid in body_rid_to_index:
                    idx = body_rid_to_index[rid]
                    tgt = body_rid_to_target.get(rid, '')
                    ext = Path(tgt).suffix.lower() or '.bin'
                    img_seq += 1
                    pos_lines.append(f'- 正文段落{para_idx}-图{img_seq}: body/image_{idx:03d}{ext} 段落文本: "{snippet}"')
                    positions_struct.append({'paragraph': para_idx, 'seq': img_seq, 'path': f'body/image_{idx:03d}{ext}', 'snippet': snippet})
            for vimg in child.findall('.//' + NS_VML + 'imagedata'):
                rid = vimg.attrib.get(NS_REL + 'id')
                if rid and rid in body_rid_to_index:
                    idx = body_rid_to_index[rid]
                    tgt = body_rid_to_target.get(rid, '')
                    ext = Path(tgt).suffix.lower() or '.bin'
                    img_seq += 1
                    pos_lines.append(f'- 正文段落{para_idx}-图{img_seq}: body/image_{idx:03d}{ext} 段落文本: "{snippet}"')
                    positions_struct.append({'paragraph': para_idx, 'seq': img_seq, 'path': f'body/image_{idx:03d}{ext}', 'snippet': snippet})
    if pos_lines:
        md_lines.append('正文图片位置:')
        md_lines.extend(pos_lines)
    if externals:
        md_lines.append('外链图片:')
        for sec, url in externals:
            md_lines.append(f'- {sec}: {url}')
    fmt_lines = []
    fmt_structs = []
    pindex = 0
    for child in doc.element.body.iterchildren():
        if child.tag == NS_W + 'p':
            pindex += 1
            p = _Paragraph(child, doc)
            pf = p.paragraph_format
            a = _align_str(p.alignment)
            sz_para = None
            try:
                sz_para = _len_pt(p.style.font.size)
            except Exception:
                sz_para = None
            sz_runs = []
            try:
                for r in p.runs:
                    sz_runs.append(_len_pt(r.font.size))
            except Exception:
                pass
            left = _len_pt(pf.left_indent)
            first = _len_pt(pf.first_line_indent)
            before = _len_pt(pf.space_before)
            after = _len_pt(pf.space_after)
            ls = pf.line_spacing if pf.line_spacing is not None else None
            name = p.style.name if p.style else ''
            line = f'- 段落{pindex}: 样式={name} 对齐={a}'
            if sz_para is not None:
                line += f' 段落字号={sz_para}pt'
            if any(x is not None for x in sz_runs):
                vals = [str(x)+'pt' if x is not None else 'default' for x in sz_runs]
                line += ' 运行字号=' + '/'.join(vals)
            if left is not None:
                line += f' 左缩进={left}pt'
            if first is not None:
                line += f' 首行缩进={first}pt'
            if before is not None:
                line += f' 段前间距={before}pt'
            if after is not None:
                line += f' 段后间距={after}pt'
            if ls is not None:
                try:
                    line += f' 行距={round(float(ls),2)}'
                except Exception:
                    pass
            fmt_lines.append(line)
            fmt_structs.append({
                'paragraph': pindex,
                'style': name,
                'align': a,
                'para_font_pt': sz_para,
                'run_fonts_pt': sz_runs,
                'indent_left_pt': left,
                'indent_first_pt': first,
                'space_before_pt': before,
                'space_after_pt': after,
                'line_spacing': float(ls) if isinstance(ls,(int,float)) else None,
            })
    if fmt_lines:
        md_lines.append('段落格式记录:')
        md_lines.extend(fmt_lines)
    if md_lines:
        md_lines.append('---')
    # build formal content
    body = doc.element.body
    for child in body.iterchildren():
        tag = child.tag
        if tag == NS_W + 'p':
            p = _Paragraph(child, doc)
            md_lines.append(_paragraph_to_md(p))
            md_lines.append('')
        elif tag == NS_W + 'tbl':
            t = _Table(child, doc)
            md_lines.extend(_table_to_md(t))
            md_lines.append('')
    out_md.write_text('\n'.join(md_lines), encoding='utf-8')
    body_order, header_order, footer_order, externals = _collect_image_order(docx_path)
    zip_path = out_md.with_suffix('.zip')
    _extract_images_zip(docx_path, zip_path, body_order, header_order, footer_order, externals, positions=positions_struct, para_formats=fmt_structs)

def main():
    import argparse
    parser = argparse.ArgumentParser(
        prog='HOS_M2F.docx2md',
        description='DOCX 逆向为 Markdown，并按出现顺序打包图片到同名压缩包',
        epilog=(
            '示例:\n'
            '  HOS_M2F.docx2md input.docx output.md\n'
            '相关命令:\n'
            '  HOS_M2F.2docx input.md output.docx [-p paper|patent|softcopy] [-nm] [-nc] [-q]\n'
            '  HOS_M2F.batch input_dir output_dir [-p paper|patent|softcopy] [-nm] [-nc] [-q]\n'
        ),
    )
    parser.add_argument('input', help='输入 DOCX 文件路径')
    parser.add_argument('output', help='输出 Markdown 文件路径')
    args = parser.parse_args()
    inp = Path(args.input)
    outp = Path(args.output)
    convert_docx_to_md(inp, outp)

if __name__ == '__main__':
    main()
