"""
バニラ 順位スクレイピング

monitor_url（ランキング一覧ページ）を開き、自店舗が何ページ目・何位にいるか確認する。
自店舗の特定には DB の clients テーブルから shop_name を取得する。

Usage:
    python scrape_vanilla.py <client_id> <monitor_url>
"""

import argparse
import csv
import logging
import os
import random
import sys
import time
from dataclasses import dataclass
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 = "vanilla"
CSV_DIR = Path(__file__).parent / "csv"
RETRY_COUNT = 1
RETRY_WAIT = 15       # 秒
PAGE_TIMEOUT = 60_000 # ms（タイムアウトを60秒に延長）
MAX_PAGES = 10        # 探索する最大ページ数

# 対策4: ビューポートをランダム化
VIEWPORTS = [
    {"width": 1280, "height": 800},
    {"width": 1366, "height": 768},
    {"width": 1920, "height": 1080},
    {"width": 1440, "height": 900},
]

# ---------------------------------------------------------------------------
# ロガー設定
# ---------------------------------------------------------------------------
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# データクラス
# ---------------------------------------------------------------------------
@dataclass
class ShopResult:
    shop_name: str
    rank: int
    page_no: int
    fetched_at: str
    sub_area: str | None = None


# ---------------------------------------------------------------------------
# DB ヘルパー
# ---------------------------------------------------------------------------
def get_client_shop_name(client_id: int) -> str:
    """media_settingsテーブルからバニラの自店舗名を取得する。
    未設定の場合はclientsテーブルにフォールバックする。
    """
    rows = execute_query(
        "SELECT shop_name FROM media_settings WHERE client_id = %s AND media_name = 'vanilla' 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, result: ShopResult) -> None:
    sql = """
        INSERT INTO scraping_results
            (client_id, media_name, shop_name, `rank`, page_no, fetched_at)
        VALUES (%s, %s, %s, %s, %s, %s)
    """
    execute_update(sql, (
        client_id,
        MEDIA_NAME,
        result.shop_name,
        result.rank,
        result.page_no,
        result.fetched_at,
    ), local=True)


def save_scraping_results(client_id: int, results: list[ShopResult]) -> None:
    """全店舗リストを一括保存する。"""
    sql = """
        INSERT INTO scraping_results
            (client_id, media_name, shop_name, `rank`, page_no, fetched_at, sub_area)
        VALUES (%s, %s, %s, %s, %s, %s, %s)
    """
    params_list = [
        (client_id, MEDIA_NAME, r.shop_name, r.rank, r.page_no, r.fetched_at, r.sub_area)
        for r in results
    ]
    for params in params_list:
        execute_update(sql, params, local=True)
    logger.info("全店舗DB保存完了: %d件", len(results))


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, 'vanilla_error', f"[{monitor_url}] {message}", datetime.now()), local=True)


# ---------------------------------------------------------------------------
# CSV ヘルパー
# ---------------------------------------------------------------------------
def append_to_csv(client_id: int, result: ShopResult) -> 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", "page_no", "media_name"],
        )
        if write_header:
            writer.writeheader()
        writer.writerow({
            "fetched_at": result.fetched_at,
            "shop_name":  result.shop_name,
            "rank":       result.rank,
            "page_no":    result.page_no,
            "media_name": MEDIA_NAME,
        })

    logger.info("CSV追記完了: %s", csv_path)


# ---------------------------------------------------------------------------
# ページパーサー
# ---------------------------------------------------------------------------
# バニラ確定セレクタ (2026-03-19 HTML構造調査済み)
SHOP_ROW_SEL  = "li.searchResult-shop-item"
SHOP_NAME_SEL = "h3.shop-name a"
PAGER_SEL     = ".mod-pager .pager-item a"


def scrape_page(
    page: Page, url: str, page_no: int, rank_offset: int = 0
) -> list[dict]:
    """
    指定 URL のページを取得し、店舗リストを返す。
    URL は呼び出し元（find_own_shop）が直接構築して渡す。
    rank_offset: 前ページまでの累積店舗数（順位計算に使用）
    """
    logger.info("ページ取得: page=%d url=%s", page_no, url)
    page.goto(url, timeout=PAGE_TIMEOUT, wait_until="domcontentloaded")  # ②
    try:
        page.wait_for_load_state("networkidle", timeout=15000)
    except Exception:
        logger.warning("networkidle待機タイムアウト、domcontentloadedで続行します")
    page.wait_for_timeout(random.randint(2000, 4000))  # 対策1: ページ取得後ランダム待機

    # 店舗行を取得
    rows_locator = page.locator(SHOP_ROW_SEL)
    count = rows_locator.count()
    if count == 0:
        raise ValueError(f"店舗行（{SHOP_ROW_SEL}）が見つかりません (page={page_no}): {url}")
    logger.info("  店舗行取得: %d件 (offset=%d)", count, rank_offset)

    shops = []
    for i in range(count):
        row = rows_locator.nth(i)

        # 店舗名: h3.shop-name a のテキスト
        name_el = row.locator(SHOP_NAME_SEL).first
        if name_el.count() == 0:
            logger.debug("  店舗名取得不可 (row=%d)、スキップ", i)
            continue
        name_text = name_el.inner_text().strip()
        if not name_text:
            continue

        # サブエリア: p.shop-info のテキストを | で分割し最初の部分を取得
        sub_area = None
        shop_info_el = row.locator("p.shop-info").first
        if shop_info_el.count() > 0:
            info_text = shop_info_el.inner_text().strip()
            if info_text:
                sub_area = info_text.split("|")[0].strip() or None

        # 順位: 前ページまでの累積数 + このページ内の連番（1始まり）
        rank_num = rank_offset + i + 1
        shops.append({"shop_name": name_text, "rank": rank_num, "sub_area": sub_area})

    return shops



# ---------------------------------------------------------------------------
# 自店舗探索
# ---------------------------------------------------------------------------
MAX_SCAN_PAGES = 2  # 最大読み込みページ数（総ページ数がこれ以上でも2ページまで）


def _get_total_pages(page: Page) -> int:
    """ページャーのリンクから総ページ数を取得する。"""
    pager_links = page.locator(PAGER_SEL)
    max_page = 1
    for idx in range(pager_links.count()):
        href = pager_links.nth(idx).get_attribute("href") or ""
        # /page2/ /page3/ のようなパスからページ番号を抽出
        for part in href.split("/"):
            if part.startswith("page") and part[4:].isdigit():
                max_page = max(max_page, int(part[4:]))
    return max_page


def scrape_all_shops(
    browser_page: Page,
    monitor_url: str,
    own_shop_name: str,
    slot_time: str = "",
) -> tuple[list[ShopResult], ShopResult | None]:
    """
    monitor_url から総ページ数を確認し、最大 MAX_SCAN_PAGES ページを読み込む。
    - 総ページ数が1→1ページ、2以上→2ページまで読み込む
    - 自店舗が見つかっても規定ページ数まで読み込みを継続する
    - 戻り値: (全店舗リスト, 自店舗結果 or None)
    """
    if slot_time:
        fetched_at = slot_time
    else:
        fetched_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    page_no = 1
    rank_offset = 0
    own_result = None
    all_results: list[ShopResult] = []
    target_pages = MAX_SCAN_PAGES  # 1ページ目読み込み後に総ページ数で上書き

    while page_no <= target_pages:
        if page_no == 1:
            # 1ページ目はURLで直接アクセス
            shops = scrape_page(browser_page, monitor_url, page_no, rank_offset)
        else:
            # 2ページ目以降はページネーションをスクロール→クリック
            # ① ページ下部までゆっくりスクロール（200〜400px刻み、300〜700ms待機）
            scroll_y = browser_page.evaluate("window.scrollY")
            page_height = browser_page.evaluate("document.body.scrollHeight")
            while scroll_y < page_height:
                scroll_y = min(scroll_y + random.randint(200, 400), page_height)
                browser_page.evaluate(f"window.scrollTo(0, {scroll_y})")
                browser_page.wait_for_timeout(random.randint(300, 700))
                # ページネーションが画面内に入ったら停止
                pager = browser_page.locator(f".mod-pager .pager-item a[href*='page{page_no}']")
                if pager.count() > 0 and pager.first.is_visible():
                    break

            # ② ランダム待機（人間らしく）
            browser_page.wait_for_timeout(random.randint(1000, 2000))

            # ③ ページネーションをhover→click
            pager = browser_page.locator(f".mod-pager .pager-item a[href*='page{page_no}']")
            if pager.count() == 0:
                logger.info("ページネーションが見つかりません。終了します。")
                break
            pager.first.scroll_into_view_if_needed()
            browser_page.wait_for_timeout(random.randint(500, 1000))
            pager.first.hover()
            browser_page.wait_for_timeout(random.randint(500, 1000))
            pager.first.click()

            # ④ クリック後待機
            browser_page.wait_for_timeout(random.randint(3000, 5000))

            # ⑤ スクレイピング処理（現在のページをそのまま使用）
            shops = scrape_page(browser_page, browser_page.url, page_no, rank_offset)

        logger.info("  page=%d: %d店舗取得", page_no, len(shops))

        # 1ページ目読み込み後に総ページ数を確定して上限を決める
        if page_no == 1:
            total_pages = _get_total_pages(browser_page)
            target_pages = min(total_pages, MAX_SCAN_PAGES)
            logger.info("総ページ数: %d → %dページまで読み込み", total_pages, target_pages)

        for shop in shops:
            shop_result = ShopResult(
                shop_name=shop["shop_name"],
                rank=shop["rank"],
                page_no=page_no,
                fetched_at=fetched_at,
                sub_area=shop.get("sub_area"),
            )
            all_results.append(shop_result)

            # 自店舗チェック（まだ見つかっていない場合のみ記録、読み込みは継続）
            if own_result is None and (
                own_shop_name in shop["shop_name"] or shop["shop_name"] in own_shop_name
            ):
                own_result = shop_result
                logger.info(
                    "  自店舗発見: '%s' page=%d rank=%d",
                    shop["shop_name"], page_no, shop["rank"],
                )

        rank_offset += len(shops)
        page_no += 1

    if own_result is None:
        # ④ 規定ページ以内に見つからなかった場合
        logger.warning("自店舗 '%s' が %dページ以内に見つかりませんでした", own_shop_name, target_pages)

    # スクリーンショット（失敗してもスクレイピング結果は返す）
    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\vanilla")
        ss_dir.mkdir(parents=True, exist_ok=True)
        ss_path = ss_dir / f"VLA-{dt.strftime('%Y%m%d-%H%M')}.jpg"
        browser_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)

    return all_results, own_result


# ---------------------------------------------------------------------------
# リトライ付き実行
# ---------------------------------------------------------------------------
def run_with_retry(
    client_id: int, monitor_url: str, own_shop_name: str, slot_time: str = ""
) -> tuple[list[ShopResult], ShopResult | None]:
    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",
                    },
                )
                browser_page = context.new_page()
                Stealth().apply_stealth_sync(browser_page)

                all_results, own_result = scrape_all_shops(browser_page, monitor_url, own_shop_name, slot_time)
                browser.close()
                return all_results, own_result

        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)

    all_results, own_result = run_with_retry(args.client_id, args.monitor_url, own_shop_name, args.slot_time)

    if own_result is None:
        # 見つからなかった場合も「未検出」としてCSV・DBに記録
        own_result = ShopResult(
            shop_name=own_shop_name,
            rank=0,
            page_no=0,
            fetched_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        )
        logger.warning("自店舗が見つかりませんでした。rank=0, page_no=0 で記録します。")
        # 自店舗未検出時は all_results に自店舗分を追加して記録
        all_results.append(own_result)

    # CSV に追記（自店舗のみ）
    append_to_csv(args.client_id, own_result)

    # DB に全店舗を一括保存
    try:
        save_scraping_results(args.client_id, all_results)
    except Exception as e:
        logger.error("DB保存エラー: %s", e)

    logger.info(
        "完了 | 全%d店舗保存 | 自店: shop='%s' page=%d rank=%d",
        len(all_results), own_result.shop_name, own_result.page_no, own_result.rank,
    )


if __name__ == "__main__":
    main()
