# -*- coding: utf-8 -*-
"""DXF 추출기: TEXT/MTEXT/ATTRIB → 표제란·리비전 블록·본문 분류.

식별 휴리스틱 (순서대로):
  1. 레이어 이름에 TITLE 포함 → 표제란 / REV 포함 → 리비전 블록
  2. 레이어 힌트가 없으면 위치로: 우하단(오른쪽 40% × 아래 25%) → 표제란,
     우상단(오른쪽 40% × 위 25%) → 리비전 블록
형상(선·도형)은 읽지 않는다 (PRINCIPLE 7).
"""

from __future__ import annotations

import logging
import re
from pathlib import Path

import ezdxf

from watcher.extract.base import Document, Field, Row, TextLine

# ezdxf가 시스템 폰트 캐시를 만들며 한글 이름 폰트마다 "cannot open font ..."
# 경고를 쏟아낸다 — 감시 동작과 무관한 소음이라 오류만 남긴다 (2026-08-27 실PC)
logging.getLogger("ezdxf").setLevel(logging.ERROR)

# 표제란 라벨 별칭 → 정규 이름
_LABEL_ALIASES = {
    "DWG_NO": "DWG_NO", "DWG NO": "DWG_NO", "DWG.NO": "DWG_NO",
    "DRAWING NO": "DWG_NO", "도면번호": "DWG_NO",
    "PROJECT": "PROJECT", "PROJ": "PROJECT", "프로젝트": "PROJECT",
    "REV": "REV", "REVISION": "REV", "리비전": "REV",
    "DATE": "DATE", "일자": "DATE",
}

_Y_TOL = 3.0  # 같은 줄로 볼 y 허용 오차 (도면 단위)


def _collect_texts(path: Path) -> list[tuple[str, float, float, str]]:
    """(text, x, y, layer) 목록. TEXT/MTEXT/ATTRIB + 블록(INSERT) 내부 텍스트.

    실무 도면은 표제란을 블록으로 삽입하는 경우가 많다 — virtual_entities()로
    삽입 위치·배율이 반영된 좌표의 내부 텍스트까지 읽는다 (실물 테스트에서 확인된 격차).
    """
    doc = ezdxf.readfile(path)
    out = []

    def take(e) -> None:
        t = e.dxftype()
        if t == "TEXT":
            p = e.dxf.insert
            out.append((e.dxf.text.strip(), p.x, p.y, e.dxf.layer))
        elif t == "MTEXT":
            p = e.dxf.insert
            out.append((e.plain_text().strip(), p.x, p.y, e.dxf.layer))

    def expand_insert(e, depth: int) -> None:
        for a in e.attribs:
            p = a.dxf.insert
            out.append((a.dxf.text.strip(), p.x, p.y, a.dxf.layer))
        if depth <= 0:
            return
        try:
            for ve in e.virtual_entities():
                if ve.dxftype() == "INSERT":
                    expand_insert(ve, depth - 1)
                else:
                    take(ve)
        except Exception:
            pass  # 깨진 블록 참조 등 — 블록 하나 때문에 추출을 멈추지 않는다

    for e in doc.modelspace():
        if e.dxftype() == "INSERT":
            expand_insert(e, depth=2)
        else:
            take(e)
    return [t for t in out if t[0]]


def _classify(texts):
    """텍스트를 title/revblock/body로 분류."""
    xs = [x for _, x, _, _ in texts]
    ys = [y for _, _, y, _ in texts]
    x_hi = min(xs) + (max(xs) - min(xs)) * 0.6 if xs else 0
    y_lo = min(ys) + (max(ys) - min(ys)) * 0.25 if ys else 0
    y_hi = min(ys) + (max(ys) - min(ys)) * 0.75 if ys else 0

    title, revblock, body = [], [], []
    for item in texts:
        _, x, y, layer = item
        lu = layer.upper()
        if "TITLE" in lu:
            title.append(item)
        elif "REV" in lu:
            revblock.append(item)
        elif x >= x_hi and y <= y_lo:
            title.append(item)
        elif x >= x_hi and y >= y_hi:
            revblock.append(item)
        else:
            body.append(item)
    return title, revblock, body


def _parse_title(title) -> dict:
    fields: dict[str, Field] = {}
    for text, x, y, layer in title:
        canon = _LABEL_ALIASES.get(text.upper())
        if not canon:
            continue
        # 값 = 같은 줄(y 허용 오차 내) 오른쪽에서 가장 가까운 비라벨 텍스트
        candidates = [
            (vx, vtext) for vtext, vx, vy, _ in title
            if abs(vy - y) <= _Y_TOL and vx > x
            and vtext.upper() not in _LABEL_ALIASES
        ]
        if candidates:
            vx, vtext = min(candidates)
            fields[canon] = Field(canon, vtext,
                                  f"표제란(레이어 {layer}, x={vx:.0f}, y={y:.0f})")
    return fields


def _merge_lines(items) -> list:
    """같은 y의 텍스트 조각을 한 줄로 병합. 위→아래, 왼쪽→오른쪽 순."""
    groups: dict[float, list] = {}
    for text, x, y, layer in items:
        key = next((k for k in groups if abs(k - y) <= _Y_TOL), y)
        groups.setdefault(key, []).append((x, text, layer))
    lines = []
    for y in sorted(groups, reverse=True):
        parts = sorted(groups[y])
        text = " ".join(p[1] for p in parts)
        layer = parts[0][2]
        lines.append(TextLine(text, f"레이어 {layer}, y={y:.0f}"))
    return lines


_DATE_TOKEN = re.compile(r"\d{4}[-./]\d{1,2}[-./]\d{1,2}|\d{6,8}")


def _looks_like_rev_row(tokens: list[str]) -> bool:
    """Rev 행 검증: 첫 토큰이 짧은 개정 코드꼴 + 둘째 토큰이 날짜꼴.
    위치 휴리스틱이 우상단의 일반 텍스트(방 이름·치수)를 리비전으로 오인하는 것 방지
    (실물 도면 테스트에서 확인된 오탐)."""
    return (len(tokens) >= 2 and len(tokens[0]) <= 4 and tokens[0].isalnum()
            and bool(_DATE_TOKEN.search(tokens[1])))


def _parse_revblock(revblock) -> tuple[list, list]:
    """(리비전 행들, 리비전이 아니어서 본문으로 돌려보낼 항목들)."""
    rows, leftover = [], []
    for line in _merge_lines(revblock):
        tokens = line.text.split()
        if tokens and tokens[0].upper() == "REV":  # 헤더 행
            continue
        if _looks_like_rev_row(tokens):
            rows.append(Row(key=tokens[0],
                            values={"DATE": tokens[1],
                                    "DESCRIPTION": " ".join(tokens[2:])},
                            where=f"리비전 블록({line.where})"))
        else:
            leftover.append(line)
    return rows, leftover


def extract(path: Path) -> Document:
    texts = _collect_texts(Path(path))
    title, revblock, body = _classify(texts)
    rev_rows, not_rev = _parse_revblock(revblock)
    return Document(
        doc_type="dxf",
        path=str(path),
        fields=_parse_title(title),
        revision_history=rev_rows,
        raw_texts=_merge_lines(body) + not_rev,
    )
