"""충청남도 11개 시·군 Phase 1 일괄 처리.

매뉴얼: D:\\01.프로젝트\\DB수집\\사이트맵_수집_매뉴얼.md
출력: 각 폴더의 {기관명}.xlsx
"""
import os
import re
import shutil
import sys
import warnings
from copy import copy
from urllib.parse import urljoin

import openpyxl
import requests
from bs4 import BeautifulSoup
from openpyxl.styles import Alignment, Font

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}
TEMPLATE = r'D:\01.프로젝트\DB수집\자료_취합_예시.xlsx'

# ================================================================
# 공통 유틸
# ================================================================


def fetch_html(url, timeout=20):
    r = requests.get(url, headers=H, timeout=timeout, verify=False, allow_redirects=True)
    r.encoding = r.apparent_encoding
    return r.text


def clean_text(s):
    return re.sub(r'\s+', ' ', s or '').strip().replace('\xa0', '')


def extract_href(a):
    if a is None:
        return ''
    href = (a.get('href') or '').strip()
    if not href or href.startswith('#') or href.lower().startswith('javascript:'):
        # Try onclick encodeURI extraction
        return ''
    return href


ENCODE_URI_PAT = re.compile(r"""(?:location\.href|window\.location(?:\.href)?)\s*=\s*encodeURI\(\s*['"]([^'"]+)['"]\s*\)""")
ONCLICK_HREF_PAT = re.compile(r"""(?:location\.href|window\.open)\s*\(?\s*['"]([^'"]+)['"]""")


def extract_href_with_onclick(a):
    """Extract href; if href is dummy (#...), check onclick for encodeURI/location.href."""
    if a is None:
        return ''
    href = (a.get('href') or '').strip()
    if href and not href.startswith('#') and not href.lower().startswith('javascript:'):
        return href
    # Try onclick
    onclick = a.get('onclick', '')
    if onclick:
        m = ENCODE_URI_PAT.search(onclick)
        if m:
            return m.group(1)
        m = ONCLICK_HREF_PAT.search(onclick)
        if m:
            return m.group(1)
    return ''


# ================================================================
# 사이트별 파서 (각각 raw_rows 리스트 반환)
# raw_rows: [{'D': str, 'E': str, 'F': str, 'G': str, 'H': str, 'I': str, 'J': str, 'href': str}, ...]
# ================================================================


def parse_eGov_type1(soup, base):
    """논산시·아산시 형: div.sitemap.type1 > [div.s_1th + div.inner > div.s_2th + ul...]."""
    sitemap = soup.select_one('div.sitemap.type1') or soup.select_one('div.sitemap')
    rows = []
    if not sitemap:
        return rows
    current_D = ''

    def walk(ul, depth, base_path, out):
        for li in ul.find_all('li', recursive=False):
            a = li.find('a', recursive=False)
            if not a:
                continue
            text = clean_text(a.get_text())
            href = extract_href(a)
            path = base_path[:depth] + [(text, href)]
            nested = li.find('ul', recursive=False)
            if nested:
                out.append({'path': list(path), 'href': href})
                walk(nested, depth + 1, path, out)
            else:
                out.append({'path': list(path), 'href': href})

    for child in sitemap.find_all('div', recursive=False):
        cls = child.get('class', [])
        if 's_1th' in cls:
            a = child.find('a')
            current_D = clean_text(a.get_text()) if a else ''
        elif 'inner' in cls:
            s2 = child.find('div', class_='s_2th')
            mid_a = s2.find('a') if s2 else None
            mid_name = clean_text(mid_a.get_text()) if mid_a else ''
            mid_href = extract_href(mid_a) if mid_a else ''
            uls = child.find_all('ul', recursive=False)
            if not uls:
                rows.append({'D': current_D, 'E': mid_name, 'href': mid_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                continue
            for ul in uls:
                tmp = []
                walk(ul, 0, [], tmp)
                for item in tmp:
                    p = item['path']
                    row = {'D': current_D, 'E': mid_name, 'href': item['href'],
                           'F': '', 'G': '', 'H': '', 'I': '', 'J': ''}
                    for di, (t, _) in enumerate(p):
                        col = 'FGHIJ'[di] if di < 5 else 'J'
                        row[col] = t
                    rows.append(row)
    return rows


def parse_amThum(soup, base):
    """당진시·태안군 형: div.amThum > h2.siteNN + div.sitemap_grep > ul.sitemap_list > li > a.first + ul > li > a (+ ul > li > a)."""
    rows = []
    for amthum in soup.select('div.amThum'):
        h2 = amthum.find(['h2', 'h3'], class_=re.compile(r'site\d+'))
        D = clean_text(h2.find('span').get_text()) if h2 and h2.find('span') else (clean_text(h2.get_text()) if h2 else '')
        grep = amthum.find('div', class_='sitemap_grep') or amthum
        for sl in grep.find_all('ul', class_='sitemap_list'):
            # Each ul.sitemap_list contains li > a.first + ul > li > a
            for top_li in sl.find_all('li', recursive=False):
                a_first = top_li.find('a', class_='first', recursive=False)
                if not a_first:
                    continue
                E = clean_text(a_first.get_text())
                E_href = extract_href(a_first)
                inner_ul = top_li.find('ul', recursive=False)
                if not inner_ul:
                    rows.append({'D': D, 'E': E, 'href': E_href,
                                 'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                    continue
                for mid_li in inner_ul.find_all('li', recursive=False):
                    mid_a = mid_li.find('a', recursive=False)
                    if not mid_a:
                        continue
                    F = clean_text(mid_a.get_text())
                    F_href = extract_href(mid_a)
                    deeper = mid_li.find('ul', recursive=False)
                    if not deeper:
                        rows.append({'D': D, 'E': E, 'F': F, 'href': F_href,
                                     'G': '', 'H': '', 'I': '', 'J': ''})
                        continue
                    rows.append({'D': D, 'E': E, 'F': F, 'href': F_href,
                                 'G': '', 'H': '', 'I': '', 'J': ''})
                    for deep_li in deeper.find_all('li', recursive=False):
                        deep_a = deep_li.find('a', recursive=False)
                        if not deep_a:
                            continue
                        G = clean_text(deep_a.get_text())
                        rows.append({'D': D, 'E': E, 'F': F, 'G': G,
                                     'href': extract_href(deep_a),
                                     'H': '', 'I': '', 'J': ''})
    return rows


def parse_ul_sitemap_h4(soup, base):
    """보령시·서천군 형: ul.sitemap > li (대분류) > h4.siteNN > span + ul > li > h5 > a + ul > li.list > a."""
    rows = []
    container = soup.select_one('ul.sitemap') or soup.select_one('div#contents ul.sitemap')
    if not container:
        return rows
    for top_li in container.find_all('li', recursive=False):
        h4 = top_li.find(['h4', 'h3'], recursive=False)
        D = ''
        if h4:
            span = h4.find('span')
            D = clean_text(span.get_text() if span else h4.get_text())
        # Each direct ul under top_li is a sub-group
        for sub_ul in top_li.find_all('ul', recursive=False):
            for sub_li in sub_ul.find_all('li', recursive=False):
                h5 = sub_li.find(['h5', 'h6'], recursive=False)
                if h5:
                    h5_a = h5.find('a')
                    E = clean_text(h5_a.get_text()) if h5_a else clean_text(h5.get_text())
                    E_href = extract_href(h5_a) if h5_a else ''
                else:
                    a = sub_li.find('a', recursive=False)
                    E = clean_text(a.get_text()) if a else ''
                    E_href = extract_href(a) if a else ''
                deeper = sub_li.find('ul', recursive=False)
                if not deeper:
                    rows.append({'D': D, 'E': E, 'href': E_href,
                                 'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                    continue
                rows.append({'D': D, 'E': E, 'href': E_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                for leaf_li in deeper.find_all('li', recursive=False):
                    leaf_a = leaf_li.find('a', recursive=False)
                    if not leaf_a:
                        continue
                    F = clean_text(leaf_a.get_text())
                    rows.append({'D': D, 'E': E, 'F': F, 'href': extract_href(leaf_a),
                                 'G': '', 'H': '', 'I': '', 'J': ''})
                    # 4-level (rare)
                    sub_ul2 = leaf_li.find('ul', recursive=False)
                    if sub_ul2:
                        for ll2 in sub_ul2.find_all('li', recursive=False):
                            la2 = ll2.find('a', recursive=False)
                            if la2:
                                G = clean_text(la2.get_text())
                                rows.append({'D': D, 'E': E, 'F': F, 'G': G,
                                             'href': extract_href(la2),
                                             'H': '', 'I': '', 'J': ''})
    return rows


def parse_dl_dt_dd(soup, base, use_onclick=False):
    """홍성군·예산군·천안시·금산군 dl 형: div.sitemap (옵션 .type2) > dl > dt + dd > b > a + ul > li > a (+ ul > li > a).

    use_onclick=True 인 경우 onclick의 encodeURI/location.href에서 URL을 추출.
    """
    rows = []
    sm = soup.select_one('div.sitemap[class*=type2]') or soup.select_one('div.sitemap')
    if not sm:
        return rows
    href_fn = extract_href_with_onclick if use_onclick else extract_href

    def walk(ul, depth, base_path, out):
        for li in ul.find_all('li', recursive=False):
            a = li.find('a', recursive=False)
            if not a:
                continue
            text = clean_text(a.get_text())
            href = href_fn(a)
            path = base_path[:depth] + [(text, href)]
            nested = li.find('ul', recursive=False)
            if nested:
                out.append({'path': list(path), 'href': href})
                walk(nested, depth + 1, path, out)
            else:
                out.append({'path': list(path), 'href': href})

    for dl in sm.find_all('dl', recursive=False):
        dt = dl.find('dt')
        dt_a = dt.find('a') if dt else None
        D = clean_text(dt_a.get_text() if dt_a else (dt.get_text() if dt else ''))
        for dd in dl.find_all('dd', recursive=False):
            b = dd.find('b')
            b_a = b.find('a') if b else None
            E = clean_text(b_a.get_text()) if b_a else ''
            E_href = href_fn(b_a) if b_a else ''
            nested = dd.find('ul', recursive=False)
            if nested:
                tmp = []
                walk(nested, 0, [], tmp)
                for item in tmp:
                    p = item['path']
                    row = {'D': D, 'E': E, 'href': item['href'],
                           'F': '', 'G': '', 'H': '', 'I': '', 'J': ''}
                    for di, (t, _) in enumerate(p):
                        col = 'FGHIJ'[di] if di < 5 else 'J'
                        row[col] = t
                    rows.append(row)
            else:
                rows.append({'D': D, 'E': E, 'href': E_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
    return rows


def parse_seosan_top_menu(soup, base):
    """서산시: ul.top_menu > li.depth1 > a.depth1_ti (D) + div...div.depth2_wrap > ul.depth2 > li > a (E) + ul.depth3 > li > a (F)."""
    rows = []
    tm = soup.select_one('ul.top_menu')
    if not tm:
        return rows
    for top_li in tm.find_all('li', class_='depth1', recursive=False):
        a1 = top_li.find('a', class_='depth1_ti')
        D = clean_text(a1.get_text()) if a1 else ''
        depth2 = top_li.select_one('ul.depth2')
        if not depth2:
            continue
        for d2_li in depth2.find_all('li', recursive=False):
            d2_a = d2_li.find('a', recursive=False)
            if not d2_a:
                continue
            E = clean_text(d2_a.get_text())
            E_href = extract_href(d2_a)
            d3 = d2_li.find('ul', class_='depth3', recursive=False)
            if not d3:
                rows.append({'D': D, 'E': E, 'href': E_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                continue
            rows.append({'D': D, 'E': E, 'href': E_href,
                         'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
            for d3_li in d3.find_all('li', recursive=False):
                d3_a = d3_li.find('a', recursive=False)
                if not d3_a:
                    continue
                F = clean_text(d3_a.get_text())
                rows.append({'D': D, 'E': E, 'F': F, 'href': extract_href(d3_a),
                             'G': '', 'H': '', 'I': '', 'J': ''})
    return rows


def parse_yesan_depth(soup, base):
    """예산군: #gnb > ul.depth1_ul > li > a.th_1st + div.item > ul.depth2_ul > li > a + ul.depth3_ul > li > a."""
    rows = []
    container = soup.select_one('#gnb ul.depth1_ul') or soup.select_one('ul.depth1_ul')
    if not container:
        return rows
    for top_li in container.find_all('li', recursive=False):
        a1 = top_li.find('a', class_='th_1st', recursive=False)
        D = clean_text(a1.get_text()) if a1 else ''
        item = top_li.find('div', class_='item')
        if not item:
            continue
        d2_ul = item.find('ul', class_='depth2_ul')
        if not d2_ul:
            continue
        for d2_li in d2_ul.find_all('li', recursive=False):
            d2_a = d2_li.find('a', recursive=False)
            if not d2_a:
                continue
            E = clean_text(d2_a.get_text())
            E_href = extract_href(d2_a)
            d3_ul = d2_li.find('ul', class_='depth3_ul', recursive=False)
            if not d3_ul:
                rows.append({'D': D, 'E': E, 'href': E_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                continue
            rows.append({'D': D, 'E': E, 'href': E_href,
                         'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
            for d3_li in d3_ul.find_all('li', recursive=False):
                d3_a = d3_li.find('a', recursive=False)
                if not d3_a:
                    continue
                F = clean_text(d3_a.get_text())
                rows.append({'D': D, 'E': E, 'F': F, 'href': extract_href(d3_a),
                             'G': '', 'H': '', 'I': '', 'J': ''})
    return rows


def parse_cheonan_depth(soup, base):
    """천안시: ul.depth1-ul > li > a.depth1-btn + div.depth1-content > div.layout > ul.depth2-ul > li > a.depth2-btn + div.depth2-content > ul.depth3-ul > li > a.depth3-btn."""
    rows = []
    container = soup.select_one('ul.depth1-ul')
    if not container:
        return rows
    for top_li in container.find_all('li', recursive=False):
        a1 = top_li.find('a', class_='depth1-btn', recursive=False)
        D = clean_text(a1.get_text()) if a1 else ''
        D_href = extract_href(a1) if a1 else ''
        d2_ul = top_li.select_one('ul.depth2-ul')
        if not d2_ul:
            rows.append({'D': D, 'E': '', 'href': D_href,
                         'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
            continue
        for d2_li in d2_ul.find_all('li', recursive=False):
            d2_a = d2_li.find('a', class_='depth2-btn', recursive=False)
            if not d2_a:
                continue
            E = clean_text(d2_a.get_text())
            E_href = extract_href(d2_a)
            d3_ul = d2_li.select_one('ul.depth3-ul')
            if not d3_ul:
                rows.append({'D': D, 'E': E, 'href': E_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                continue
            rows.append({'D': D, 'E': E, 'href': E_href,
                         'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
            for d3_li in d3_ul.find_all('li', recursive=False):
                d3_a = d3_li.find('a', class_='depth3-btn', recursive=False)
                if not d3_a:
                    continue
                F = clean_text(d3_a.get_text())
                rows.append({'D': D, 'E': E, 'F': F, 'href': extract_href(d3_a),
                             'G': '', 'H': '', 'I': '', 'J': ''})
    return rows


def parse_cheongyang_depth1(soup, base):
    """청양군: ul.depth1_ul > li > a.th_1st + div.item > ul.depth2_ul > li > a + ul.depth3_ul > li > a (예산군 형)."""
    # 청양군은 예산군과 동일한 e-Gov GNB 구조 — yesan 파서 재사용
    return parse_yesan_depth(soup, base)


def parse_asan_gnb(soup, base):
    """아산시: div#mGnb-anchor{n}.gnb-sub-list > ul > li > a.gnb-sub-trigger + ul.sub-ul > li > a.subm."""
    rows = []
    # 대분류 이름 — mobile-nav의 gnb-main-trigger 텍스트 + href(#mGnb-anchorN) 매핑
    nav_main = soup.select_one('nav#mobile-nav')
    main_categories = []  # [(D, anchor_id)]
    if nav_main:
        for trig in nav_main.find_all(['a', 'button'], class_='gnb-main-trigger'):
            text = clean_text(trig.get_text())
            target = trig.get('href') or trig.get('data-target') or ''
            if target.startswith('#mGnb-anchor'):
                main_categories.append((text, target.lstrip('#')))
    # Fallback: sections without name mapping
    if not main_categories:
        for sec in soup.select('div[id^=mGnb-anchor]'):
            main_categories.append((sec.get('id'), sec.get('id')))

    for D, anchor_id in main_categories:
        section = soup.find('div', id=anchor_id)
        if not section:
            continue
        # section > ul > li > a.gnb-sub-trigger + ul.sub-ul > li > a.subm
        for ul in section.find_all('ul', recursive=False):
            for li in ul.find_all('li', recursive=False):
                a_E = li.find('a', class_='gnb-sub-trigger', recursive=False) or li.find('a', recursive=False)
                if not a_E:
                    continue
                E = clean_text(a_E.get_text())
                E_href = extract_href(a_E)
                sub_ul = li.find('ul', class_='sub-ul', recursive=False) or li.find('ul', recursive=False)
                if not sub_ul:
                    rows.append({'D': D, 'E': E, 'href': E_href,
                                 'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                    continue
                rows.append({'D': D, 'E': E, 'href': E_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                for sub_li in sub_ul.find_all('li', recursive=False):
                    sub_a = sub_li.find('a', recursive=False)
                    if not sub_a:
                        continue
                    F = clean_text(sub_a.get_text())
                    rows.append({'D': D, 'E': E, 'F': F, 'href': extract_href(sub_a),
                                 'G': '', 'H': '', 'I': '', 'J': ''})
    return rows


def parse_buyeo_topmenu(soup, base):
    """부여군: ul#tm > li.th1 > a.th1_lnk (D) + div.summry > ul.th2 > li > a.th2_lnk (E) + ul.th3 > li > a (F)."""
    rows = []
    tm = soup.select_one('ul#tm')
    if not tm:
        return rows
    for top_li in tm.find_all('li', class_=re.compile(r'th1?'), recursive=False):
        a1 = top_li.find('a', class_='th1_lnk', recursive=False)
        if not a1:
            a1 = top_li.find('a', recursive=False)
        if not a1:
            continue
        D = clean_text(a1.get_text())
        D_href = extract_href(a1)
        # Find ul.th2 inside div.summry
        summry = top_li.find('div', class_=re.compile(r'summry'), recursive=False)
        th2 = (summry.find('ul', class_='th2') if summry else None) or top_li.find('ul', class_='th2')
        if not th2:
            rows.append({'D': D, 'E': '', 'href': D_href,
                         'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
            continue
        for li2 in th2.find_all('li', recursive=False):
            a2 = li2.find('a', class_='th2_lnk', recursive=False) or li2.find('a', recursive=False)
            if not a2:
                continue
            E = clean_text(a2.get_text())
            E_href = extract_href(a2)
            th3 = li2.find('ul', class_='th3', recursive=False)
            if not th3:
                rows.append({'D': D, 'E': E, 'href': E_href,
                             'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
                continue
            rows.append({'D': D, 'E': E, 'href': E_href,
                         'F': '', 'G': '', 'H': '', 'I': '', 'J': ''})
            for li3 in th3.find_all('li', recursive=False):
                a3 = li3.find('a', recursive=False)
                if not a3:
                    continue
                F = clean_text(a3.get_text())
                rows.append({'D': D, 'E': E, 'F': F, 'href': extract_href(a3),
                             'G': '', 'H': '', 'I': '', 'J': ''})
    return rows


# ================================================================
# 사이트 설정
# ================================================================

SITES = [
    {
        'idx': 5, 'name': '당진시', 'base': 'https://www.dangjin.go.kr',
        'sitemap': 'https://www.dangjin.go.kr/kor/sitemap_11.do',
        'sheet': '05_당진시', 'parser': parse_amThum,
        'domain': 'dangjin.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\5.당진시',
    },
    {
        'idx': 6, 'name': '보령시', 'base': 'https://www.brcn.go.kr',
        'sitemap': 'https://www.brcn.go.kr/kor/sitemap_11.do',
        'sheet': '06_보령시', 'parser': parse_ul_sitemap_h4,
        'domain': 'brcn.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\6.보령시',
    },
    {
        'idx': 7, 'name': '부여군', 'base': 'https://www.buyeo.go.kr',
        'sitemap': 'https://www.buyeo.go.kr/html/kr/',
        'sheet': '07_부여군', 'parser': parse_buyeo_topmenu,
        'domain': 'buyeo.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\7.부여군',
    },
    {
        'idx': 8, 'name': '서산시', 'base': 'https://www.seosan.go.kr',
        'sitemap': 'https://www.seosan.go.kr/www/index.do',
        'sheet': '08_서산시', 'parser': parse_seosan_top_menu,
        'domain': 'seosan.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\8.서산시',
    },
    {
        'idx': 9, 'name': '서천군', 'base': 'https://www.seocheon.go.kr',
        'sitemap': 'https://www.seocheon.go.kr/kor/sitemap_11.do',
        'sheet': '09_서천군', 'parser': parse_ul_sitemap_h4,
        'domain': 'seocheon.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\9.서천군',
    },
    {
        'idx': 10, 'name': '아산시', 'base': 'https://www.asan.go.kr',
        'sitemap': 'https://www.asan.go.kr/main/',
        'sheet': '10_아산시', 'parser': parse_asan_gnb,
        'domain': 'asan.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\10.아산시',
    },
    {
        'idx': 11, 'name': '예산군', 'base': 'https://www.yesan.go.kr',
        'sitemap': 'https://www.yesan.go.kr/kor/sitemap.do',
        'sheet': '11_예산군', 'parser': parse_yesan_depth,
        'domain': 'yesan.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\11.예산군',
    },
    {
        'idx': 12, 'name': '천안시', 'base': 'https://www.cheonan.go.kr',
        'sitemap': 'https://www.cheonan.go.kr/kor/sitemap.do',
        'sheet': '12_천안시', 'parser': parse_cheonan_depth,
        'domain': 'cheonan.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\12.천안시',
    },
    {
        'idx': 13, 'name': '청양군', 'base': 'https://www.cheongyang.go.kr',
        'sitemap': 'https://www.cheongyang.go.kr/kor/sitemap_11.do',
        'sheet': '13_청양군', 'parser': parse_cheongyang_depth1,
        'domain': 'cheongyang.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\13.청양군',
    },
    {
        'idx': 14, 'name': '태안군', 'base': 'https://www.taean.go.kr',
        'sitemap': 'https://www.taean.go.kr/kor/sitemap_11.do',
        'sheet': '14_태안군', 'parser': parse_amThum,
        'domain': 'taean.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\14.태안군',
    },
    {
        'idx': 15, 'name': '홍성군', 'base': 'https://www.hongseong.go.kr',
        'sitemap': 'https://www.hongseong.go.kr/kor/sitemap.do',
        'sheet': '15_홍성군',
        'parser': lambda soup, base: parse_dl_dt_dd(soup, base, use_onclick=True),
        'domain': 'hongseong.go.kr',
        'folder': r'D:\01.프로젝트\DB수집\작업파일\광역_사이트맵\충청남도\15.홍성군',
    },
]


# ================================================================
# 엑셀 생성 (공통)
# ================================================================


def write_excel(site, raw_rows):
    name = site['name']
    base = site['base']
    domain = site['domain']
    output = f"{site['folder']}\\{os.path.basename(os.path.dirname(site['folder']))}_{name}.xlsx"

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

    def is_external(url):
        return url.startswith(('http://', 'https://')) and domain not in url

    # 부모-자식 URL 중복 제거
    final_rows = []
    i = 0
    removed = 0
    while i < len(raw_rows):
        row = raw_rows[i]
        if (i + 1 < len(raw_rows)
                and row.get('G', '') == ''
                and raw_rows[i + 1].get('D') == row.get('D')
                and raw_rows[i + 1].get('E') == row.get('E')
                and raw_rows[i + 1].get('F') == row.get('F')
                and raw_rows[i + 1].get('G', '') != ''
                and raw_rows[i + 1].get('href') == row.get('href')):
            removed += 1
            i += 1
            continue
        final_rows.append(row)
        i += 1

    print(f'  [{name}] 원본 {len(raw_rows)} → 중복 {removed} → 최종 {len(final_rows)}')
    if not final_rows:
        print(f'  [{name}] !! 행 0개 — 파서 점검 필요. 엑셀 생성 스킵.')
        return False

    shutil.copy(TEMPLATE, output)
    wb = openpyxl.load_workbook(output)
    ws = wb.active
    ws.title = site['sheet']

    HEADER = {'B1:R1', 'S1:W1', 'Y1:AA1'}
    for rng in [str(m) for m in ws.merged_cells.ranges if str(m) not in HEADER]:
        ws.unmerge_cells(rng)
    for row in ws.iter_rows(min_row=3, max_row=ws.max_row, min_col=1, max_col=ws.max_column):
        for cell in row:
            cell.value = None

    START = 3
    template_r = 3
    cur_max = ws.max_row
    for idx, item in enumerate(final_rows, start=START):
        if idx > cur_max:
            for c in range(1, ws.max_column + 1):
                srcc = ws.cell(template_r, c)
                tgt = ws.cell(idx, c)
                if srcc.has_style:
                    tgt.font = copy(srcc.font)
                    tgt.fill = copy(srcc.fill)
                    tgt.border = copy(srcc.border)
                    tgt.alignment = copy(srcc.alignment)
                    tgt.number_format = srcc.number_format
                    tgt.protection = copy(srcc.protection)
        url = abs_url(item.get('href', ''))
        ws.cell(idx, 2).value = idx - 2
        ws.cell(idx, 3).value = name
        ws.cell(idx, 4).value = item.get('D', '')
        ws.cell(idx, 5).value = item.get('E', '')
        ws.cell(idx, 6).value = item.get('F', '')
        ws.cell(idx, 7).value = item.get('G', '')
        ws.cell(idx, 8).value = item.get('H', '')
        ws.cell(idx, 9).value = item.get('I', '')
        ws.cell(idx, 10).value = item.get('J', '')
        ws.cell(idx, 11).value = url
        if is_external(url):
            ws.cell(idx, 19).value = '외부링크'

    END = START + len(final_rows) - 1
    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=()):
        runs = []
        cur_val = ws.cell(START, col_idx).value
        cur_grp = tuple(ws.cell(START, g).value for g in group_cols)
        run_start = START
        for r in range(START + 1, END + 1):
            v = ws.cell(r, col_idx).value
            g = tuple(ws.cell(r, gg).value for gg in group_cols)
            if v == cur_val and g == 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 = v, g, 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
        return len(runs)

    # 병합 순서 F→E→D (D를 먼저 병합하면 2행부터 D=None이 되어 E 그룹키가 깨짐)
    n_f = merge_runs('F', 6, group_cols=(4, 5))
    n_e = merge_runs('E', 5, group_cols=(4,))
    n_d = 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

    link_n = 0
    for r in range(START, END + 1):
        cell = ws.cell(r, 11)
        u = cell.value
        if u and 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
            link_n += 1

    wb.save(output)
    ext_n = sum(1 for r in range(START, END + 1) if ws.cell(r, 19).value == '외부링크')
    print(f'  [{name}] 병합 D:{n_d} E:{n_e} F:{n_f} | 외부링크 {ext_n} | K링크 {link_n} → {output}')
    return True


def main():
    targets = sys.argv[1:] if len(sys.argv) > 1 else [s['name'] for s in SITES]
    for site in SITES:
        if site['name'] not in targets:
            continue
        print(f"\n{'='*60}\n[{site['idx']}.{site['name']}] {site['sitemap']}\n{'='*60}")
        try:
            html = fetch_html(site['sitemap'])
            soup = BeautifulSoup(html, 'html.parser')
            raw_rows = site['parser'](soup, site['base'])
            write_excel(site, raw_rows)
        except Exception as e:
            print(f'  [{site["name"]}] !! 실패: {e}')
            import traceback
            traceback.print_exc()


if __name__ == '__main__':
    main()
