"""본문 탭(서브내비) → 카테고리 하위 확장.

규칙(공주시 기준, 일반화):
  본문에서 '탭 UL'을 찾아, 아래 조건을 모두 만족하는 탭 그룹만 하위 카테고리로 확장한다.
    1) UL(또는 직계 div)의 class 에 탭 패턴(tab-ul 등)이 있다.
    2) 탭이 2개 이상이고, 모든 탭 href 가 '실제 페이지 링크'다.
       (#anchor 인페이지 탭, javascript:, 빈 href, 쿼리스트링만 다른 필터 탭은 제외)
    3) 현재 페이지 URL 이 탭 URL 집합에 포함된다(= 자기 자신의 탭 그룹).
    4) 탭 중 '엑셀에 아직 없는 URL'이 1개 이상 있다(이미 사이트맵에 다 있으면 상위 nav → 제외).

  확장 방식(=사용자 지시: 탭들은 한 단계 아래 컬럼으로):
    - 원래 행의 leaf 컬럼(E~J 중 가장 깊은 값)을 부모(카테고리)로 두고,
      탭들을 leaf+1 컬럼에 탭 순서대로 채운다.
    - 현재 페이지와 같은 URL의 탭 = 원래 행을 재사용(L~T 기존 데이터 보존, G만 탭명 기입).
    - 나머지 탭 = 신규 행(L~T 비움, Phase 2~4 별도 수행 대상).

사용법:
  python -X utf8 _tab_expand.py <엑셀경로> <BASE_URL> <도메인키워드> [--write]
  예) python -X utf8 _tab_expand.py 충청남도/2.공주시/충청남도_공주시.xlsx https://www.gongju.go.kr gongju.go.kr
      (--write 없으면 계획만 출력 / 있으면 백업 후 실제 기입)
"""
import re
import sys
import shutil
import warnings
from copy import copy
from concurrent.futures import ThreadPoolExecutor, as_completed
from urllib.parse import urljoin, urlsplit, urlunsplit

import ssl
import openpyxl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
from bs4 import BeautifulSoup
from openpyxl.styles import Alignment, Font
from openpyxl.utils import get_column_letter

warnings.filterwarnings('ignore')

UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
H = {'User-Agent': UA}


class WeakSSLAdapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        ctx = create_urllib3_context()
        ctx.set_ciphers('DEFAULT@SECLEVEL=0')
        ctx.options |= 0x4
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        kwargs['ssl_context'] = ctx
        return super().init_poolmanager(*args, **kwargs)


SESSION = requests.Session()
SESSION.headers.update(H)

TAB_CLASS_PATS = ['tab-ul', 'tab_ul', 'tabmenu', 'tab-menu', 'tab_menu',
                  'tablist', 'tab-list', 'tab_list', 'subtab', 'sub-tab', 'sub_tab']
# 토큰 단위 탭 클래스 정규식: 'basic_tab', 'tab_wrap', 'tab-ul', 단독 'tab' 등 매칭.
# (UL 자신뿐 아니라 직계 부모 div 클래스도 검사 → div.basic_tab > ul 구조 대응)
TAB_CLASS_RE = re.compile(
    r'(?:^|[-_ ])tab(?:[-_ ]|$)|tabmenu|tablist|tab[-_]?(?:ul|wrap|list|menu)|basic[-_]tab')

MAXCOL = 27  # AA
CAT_COLS = [5, 6, 7, 8, 9, 10]  # E F G H I J
HEADER_MERGES = {'B1:R1', 'S1:W1', 'Y1:AA1'}


def norm_url(u):
    """비교용 키: 프래그먼트 제거 + 끝 슬래시 정리 + 쿼리 정렬 보존.
    (쿼리만 다른 게시판 분류 탭 ?code=A vs ?code=B 를 서로 다른 URL로 구분 →
     is_cur·existing 중복판정 오류 방지)"""
    if not u:
        return ''
    s = urlsplit(u)
    path = s.path.rstrip('/')
    q = '&'.join(sorted(s.query.split('&'))) if s.query else ''
    return urlunsplit((s.scheme, s.netloc, path, q, '')).lower()


def path_key(u):
    """쿼리 제거한 경로 키 (필터 탭 판별용)."""
    if not u:
        return ''
    s = urlsplit(u)
    return urlunsplit((s.scheme, s.netloc, s.path.rstrip('/'), '', '')).lower()


def abs_url(href, base):
    if not href:
        return ''
    href = href.strip()
    if href.startswith(('javascript:', '#')):
        return ''
    if href.startswith(('http://', 'https://')):
        return href
    return urljoin(base.rstrip('/') + '/', href.lstrip('/'))


def has_tab_class(el):
    if el is None or not getattr(el, 'get', None):
        return False, ''
    cls = ' '.join(el.get('class') or []).lower()
    ok = any(p in cls for p in TAB_CLASS_PATS) or bool(TAB_CLASS_RE.search(cls))
    return ok, cls


def _has_on_li(ul):
    """ul 직계 li(또는 그 a)에 활성 탭 마커(on/active/current/selected)가 있나."""
    marks = {'on', 'active', 'current', 'selected', 'sel'}
    for li in ul.find_all('li', recursive=False):
        if marks & set(c.lower() for c in (li.get('class') or [])):
            return True
        a = li.find('a')
        if a and (marks & set(c.lower() for c in (a.get('class') or []))):
            return True
    return False


def find_tab_groups(soup, base):
    """조건 1·2 만족하는 탭 그룹 후보 반환: [(cls, [(label, abs_url),...], has_on), ...]"""
    out = []
    seen = set()
    for ul in soup.find_all('ul'):
        ok, cls = has_tab_class(ul)
        if not ok:
            # UL 무클래스라도 직계 부모 div 에 탭 클래스가 있으면 인정 (div.basic_tab > ul)
            pok, pcls = has_tab_class(ul.parent)
            if not pok:
                continue  # 쿼리필터·무클래스 ul 배제
            cls = pcls
        links = []
        real = True
        for a in ul.find_all('a'):
            t = a.get_text(strip=True).replace('\xa0', '').strip()
            raw = (a.get('href') or '').strip()
            au = abs_url(raw, base)
            if not t:
                continue
            if not au:           # #anchor / javascript / 빈 href → 인페이지 탭
                real = False
                break
            links.append((t, au))
        if not real or len(links) < 2:
            continue
        key = tuple(nu for _, (nu) in [(t, u) for t, u in links])
        if key in seen:
            continue
        seen.add(key)
        out.append((cls, links, _has_on_li(ul)))
    return out


def fetch(r, url):
    try:
        resp = SESSION.get(url, timeout=15, verify=False)
        # 바이트로 받아 BeautifulSoup가 meta charset으로 인코딩 판별 (apparent_encoding 오판 방지)
        return r, url, resp.content, None
    except Exception as e:
        return r, url, None, str(e)[:60]


def load_flat(ws):
    """병합값을 채워넣어 행별 완전 데이터로 평탄화. 반환: rows(list of dict), 스타일/하이퍼링크 캡처."""
    # 1) 병합값 채우기 (헤더 제외, 데이터 컬럼)
    for mr in list(ws.merged_cells.ranges):
        s = str(mr)
        if s in HEADER_MERGES:
            continue
        top = ws.cell(mr.min_row, mr.min_col).value
        ws.unmerge_cells(s)
        for rr in range(mr.min_row, mr.max_row + 1):
            for cc in range(mr.min_col, mr.max_col + 1):
                if ws.cell(rr, cc).value in (None, ''):
                    ws.cell(rr, cc).value = top
    rows = []
    for r in range(3, ws.max_row + 1):
        # 빈 행 스킵
        if all(ws.cell(r, c).value in (None, '') for c in range(2, MAXCOL + 1)):
            continue
        vals = {c: ws.cell(r, c).value for c in range(1, MAXCOL + 1)}
        styles = {}
        for c in range(1, MAXCOL + 1):
            sc = ws.cell(r, c)
            styles[c] = (copy(sc.font), copy(sc.fill), copy(sc.border),
                         copy(sc.alignment), sc.number_format, copy(sc.protection))
        hl = ws.cell(r, 11).hyperlink
        rows.append({'src': r, 'vals': vals, 'styles': styles,
                     'hyperlink': hl.target if hl else None})
    return rows


def leaf_col(vals):
    """E~J 중 값이 있는 가장 깊은 컬럼 인덱스."""
    deep = 5
    for c in CAT_COLS:
        if vals.get(c) not in (None, ''):
            deep = c
    return deep


def main():
    xlsx, base, domain = sys.argv[1], sys.argv[2], sys.argv[3]
    write = '--write' in sys.argv
    # menuCd 벤더(고창·임실·정읍·진안 등 index.{name}?menuCd=…): 모든 페이지가 같은 경로,
    # 쿼리(menuCd)만 다름 = 다른 페이지. → 쿼리-경로 가드(조건2) 스킵 + cur∈탭(조건3) 대신
    # div.basic_tab 등에 활성탭(li.on) 마커가 있는 '자기 sub-nav'만 인정(랜딩 URL이 탭과 달라도).
    menucd = '--menucd' in sys.argv
    # 서산 등 쿼리기반(contents.do?key=, selectBbsNttList.do?bbsNo=) 벤더:
    # 모든 탭이 같은 경로·쿼리만 다름(=다른 페이지)이고 한 탭바에 페이지+게시판 혼재.
    # → 쿼리가드(같은경로=필터 제외) 끄고, div.tab_menu>ul.tab_button 그룹만 인정.
    noqg = '--noqg' in sys.argv
    if '--weak-ssl' in sys.argv:
        SESSION.mount('https://', WeakSSLAdapter())

    wb = openpyxl.load_workbook(xlsx)
    ws = wb.active
    rows = load_flat(ws)
    existing = set()
    for row in rows:
        u = row['vals'].get(11)
        if isinstance(u, str) and u.startswith('http'):
            existing.add(norm_url(u))

    # 같은 도메인 행 fetch
    targets = [(row['src'], row['vals'].get(11)) for row in rows
               if isinstance(row['vals'].get(11), str) and domain in row['vals'].get(11)]
    html_by_src = {}
    with ThreadPoolExecutor(max_workers=8) as ex:
        futs = [ex.submit(fetch, r, u) for r, u in targets]
        for fut in as_completed(futs):
            r, url, html, err = fut.result()
            if html:
                html_by_src[r] = (url, html)

    # 이미 사이트맵에 자식행이 있는 '랜딩행' 집합 (다음 평탄행이 같은 상위카테고리 + 더 깊은 leaf).
    # menuCd 벤더 랜딩은 첫 자식의 탭을 렌더하므로, 확장하면 그 자식행과 중복 → 제외.
    landing_src = set()
    for i in range(len(rows) - 1):
        v, nv = rows[i]['vals'], rows[i + 1]['vals']
        lc = leaf_col(v)
        if lc < 10 and nv.get(lc + 1) not in (None, '') \
           and all((v.get(c) or '') == (nv.get(c) or '') for c in CAT_COLS if c <= lc):
            landing_src.add(rows[i]['src'])

    # 행별 확장 계획
    plan = {}   # src_row -> ordered [(label, url, is_existing)]
    planned_new = set()   # 이미 어느 부모행이 추가한 신규 탭 URL(전역 중복 방지)
    for row in rows:
        src = row['src']
        if src not in html_by_src:
            continue
        if menucd and src in landing_src:
            continue   # 자식 보유 랜딩 → 확장 금지(첫 자식 탭 중복 방지)
        url, html = html_by_src[src]
        cur = norm_url(url)
        soup = BeautifulSoup(html, 'html.parser')
        groups = find_tab_groups(soup, base)
        chosen = None
        for cls, links, has_on in groups:
            if menucd:
                # menuCd 벤더: 현재 페이지와 '같은 경로'(menuCd만 다른 형제)인 탭만 인정.
                # → /gochang/toc/GC…(향토문화대전 백과 목록) 등 다른경로 링크 배제.
                # 활성탭(li.on) 마커가 있는 자기 sub-nav만(랜딩 URL이 탭에 없어도 OK).
                if not has_on:
                    continue
                links = [(t, u) for t, u in links if path_key(u) == path_key(cur)]
                if len(links) < 2:
                    continue
                tab_norms = [norm_url(u) for _, u in links]
            else:
                if noqg and not ('tab_button' in cls or 'tab_menu' in cls):
                    continue   # 서산: 본문 탭바(tab_menu/tab_button)만 — GNB·푸터 nav 배제
                tab_norms = [norm_url(u) for _, u in links]
                if not noqg and len({path_key(u) for _, u in links}) == 1:
                    continue   # 조건2: 같은 경로(쿼리만 다른 필터 탭) → 제외 (noqg면 쿼리=다른페이지라 허용)
                if cur not in tab_norms:
                    continue                   # 조건3: 자기 탭그룹만
            new_cnt = sum(1 for n in tab_norms if n not in existing)
            if new_cnt < 1:
                continue                       # 조건4: 신규 0 → 상위nav, 제외
            chosen = links
            break
        if chosen:
            ch = []
            for label, u in chosen:
                is_cur = norm_url(u) == cur
                nu = norm_url(u)
                if not is_cur and nu in existing:
                    continue  # 이미 사이트맵에 별도 행으로 존재 → 중복행 방지(skip)
                if not is_cur and nu in planned_new:
                    continue  # 다른 부모행이 이미 추가한 신규 URL → 전역 중복 방지
                if not is_cur:
                    planned_new.add(nu)
                is_ext = domain not in u   # 같은 도메인 아님 = 외부 사이트 탭
                ch.append((label, u, is_cur, is_ext))
            # 신규(비-cur) 탭이 1개 이상일 때만 확장. 아니면 원본행 그대로 둠(삭제 방지).
            if any(not c[2] for c in ch):
                plan[src] = ch

    # 계획 출력
    print(f'=== 탭 확장 계획: {len(plan)}개 그룹 ===\n')
    total_new = 0
    for src in sorted(plan):
        row = next(r for r in rows if r['src'] == src)
        v = row['vals']
        lc = leaf_col(v)
        childc = lc + 1
        ev = v.get(5) or ''
        fv = v.get(6) or ''
        print(f'[행{src}] E={ev} F={fv} '
              f'(leaf={get_column_letter(lc)} → 자식={get_column_letter(childc)})')
        for label, u, is_cur, is_ext in plan[src]:
            if is_cur:
                tag = '재사용'
            elif is_ext:
                tag = '신규+(외부=사이트)'
                total_new += 1
            else:
                tag = '신규+'
                total_new += 1
            print(f'      [{tag}] {label}  ->  {u}')
        print()
    print(f'신규 추가 행: {total_new}개  /  기존 {len(rows)}행 → 최종 {len(rows)+total_new}행')

    if not write:
        print('\n(계획만 출력. 실제 기입하려면 --write)')
        return

    # ===== 실제 기입 =====
    backup = xlsx.replace('.xlsx', '_backup_tab전.xlsx')
    shutil.copy(xlsx, backup)
    print(f'\n백업: {backup}')

    # 새 평탄 행 목록 구성
    out_rows = []   # 각 원소 = {'vals':..., 'style_src':row_dict, 'url':..}
    for row in rows:
        src = row['src']
        if src in plan:
            v = row['vals']
            lc = leaf_col(v)
            childc = lc + 1
            for label, u, is_cur, is_ext in plan[src]:
                nv = dict(v)
                # leaf 보다 깊은 컬럼 초기화 후 자식 컬럼에 탭명
                for c in CAT_COLS:
                    if c > lc:
                        nv[c] = None
                nv[childc] = label
                if is_cur:
                    nv[11] = v.get(11)        # 기존 URL/데이터 유지
                    out_rows.append({'vals': nv, 'style': row, 'url': nv.get(11)})
                else:
                    for c in range(12, MAXCOL + 1):
                        nv[c] = None          # L~T 비움 (신규)
                    nv[11] = u
                    nv[19] = None
                    if is_ext:                # 외부 사이트 탭 → 즉시 L=사이트/M=1(Phase234 대상 아님)
                        nv[12] = '사이트'
                        nv[13] = 1
                    out_rows.append({'vals': nv, 'style': row, 'url': u})
        else:
            out_rows.append({'vals': dict(row['vals']), 'style': row, 'url': row['vals'].get(11)})

    # 데이터 영역 클리어
    for r in range(3, ws.max_row + 1):
        for c in range(1, MAXCOL + 1):
            ws.cell(r, c).value = None
            ws.cell(r, c).hyperlink = None

    START = 3
    for i, orow in enumerate(out_rows):
        r = START + i
        sty = orow['style']['styles']
        for c in range(1, MAXCOL + 1):
            cell = ws.cell(r, c)
            f, fl, bd, al, nf, pr = sty[c]
            cell.font = copy(f); cell.fill = copy(fl); cell.border = copy(bd)
            cell.alignment = copy(al); cell.number_format = nf; cell.protection = copy(pr)
        v = orow['vals']
        for c in range(1, MAXCOL + 1):
            ws.cell(r, c).value = v.get(c)
        ws.cell(r, 2).value = i + 1        # B 순번 재부여
    END = START + len(out_rows) - 1

    # D/E/F 재병합 (G는 leaf라 병합 안 함)
    center = Alignment(horizontal='center', vertical='center', wrap_text=True)
    left = Alignment(horizontal='left', vertical='center', wrap_text=False)

    def merge_runs(col_letter, col_idx, group_cols=()):
        cur_val = ws.cell(START, col_idx).value
        cur_grp = tuple(ws.cell(START, g).value for g in group_cols)
        run_start = START
        runs = []
        for r in range(START + 1, END + 1):
            val = ws.cell(r, col_idx).value
            grp = tuple(ws.cell(r, gg).value for gg in group_cols)
            if val == cur_val and grp == cur_grp:
                continue
            if cur_val not in (None, '') and r - 1 > run_start:
                runs.append((run_start, r - 1))
            cur_val, cur_grp, run_start = val, grp, r
        if cur_val not in (None, '') and END > run_start:
            runs.append((run_start, END))
        for s, e in runs:
            ws.merge_cells(f'{col_letter}{s}:{col_letter}{e}')
            ws.cell(s, col_idx).alignment = center

    merge_runs('F', 6, group_cols=(4, 5))
    merge_runs('E', 5, group_cols=(4,))
    merge_runs('D', 4)
    for r in range(START, END + 1):
        for c in (4, 5, 6, 7):
            if ws.cell(r, c).value is not None:
                ws.cell(r, c).alignment = center

    for r in range(1, END + 1):
        ws.row_dimensions[r].height = 15

    # K 하이퍼링크 재설정
    for r in range(START, END + 1):
        cell = ws.cell(r, 11)
        u = cell.value
        if isinstance(u, str) and u.startswith(('http://', 'https://')):
            cell.hyperlink = u
            old = cell.font
            cell.font = Font(name=old.name or '맑은 고딕', size=old.size or 11,
                             bold=old.bold, italic=old.italic,
                             color='0000FF', underline='single')
            cell.alignment = left

    wb.save(xlsx)
    print(f'저장 완료: {xlsx}  (총 {len(out_rows)}행)')


if __name__ == '__main__':
    main()
