#!/usr/bin/env python3
"""
=============================================================
  Qoo10 Japan 범용 리뷰 크롤러
  사용법: python qoo10_crawler.py <상품URL 또는 상품코드>
=============================================================
  - 어떤 Qoo10 JP 상품이든 URL만 넣으면 전체 리뷰 크롤링
  - 출력: output/<상품코드>/p1.html ~ pN.html
  - 이후 analyzer.html에 업로드하면 자동 분석
=============================================================
"""

import asyncio
import sys
import os
import re
import time
from pathlib import Path

try:
    from playwright.async_api import async_playwright
except ImportError:
    print("❌ Playwright가 설치되지 않았습니다.")
    print("   다음 명령어로 설치해주세요:")
    print("   pip install playwright && playwright install chromium")
    sys.exit(1)

try:
    from tqdm import tqdm
except ImportError:
    # tqdm 없어도 동작하도록 fallback
    class tqdm:
        def __init__(self, iterable=None, total=None, desc="", **kwargs):
            self.iterable = iterable
            self.total = total
            self.desc = desc
            self.n = 0
        def __iter__(self):
            for item in self.iterable:
                yield item
                self.n += 1
                print(f"\r  {self.desc} {self.n}/{self.total}", end="", flush=True)
            print()
        def update(self, n=1):
            self.n += n
            print(f"\r  {self.desc} {self.n}/{self.total}", end="", flush=True)
        def close(self):
            print()


# ─────────────────────────────────────────
# 1) 상품코드 추출
# ─────────────────────────────────────────
def extract_goods_code(input_str: str) -> str:
    """URL 또는 숫자에서 상품코드 추출"""
    # 순수 숫자
    if re.match(r'^\d+$', input_str.strip()):
        return input_str.strip()
    # URL 패턴: /item/.../<숫자>
    m = re.search(r'/(\d{8,12})(?:\?|$|#)', input_str)
    if m:
        return m.group(1)
    # URL 끝에 숫자
    m = re.search(r'/(\d{8,12})$', input_str.strip().rstrip('/'))
    if m:
        return m.group(1)
    # goodsNo 파라미터
    m = re.search(r'goodsNo=(\d+)', input_str)
    if m:
        return m.group(1)
    return None


# ─────────────────────────────────────────
# 2) 리뷰 페이지 수 감지
# ─────────────────────────────────────────
REVIEW_API_URL = "https://review.qoo10.jp/api/v1/review/list"

async def get_total_pages(page, goods_code: str) -> tuple:
    """첫 페이지를 열어 총 리뷰 수와 페이지 수를 감지"""
    url = f"https://www.qoo10.jp/item/test/{goods_code}"
    print(f"\n📦 상품 페이지 접속 중: {url}")
    
    try:
        await page.goto(url, wait_until="domcontentloaded", timeout=30000)
        await page.wait_for_timeout(3000)
    except Exception as e:
        print(f"⚠️  페이지 로딩 경고: {e}")

    # 상품명 추출
    product_name = ""
    try:
        product_name = await page.locator("h1, .goods_name, #goods_name").first.inner_text()
        product_name = product_name.strip()[:80]
    except:
        product_name = f"상품코드_{goods_code}"
    
    # 리뷰 수 추출 시도
    total_reviews = 0
    try:
        # 여러 셀렉터 시도
        for selector in [
            '.review_total_count', '.review_count', '#review_count',
            'span:has-text("件")', '.total_count'
        ]:
            el = page.locator(selector).first
            if await el.count() > 0:
                txt = await el.inner_text()
                m = re.search(r'[\d,]+', txt.replace(',', ''))
                if m:
                    total_reviews = int(m.group().replace(',', ''))
                    break
    except:
        pass

    # 리뷰 API로 직접 확인
    if total_reviews == 0:
        try:
            resp = await page.request.get(
                f"{REVIEW_API_URL}?goods_code={goods_code}&page=1&limit=50"
            )
            data = await resp.json()
            total_reviews = data.get('total', 0)
        except:
            pass
    
    # 그래도 없으면 수동 탐색
    if total_reviews == 0:
        print("⚠️  리뷰 수 자동 감지 실패. 바이너리 서치로 페이지 수 탐색...")
        total_pages = await binary_search_pages(page, goods_code)
        return product_name, total_reviews, total_pages
    
    total_pages = (total_reviews + 49) // 50  # 50개씩
    print(f"✅ 상품명: {product_name}")
    print(f"✅ 총 리뷰: {total_reviews:,}건 → {total_pages}페이지")
    
    return product_name, total_reviews, total_pages


async def binary_search_pages(page, goods_code: str) -> int:
    """바이너리 서치로 마지막 페이지 찾기"""
    review_url = f"https://www.qoo10.jp/gmkt.inc/Goods/QnA/BuyerReviewList.aspx?goods_code={goods_code}"
    
    low, high = 1, 200
    last_valid = 1
    
    while low <= high:
        mid = (low + high) // 2
        try:
            resp = await page.request.get(f"{review_url}&page={mid}")
            html = await resp.text()
            if '<li' in html and 'review_star_area' in html:
                last_valid = mid
                low = mid + 1
            else:
                high = mid - 1
        except:
            high = mid - 1
    
    print(f"✅ 바이너리 서치 결과: 약 {last_valid}페이지")
    return last_valid


# ─────────────────────────────────────────
# 3) 리뷰 페이지 크롤링
# ─────────────────────────────────────────
async def crawl_review_page(page, goods_code: str, page_num: int) -> str:
    """단일 리뷰 페이지 HTML 가져오기"""
    url = (
        f"https://www.qoo10.jp/gmkt.inc/Goods/QnA/BuyerReviewList.aspx"
        f"?goods_code={goods_code}&page={page_num}"
    )
    try:
        resp = await page.request.get(url, timeout=15000)
        html = await resp.text()
        return html
    except Exception as e:
        return ""


async def crawl_all_reviews(goods_code: str, total_pages: int, output_dir: Path):
    """전체 리뷰 크롤링"""
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context(
            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",
            locale="ja-JP"
        )
        page = await context.new_page()
        
        # 상품 페이지 먼저 방문 (쿠키/세션용)
        try:
            await page.goto(
                f"https://www.qoo10.jp/item/test/{goods_code}",
                wait_until="domcontentloaded", timeout=20000
            )
            await page.wait_for_timeout(2000)
        except:
            pass

        success = 0
        failed = 0
        
        # 배치 처리 (10개씩 병렬은 아니지만, 순차적으로 빠르게)
        pbar = tqdm(range(1, total_pages + 1), total=total_pages, desc="📥 크롤링")
        
        for page_num in pbar:
            html = await crawl_review_page(page, goods_code, page_num)
            
            if html and '<li' in html:
                filepath = output_dir / f"p{page_num}.html"
                filepath.write_text(html, encoding='utf-8')
                success += 1
            else:
                failed += 1
                # 3번 재시도
                for retry in range(3):
                    await page.wait_for_timeout(1000 * (retry + 1))
                    html = await crawl_review_page(page, goods_code, page_num)
                    if html and '<li' in html:
                        filepath = output_dir / f"p{page_num}.html"
                        filepath.write_text(html, encoding='utf-8')
                        success += 1
                        failed -= 1
                        break
            
            # 너무 빠르면 차단될 수 있으므로 딜레이
            if page_num % 10 == 0:
                await page.wait_for_timeout(500)
        
        await browser.close()
        return success, failed


# ─────────────────────────────────────────
# 4) 메타데이터 저장
# ─────────────────────────────────────────
def save_metadata(output_dir: Path, goods_code: str, product_name: str, 
                   total_reviews: int, total_pages: int, success: int):
    """크롤링 메타정보 저장 (대시보드에서 활용)"""
    import json
    meta = {
        "goods_code": goods_code,
        "product_name": product_name,
        "product_url": f"https://www.qoo10.jp/item/test/{goods_code}",
        "product_image": f"https://gd3.image-qoo10.jp/ai/goods/{goods_code}/0_400.jpg",
        "total_reviews": total_reviews,
        "total_pages": total_pages,
        "crawled_pages": success,
        "crawled_at": time.strftime("%Y-%m-%d %H:%M:%S"),
    }
    meta_path = output_dir / "meta.json"
    meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding='utf-8')
    print(f"📄 메타데이터 저장: {meta_path}")


# ─────────────────────────────────────────
# 5) 메인
# ─────────────────────────────────────────
async def main():
    print("=" * 60)
    print("  🛒 Qoo10 Japan 범용 리뷰 크롤러 v1.0")
    print("=" * 60)

    # 인자 처리
    if len(sys.argv) < 2:
        print("\n사용법:")
        print("  python qoo10_crawler.py <상품URL 또는 상품코드>")
        print("\n예시:")
        print("  python qoo10_crawler.py https://www.qoo10.jp/item/test/1184844785")
        print("  python qoo10_crawler.py 1184844785")
        sys.exit(0)

    input_str = sys.argv[1]
    goods_code = extract_goods_code(input_str)
    
    if not goods_code:
        print(f"\n❌ 상품코드를 추출할 수 없습니다: {input_str}")
        print("   URL 형식: https://www.qoo10.jp/item/.../숫자")
        sys.exit(1)
    
    print(f"\n🔍 상품코드: {goods_code}")

    # 출력 폴더
    output_dir = Path("output") / goods_code
    output_dir.mkdir(parents=True, exist_ok=True)
    print(f"📂 출력 폴더: {output_dir}")

    # Playwright 시작
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
            locale="ja-JP"
        )
        page = await context.new_page()
        
        # 페이지 수 감지
        product_name, total_reviews, total_pages = await get_total_pages(page, goods_code)
        await browser.close()

    if total_pages == 0:
        print("\n❌ 리뷰를 찾을 수 없습니다.")
        sys.exit(1)

    # 크롤링 시작
    print(f"\n🚀 크롤링 시작! ({total_pages}페이지)")
    start_time = time.time()
    
    success, failed = await crawl_all_reviews(goods_code, total_pages, output_dir)
    
    elapsed = time.time() - start_time

    # 메타데이터 저장
    save_metadata(output_dir, goods_code, product_name, total_reviews, total_pages, success)

    # 결과 출력
    print(f"\n{'=' * 60}")
    print(f"  ✅ 크롤링 완료!")
    print(f"{'=' * 60}")
    print(f"  상품명    : {product_name}")
    print(f"  상품코드  : {goods_code}")
    print(f"  총 리뷰   : {total_reviews:,}건")
    print(f"  성공 페이지: {success} / {total_pages}")
    print(f"  실패 페이지: {failed}")
    print(f"  소요 시간  : {elapsed:.1f}초")
    print(f"  저장 위치  : {output_dir}/")
    print(f"{'=' * 60}")
    print(f"\n👉 다음 단계: analyzer.html을 열고 '{output_dir}' 폴더를 업로드하세요!")


if __name__ == "__main__":
    asyncio.run(main())
