# -*- coding: utf-8 -*-
"""일관성 검사. 의미(옳다/그르다)는 판단하지 않는다 — 서로 안 맞는 것만 찾는다 (PRINCIPLE 6).

검사 4종:
  CONTENT_CHANGED_REV_SAME       내용이 바뀌었는데 표제란 Rev 그대로 (실수1)
  REV_CHANGED_CONTENT_SAME       Rev만 올라가고 내용 동일 (실수2)
  TAG_MISMATCH_ACROSS_DRAWINGS   같은 프로젝트 내 도면 간 같은 태그의 사양 불일치
  BOM_MISMATCH                   도면 태그 사양 ↔ BOM SPEC 불일치
모든 Issue는 근거(evidence: 구값/신값/위치) 없이는 만들 수 없다 (PRINCIPLE 4).
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import NamedTuple

from watcher.diff import ChangeItem, content_items
from watcher.extract.base import Document

CODE_LABEL = {
    "CONTENT_CHANGED_REV_SAME": "내용 변경, Rev 미갱신",
    "REV_CHANGED_CONTENT_SAME": "Rev만 갱신, 내용 동일",
    "TAG_MISMATCH_ACROSS_DRAWINGS": "도면 간 태그 사양 불일치",
    "BOM_MISMATCH": "도면-BOM 불일치",
}


@dataclass(frozen=True)
class Issue:
    code: str
    path: str          # 대상 파일 (프로젝트 수준이면 대표 경로)
    message: str
    evidence: tuple = field(default_factory=tuple)  # 근거 문자열들


def _fmt_item(i: ChangeItem) -> str:
    return f"{i.key}: {i.old or '—'} → {i.new or '—'} ({i.where})"


def check_revision(path: str, old: Document, new: Document,
                   items: list[ChangeItem]) -> list[Issue]:
    """한 파일의 구/신 쌍에 대한 Rev 일관성."""
    issues = []
    rev_change = next((i for i in items if i.kind == "FIELD_CHANGED" and i.key == "REV"), None)
    content = content_items(items)

    if content and rev_change is None and new.revision is not None:
        # 메시지에 건수를 넣지 않는다 — 발행 전 작업본을 매일 저장하는 동안
        # 건수만 바뀐 같은 경고가 날마다 재알림되던 문제 (2026-08-26 설계 검토:
        # 같은 내용은 30일 중복 억제에 걸려 브리핑에 1회만 실린다)
        ev = [f"표제란 REV: {new.revision} (변경 전후 동일)",
              f"이번 변경 {len(content)}건 중 예시:"]
        ev += [_fmt_item(i) for i in content[:5]]
        issues.append(Issue(
            "CONTENT_CHANGED_REV_SAME", path,
            f"내용이 바뀌었는데 표제란 Rev가 {new.revision} 그대로입니다. "
            f"(발행 전 작업본이면 무시하세요 — 발행 시 Rev를 올리면 해소됩니다)",
            tuple(ev)))

    if rev_change is not None and not content:
        ev = [f"표제란 REV: {rev_change.old} → {rev_change.new} ({rev_change.where})",
              "글자·표 변경: 0건 — 형상(선·도형)만 바뀐 정상 개정일 수 있습니다"
              " (그림 변경은 텍스트 비교에 잡히지 않습니다)"]
        claimed = [i for i in items if i.kind == "REVROW_ADDED"]
        ev += [f"리비전 블록 기재: {_fmt_item(i)}" for i in claimed[:2]]
        issues.append(Issue(
            "REV_CHANGED_CONTENT_SAME", path,
            f"Rev가 {rev_change.old}→{rev_change.new}로 올라갔는데 글자 변경이 없습니다.",
            tuple(ev)))
    return issues


class _Occ(NamedTuple):
    """한 도면에서 태그가 등장한 지점 (사양 토큰 + 근거)."""
    spec: str
    text: str
    path: str
    where: str
    dwg_no: str


# 구분자만 있는 토큰 — 사양값이 아니다 ('M-302 : 15kW'의 ':')
_SEP_TOKENS = {":", "-", "=", "·", "|", "/", "→", ","}

# 표기 차이를 흡수하는 정규화: 소문자화 + 공백/구분자 제거 + 단위 동의어
# ('5.5kW' vs '5.5 KW' vs '5.5㎾'가 전부 경보가 되던 문제 — 2026-08-26 검토)
_UNIT_SYNONYM = (("㎾", "kw"), ("㎟", "sq"), ("ø", "d"), ("Φ", "d"), ("φ", "d"))


def _norm(s: str) -> str:
    out = s.lower()
    for a, b in _UNIT_SYNONYM:
        out = b.join(out.split(a))
    return "".join(ch for ch in out if ch not in " \t:·|,")


def _spec_token(line_text: str, tag: str) -> str:
    """태그 다음의 첫 '값' 토큰 = 사양값 (예: 'M-302 : 15kW DOL' → '15kW').

    줄 전체를 비교하면 문서 유형별 표기 차이('11kW' vs '11kW INVERTER DRIVEN')가
    전부 오탐이 된다. 값 토큰 하나만 비교하되, 구분자 토큰(':' 등)은 건너뛴다.
    """
    idx = line_text.find(tag)
    for tok in line_text[idx + len(tag):].split():
        if tok not in _SEP_TOKENS:
            return tok
    return ""


def check_project_consistency(docs: list[tuple[str, Document]]) -> list[Issue]:
    """프로젝트 단위 교차 검사: 도면 간 태그 불일치 + 도면-BOM 불일치."""
    issues = []
    by_project: dict[str, list[tuple[str, Document]]] = {}
    for path, doc in docs:
        proj = doc.project
        if proj:
            by_project.setdefault(proj, []).append((path, doc))

    for proj, members in sorted(by_project.items()):
        drawings = [(p, d) for p, d in members if d.doc_type in ("dxf", "pdf")]
        boms = [(p, d) for p, d in members if d.doc_type == "xlsx" and d.rows]

        # 도면별 태그→줄 사전은 1회만 계산해 재사용 — BOM 행마다 재계산하면
        # (BOM 1,000행 × 도면 30장 = 3만 번 전체 스캔) 프로젝트당 수 초가 걸린다
        # (2026-08-25 확장성 검토: 7.9초 → 캐시 후 밀리초)
        # 태그의 전 출현 줄 — 첫 줄만 보면 심볼 옆/결선표 두 곳에 나오는
        # 태그에서 엉뚱한 줄과 비교하게 된다 (2026-08-26)
        tag_map = {path: d.tag_occurrences() for path, d in drawings}

        # 도면 간: 같은 태그의 사양값 토큰이 다르게 등장 (도면 번호가 다른 것끼리만).
        # 전 출현을 모은 뒤 비교한다 — 첫 출현과만 비교하면 사양 없는 줄(태그가 줄 끝)이
        # 먼저 나올 때 이후 도면들 간의 실제 불일치가 가려진다.
        occurrences: dict[str, list[_Occ]] = {}
        for path, d in drawings:
            dwg_no = d.fields["DWG_NO"].value if "DWG_NO" in d.fields else path
            for tag, lines in tag_map[path].items():
                for line in lines:
                    spec = _spec_token(line.text, tag)
                    if spec:  # 사양 없는 출현은 비교 대상이 아니고, 다른 출현을 가리지도 않는다
                        occurrences.setdefault(tag, []).append(
                            _Occ(spec, line.text, path, line.where, dwg_no))
        for tag, occs in sorted(occurrences.items()):
            base = occs[0]
            conflict = next((o for o in occs[1:]
                             if o.dwg_no != base.dwg_no
                             and _norm(o.spec) != _norm(base.spec)), None)
            if conflict:
                issues.append(Issue(
                    "TAG_MISMATCH_ACROSS_DRAWINGS", conflict.path,
                    f"[{proj}] 태그 {tag}의 사양이 도면 간에 다릅니다"
                    f" ({base.spec} vs {conflict.spec}).",
                    (f"{base.dwg_no}: {base.text} ({base.where}, {base.path})",
                     f"{conflict.dwg_no}: {conflict.text} ({conflict.where}, {conflict.path})")))

        # 도면-BOM: BOM SPEC 값이 해당 태그의 도면 줄에 없으면 불일치.
        # 같은 태그가 도면 30장에 있으면 30건이 아니라 **태그당 1건**만 —
        # 대형 프로젝트에서 이슈 수천 건으로 화면·브리핑이 마비되는 것 방지
        # (2026-08-25 확장성 검토: 시나리오 1,493건 → 태그당 1건)
        for bom_path, bom in boms:
            seen_tags: set[str] = set()
            for row in bom.rows:
                tag = row.values.get("TAG")
                spec = row.values.get("SPEC")
                if not tag or not spec or tag in seen_tags:
                    continue
                nspec = _norm(spec)

                def _mismatch(lines) -> bool:
                    # 태그의 어느 출현 줄에라도 사양이 (정규화 후) 있으면 일치
                    return not any(nspec in _norm(ln.text) for ln in lines)

                for path, d in drawings:
                    lines = tag_map[path].get(tag)
                    if lines and _mismatch(lines):
                        n_more = sum(
                            1 for p2, _ in drawings
                            if p2 != path and tag_map[p2].get(tag)
                            and _mismatch(tag_map[p2][tag]))
                        extra = f" (외 도면 {n_more}장에서도 불일치)" if n_more else ""
                        issues.append(Issue(
                            "BOM_MISMATCH", bom_path,
                            f"[{proj}] {tag}: BOM 사양과 도면 기재가 다릅니다.{extra}",
                            (f"BOM: {tag} SPEC={spec} ({row.where}, {bom_path})",
                             f"도면: {lines[0].text} ({lines[0].where}, {path})")))
                        seen_tags.add(tag)
                        break
    return issues
