# -*- coding: utf-8 -*-
"""Step 6: 네이버 뉴스 최초 출현일 검색

입력:
  - output/{corpus_type}/step5_llm_assisted_annotation/review.xlsx (filtered 시트)
출력:
  - output/{corpus_type}/step6_naver_first_occurrence/
"""

from __future__ import annotations

import re
import subprocess
import time
from pathlib import Path
from urllib.parse import quote
from multiprocessing import Process, Manager
from typing import Dict, Set, Tuple

import pandas as pd
import openpyxl
from tqdm import tqdm

from wordextractor.config import PipelineConfig

CANDIDATE_COLUMN = "혼성어_어형"
DATE_RE = re.compile(r"\d{4}\.\d{2}\.\d{2}\.?")

RESULT_COLS = [
    "candidate",
    "naver_search_url",
    "naver_search_status",
    "naver_first_date",
    "naver_first_press",
    "non_naver_first_date",
    "non_naver_first_press",
]


# ── 캐시 ──────────────────────────────────────────────────


def load_global_cache(cache_path: Path) -> Dict[str, dict]:
    cache: Dict[str, dict] = {}
    if cache_path and cache_path.exists():
        try:
            df = pd.read_csv(cache_path, encoding="utf-8-sig", low_memory=False)
            for _, row in df.iterrows():
                candidate = str(row["candidate"])
                cache[candidate] = row.to_dict()
            print(f"  글로벌 캐시 로드: {cache_path}")
            print(f"  캐시된 어형: {len(cache):,}개")
        except Exception as e:
            print(f"  [경고] 캐시 로드 실패: {e}")
    else:
        print(f"  글로벌 캐시 없음 (새로 생성됨)")
    return cache


def save_global_cache(cache: Dict[str, dict], cache_path: Path):
    if not cache:
        return

    cache_path.parent.mkdir(parents=True, exist_ok=True)

    rows = list(cache.values())
    df = pd.DataFrame(rows)

    cols = [c for c in RESULT_COLS if c in df.columns]
    other_cols = [c for c in df.columns if c not in cols]
    df = df[cols + other_cols]

    df.to_csv(cache_path, index=False, encoding="utf-8-sig")
    print(f"  글로벌 캐시 저장: {cache_path} ({len(cache):,}개)")


# ── 네이버 검색 ───────────────────────────────────────────


def build_search_url(word: str) -> str:
    search_q = quote(f'"{word}"')
    params = [
        "ssc=tab.news.all",
        f"query={search_q}",
        "sm=tab_opt",
        "sort=2",
        "photo=0",
        "field=0",
        "pd=0",
        "qdt=1",
        "related=0",
        "mynews=0",
        "office_type=0",
        "office_section_code=0",
        "news_office_checked=",
        "nso=so%3Ar%2Cp%3Aall",
        "is_sug_officeid=0",
        "office_category=0",
        "service_area=0",
    ]
    return "https://search.naver.com/search.naver?" + "&".join(params)


def _create_driver():
    try:
        from selenium import webdriver
        from selenium.webdriver.chrome.options import Options
        from selenium.webdriver.chrome.service import Service
    except ImportError:
        raise ImportError(
            "Step 6 requires the 'naver' extra. "
            "Install with: pip install wordextractor[naver]"
        )

    options = Options()
    options.add_argument("--headless")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_argument("--disable-gpu")
    options.add_argument("--log-level=3")
    options.add_argument("--silent")
    options.add_argument("--disable-logging")
    options.add_argument("--disable-extensions")
    options.add_argument("--disable-infobars")
    options.add_experimental_option("excludeSwitches", ["enable-logging", "enable-automation"])
    options.add_experimental_option("useAutomationExtension", False)
    options.add_argument(
        "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    )

    service = Service(log_output=subprocess.DEVNULL)
    driver = webdriver.Chrome(service=service, options=options)
    return driver


def _extract_date_from_profile(profile) -> str:
    from bs4 import Tag  # noqa: F811 — lazy import

    subtexts = profile.select(".sds-comps-profile-info-subtext")
    for st in subtexts:
        txt = st.get_text(strip=True)
        if DATE_RE.fullmatch(txt):
            return txt
        for span in st.select("span"):
            txt = span.get_text(strip=True)
            if DATE_RE.fullmatch(txt):
                return txt

    profile_text = profile.get_text()
    match = DATE_RE.search(profile_text)
    if match:
        return match.group()

    return ""


def search_word(driver, word: str) -> dict:
    from bs4 import BeautifulSoup

    url = build_search_url(word)

    result = {
        "candidate": word,
        "naver_search_url": url,
        "naver_search_status": "",
        "naver_first_date": "",
        "naver_first_press": "",
        "non_naver_first_date": "",
        "non_naver_first_press": "",
    }

    try:
        driver.get(url)
        time.sleep(0.5)

        html = driver.page_source
        soup = BeautifulSoup(html, "html.parser")

        not_found = soup.select(".not_found02")
        if any("검색결과가없습니다" in el.get_text().replace(" ", "") for el in not_found):
            result["naver_search_status"] = "검색 결과 없음"
            return result

        profiles = soup.select("div.sds-comps-profile")
        if not profiles:
            result["naver_search_status"] = "파싱 실패"
            return result

        result["naver_search_status"] = "성공"

        for profile in profiles:
            is_naver = any("네이버뉴스" in a.get_text() for a in profile.select("a"))

            press = ""
            press_el = profile.select_one(".sds-comps-profile-info-title-text span")
            if press_el:
                press = press_el.get_text(strip=True)

            date = _extract_date_from_profile(profile)

            if is_naver and not result["naver_first_date"]:
                result["naver_first_date"] = date
                result["naver_first_press"] = press
            elif not is_naver and not result["non_naver_first_date"]:
                result["non_naver_first_date"] = date
                result["non_naver_first_press"] = press

            if result["naver_first_date"] and result["non_naver_first_date"]:
                break

    except Exception as e:
        result["naver_search_status"] = f"실패: {e}"

    return result


# ── 배치 저장 / 로드 ──────────────────────────────────────


def load_searched_words_from_batch(batch_dir: Path) -> Tuple[Set[str], Dict[str, dict]]:
    searched: Set[str] = set()
    results: Dict[str, dict] = {}
    if batch_dir and batch_dir.exists():
        for batch_file in batch_dir.glob("batch_worker*.csv"):
            try:
                batch_df = pd.read_csv(batch_file, encoding="utf-8-sig")
                for _, row in batch_df.iterrows():
                    candidate = str(row["candidate"])
                    searched.add(candidate)
                    results[candidate] = row.to_dict()
            except Exception as e:
                print(f"[경고] 배치 파일 로드 실패: {batch_file} - {e}")
    return searched, results


def save_batch(results: list, output_dir: Path, worker_id: int, batch_num: int):
    if not results:
        return

    batch_df = pd.DataFrame(results)
    batch_path = output_dir / f"batch_worker{worker_id:02d}_{batch_num:04d}.csv"
    batch_df.to_csv(batch_path, index=False, encoding="utf-8-sig")


# ── 워커 ──────────────────────────────────────────────────


def worker(
    worker_id: int,
    words: list,
    results_dict: dict,
    delay: float,
    restart_every: int,
    batch_save_every: int = 2,
    output_dir: Path | None = None,
):
    driver = _create_driver()
    request_count = 0
    restart_count = 0
    batch_num = 0
    local_results: list = []

    try:
        for word in tqdm(words, desc=f"Worker {worker_id}", position=worker_id):
            result = search_word(driver, word)
            results_dict[word] = result
            local_results.append(result)
            request_count += 1

            if request_count >= restart_every:
                driver.quit()
                time.sleep(2)
                driver = _create_driver()
                request_count = 0
                restart_count += 1

                if batch_save_every > 0 and restart_count >= batch_save_every:
                    if output_dir and local_results:
                        batch_num += 1
                        save_batch(local_results, output_dir, worker_id, batch_num)
                        local_results = []
                    restart_count = 0

            time.sleep(delay)

        if output_dir and local_results:
            batch_num += 1
            save_batch(local_results, output_dir, worker_id, batch_num)

    finally:
        driver.quit()


# ── 메인 검색 실행 ────────────────────────────────────────


def search_naver(
    df: pd.DataFrame,
    candidate_column: str = "candidate",
    delay: float = 1.0,
    n_workers: int = 12,
    restart_every: int = 50,
    batch_save_every: int = 2,
    output_dir: Path | None = None,
    cache_path: Path | None = None,
) -> pd.DataFrame:
    print("=" * 60)
    print("네이버 뉴스 최초 출현일 검색")
    print(f"  워커: {n_workers}")
    print(f"  딜레이: {delay}초")
    print(f"  드라이버 재시작: {restart_every}개마다")
    if batch_save_every > 0:
        print(f"  배치 저장: 리스타트 {batch_save_every}번마다 ({restart_every * batch_save_every}개)")
    print("=" * 60)

    global_cache: Dict[str, dict] = {}
    if cache_path:
        global_cache = load_global_cache(cache_path)

    if "passed" in df.columns:
        search_mask = df["passed"] == True  # noqa: E712
    else:
        search_mask = pd.Series([True] * len(df))

    words = df.loc[search_mask, candidate_column].dropna().astype(str).tolist()
    words = list(set(words))
    total_words = len(words)

    cached_words = [w for w in words if w in global_cache]
    new_words = [w for w in words if w not in global_cache]

    print(f"\n검색 대상 분석:")
    print(f"  전체 후보: {total_words:,}개")
    print(f"  캐시 재활용: {len(cached_words):,}개")
    print(f"  신규 검색 필요: {len(new_words):,}개")

    batch_dir = None
    batch_results: Dict[str, dict] = {}
    if output_dir:
        batch_dir = Path(output_dir) / "batch"
        batch_dir.mkdir(parents=True, exist_ok=True)

        searched_in_batch, batch_results = load_searched_words_from_batch(batch_dir)
        if searched_in_batch:
            already_in_batch = [w for w in new_words if w in searched_in_batch]
            new_words = [w for w in new_words if w not in searched_in_batch]
            print(f"  배치에서 이미 검색됨: {len(already_in_batch):,}개")
            print(f"  실제 검색 대상: {len(new_words):,}개")

    new_search_results: Dict[str, dict] = {}

    if new_words:
        print(f"\n신규 검색 시작: {len(new_words):,}개")
        print(f"  배치 저장 경로: {batch_dir}")

        manager = Manager()
        results_dict = manager.dict()

        chunks = []
        chunk_size = len(new_words) // n_workers if n_workers > 0 else len(new_words)
        for i in range(n_workers):
            start = i * chunk_size
            end = start + chunk_size if i < n_workers - 1 else len(new_words)
            chunks.append(new_words[start:end])

        processes = []
        for i, chunk in enumerate(chunks):
            if chunk:
                p = Process(
                    target=worker,
                    args=(i, chunk, results_dict, delay, restart_every, batch_save_every, batch_dir),
                )
                p.start()
                processes.append(p)

        for p in processes:
            p.join()

        print(f"\n워커 완료")
        _, new_search_results = load_searched_words_from_batch(batch_dir)
    else:
        print("\n신규 검색 대상 없음 (모두 캐시에서 재활용)")

    all_results: Dict[str, dict] = {}

    for word in cached_words:
        all_results[word] = global_cache[word]

    all_results.update(batch_results)
    all_results.update(new_search_results)

    if cache_path and (new_search_results or batch_results):
        new_for_cache = {**batch_results, **new_search_results}
        global_cache.update(new_for_cache)
        save_global_cache(global_cache, cache_path)

    new_cols = [
        "naver_search_url",
        "naver_search_status",
        "naver_first_date",
        "naver_first_press",
        "non_naver_first_date",
        "non_naver_first_press",
    ]

    for col in new_cols:
        df[col] = ""

    for idx, row in df.iterrows():
        word = str(row[candidate_column])
        if word in all_results:
            for col in new_cols:
                df.at[idx, col] = all_results[word].get(col, "")

    searched = df[df["naver_search_status"] != ""]
    success = (searched["naver_search_status"] == "성공").sum()
    no_result = (searched["naver_search_status"] == "검색 결과 없음").sum()
    has_naver = (searched["naver_first_date"] != "").sum()

    print("\n" + "=" * 60)
    print("완료")
    print(f"  검색 완료: {len(searched):,}개")
    print(f"    - 캐시 재활용: {len(cached_words):,}개")
    print(f"    - 신규 검색: {len(new_search_results) + len(batch_results):,}개")
    print(f"  성공: {success:,}개")
    print(f"  검색 결과 없음: {no_result:,}개")
    print(f"  네이버 뉴스 있음: {has_naver:,}개")
    if cache_path:
        print(f"  글로벌 캐시 총 어형: {len(global_cache):,}개")
    print("=" * 60)

    return df


def run(cfg: PipelineConfig) -> None:
    out_dir = cfg.ensure_output_dir()
    step5_dir = out_dir / "step5_llm_assisted_annotation"
    review_xlsx = step5_dir / "review.xlsx"
    step6_dir = out_dir / "step6_naver_first_occurrence"
    step6_dir.mkdir(parents=True, exist_ok=True)
    cache_path = step6_dir / "naver_search_cache.csv"

    print("=" * 60)
    print("Step 6: 네이버 뉴스 최초 출현일 검색")
    print(f"  소스: {review_xlsx}")
    print(f"  검색 대상 컬럼: {CANDIDATE_COLUMN}")
    print("=" * 60)

    df = pd.read_excel(review_xlsx, sheet_name="filtered")
    print(f"  filtered 시트 로드: {len(df):,}행")

    df_result = search_naver(
        df=df,
        candidate_column=CANDIDATE_COLUMN,
        delay=cfg.naver_delay,
        n_workers=cfg.naver_n_workers,
        restart_every=cfg.naver_restart_every,
        batch_save_every=cfg.naver_batch_save_every,
        output_dir=step6_dir,
        cache_path=cache_path,
    )

    # CSV 저장
    csv_path = step6_dir / "filtered_with_naver.csv"
    df_result.to_csv(csv_path, index=False, encoding="utf-8-sig")
    print(f"\nCSV 저장: {csv_path}")

    # review.xlsx의 filtered 시트 업데이트
    wb = openpyxl.load_workbook(review_xlsx)
    if "filtered" in wb.sheetnames:
        del wb["filtered"]

    ws = wb.create_sheet("filtered")

    headers = list(df_result.columns)
    for col_idx, header in enumerate(headers, 1):
        ws.cell(row=1, column=col_idx, value=header)

    for row_idx, (_, row) in enumerate(df_result.iterrows(), 2):
        for col_idx, header in enumerate(headers, 1):
            val = row[header]
            if pd.isna(val):
                val = None
            ws.cell(row=row_idx, column=col_idx, value=val)

    wb.save(review_xlsx)
    print(f"review.xlsx filtered 시트 업데이트 완료")
    print("=" * 60)
