# -*- coding: utf-8 -*-
r"""
tools/generate_realtor_naver_map.py

중개사 주소/좌표를 기준으로 네이버 지도 화면을 Playwright headless로 캡처하고,
Cafe24 CDN에 업로드한 뒤 blog_realtors.map_* 컬럼을 업데이트한다.

카카오 JS SDK 대신 네이버 지도 웹 화면을 캡처하는 방식이다.
- 카카오 Local REST API: 주소 → 좌표 변환용
- 네이버 지도 웹: 실제 지도 렌더링/캡처용
- Cafe24 CDN: 블로그 본문 삽입용 이미지 저장

사용 예:
    cd D:\honghee\blog_api

    set KAKAO_REST_API_KEY=카카오_REST_API_키
    python tools\generate_realtor_naver_map.py --realtor-id 4

    python tools\generate_realtor_naver_map.py --all
"""

import os
import sys
import argparse
from pathlib import Path
from datetime import datetime
from urllib.parse import quote

import requests
from playwright.sync_api import sync_playwright

BASE_DIR = Path(__file__).resolve().parents[1]
sys.path.append(str(BASE_DIR))

from db import get_conn
from services.cafe24_cdn_service import upload_header_image_to_cafe24


OUTPUT_DIR = BASE_DIR / "storage" / "realtor_maps"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)

MAP_WIDTH = 900
MAP_HEIGHT = 540
BROWSER_WIDTH = 1500
BROWSER_HEIGHT = 620

# STEP102-14D:
# 네이버지도 c=lng,lat 중심점은 브라우저 viewport 중앙에 위치한다.
# 따라서 900px 캡처 영역도 viewport 중앙(1500/2=750)에 맞춰야 한다.
# 기존 CLIP_X=520은 캡처 중앙이 970px이라 지도 중심과 핀이 어긋났다.
CLIP_X = 300
CLIP_Y = 20


def clean_text(value):
    return str(value or "").strip()


def table_columns(conn, table_name):
    try:
        with conn.cursor() as cur:
            cur.execute(f"SHOW COLUMNS FROM {table_name}")
            rows = cur.fetchall()
        return set(row["Field"] for row in rows)
    except Exception:
        return set()


def fetch_realtor(conn, realtor_id):
    with conn.cursor() as cur:
        cur.execute("""
            SELECT *
            FROM blog_realtors
            WHERE id = %s
            LIMIT 1
        """, (int(realtor_id),))
        return cur.fetchone() or {}


def fetch_realtors_for_batch(conn):
    with conn.cursor() as cur:
        cur.execute("""
            SELECT *
            FROM blog_realtors
            WHERE COALESCE(office_address, address, '') <> ''
              AND COALESCE(status, 'active') = 'active'
            ORDER BY id ASC
        """)
        return cur.fetchall() or []


def kakao_geocode(address, rest_api_key):
    address = clean_text(address)

    if not address:
        raise Exception("주소가 비어 있습니다.")

    if not rest_api_key:
        raise Exception("KAKAO_REST_API_KEY 값이 없습니다.")

    res = requests.get(
        "https://dapi.kakao.com/v2/local/search/address.json",
        headers={"Authorization": f"KakaoAK {rest_api_key}"},
        params={"query": address},
        timeout=20,
    )

    if res.status_code >= 400:
        raise Exception(f"Kakao geocode HTTP {res.status_code}: {res.text[:500]}")

    data = res.json()
    docs = data.get("documents") or []

    if not docs:
        raise Exception(f"주소 좌표 변환 결과가 없습니다: {address}")

    doc = docs[0]

    return {
        "lat": float(doc.get("y")),
        "lng": float(doc.get("x")),
        "raw": doc,
    }



def normalize_digits(value):
    return "".join(ch for ch in str(value or "") if ch.isdigit())


def kakao_keyword_place_search(realtor, rest_api_key):
    """
    STEP102-14C 안전버전:
    네이버지도 URL 중심좌표를 직접 읽지 않는다.
    카카오 Local 키워드 검색으로 '중개사무소 상호명'을 먼저 찾고,
    실패할 때만 기존 주소 지오코딩으로 fallback한다.

    우선순위:
    1) 상호명 + 전화번호/주소 일부 일치
    2) 상호명 일치
    3) 실패 시 None
    """
    if not rest_api_key:
        return None

    office_name = clean_text(realtor.get("office_name") or "")
    office_phone = clean_text(realtor.get("office_phone") or realtor.get("phone") or "")
    mobile_phone = clean_text(realtor.get("mobile_phone") or realtor.get("mobile") or "")
    base_address = clean_text(realtor.get("office_address") or realtor.get("address") or "")
    detail_address = clean_text(realtor.get("office_detail_address") or "")
    address = clean_text(f"{base_address} {detail_address}")

    if not office_name:
        return None

    short_address = address[:35].strip() if len(address) > 35 else address

    queries = [
        f"{office_name} {office_phone}" if office_phone else "",
        f"{office_name} {mobile_phone}" if mobile_phone and mobile_phone != office_phone else "",
        f"{office_name} {short_address}" if short_address else "",
        office_name,
    ]

    seen = set()
    clean_queries = []
    for q in queries:
        q = clean_text(q)
        if not q:
            continue
        if q in seen:
            continue
        seen.add(q)
        clean_queries.append(q)

    expected_phones = {
        normalize_digits(office_phone),
        normalize_digits(mobile_phone),
    }
    expected_phones.discard("")

    office_key = office_name.replace("공인중개사사무소", "").replace("공인중개사", "").replace("부동산", "")
    office_key = office_key.replace(" ", "").lower()

    best = None

    for q in clean_queries:
        print(f"[KAKAO KEYWORD SEARCH] {q}", flush=True)

        try:
            res = requests.get(
                "https://dapi.kakao.com/v2/local/search/keyword.json",
                headers={"Authorization": f"KakaoAK {rest_api_key}"},
                params={"query": q, "size": 10},
                timeout=20,
            )

            if res.status_code >= 400:
                print(f"[KAKAO KEYWORD HTTP {res.status_code}] {res.text[:300]}", flush=True)
                continue

            docs = (res.json() or {}).get("documents") or []

            for doc in docs:
                place_name = clean_text(doc.get("place_name"))
                road_address_name = clean_text(doc.get("road_address_name"))
                address_name = clean_text(doc.get("address_name"))
                phone = clean_text(doc.get("phone"))
                place_url = clean_text(doc.get("place_url"))
                category_name = clean_text(doc.get("category_name"))

                text_blob = " ".join([place_name, road_address_name, address_name, phone, category_name]).replace(" ", "").lower()
                score = 0

                # 상호명 매칭
                if office_name.replace(" ", "").lower() in text_blob:
                    score += 100
                elif office_key and office_key in text_blob:
                    score += 70

                # 업종 매칭
                if "부동산" in text_blob or "중개" in text_blob:
                    score += 20

                # 전화번호 매칭
                found_phone = normalize_digits(phone)
                phone_match = bool(found_phone and found_phone in expected_phones)
                if phone_match:
                    score += 80

                # 주소 일부 매칭
                address_match_count = 0
                address_tokens = []
                for token in address.replace(",", " ").split():
                    token = token.strip()
                    if len(token) < 3:
                        continue
                    normalized_token = token.replace(" ", "").lower()
                    if normalized_token in text_blob:
                        address_match_count += 1
                        address_tokens.append(token)
                        score += 8

                try:
                    lat = float(doc.get("y"))
                    lng = float(doc.get("x"))
                except Exception:
                    continue

                candidate = {
                    "lat": lat,
                    "lng": lng,
                    "score": score,
                    "phone_match": phone_match,
                    "address_match_count": address_match_count,
                    "address_tokens": address_tokens,
                    "place_name": place_name,
                    "address": road_address_name or address_name,
                    "phone": phone,
                    "place_url": place_url,
                    "raw": doc,
                    "query": q,
                }

                print(
                    f"[KAKAO KEYWORD CANDIDATE] score={score} place={place_name} addr={candidate['address']} phone={phone}",
                    flush=True,
                )

                if best is None or candidate["score"] > best["score"]:
                    best = candidate

                # 상호명만 같은 타 지역 업체를 즉시 선택하지 않는다.
                # 전화번호가 일치하거나 주소 토큰이 2개 이상 일치할 때만 확정한다.
                has_location_evidence = phone_match or address_match_count >= 2

                if score >= 100 and has_location_evidence:
                    print(f"[KAKAO KEYWORD PLACE OK] {candidate}", flush=True)
                    return candidate

        except Exception as e:
            print(f"[KAKAO KEYWORD ERROR] {q} / {e}", flush=True)

    if best:
        has_location_evidence = (
            bool(best.get("phone_match"))
            or int(best.get("address_match_count") or 0) >= 2
        )

        if best.get("score", 0) >= 90 and has_location_evidence:
            print(f"[KAKAO KEYWORD PLACE BEST] {best}", flush=True)
            return best

        print(
            f"[KAKAO KEYWORD PLACE REJECT] insufficient location evidence / {best}",
            flush=True,
        )

    print("[KAKAO KEYWORD PLACE EMPTY] fallback to address geocode", flush=True)
    return None


def build_naver_map_url(lat=None, lng=None, office_name="", address=""):
    """
    좌표가 있으면 좌표 중심 지도를 연다.
    좌표 변환에 실패하면 네이버지도 주소 검색 화면을 직접 연다.
    """
    office_name = clean_text(office_name)
    address = clean_text(address)

    if lat is not None and lng is not None:
        return f"https://map.naver.com/v5/?c={lng},{lat},15,0,0,0,dh"

    search_query = address or office_name
    if not search_query:
        raise Exception("네이버지도 검색어가 없습니다.")

    return f"https://map.naver.com/p/search/{quote(search_query)}"


def render_naver_map_screenshot(lat, lng, office_name, address, phone_text, output_path):
    output_path = Path(output_path)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    url = build_naver_map_url(lat, lng, office_name, address)

    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=[
                "--disable-gpu",
                "--no-sandbox",
                "--disable-dev-shm-usage",
                "--lang=ko-KR",
            ],
        )

        page = browser.new_page(
            viewport={"width": BROWSER_WIDTH, "height": BROWSER_HEIGHT},
            device_scale_factor=1,
            locale="ko-KR",
        )

        page.goto(url, wait_until="domcontentloaded", timeout=45000)

        # 네이버 지도는 타일/검색 결과 로딩이 늦을 수 있다.
        page.wait_for_timeout(7000 if lat is None or lng is None else 5000)

        # 주소 검색 fallback에서는 첫 번째 검색 결과를 클릭해 지도 중심을 맞춘다.
        if lat is None or lng is None:
            try:
                selectors = [
                    'a[href*="/place/"]',
                    'li a',
                    '[role="listitem"] a',
                ]
                clicked = False
                for selector in selectors:
                    locator = page.locator(selector)
                    if locator.count() > 0:
                        locator.first.click(timeout=3000)
                        clicked = True
                        break
                if clicked:
                    page.wait_for_timeout(3500)
            except Exception as e:
                print(f"[NAVER SEARCH RESULT CLICK SKIP] {e}", flush=True)

        cleanup_js = """
        () => {
          const style = document.createElement('style');
          style.id = 'real-auto-map-clean-style';
          style.textContent = `
            input, button, textarea,
            [role="button"],
            [class*="search"],
            [class*="Search"],
            [class*="panel"],
            [class*="Panel"],
            [class*="sidebar"],
            [class*="Sidebar"],
            [class*="control"],
            [class*="Control"],
            [class*="toolbar"],
            [class*="Toolbar"],
            [class*="toast"],
            [class*="Toast"],
            [class*="banner"],
            [class*="Banner"],
            [class*="promotion"],
            [class*="Promotion"],
            [class*="ad_"],
            [class*="Ad"],
            iframe {
              visibility: hidden !important;
              opacity: 0 !important;
              pointer-events: none !important;
            }
          `;
          const oldStyle = document.getElementById('real-auto-map-clean-style');
          if (oldStyle) oldStyle.remove();
          document.head.appendChild(style);

          const keywords = [
            '네이버지도가',
            '밥값',
            '맛집이면',
            '지도앱 업데이트',
            '실시간 도로',
            '업데이트 안내',
            'SmartAround',
            '주변',
            '리뷰'
          ];

          const nodes = Array.from(document.querySelectorAll('div, section, article, aside, nav'));
          for (const node of nodes) {
            const text = (node.innerText || '').trim();
            if (!text) continue;

            if (keywords.some(k => text.includes(k))) {
              let target = node;
              for (let i = 0; i < 3; i++) {
                if (target.parentElement && target.getBoundingClientRect().width < 900) {
                  target = target.parentElement;
                }
              }
              target.style.visibility = 'hidden';
              target.style.opacity = '0';
              target.style.pointerEvents = 'none';
            }
          }
        }
        """

        page.evaluate(cleanup_js)
        page.wait_for_timeout(800)
        page.evaluate(cleanup_js)

        # 지도 위에 자체 오버레이를 얹는다.
        # crop 영역(CLIP_X~CLIP_X+900)의 중앙은 page 좌표 750px이다.
        overlay_js = """
        (args) => {
          args = args || [];
          const officeName = args[0] || '';
          const address = args[1] || '';
          const phoneText = args[2] || '';

          const old = document.getElementById('real-auto-map-overlay');
          if (old) old.remove();

          const wrap = document.createElement('div');
          wrap.id = 'real-auto-map-overlay';
          wrap.style.position = 'fixed';
          wrap.style.left = '0';
          wrap.style.top = '0';
          wrap.style.width = '100vw';
          wrap.style.height = '100vh';
          wrap.style.pointerEvents = 'none';
          wrap.style.zIndex = '999999';
          wrap.style.fontFamily = "Arial, 'Malgun Gothic', sans-serif";

          const veil = document.createElement('div');
          veil.style.position = 'absolute';
          veil.style.left = '300px';
          veil.style.top = '20px';
          veil.style.width = '900px';
          veil.style.height = '540px';
          veil.style.background = 'rgba(255,255,255,.28)';
          wrap.appendChild(veil);

          const label = document.createElement('div');
          label.style.position = 'absolute';
          label.style.left = '750px';
          label.style.top = '230px';
          label.style.transform = 'translate(-50%, -100%)';
          label.style.minWidth = '410px';
          label.style.maxWidth = '620px';
          label.style.padding = '16px 22px 18px';
          label.style.borderRadius = '18px';
          label.style.background = 'rgba(17,24,39,.94)';
          label.style.color = '#fff';
          label.style.boxShadow = '0 16px 36px rgba(0,0,0,.30)';
          label.style.textAlign = 'center';

          const title = document.createElement('div');
          title.textContent = officeName || '중개사무소';
          title.style.fontSize = '25px';
          title.style.fontWeight = '900';
          title.style.lineHeight = '1.32';
          title.style.wordBreak = 'keep-all';
          label.appendChild(title);

          if (address) {
            const addr = document.createElement('div');
            addr.textContent = address;
            addr.style.marginTop = '8px';
            addr.style.fontSize = '14px';
            addr.style.lineHeight = '1.55';
            addr.style.color = 'rgba(255,255,255,.88)';
            addr.style.wordBreak = 'keep-all';
            label.appendChild(addr);
          }

          if (phoneText) {
            const phone = document.createElement('div');
            phone.textContent = phoneText;
            phone.style.marginTop = '8px';
            phone.style.fontSize = '15px';
            phone.style.fontWeight = '900';
            phone.style.color = '#bbf7d0';
            label.appendChild(phone);
          }

          const tail = document.createElement('div');
          tail.style.position = 'absolute';
          tail.style.left = '50%';
          tail.style.bottom = '-20px';
          tail.style.transform = 'translateX(-50%)';
          tail.style.width = '0';
          tail.style.height = '0';
          tail.style.borderLeft = '20px solid transparent';
          tail.style.borderRight = '20px solid transparent';
          tail.style.borderTop = '22px solid rgba(17,24,39,.94)';
          label.appendChild(tail);

          const pin = document.createElement('div');
          pin.style.position = 'absolute';
          pin.style.left = '750px';
          pin.style.top = '275px';
          pin.style.width = '38px';
          pin.style.height = '38px';
          pin.style.background = '#ef4444';
          pin.style.border = '5px solid #fff';
          pin.style.borderRadius = '50% 50% 50% 0';
          pin.style.transform = 'translate(-50%, -10%) rotate(-45deg)';
          pin.style.boxShadow = '0 8px 20px rgba(0,0,0,.30)';

          const dot = document.createElement('div');
          dot.style.position = 'absolute';
          dot.style.left = '13px';
          dot.style.top = '13px';
          dot.style.width = '11px';
          dot.style.height = '11px';
          dot.style.background = '#fff';
          dot.style.borderRadius = '50%';
          pin.appendChild(dot);

          wrap.appendChild(label);
          wrap.appendChild(pin);
          document.body.appendChild(wrap);
        }
        """

        page.evaluate(overlay_js, [office_name, address, phone_text])

        # 오버레이 반영 대기
        page.wait_for_timeout(500)

        page.screenshot(
            path=str(output_path),
            full_page=False,
            type="png",
            clip={"x": CLIP_X, "y": CLIP_Y, "width": MAP_WIDTH, "height": MAP_HEIGHT},
        )

        browser.close()

    return str(output_path)


def update_realtor_map_result(conn, realtor_id, lat=None, lng=None, image_path="", image_url="", error_message=""):
    columns = table_columns(conn, "blog_realtors")
    data = {}

    if "map_lat" in columns:
        data["map_lat"] = lat

    if "map_lng" in columns:
        data["map_lng"] = lng

    if "map_image_path" in columns:
        data["map_image_path"] = image_path

    if "map_image_url" in columns:
        data["map_image_url"] = image_url

    if "map_generated_at" in columns:
        data["map_generated_at"] = datetime.now() if image_url else None

    if "map_error_message" in columns:
        data["map_error_message"] = error_message[:2000] if error_message else None

    if "updated_at" in columns:
        data["updated_at"] = datetime.now()

    if not data:
        print("[MAP UPDATE SKIP] blog_realtors map columns not found")
        return

    sets = ", ".join([f"{k} = %s" for k in data.keys()])
    values = list(data.values())
    values.append(int(realtor_id))

    with conn.cursor() as cur:
        cur.execute(f"UPDATE blog_realtors SET {sets} WHERE id = %s", values)

    conn.commit()


def generate_for_realtor(conn, realtor, rest_api_key):
    realtor_id = int(realtor.get("id") or 0)
    office_name = clean_text(realtor.get("office_name") or "중개사무소")
    office_phone = clean_text(realtor.get("office_phone") or realtor.get("phone") or "")
    mobile_phone = clean_text(realtor.get("mobile_phone") or realtor.get("mobile") or "")
    base_address = clean_text(realtor.get("office_address") or realtor.get("address") or "")
    detail_address = clean_text(realtor.get("office_detail_address") or "")
    address = clean_text(f"{base_address} {detail_address}")

    if not realtor_id:
        raise Exception("realtor_id가 없습니다.")

    if not address:
        raise Exception(f"주소가 없습니다. realtor_id={realtor_id}")

    phone_text = " / ".join([x for x in [office_phone, mobile_phone] if x])

    print(f"[MAP START] realtor_id={realtor_id}, office={office_name}")

    # 1순위: 카카오 키워드 검색으로 실제 장소 좌표
    # 2순위: 카카오 주소 지오코딩
    # 3순위: 좌표 없이 네이버지도 주소 검색 화면 직접 캡처
    lat = None
    lng = None
    geo_source = "naver_address_search"

    place = kakao_keyword_place_search(realtor, rest_api_key)

    if place:
        lat = float(place["lat"])
        lng = float(place["lng"])
        geo_source = "kakao_keyword_place"
        print(
            f"[MAP GEO SOURCE] {geo_source} lat={lat} lng={lng} "
            f"place={place.get('place_name')}",
            flush=True,
        )
    else:
        try:
            geo = kakao_geocode(address, rest_api_key)
            lat = float(geo["lat"])
            lng = float(geo["lng"])
            geo_source = "kakao_address"
            print(
                f"[MAP GEO SOURCE] {geo_source} lat={lat} lng={lng}",
                flush=True,
            )
        except Exception as geocode_error:
            # 좌표 변환 실패만으로 지도 이미지 생성 전체를 중단하지 않는다.
            print(
                f"[MAP GEO FALLBACK] naver address search / {geocode_error}",
                flush=True,
            )

    filename = f"realtor_naver_map_{realtor_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}.png"
    local_path = OUTPUT_DIR / filename

    image_path = render_naver_map_screenshot(
        lat=lat,
        lng=lng,
        office_name=office_name,
        address=address,
        phone_text=phone_text,
        output_path=local_path,
    )

    cdn = upload_header_image_to_cafe24(
        conn=conn,
        article_no=f"realtor_{realtor_id}",
        local_path=image_path,
        filename=filename,
    )

    image_url = clean_text((cdn or {}).get("cdn_url") or "")

    if not image_url:
        raise Exception("Cafe24 CDN 업로드 결과 URL이 없습니다.")

    update_realtor_map_result(
        conn=conn,
        realtor_id=realtor_id,
        lat=lat,
        lng=lng,
        image_path=image_path,
        image_url=image_url,
        error_message="",
    )

    print(f"[MAP DONE] realtor_id={realtor_id}, source={geo_source}, url={image_url}")

    return {
        "realtor_id": realtor_id,
        "lat": lat,
        "lng": lng,
        "geo_source": geo_source,
        "image_path": image_path,
        "image_url": image_url,
    }


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--realtor-id", type=int, default=0)
    parser.add_argument("--all", action="store_true")
    parser.add_argument("--kakao-rest-api-key", type=str, default="")
    args = parser.parse_args()

    rest_api_key = args.kakao_rest_api_key or os.environ.get("KAKAO_REST_API_KEY", "")

    conn = get_conn()

    success = 0
    failed = 0

    try:
        if args.all:
            targets = fetch_realtors_for_batch(conn)
        else:
            if args.realtor_id <= 0:
                raise Exception("--realtor-id 또는 --all 옵션이 필요합니다.")
            targets = [fetch_realtor(conn, args.realtor_id)]

        print("=" * 80)
        print(f"[MAP TARGET COUNT] {len(targets)}")
        print("=" * 80)

        for realtor in targets:
            if not realtor:
                continue

            realtor_id = int(realtor.get("id") or 0)

            try:
                generate_for_realtor(
                    conn=conn,
                    realtor=realtor,
                    rest_api_key=rest_api_key,
                )
                success += 1

            except Exception as e:
                failed += 1
                print(f"[MAP ERROR] realtor_id={realtor_id} / {e}")

                try:
                    update_realtor_map_result(
                        conn=conn,
                        realtor_id=realtor_id,
                        error_message=str(e),
                    )
                except Exception:
                    pass

        print("=" * 80)
        print(f"[MAP FINISH] success={success}, failed={failed}")
        print("=" * 80)

        if success == 0 and failed > 0:
            raise SystemExit(1)

    finally:
        try:
            conn.close()
        except Exception:
            pass


if __name__ == "__main__":
    main()
