# -*- coding: utf-8 -*-
"""수집완료 기관 N(저작물 유형) 일괄 재검토 (2026-06-01, 당진 규칙 일반화).

매뉴얼 3-1a/3-3/3-4b 반영:
- 이미지 = 별도 정보 주는 것만(사진·평면도·지도·QR·악보·소식지·상징물·도표).
- 어문 = 글 설명 인포그래픽(한눈에式)·로고·파트너로고·아이콘·배너·헤드라인텍스트·버튼·팝업·웹접근성마크.
- 오디오 = <audio>·mp3/wav/m4a 링크(다운로드 쿼리형 포함).
- 지도/PDF 임베드 = 이미지.
- 빈 게시판(M=0) = 없음. 사이트(외부) = N 미변경(빈칸 유지).

N만 재계산하고 L/M/O/P/Q 등 다른 컬럼은 보존. 지역 phase234 모듈의 fetch/get_body/
extract_detail_urls/정규식/SITES 재사용. 검수완료(계룡·공주·금산·논산·보령)+당진 제외.

사용:
  python -X utf8 _redo_N_all.py dry [기관...]     # 변경 로그만(읽기전용)
  python -X utf8 _redo_N_all.py run [기관...]     # 백업 후 N 기입
  (기관 미지정 시 3개 지역 전체 ✅ 기관)
"""
import sys, os, io, re, shutil, importlib.util, warnings
from concurrent.futures import ThreadPoolExecutor, as_completed
import openpyxl
warnings.filterwarnings('ignore')

HERE = os.path.dirname(os.path.abspath(__file__))
MODULES = ['_chungnam_phase234_all.py', '_chungbuk_phase234_all.py', '_jeonbuk_phase234_all.py']
EXCLUDE_INST = {'계룡시', '공주시', '금산군', '논산시', '보령시', '당진시'}  # 검수완료 + 당진

# ── 이미지 분류 (벤더 공통) ───────────────────────────────
DECO = re.compile(r'/common/|move\.png|no[-_]?img|blank|spacer|/ico|/btn|bullet|arrow|/bg|icon|see_btn|/sample|mimetype|/file_|filedown|btn_dir', re.I)
EXCLUDE = re.compile(
    r'한눈에|흐름도|절차도|처리절차|이용절차'
    r'|로고(?!송)|logo(?!song)|아이콘|배너|banner'
    r'|신문고|relation_item|tracer|headline'
    r'|카피라이트|copyright|copy_logo|popup|/pup/|wa_mk|웹접근성|품질인증', re.I)
AUDIO = re.compile(r'\.(?:mp3|wav|m4a|ogg|flac)\b', re.I)
MAPPDF = re.compile(r'pdf|viewer\.html|/map|kakao|daum.*map', re.I)


def load_module(fname):
    spec = importlib.util.spec_from_file_location(fname[:-3], os.path.join(HERE, fname))
    m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
    return m


def real_imgs(M, body):
    out = []
    for img in body.find_all('img'):
        src = img.get('src') or ''
        if not src or M.KOGL_IMG_PAT.search(src) or DECO.search(src):
            continue
        if EXCLUDE.search(src + ' ' + (img.get('alt') or '')):
            continue
        out.append(img)
    return out


def media2(M, body):
    has_text = len(body.get_text(strip=True)) > 30
    img = len(real_imgs(M, body)) > 0
    vid = False
    for ifr in body.find_all('iframe'):
        s = ifr.get('src') or ''
        if M.YOUTUBE_PAT.search(s):
            vid = True
        elif MAPPDF.search(s):
            img = True
    if not vid and (body.find('video') or body.find('a', href=M.YOUTUBE_PAT) or M.VIDEO_EXT.search(str(body))):
        vid = True
    aud = bool(body.find('audio')) or bool(AUDIO.search(str(body)))
    return img, vid, aud, has_text


def make_fetch(M, cfg):
    """모듈별 fetch 시그니처 차이 흡수: 충남=fetch(url), 충북/전북=fetch(session,url)."""
    if hasattr(M, 'make_session'):
        sess = M.make_session(weak_ssl=cfg.get('weak_ssl', False))
        return lambda u: M.fetch(sess, u)
    return lambda u: M.fetch(u)


def _fetch_body(M, url, body_sel, fetchfn, tries=3):
    """throttle 대비 재시도. 본문 텍스트>30 또는 미디어 잡히면 즉시 반환."""
    import time
    last = None
    for k in range(tries):
        soup = fetchfn(url)
        if soup is not None:
            body = M.get_body(soup, body_sel)
            last = media2(M, body) + (body,)
            if last[3] or last[0] or last[1] or last[2]:  # txt/img/vid/aud 중 하나라도
                return last
        time.sleep(0.6 * (k + 1))
    return last  # 끝까지 비면 마지막(또는 None)


def n_of(M, url, L, body_sel, fetchfn):
    res = _fetch_body(M, url, body_sel, fetchfn)
    if res is None:
        return None  # 접근 실패 → 기존 N 유지
    img, vid, aud, txt, body = res
    if not (txt or img or vid or aud):
        return None  # 재시도해도 빈 본문 → 신뢰불가, 기존 N 유지(거짓 '없음' 차단)
    if L == '게시판':
        detail_urls = M.extract_detail_urls(body, url, limit=5)
        data_rows = [tr for tr in body.select('table tbody tr, .board_list li, ul.bbs_list li') if tr.find('a')]
        empty_msg = bool(re.search(r'게시물이?\s*없|등록된\s*(?:게시물|자료)\s*가?\s*없|자료가\s*없', body.get_text(' ', strip=True)))
        if not detail_urls and not data_rows and (empty_msg or not txt):
            return '없음'   # 실제 글 0개 = 진짜 빈 게시판 (M값 무시, 내용기반)
        for du in detail_urls:
            ds = fetchfn(du)
            if not ds:
                continue
            db = M.get_body(ds, body_sel)
            i2, v2, a2, t2 = media2(M, db)
            img = img or i2; vid = vid or v2; aud = aud or a2; txt = txt or t2
    parts = []
    if txt:
        parts.append('어문')
    if img:
        parts.append('이미지')
    if vid:
        parts.append('영상')
    if aud:
        parts.append('오디오')
    return ','.join(parts) if parts else '없음'


def do_inst(M, name, cfg, mode, log):
    xlsx = cfg['xlsx']
    if not os.path.exists(xlsx):
        log.write(f'[{name}] 엑셀 없음\n'); return (name, 0, 0)
    body_sel = cfg['body_sel']
    fetchfn = make_fetch(M, cfg)
    wb = openpyxl.load_workbook(xlsx); ws = wb.active
    jobs = []
    for r in range(3, ws.max_row + 1):
        L = ws.cell(r, 12).value
        K = ws.cell(r, 11).value
        Mq = ws.cell(r, 13).value
        if not (isinstance(K, str) and K.startswith('http')):
            continue
        if L == '사이트':
            continue
        if L not in ('페이지', '게시판'):
            continue
        jobs.append((r, K, L))   # M=0이어도 내용기반으로 n_of가 판정(거짓 M=0 대응)
    results = {}
    with ThreadPoolExecutor(max_workers=4) as ex:
        futs = {}
        for r, K, L in jobs:
            if K == '__EMPTY__':
                results[r] = '없음'
            else:
                futs[ex.submit(n_of, M, K, L, body_sel, fetchfn)] = r
        for f in as_completed(futs):
            r = futs[f]
            try:
                results[r] = f.result()
            except Exception:
                results[r] = None
    changed = 0
    for r, newN in sorted(results.items()):
        if newN is None:
            continue
        oldN = ws.cell(r, 14).value
        if newN != oldN:
            changed += 1
            log.write(f'  [{name}] r{r} {oldN}→{newN}\n')
            if mode == 'run':
                ws.cell(r, 14).value = newN
    total = len([j for j in jobs])
    if mode == 'run' and changed:
        bak = xlsx.replace('.xlsx', '_backup_N재검토전.xlsx')
        shutil.copy(xlsx, bak)
        wb.save(xlsx)
    return (name, total, changed)


def main():
    mode = sys.argv[1] if len(sys.argv) > 1 else 'dry'
    only = set(a for a in sys.argv[2:] if not a.startswith('--'))
    log = io.open(os.path.join(HERE, '_redo_N_all_log.txt'), 'w', encoding='utf-8')
    grand = []
    for fname in MODULES:
        M = load_module(fname)
        for name, cfg in M.SITES.items():
            if name in EXCLUDE_INST:
                continue
            if only and name not in only:
                continue
            res = do_inst(M, name, cfg, mode, log)
            grand.append(res)
            print(f'{name:7} 처리 {res[1]:4}행 | N변경 {res[2]:4}  ({mode})', flush=True)
    log.close()
    print('=' * 50)
    print(f'총 {len(grand)}개 기관 | 변경합계 {sum(r[2] for r in grand)}행 ({mode}) | 로그 _redo_N_all_log.txt')


if __name__ == '__main__':
    main()
