# -*- coding: utf-8 -*-
"""부여군 N(저작물 유형) + O/P/Q(공공누리) 전수 재판정.
사용자 검수(3~37행)를 정답으로 검증 후 38행~ 적용.

핵심(부여 buyeo 템플릿 특성):
- KOGL 마크는 게시판 '상세글'의 `div.gnuri_layer > .codeView01 > img[src=/_module/gnuri/images/img_opentypeNN.png]`
  + kogl.or.kr/info/licenseTypeN.do 링크. **본문(#txt) 바깥**이라 기존 phase234가 놓침.
- 상세 URL = `?mode=V&no=...`.
- 게시판은 글마다 유형 다를 수 있음 → O = **첫 마크 글의 유형**(사용자 컨벤션: 01030502 글1=1유형 → O=1유형).
- N 이미지: 장식/공통UI(file_icon·see_btn·btn_page·/skin/·/common/·ico/btn/bg) + KOGL 마크 제외.
  지도/PDF iframe = 이미지. 게시판 M=0 = 없음. 실제 업로드 사진만 이미지.

사용: python -X utf8 _buyeo_rejudge_NO.py --validate   # 3~37 사용자값과 비교
      python -X utf8 _buyeo_rejudge_NO.py --write       # 38행~ 기입(백업)
"""
import sys, warnings, re, time
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests, openpyxl
from bs4 import BeautifulSoup

warnings.filterwarnings('ignore')
S = requests.Session(); S.headers.update({'User-Agent': 'Mozilla/5.0'})
XLSX = r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\7.부여군\충청남도_부여군.xlsx'
DOMAIN = 'buyeo.go.kr'
REF_MAX = 37   # 3~37 = 사용자 검수 정답

KOGL_IMG = re.compile(r'(?:new_)?img_open(?:type|code)(\d{1,2})\.(?:png|jpe?g|gif)', re.I)
KOGL_LINK = re.compile(r'licenseType(\d)', re.I)
DECO = re.compile(
    r'/site/common/img/|move\.png|no[-_]?img|blank\.|spacer\.|'
    r'/img/(?:icon|ico|bul|bullet|arrow|btn|bg|tit|h\d)|'
    r'ico_|btn_|bul_|bg_|_bg\.|icon_|'
    r'file_icon|_icon\.|icon\.gif|_btn\.|see_btn|flag\.|webaccess|'
    r'/skin/|/images/(?:kr/)?common(?:_new)?/|/common/img/|/_module/gnuri/|'
    r'btn_page|page_(?:next|prev|first|last)', re.I)
MAPPDF = re.compile(r'pdf|viewer\.html|/map|kakao|daum|=map|naver.*map|google.*map', re.I)
VIDEO = re.compile(r'youtube\.com|youtu\.be|/embed/|\.mp4|\.webm|vimeo', re.I)
DETAIL = re.compile(r'mode=V&no=', re.I)


def get(url):
    last = None
    for _ in range(3):
        try:
            r = S.get(url, verify=False, timeout=20)
            return BeautifulSoup(r.content, 'html.parser')
        except Exception as e:
            last = e
            time.sleep(1.2)
    raise last


def body(soup):
    return soup.select_one('#txt') or soup.select_one('#contents') or soup.select_one('main') or soup


def real_img(node):
    for im in node.find_all('img'):
        src = im.get('src') or ''
        if not src or KOGL_IMG.search(src) or DECO.search(src):
            continue
        return True
    return False


def media_flags(node):
    img = real_img(node)
    vid = bool(node.find('video'))
    for ifr in node.find_all('iframe'):
        s = ifr.get('src') or ''
        if VIDEO.search(s):
            vid = True
        elif MAPPDF.search(s):
            img = True
    for a in node.find_all('a'):
        if VIDEO.search(a.get('href') or ''):
            vid = True
    return img, vid


def kogl_from(html):
    """전체 HTML에서 KOGL 이미지유형·링크유형 추출."""
    imgs = [int(x) for x in KOGL_IMG.findall(html) if 1 <= int(x) <= 4]
    links = [int(x) for x in KOGL_LINK.findall(html) if 1 <= int(x) <= 4]
    return imgs, links


def _menu_cd(u):
    m = re.search(r'menu_dvs_cd=([^&]+)', u or '')
    return m.group(1) if m else None


def detail_links(soup, base):
    """같은 게시판(_prog/_board + 동일 menu_dvs_cd) 상세글만. 타 게시판 stray 링크 배제."""
    if '/_prog/_board/' not in base:
        return []                         # 게시판 템플릿이 아니면 상세 없음(scate 등)
    base_menu = _menu_cd(base)
    out = []
    for a in body(soup).find_all('a', href=True):
        h = a['href']
        if not DETAIL.search(h):
            continue
        full = urljoin(base, h)
        if base_menu and _menu_cd(full) and _menu_cd(full) != base_menu:
            continue                      # 다른 게시판 글 → 제외
        out.append(full)
    seen = set(); res = []
    for u in out:
        if u not in seen:
            seen.add(u); res.append(u)
    return res[:5]


def decide_O(img_types, link_types):
    """KOGL 규칙: 이미지유형 우선. 링크만이면 licenseType, 1·2·3·4 전부=범례→미부착."""
    if img_types:
        t = img_types[0]
        mism = bool(link_types) and (link_types[0] != t)
        return f'{t}유형', mism
    if link_types:
        if {1, 2, 3, 4}.issubset(set(link_types)):
            return '미부착', False
        return f'{link_types[0]}유형', False
    return '미부착', False


def judge(k, L, M):
    """반환 dict: N, O, P, Q, S(mismatch)."""
    try:
        soup = get(k)
    except Exception as e:
        return {'err': f'fetch:{str(e)[:25]}'}
    bd = body(soup); full = str(soup)
    txt = len(bd.get_text(strip=True)) >= 1
    is_board_url = '/_prog/_board/' in k        # 실제 게시판 URL만(.html·서브사이트는 게시판 아님)

    has_img, has_vid = media_flags(bd)
    # O: 페이지/리스트 자체 마크 우선
    img_t, link_t = kogl_from(full)
    o_loc = '게시판' if L == '게시판' else '페이지'
    o_imgs, o_links = list(img_t), list(link_t)

    # '빈 게시판→없음'은 진짜 게시판(_prog/_board)에 글이 0개일 때만.
    # L=게시판 M=0 이라도 .html 페이지·서브사이트는 본문내용으로 판정(원본 L/M 오기 보정).
    dls = detail_links(soup, k) if (L == '게시판') else []
    board0 = (L == '게시판' and is_board_url and (M in (0, '0', None)) and not dls)

    if L == '게시판' and not board0:
        # N: 상위 5글 본문에서 실제 이미지/영상 누적(이미지 콘텐츠 회수율↑)
        for idx, du in enumerate(dls):
            try:
                ds = get(du); db = body(ds); df = str(ds)
            except Exception:
                continue
            di, dv = media_flags(db)
            if di: has_img = True
            if dv: has_vid = True
            # O: 리스트 마크 없으면 '첫(대표) 글'만으로 판정
            #    (사용자 컨벤션: 01030502 글1=1유형→1유형 / 업무추진비 글1무·글5만3유형→미부착)
            if idx == 0 and not o_imgs and not o_links:
                dimg, dlink = kogl_from(df)
                if dimg or dlink:
                    o_imgs, o_links, o_loc = dimg, dlink, '게시물'
            if has_img and has_vid:
                break

    # N
    if board0:
        N = '없음'
    else:
        parts = []
        if txt: parts.append('어문')
        if has_img: parts.append('이미지')
        if has_vid: parts.append('영상')
        N = ','.join(parts) if parts else '없음'

    O, mism = decide_O(o_imgs, o_links)
    P = (o_loc if O != '미부착' else None)
    Q = ('Y' if (O != '미부착' and o_links) else None)
    return {'N': N, 'O': O, 'P': P, 'Q': Q, 'mismatch': mism}


def main():
    mode = '--validate' if '--validate' in sys.argv else ('--write' if '--write' in sys.argv else '--dry')
    wb = openpyxl.load_workbook(XLSX); ws = wb.active

    rows = []
    for r in range(3, ws.max_row + 1):
        k = ws.cell(r, 11).value; L = ws.cell(r, 12).value; M = ws.cell(r, 13).value
        if not (isinstance(k, str) and DOMAIN in k):
            continue
        if L == '사이트':
            continue
        if mode == '--validate' and r > REF_MAX:
            continue
        if mode == '--write' and r <= REF_MAX:
            continue
        rows.append((r, k, L, M))
    print(f'mode={mode} | 대상 {len(rows)}행')

    res = {}
    t0 = time.time()
    with ThreadPoolExecutor(max_workers=3) as ex:
        futs = {ex.submit(judge, k, L, M): (r, k, L, M) for r, k, L, M in rows}
        done = 0
        for fut in as_completed(futs):
            r, k, L, M = futs[fut]
            res[r] = fut.result()
            done += 1
            if done % 40 == 0:
                print(f'  {done}/{len(rows)} ({time.time()-t0:.0f}s)')
    print(f'크롤링 완료 ({time.time()-t0:.0f}s)')

    if mode == '--validate':
        miss = 0
        for r, k, L, M in rows:
            d = res[r]
            if d.get('err'):
                print(f'  행{r} ERR {d["err"]}'); continue
            curN = ws.cell(r, 14).value; curO = ws.cell(r, 15).value
            curP = ws.cell(r, 16).value; curQ = ws.cell(r, 17).value
            dn = (d['N'] or '') != (curN or '')
            do = (d['O'] or '') != (curO or '')
            dp = (d['P'] or '') != (curP or '')
            dq = (d['Q'] or '') != (curQ or '')
            if dn or do or dp or dq:
                miss += 1
                g = ws.cell(r, 7).value or ws.cell(r, 6).value
                print(f'  ✗행{r} {str(g)[:16]:16}| N:{curN}→{d["N"]} {"≠" if dn else "="} | '
                      f'O:{curO}→{d["O"]}{"≠" if do else "="} P:{curP}→{d["P"]}{"≠" if dp else ""} Q:{curQ}→{d["Q"]}{"≠" if dq else ""}')
        print(f'\n불일치 {miss}/{len(rows)}행 (0이면 로직이 사용자 검수와 100% 일치)')
        return

    # write (38행~)
    if mode == '--write':
        import shutil
        bk = XLSX.replace('.xlsx', '_backup_NO재판정전.xlsx'); shutil.copy(XLSX, bk)
        print('백업:', bk)
        chg = 0
        for r, k, L, M in rows:
            d = res[r]
            if d.get('err'):
                print(f'  행{r} ERR {d["err"]}'); continue
            old = (ws.cell(r,14).value, ws.cell(r,15).value, ws.cell(r,16).value, ws.cell(r,17).value)
            ws.cell(r, 14).value = d['N']
            ws.cell(r, 15).value = d['O']
            ws.cell(r, 16).value = d['P']
            ws.cell(r, 17).value = d['Q']
            if d['mismatch']:
                cur = (ws.cell(r,19).value or '').strip()
                ws.cell(r,19).value = '링크주소 오기' if not cur else cur+' / 링크주소 오기'
            new = (d['N'], d['O'], d['P'], d['Q'])
            if old != new:
                chg += 1
                g = ws.cell(r,7).value or ws.cell(r,6).value
                print(f'  행{r} {str(g)[:16]:16}| N:{old[0]}→{new[0]} O:{old[1]}→{new[1]} P:{old[2]}→{new[2]} Q:{old[3]}→{new[3]}')
        wb.save(XLSX)
        print(f'\n저장: {XLSX} ({chg}행 변경)')
        return

    print('(--validate 또는 --write 지정)')


if __name__ == '__main__':
    main()
