"""
ガールズヘブン 順位スクレイピング

monitor_url（店舗一覧ページ）を開き、同一プランの全店舗の順位を取得する。

HTML 構造（実サイト調査済み / 2026-03-19）:
  - 年齢確認 : .over18confirm-yes をクリックして通過
  - 店舗行   : .shopBox.rankS.yellowColor （Sタイプのみ。yellowColor がない .shopBox.rankS は広告のため除外）
  - もっとみる: 店舗数が多い地域では「もっとみる」ボタンが表示される。全店舗展開まで繰り返しクリック
  - 店舗名   : span.shopName の inner_text()
  - 順位     : HTML コメント <!-- page:rank --> を正規表現で抽出
               例: <!-- 1:3 --> → rank=3

Usage:
    python scrape_girlsheaven.py <client_id> <monitor_url>
"""

import argparse
import csv
import logging
import os
import random
import re
import sys
import time
from datetime import datetime
from pathlib import Path

from playwright.sync_api import Page, sync_playwright
from playwright_stealth import Stealth

sys.path.append(str(Path(__file__).resolve().parents[1]))
from config.database import execute_query, execute_update

# ---------------------------------------------------------------------------
# 定数
# ---------------------------------------------------------------------------
MEDIA_NAME = "girlsheaven"
CSV_DIR = Path(__file__).parent / "csv"
RETRY_COUNT = 1
RETRY_WAIT = 15        # 秒
PAGE_TIMEOUT = 30_000  # ms
MAX_MORE_CLICKS = 20   # 「もっとみる」の最大クリック回数（無限ループ防止）

# 「もっとみる」ボタンのセレクタ（確定: 2026-03-20）
MORE_BTN_SELECTORS = [
    "a.nextBtn",
]

# 対策4: ビューポートをランダム化
VIEWPORTS = [
    {"width": 1280, "height": 800},
    {"width": 1366, "height": 768},
    {"width": 1920, "height": 1080},
    {"width": 1440, "height": 900},
]

# ---------------------------------------------------------------------------
# URL正規化
# ---------------------------------------------------------------------------
def normalize_url(url: str) -> str:
    if not url.endswith('/') and '?' not in url:
        url = url + '/'
    if '?op=newc' not in url:
        if url.endswith('/'):
            url = url + '?op=newc'
        else:
            url = url + '/?op=newc'
    return url


# ---------------------------------------------------------------------------
# ロガー設定
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# DB ヘルパー
# ---------------------------------------------------------------------------
def get_client_shop_name(client_id: int) -> str:
    """media_settingsテーブルからGHの自店舗名を取得する。
    未設定の場合はclientsテーブルにフォールバックする。
    """
    rows = execute_query(
        "SELECT shop_name FROM media_settings WHERE client_id = %s AND media_name = 'girlsheaven' AND is_active = 1 LIMIT 1",
        (client_id,),
        local=False,
    )
    if rows and rows[0]["shop_name"]:
        return rows[0]["shop_name"]

    # フォールバック：clientsテーブルから取得
    rows = execute_query(
        "SELECT shop_name FROM clients WHERE id = %s LIMIT 1",
        (client_id,),
        local=False,
    )
    if not rows:
        raise ValueError(f"client_id={client_id} が clients テーブルに存在しません")
    return rows[0]["shop_name"]


def save_scraping_result(client_id: int, shop_name: str, rank: int) -> None:
    sql = """
        INSERT INTO scraping_results
            (client_id, media_name, shop_name, `rank`, fetched_at)
        VALUES (%s, %s, %s, %s, %s)
    """
    execute_update(sql, (client_id, MEDIA_NAME, shop_name, rank, datetime.now()), local=True)


def save_error_log(client_id: int, monitor_url: str, message: str) -> None:
    sql = """
        INSERT INTO error_logs
            (client_id, media_name, error_type, error_message, occurred_at)
        VALUES (%s, %s, %s, %s, %s)
    """
    execute_update(sql, (client_id, MEDIA_NAME, 'girlsheaven_error', f"[{monitor_url}] {message}", datetime.now()), local=True)


# ---------------------------------------------------------------------------
# CSV ヘルパー
# ---------------------------------------------------------------------------
def append_to_csv(client_id: int, rows: list[dict]) -> None:
    CSV_DIR.mkdir(parents=True, exist_ok=True)
    csv_path = CSV_DIR / f"{MEDIA_NAME}_{client_id}.csv"
    write_header = not csv_path.exists()

    with open(csv_path, "a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["fetched_at", "shop_name", "rank", "media_name"])
        if write_header:
            writer.writeheader()
        writer.writerows(rows)

    logger.info("CSV追記完了: %s (%d件)", csv_path, len(rows))


# ---------------------------------------------------------------------------
# スクレイピング本体
# ---------------------------------------------------------------------------
def scrape(page: Page, monitor_url: str, headless: bool = True, slot_time: str = "") -> list[dict]:
    """monitor_url を開いて同一プランの全店舗の順位を取得する。"""
    monitor_url = normalize_url(monitor_url)
    logger.info("ページ取得開始: %s", monitor_url)
    page.goto(monitor_url, timeout=PAGE_TIMEOUT, wait_until="load")
    page.wait_for_timeout(random.randint(2000, 4000))  # 対策1: ページ取得後ランダム待機

    # 年齢確認ダイアログを処理
    enter_btn = page.locator(".over18confirm-yes")
    if enter_btn.count() > 0:
        logger.info("年齢確認ダイアログを通過")
        enter_btn.hover()                               # 対策3: ホバーしてからクリック
        page.wait_for_timeout(random.randint(500, 1500))
        enter_btn.click()
        page.wait_for_timeout(random.randint(500, 1500))  # 対策1: クリック後ランダム待機

    # 「もっとみる」ボタンを繰り返しクリックして全店舗を展開
    for click_count in range(1, MAX_MORE_CLICKS + 1):
        # ① 事前チェック: ボタンが存在しない場合はスクロールせず即終了
        more_btn = None
        for sel in MORE_BTN_SELECTORS:
            el = page.locator(sel)
            if el.count() > 0:
                more_btn = el.first
                break
        if more_btn is None:
            logger.info("「もっとみる」ボタンなし（全店舗展開済み）")
            break

        # ② ボタンが存在する場合のみスクロールして表示させる
        page_height = page.evaluate("document.body.scrollHeight")
        scroll_y = page.evaluate("window.scrollY")
        while scroll_y < page_height:
            scroll_y = min(scroll_y + random.randint(200, 400), page_height)
            page.evaluate(f"window.scrollTo(0, {scroll_y})")
            page.wait_for_timeout(random.randint(300, 700))
            if more_btn.is_visible():
                break

        # ③ ランダム待機 → hover → クリック
        page.wait_for_timeout(random.randint(1000, 2000))
        if not more_btn.is_visible():
            logger.info("「もっとみる」ボタンが非表示のため終了")
            break
        logger.info("「もっとみる」ボタンクリック (%d回目)", click_count)
        more_btn.scroll_into_view_if_needed()
        page.wait_for_timeout(random.randint(500, 1000))
        more_btn.hover()
        page.wait_for_timeout(random.randint(500, 1500))
        more_btn.click()

        # ④ クリック後: 3〜5秒待機 → 少しスクロールして読み込みを確認
        page.wait_for_timeout(random.randint(3000, 5000))
        current_y = page.evaluate("window.scrollY")
        page.evaluate(f"window.scrollTo(0, {current_y + random.randint(100, 300)})")
        page.wait_for_timeout(random.randint(300, 700))

        # ⑤ 停止条件: クリック後にAタイプが出現したら終了
        if page.locator(".shopBox.rankS:not(.yellowColor)").count() > 0:
            logger.info("クリック後にAタイプが出現したため「もっとみる」クリックを停止")
            break
    else:
        logger.warning("「もっとみる」の最大クリック数（%d回）に達しました", MAX_MORE_CLICKS)

    if slot_time:
        fetched_at = slot_time
    else:
        fetched_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    rows_locator = page.locator(".shopBox.rankS.yellowColor")  # SタイプはyellowColorクラス
    count = rows_locator.count()
    if count == 0:
        raise ValueError(f"店舗行（.shopBox.rankS.yellowColor）が見つかりません: {monitor_url}")
    logger.info("店舗行取得（Sタイプ）: %d件", count)

    results = []
    for i in range(count):
        row = rows_locator.nth(i)
        html = row.inner_html()

        # 店舗名
        name_el = row.locator("span.shopName").first
        if name_el.count() == 0:
            logger.debug("店舗名取得不可 (row=%d)、スキップ", i)
            continue
        name_text = name_el.inner_text().strip()

        # 順位: <!-- page:rank --> コメントから抽出
        m = re.search(r"<!--\s*\d+:(\d+)\s*-->", html)
        if not m:
            logger.debug("順位コメント取得不可 (row=%d, shop=%s)、スキップ", i, name_text)
            continue
        rank_num = int(m.group(1))

        results.append({
            "fetched_at": fetched_at,
            "shop_name":  name_text,
            "rank":       rank_num,
            "media_name": MEDIA_NAME,
        })

    logger.info("取得件数: %d", len(results))

    # スクリーンショット（失敗してもスクレイピング結果は返す）
    try:
        if slot_time:
            dt = datetime.strptime(slot_time, "%Y-%m-%d %H:%M:%S")
        else:
            dt = datetime.now()
        ss_dir = Path(r"C:\apps\ranking_system\screenshots\girlsheaven")
        ss_dir.mkdir(parents=True, exist_ok=True)
        ss_path = ss_dir / f"GH-{dt.strftime('%Y%m%d-%H%M')}.jpg"
        page.screenshot(path=str(ss_path), full_page=True, type="jpeg", quality=50)
        logger.info("スクリーンショット保存: %s", ss_path)
    except Exception as ss_err:
        logger.warning("スクリーンショット失敗: %s", ss_err)

    if not headless:
        page.wait_for_timeout(5000)
    return results


# ---------------------------------------------------------------------------
# リトライ付き実行
# ---------------------------------------------------------------------------
def scrape_with_retry(client_id: int, monitor_url: str, slot_time: str = "") -> list[dict]:
    last_error = None

    for attempt in range(1, RETRY_COUNT + 1):
        try:
            with sync_playwright() as p:
                headless = os.getenv("HEADLESS", "true").lower() != "false"
                if headless:
                    browser = p.chromium.launch(
                        headless=True,
                        args=[
                            "--no-sandbox",
                            "--disable-blink-features=AutomationControlled",
                        ],
                    )
                else:
                    browser = p.chromium.launch(
                        headless=False,
                        executable_path=r"C:\Program Files\Google\Chrome\Application\chrome.exe",
                        slow_mo=500,
                        args=[
                            "--no-sandbox",
                            "--disable-blink-features=AutomationControlled",
                        ],
                    )
                context = browser.new_context(
                    user_agent=(
                        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                        "AppleWebKit/537.36 (KHTML, like Gecko) "
                        "Chrome/124.0.0.0 Safari/537.36"
                    ),
                    viewport=random.choice(VIEWPORTS),            # 対策4: ビューポートランダム化
                    locale="ja-JP",
                    timezone_id="Asia/Tokyo",                     # 対策5: タイムゾーン設定
                    extra_http_headers={                          # 対策2: HTTPヘッダー補完
                        "Accept": (
                            "text/html,application/xhtml+xml,"
                            "application/xml;q=0.9,"
                            "image/avif,image/webp,*/*;q=0.8"
                        ),
                        "Accept-Language": "ja,en-US;q=0.7,en;q=0.3",
                        "Accept-Encoding": "gzip, deflate, br",
                        "Connection": "keep-alive",
                        "Upgrade-Insecure-Requests": "1",
                        "Sec-Fetch-Dest": "document",
                        "Sec-Fetch-Mode": "navigate",
                        "Sec-Fetch-Site": "none",
                        "Sec-Fetch-User": "?1",
                    },
                )
                page = context.new_page()
                Stealth().apply_stealth_sync(page)

                results = scrape(page, monitor_url, headless, slot_time)
                browser.close()
                return results

        except Exception as e:
            last_error = e
            logger.warning("取得失敗 (試行 %d/%d): %s", attempt, RETRY_COUNT, e)
            if attempt < RETRY_COUNT:
                logger.info("%d秒後にリトライします...", RETRY_WAIT)
                time.sleep(RETRY_WAIT)

    # 全リトライ失敗 → DB にエラーを記録
    error_msg = f"取得失敗 ({RETRY_COUNT}回リトライ後): {last_error}"
    logger.error(error_msg)
    try:
        save_error_log(client_id, monitor_url, error_msg)
    except Exception as db_err:
        logger.error("エラーログのDB保存も失敗: %s", db_err)

    raise RuntimeError(error_msg) from last_error


# ---------------------------------------------------------------------------
# エントリポイント
# ---------------------------------------------------------------------------
def main() -> None:
    parser = argparse.ArgumentParser(description="ガールズヘブン 順位スクレイピング")
    parser.add_argument("client_id",   type=int, help="クライアントID")
    parser.add_argument("monitor_url", type=str, help="監視対象URL（店舗一覧ページ）")
    parser.add_argument("slot_time",   type=str, nargs="?", default="", help="定刻スロット時刻")
    args = parser.parse_args()

    logger.info("スクレイピング開始 | client_id=%d url=%s", args.client_id, args.monitor_url)

    # 自店舗名を DB から取得
    try:
        own_shop_name = get_client_shop_name(args.client_id)
    except Exception as e:
        logger.error("店舗名取得エラー: %s", e)
        sys.exit(1)

    logger.info("自店舗名: '%s'", own_shop_name)

    results = scrape_with_retry(args.client_id, args.monitor_url, args.slot_time)

    if not results:
        logger.warning("取得結果が0件でした。")
        sys.exit(1)

    # 自店舗を特定してログ出力
    own_shop = next(
        (r for r in results if own_shop_name in r["shop_name"] or r["shop_name"] in own_shop_name),
        None,
    )
    if own_shop:
        logger.info("自店舗「%s」現在の順位：%d位", own_shop["shop_name"], own_shop["rank"])
    else:
        logger.warning("WARNING: 自店舗が見つかりません shop_name=%s", own_shop_name)

    # CSV に追記
    append_to_csv(args.client_id, results)

    # DB に保存（全店舗分）
    for row in results:
        try:
            save_scraping_result(args.client_id, row["shop_name"], row["rank"])
        except Exception as e:
            logger.error("DB保存エラー (shop=%s): %s", row["shop_name"], e)

    logger.info("完了 | 取得件数=%d", len(results))


if __name__ == "__main__":
    main()
