# -*- coding: utf-8 -*-
"""
content_order.py — HWPX section*.xml 본문 콘텐츠 순서 추출

기존 12개 tool이 header.xml의 "정의(definition)"를 추출하는 반면,
이 tool은 section0.xml의 "본문(content)" 순서를 추출한다.

추출 결과는 template_manager._build_body_html()이
원본 순서 그대로 HTML을 조립하는 데 사용된다.

콘텐츠 유형:
  - paragraph : 일반 텍스트 문단
  - table     : 표 (<hp:tbl>)
  - image     : 이미지 (<hp:pic>)
  - empty     : 빈 문단 (줄바꿈 역할)

참조: hwpx_domain_guide.md §6(표), §7(본문 구조)
"""

import re
import logging

logger = logging.getLogger(__name__)

# ================================================================
#  네임스페이스
# ================================================================
# HWPX는 여러 네임스페이스를 사용한다.
# section*.xml: hp: (본문), ha: (속성)
# header.xml:   hh: (헤더 정의)
# 실제 파일에서 네임스페이스 URI가 다를 수 있으므로 로컬명 기반 탐색도 병행한다.

DEFAULT_NS = {
    'hp': 'http://www.hancom.co.kr/hwpml/2011/paragraph',
    'ha': 'http://www.hancom.co.kr/hwpml/2011/attributes',
    'hh': 'http://www.hancom.co.kr/hwpml/2011/head',
    'hc': 'http://www.hancom.co.kr/hwpml/2011/core',
}


# ================================================================
#  공개 API
# ================================================================

def extract(raw_xml, parsed, ns=None):
    """section*.xml에서 본문 콘텐츠 순서를 추출한다.

    Args:
        raw_xml (dict): 원본 XML 문자열 딕셔너리.
                        raw_xml.get("section0") 등으로 section XML에 접근.
        parsed (dict): processor.py가 HWPX를 파싱한 전체 결과 dict.
                       parsed.get("section_xml") 등으로 parsed Element에 접근.
        ns (dict, optional): 네임스페이스 매핑. None이면 자동 감지.

    Returns:
        list[dict]: 콘텐츠 순서 리스트. 각 항목은 다음 키를 포함:
            - type: "paragraph" | "table" | "image" | "empty"
            - index: 전체 순서 내 인덱스 (0부터)
            - paraPrIDRef: 문단모양 참조 ID (str or None)
            - styleIDRef: 스타일 참조 ID (str or None)
            + type별 추가 키 (아래 참조)
        추출 실패 시 None 반환 (analyzer가 결과에서 제외함).
    """
    # ── section XML 찾기 ──
    # raw_xml dict에서 section 원본 문자열 추출
    section_raw = None
    if isinstance(raw_xml, dict):
        # 키 이름은 프로젝트마다 다를 수 있음: section0, section_xml 등
        for key in ['section0', 'section_xml', 'section0.xml']:
            if key in raw_xml:
                section_raw = raw_xml[key]
                break
        # 못 찾으면 "section"으로 시작하는 첫 번째 키
        if section_raw is None:
            for key, val in raw_xml.items():
                if key.startswith('section') and isinstance(val, str):
                    section_raw = val
                    break
    elif isinstance(raw_xml, str):
        section_raw = raw_xml

    # parsed dict에서 section Element 또는 문자열 추출
    section_parsed = None
    if isinstance(parsed, dict):
        for key in ['section_xml', 'section0', 'section_parsed', 'section0_parsed']:
            val = parsed.get(key)
            if val is None:
                continue
            if isinstance(val, str):
                # 문자열이면 section_raw로 활용 (table.py와 동일)
                if section_raw is None:
                    section_raw = val
            elif not isinstance(val, dict):
                # Element 객체로 추정
                section_parsed = val
                break
        # fallback: raw_xml 문자열을 직접 파싱
        if section_parsed is None and section_raw:
            import xml.etree.ElementTree as ET
            try:
                section_parsed = ET.fromstring(section_raw)
            except ET.ParseError:
                logger.warning("section XML 파싱 실패")
                return None
    else:
        # parsed 자체가 Element일 수 있음 (직접 호출 시)
        section_parsed = parsed

    if section_parsed is None:
        logger.warning("section XML을 찾을 수 없음 — content_order 추출 생략")
        return None

    if ns is None:
        ns = _detect_namespaces(section_raw or '', section_parsed)

    # <hp:p> 엘리먼트 수집 — secPr 내부는 제외
    paragraphs = _collect_body_paragraphs(section_parsed, ns)

    content_order = []
    table_idx = 0
    image_idx = 0

    for p_elem in paragraphs:
        para_pr_id = _get_attr(p_elem, 'paraPrIDRef')
        style_id = _get_attr(p_elem, 'styleIDRef')

        base = {
            'index': len(content_order),
            'paraPrIDRef': para_pr_id,
            'styleIDRef': style_id,
        }

        # ── (1) 표 확인 ──
        tbl = _find_element(p_elem, 'tbl', ns)
        if tbl is not None:
            tbl_info = _extract_table_info(tbl, ns)
            content_order.append({
                **base,
                'type': 'table',
                'table_idx': table_idx,
                **tbl_info,
            })
            table_idx += 1
            continue

        # ── (2) 이미지 확인 ──
        pic = _find_element(p_elem, 'pic', ns)
        if pic is not None:
            img_info = _extract_image_info(pic, p_elem, ns)
            content_order.append({
                **base,
                'type': 'image',
                'image_idx': image_idx,
                **img_info,
            })
            image_idx += 1
            continue

        # ── (3) 텍스트 문단 / 빈 문단 ──
        text = _collect_text(p_elem, ns)
        runs_info = _extract_runs_info(p_elem, ns)

        if not text.strip():
            content_order.append({
                **base,
                'type': 'empty',
            })
        else:
            content_order.append({
                **base,
                'type': 'paragraph',
                'text': text,
                'charPrIDRef': runs_info.get('first_charPrIDRef'),
                'runs': runs_info.get('runs', []),
            })

    logger.info(
        "content_order 추출 완료: %d items "
        "(paragraphs=%d, tables=%d, images=%d, empty=%d)",
        len(content_order),
        sum(1 for c in content_order if c['type'] == 'paragraph'),
        table_idx,
        image_idx,
        sum(1 for c in content_order if c['type'] == 'empty'),
    )

    return content_order


# ================================================================
#  본문 <hp:p> 수집 — secPr 내부 제외
# ================================================================

def _collect_body_paragraphs(root, ns):
    """<hp:sec> 직계 <hp:p> 만 수집한다.
    
    secPr, headerFooter 내부의 <hp:p>는 본문이 아니므로 제외.
    subList 내부(셀 안 문단)도 제외 — 표는 통째로 하나의 항목.
    """
    paragraphs = []

    # 방법 1: sec 직계 자식 중 p 태그만
    sec = _find_element(root, 'sec', ns)
    if sec is None:
        # 루트 자체가 sec일 수 있음
        sec = root

    for child in sec:
        tag = _local_tag(child)
        if tag == 'p':
            paragraphs.append(child)

    # 직계 자식에서 못 찾았으면 fallback: 전체 탐색 (but secPr/subList 제외)
    if not paragraphs:
        paragraphs = _collect_paragraphs_fallback(root, ns)

    return paragraphs


def _collect_paragraphs_fallback(root, ns):
    """fallback: 전체에서 <hp:p>를 찾되, secPr/headerFooter/subList 내부는 제외"""
    skip_tags = {'secPr', 'headerFooter', 'subList', 'tc'}
    result = []

    def _walk(elem, skip=False):
        if skip:
            return
        tag = _local_tag(elem)
        if tag in skip_tags:
            return
        if tag == 'p':
            # 부모가 sec이거나 루트 직계인 경우만
            result.append(elem)
            return  # p 내부의 하위 p는 수집하지 않음
        for child in elem:
            _walk(child)

    _walk(root)
    return result


# ================================================================
#  표 정보 추출
# ================================================================

def _extract_table_info(tbl, ns):
    """<hp:tbl> 에서 기본 메타 정보 추출"""
    info = {
        'rowCnt': _get_attr(tbl, 'rowCnt'),
        'colCnt': _get_attr(tbl, 'colCnt'),
        'borderFillIDRef': _get_attr(tbl, 'borderFillIDRef'),
    }

    # 열 너비
    col_sz = _find_element(tbl, 'colSz', ns)
    if col_sz is not None:
        width_list_elem = _find_element(col_sz, 'widthList', ns)
        if width_list_elem is not None and width_list_elem.text:
            info['colWidths'] = width_list_elem.text.strip().split()

    return info


# ================================================================
#  이미지 정보 추출
# ================================================================

def _extract_image_info(pic, p_elem, ns):
    """<hp:pic> 에서 이미지 참조 정보 추출"""
    info = {
        'binaryItemIDRef': None,
        'text': '',  # 이미지와 같은 문단에 있는 텍스트 (캡션 등)
    }

    # img 태그에서 binaryItemIDRef
    img = _find_element(pic, 'img', ns)
    if img is not None:
        info['binaryItemIDRef'] = _get_attr(img, 'binaryItemIDRef')

    # imgRect에서 크기 정보
    img_rect = _find_element(pic, 'imgRect', ns)
    if img_rect is not None:
        info['imgRect'] = {
            'x': _get_attr(img_rect, 'x'),
            'y': _get_attr(img_rect, 'y'),
            'w': _get_attr(img_rect, 'w'),
            'h': _get_attr(img_rect, 'h'),
        }

    # 같은 문단 내 텍스트 (pic 바깥의 run들)
    info['text'] = _collect_text_outside(p_elem, pic, ns)

    return info


# ================================================================
#  텍스트 수집
# ================================================================

def _collect_text(p_elem, ns):
    """<hp:p> 내 모든 <hp:t> 텍스트를 순서대로 합침
    
    주의: t.tail은 XML 들여쓰기 공백이므로 수집하지 않는다.
    HWPX에서 실제 텍스트는 항상 <hp:t>...</hp:t> 안에 있다.
    """
    parts = []
    for t in _find_all_elements(p_elem, 't', ns):
        if t.text:
            parts.append(t.text)
    return ''.join(parts)


def _collect_text_outside(p_elem, exclude_elem, ns):
    """p_elem 내에서 exclude_elem(예: pic) 바깥의 텍스트만 수집"""
    parts = []

    def _walk(elem):
        if elem is exclude_elem:
            return
        tag = _local_tag(elem)
        if tag == 't' and elem.text:
            parts.append(elem.text)
        for child in elem:
            _walk(child)

    _walk(p_elem)
    return ''.join(parts)


# ================================================================
#  Run 정보 추출
# ================================================================

def _extract_runs_info(p_elem, ns):
    """<hp:p> 내 <hp:run> 들의 charPrIDRef와 텍스트 추출
    
    Returns:
        {
            'first_charPrIDRef': str or None,
            'runs': [
                {'charPrIDRef': '8', 'text': '1. SamanPro...'},
                {'charPrIDRef': '24', 'text': '포장설계...'},
            ]
        }
    """
    runs = []
    first_char_pr = None

    for run_elem in _find_direct_runs(p_elem, ns):
        char_pr = _get_attr(run_elem, 'charPrIDRef')
        if first_char_pr is None and char_pr is not None:
            first_char_pr = char_pr

        text_parts = []
        for t in _find_all_elements(run_elem, 't', ns):
            if t.text:
                text_parts.append(t.text)

        if text_parts:
            runs.append({
                'charPrIDRef': char_pr,
                'text': ''.join(text_parts),
            })

    return {
        'first_charPrIDRef': first_char_pr,
        'runs': runs,
    }


def _find_direct_runs(p_elem, ns):
    """<hp:p> 직계 <hp:run>만 찾음 (subList 내부 제외)"""
    results = []
    for child in p_elem:
        tag = _local_tag(child)
        if tag == 'run':
            results.append(child)
    return results


# ================================================================
#  네임스페이스 감지
# ================================================================

def _detect_namespaces(raw_xml, parsed):
    """XML에서 실제 사용된 네임스페이스 URI를 감지한다.
    
    HWPX 버전에 따라 네임스페이스 URI가 다를 수 있다:
    - 2011 버전: http://www.hancom.co.kr/hwpml/2011/paragraph
    - 2016 버전: http://www.hancom.co.kr/hwpml/2016/paragraph (일부)
    """
    ns = dict(DEFAULT_NS)

    if raw_xml:
        # xmlns:hp="..." 패턴으로 실제 URI 추출
        for prefix in ['hp', 'ha', 'hh', 'hc']:
            pattern = rf'xmlns:{prefix}="([^"]+)"'
            match = re.search(pattern, raw_xml)
            if match:
                ns[prefix] = match.group(1)

    return ns


# ================================================================
#  XML 유틸리티 — 네임스페이스 불가지론적 탐색
# ================================================================

def _local_tag(elem):
    """'{namespace}localname' → 'localname'"""
    tag = elem.tag
    if '}' in tag:
        return tag.split('}', 1)[1]
    return tag


def _get_attr(elem, attr_name):
    """속성값 가져오기. 네임스페이스 유무 모두 시도."""
    # 직접 속성명
    val = elem.get(attr_name)
    if val is not None:
        return val

    # 네임스페이스 접두사가 붙은 속성 시도
    for full_attr in elem.attrib:
        if full_attr.endswith(attr_name):
            return elem.attrib[full_attr]

    return None


def _find_element(parent, local_name, ns):
    """자식 중 로컬명이 일치하는 첫 번째 엘리먼트를 찾는다.
    
    네임스페이스 prefix 시도 후, 실패하면 로컬명 직접 비교.
    """
    # 1차: 네임스페이스 prefix로 탐색
    for prefix in ['hp', 'hh', 'hc', 'ha']:
        uri = ns.get(prefix, '')
        found = parent.find(f'{{{uri}}}{local_name}')
        if found is not None:
            return found

    # 2차: 직계 자식 로컬명 비교
    for child in parent:
        if _local_tag(child) == local_name:
            return child

    # 3차: 재귀 탐색 (1단계만)
    for child in parent:
        for grandchild in child:
            if _local_tag(grandchild) == local_name:
                return grandchild

    return None


def _find_all_elements(parent, local_name, ns):
    """하위 전체에서 로컬명이 일치하는 모든 엘리먼트를 찾는다."""
    results = []

    def _walk(elem):
        if _local_tag(elem) == local_name:
            results.append(elem)
        for child in elem:
            _walk(child)

    _walk(parent)
    return results


# ================================================================
#  편의 함수
# ================================================================

def summarize(content_order):
    """content_order 리스트를 사람이 읽기 쉬운 요약으로 변환"""
    lines = []
    for item in content_order:
        idx = item['index']
        t = item['type']

        if t == 'paragraph':
            text_preview = item['text'][:50]
            if len(item['text']) > 50:
                text_preview += '...'
            lines.append(
                f"[{idx:3d}] P  paraPr={item['paraPrIDRef']:<4s} "
                f"charPr={item.get('charPrIDRef', '-'):<4s} "
                f"\"{text_preview}\""
            )
        elif t == 'table':
            lines.append(
                f"[{idx:3d}] T  table_idx={item['table_idx']} "
                f"({item.get('rowCnt', '?')}×{item.get('colCnt', '?')})"
            )
        elif t == 'image':
            ref = item.get('binaryItemIDRef', '?')
            caption = item.get('text', '')[:30]
            lines.append(
                f"[{idx:3d}] I  image_idx={item['image_idx']} "
                f"ref={ref} \"{caption}\""
            )
        elif t == 'empty':
            lines.append(f"[{idx:3d}] _  (empty)")

    return '\n'.join(lines)


def get_stats(content_order):
    """content_order 통계 반환"""
    type_map = {
        'paragraph': 'paragraphs',
        'table': 'tables',
        'image': 'images',
        'empty': 'empty',
    }
    stats = {
        'total': len(content_order),
        'paragraphs': 0,
        'tables': 0,
        'images': 0,
        'empty': 0,
    }
    for item in content_order:
        key = type_map.get(item['type'])
        if key:
            stats[key] += 1
    return stats