"""
collect_local_news.py
台東区エリアの街ネタ・営業フック情報を収集し local-news.json に整形出力する。

実行方法:
    py -3 collect_local_news.py

冪等設計:
    - 既存 local-news.json があれば読み込み、既存 id と重複しないものだけ先頭に追記。
    - 収集件数が 0 でも既存データを壊さない。

依存ライブラリ (標準ライブラリのみ):
    urllib.request, urllib.parse, json, re, hashlib, datetime, pathlib
    ※ requests 不要（AppData\\Local 環境の死んだ venv を回避）
"""

import json
import re
import hashlib
import datetime
import urllib.request
import urllib.parse
import urllib.error
import pathlib
import sys
import time

# ─────────────────────────────────────────────
# 定数
# ─────────────────────────────────────────────
SCRIPT_DIR = pathlib.Path(__file__).parent.resolve()
OUTPUT_FILE = SCRIPT_DIR / "local-news.json"

UA = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/124.0.0.0 Safari/537.36"
)

TODAY = datetime.date.today().isoformat()

# 取得上限（低コスト運用：1回の実行でフェッチするURLを絞る）
MAX_FETCH_PER_SOURCE = 1
FETCH_TIMEOUT = 10


# ─────────────────────────────────────────────
# ユーティリティ
# ─────────────────────────────────────────────
def make_id(title: str, source_url: str) -> str:
    """タイトル+URL の先頭8文字 hex を ID に使用（重複排除キー）"""
    key = (title + source_url).encode("utf-8")
    return hashlib.md5(key).hexdigest()[:8]


def fetch_html(url: str) -> str:
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    try:
        with urllib.request.urlopen(req, timeout=FETCH_TIMEOUT) as resp:
            raw = resp.read()
        # charset 推定
        charset = "utf-8"
        ct = resp.headers.get("Content-Type", "")
        m = re.search(r"charset=([^\s;]+)", ct)
        if m:
            charset = m.group(1).strip()
        try:
            return raw.decode(charset, errors="replace")
        except LookupError:
            return raw.decode("utf-8", errors="replace")
    except Exception as e:
        print(f"  [fetch error] {url}: {e}", file=sys.stderr)
        return ""


def strip_tags(html: str) -> str:
    """簡易タグ除去"""
    text = re.sub(r"<[^>]+>", " ", html)
    text = re.sub(r"&nbsp;", " ", text)
    text = re.sub(r"&[a-z]+;", "", text)
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def truncate(text: str, n: int = 120) -> str:
    return text[:n] + "…" if len(text) > n else text


# ─────────────────────────────────────────────
# 収集ソース定義
# ─────────────────────────────────────────────
# 各ソースは (label, url, extractor_fn) のタプル。
# extractor_fn(html) -> list[dict] で raw item を返す。
# raw item は最低限 title, summary, area, category を持つ。

def extract_goguynet_taito(html: str) -> list[dict]:
    """号外NET 台東区 開店/閉店記事を抽出"""
    items = []
    # 記事タイトル候補をパターンマッチ
    pattern = re.compile(
        r'<h2[^>]*class="[^"]*entry-title[^"]*"[^>]*>.*?<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>',
        re.DOTALL
    )
    for m in pattern.finditer(html):
        url = m.group(1).strip()
        title = strip_tags(m.group(2)).strip()
        if not title:
            continue
        # カテゴリ判定
        cat = "新店/閉店" if re.search(r"(オープン|開店|閉店|OPEN)", title) else "地域の話題"
        # エリア推定
        area = "浅草"
        for kw, a in [("谷中", "谷中"), ("上野", "上野"), ("蔵前", "蔵前"),
                      ("浅草橋", "浅草橋"), ("入谷", "入谷"), ("根岸", "根岸"),
                      ("御徒町", "上野"), ("上野桜木", "上野桜木")]:
            if kw in title:
                area = a
                break
        items.append({
            "title": title,
            "summary": title,  # 一覧ページでは本文が取れないため title で代替
            "area": area,
            "category": cat,
            "sourceUrl": url,
            "sourceName": "号外NET 台東区",
            "talkingPoint": f"{area}エリアの{cat}情報として訪問時の話題づくりに活用",
            "relatedTags": [area, cat],
        })
    return items[:5]  # 上位5件


def extract_tnavi_events(html: str) -> list[dict]:
    """TAITOおでかけナビ イベント一覧"""
    items = []
    pattern = re.compile(
        r'<(?:h2|h3|div)[^>]*class="[^"]*(?:title|event-title)[^"]*"[^>]*>(.*?)</(?:h2|h3|div)>',
        re.DOTALL
    )
    for m in pattern.finditer(html):
        title = strip_tags(m.group(1)).strip()
        if len(title) < 3:
            continue
        area = "台東区"
        for kw, a in [("浅草", "浅草"), ("上野", "上野"), ("谷中", "谷中"),
                      ("蔵前", "蔵前"), ("浅草橋", "浅草橋"), ("入谷", "入谷")]:
            if kw in title:
                area = a
                break
        cat = "イベント"
        if re.search(r"(祭|まつり|市|能|展)", title):
            cat = "イベント"
        items.append({
            "title": title,
            "summary": f"{area}で開催される「{title}」の情報。",
            "area": area,
            "category": cat,
            "sourceUrl": "https://t-navi.city.taito.lg.jp/event",
            "sourceName": "TAITOおでかけナビ",
            "talkingPoint": f"「{title}」の話題で{area}の訪問先と場を温める",
            "relatedTags": [area, "イベント", "観光"],
        })
    return items[:5]


def extract_taito_calendar(html: str) -> list[dict]:
    """台東区公式カレンダー"""
    items = []
    pattern = re.compile(
        r'<(?:li|div|td)[^>]*>(.*?(?:催し|イベント|まつり|祭|市|展)[^<]{5,100})</(?:li|div|td)>',
        re.DOTALL | re.IGNORECASE
    )
    seen = set()
    for m in pattern.finditer(html):
        title = strip_tags(m.group(1)).strip()
        title = re.sub(r"\s+", " ", title)[:60]
        if title in seen or len(title) < 5:
            continue
        seen.add(title)
        area = "台東区"
        for kw, a in [("浅草", "浅草"), ("上野", "上野"), ("谷中", "谷中"),
                      ("蔵前", "蔵前"), ("入谷", "入谷"), ("鳥越", "蔵前")]:
            if kw in title:
                area = a
                break
        items.append({
            "title": title,
            "summary": f"台東区カレンダーに掲載。{area}エリアのイベント・催し情報。",
            "area": area,
            "category": "イベント",
            "sourceUrl": "https://www.city.taito.lg.jp/event/calendar/list_calendar.html",
            "sourceName": "台東区公式カレンダー",
            "talkingPoint": f"区の公式情報として信頼性高く、{area}訪問先との話題に活用",
            "relatedTags": [area, "行政", "イベント"],
        })
    return items[:4]


# ─────────────────────────────────────────────
# 手動シード（確認済みの確実情報）
# ─────────────────────────────────────────────
SEEDED_ITEMS = [
    {
        "date": "2026-07-06",
        "category": "季節・歳時",
        "title": "入谷朝顔まつり 2026（7月6日〜8日）",
        "summary": (
            "入谷鬼子母神（真源寺）を中心に言問通りへ30件の朝顔業者と約100件の露天商が並ぶ、"
            "江戸情緒あふれる都内最大級の朝顔市。3日間で約10万人が訪れる。"
            "2026年は7月6日（月）〜8日（水）開催、5:00〜21:00。"
        ),
        "talkingPoint": "入谷・根岸エリアの店舗訪問時に「朝顔市で賑わいますね」と切り出すと親近感が生まれる",
        "area": "入谷",
        "sourceName": "入谷朝顔まつり公式・event-checker.info",
        "sourceUrl": "https://event-checker.info/iriya-asagao-maturi/",
        "relatedTags": ["入谷", "朝顔市", "歳時記", "夏", "観光", "商店街"],
    },
    {
        "date": "2026-06-06",
        "category": "イベント",
        "title": "鳥越祭 2026（6月6〜7・9日）千貫神輿が蔵前橋通りを巡行",
        "summary": (
            "蔵前・浅草橋エリアの鳥越神社で毎年6月に開催。"
            "都内最大級・約4トンの千貫神輿が氏子町会を一日かけて巡行し、"
            "日没後は弓張提灯に灯りが入る「夜まつり」が最高潮に。"
            "250軒超の屋台も並ぶ。蔵前駅徒歩6分、浅草橋駅徒歩8分。"
        ),
        "talkingPoint": "蔵前・浅草橋エリアの訪問では「鳥越祭のお神輿、見ましたか？」が最強の入り口",
        "area": "蔵前",
        "sourceName": "台東区公式・かいぶつくんブログ",
        "sourceUrl": "https://kaibutsu-blog.com/torikoe-matsuri-2026-guide/",
        "relatedTags": ["蔵前", "浅草橋", "神社", "夏祭り", "神輿", "歳時記"],
    },
    {
        "date": "2026-06-03",
        "category": "イベント",
        "title": "第46回 台東薪能（浅草寺境内・6月3日）",
        "summary": (
            "浅草寺境内の屋外能舞台で行われる本格的な薪能。"
            "能「隅田川」「小鍛冶」と狂言「口真似」を上演。"
            "開場17:15、開演18:00。雨天時は台東区立浅草公会堂。"
            "木遣り・纏振りの火入れ式で幕を開ける江戸情緒満点の文化イベント。"
        ),
        "talkingPoint": "浅草エリアの文化・教育系施設、旅館・ホテルへの訪問で「薪能ご存知ですか？」と話題に",
        "area": "浅草",
        "sourceName": "浅草観光連盟",
        "sourceUrl": "https://e-asakusa.jp/?p=109053",
        "relatedTags": ["浅草", "伝統芸能", "能楽", "浅草寺", "文化イベント"],
    },
    {
        "date": "2026-05-27",
        "category": "新店/閉店",
        "title": "お祭りBBQビアガーデン「浅草エキミセ屋台村」オープン（5月27日）",
        "summary": (
            "浅草EKIMISE屋上（東武浅草駅直結）にBBQビアガーデンがオープン。"
            "夏季限定でアクセス抜群の浅草の新定番スポットとなる見込み。"
        ),
        "talkingPoint": "浅草エリアの飲食・接待需要の拡大を示す事例として、同エリア飲食店へのアプローチ材料に",
        "area": "浅草",
        "sourceName": "号外NET 台東区",
        "sourceUrl": "https://taito.goguynet.jp/category/cat_openclose/",
        "relatedTags": ["浅草", "新店", "ビアガーデン", "夏", "EKIMISE"],
    },
    {
        "date": "2026-05-13",
        "category": "新店/閉店",
        "title": "ラーメン「ニクノイロ」浅草地下商店街にグランドオープン（5月13日）",
        "summary": (
            "東京メトロ銀座線浅草駅直結の浅草地下商店街に新ラーメン店がオープン。"
            "駅直結という好立地で観光客・地元民双方の需要を取り込む。"
        ),
        "talkingPoint": "地下商店街の空き区画が埋まりつつある動向として、浅草橋・蔵前方面の商業者に紹介できる",
        "area": "浅草",
        "sourceName": "号外NET 台東区",
        "sourceUrl": "https://taito.goguynet.jp/category/cat_openclose/",
        "relatedTags": ["浅草", "新店", "ラーメン", "地下商店街"],
    },
    {
        "date": "2026-05-02",
        "category": "新店/閉店",
        "title": "あだちブルワリー「ビアスタンド浅草」オープン（5月2日）",
        "summary": (
            "浅草1丁目に地域密着クラフトビール店がオープン。"
            "足立ブルワリーが台東区に初出店。地元産クラフトビールが飲める下町文化の新スポット。"
        ),
        "talkingPoint": "「クラフトビール、浅草でも飲めるようになりましたね」と飲食店・宿泊施設への会話の糸口に",
        "area": "浅草",
        "sourceName": "号外NET 台東区",
        "sourceUrl": "https://taito.goguynet.jp/category/cat_openclose/",
        "relatedTags": ["浅草", "新店", "クラフトビール", "飲食"],
    },
    {
        "date": "2026-05-05",
        "category": "新店/閉店",
        "title": "谷中岡埜栄泉（やなかおかのえいせん）閉店（5月5日最終営業）",
        "summary": (
            "谷中の老舗和菓子店「岡埜栄泉」が2026年5月5日に閉店。"
            "豆大福で有名な創業百年超の名店が惜しまれつつ幕。"
            "谷根千散策の定番スポットが一つ消えた。"
        ),
        "talkingPoint": "谷中エリアの訪問で「岡埜栄泉、なくなってしまいましたね」と地域愛を共有、信頼関係づくりに",
        "area": "谷中",
        "sourceName": "号外NET 台東区",
        "sourceUrl": "https://taito.goguynet.jp/category/cat_openclose/",
        "relatedTags": ["谷中", "閉店", "和菓子", "老舗", "谷根千"],
    },
    {
        "date": "2026-05-21",
        "category": "新店/閉店",
        "title": "ミスターデンジャー浅草観音店 閉店（5月21日）→立花本店へ",
        "summary": (
            "浅草の名物店「ミスターデンジャー観音店」が閉店し、墨田区の立花本店に集約。"
            "浅草エリアの飲食テナント流動が続いていることを示す動向。"
        ),
        "talkingPoint": "浅草エリアの話題として「あのお店、閉まってしまいましたね」と共感トークで場を作る",
        "area": "浅草",
        "sourceName": "号外NET 台東区",
        "sourceUrl": "https://taito.goguynet.jp/category/cat_openclose/",
        "relatedTags": ["浅草", "閉店", "飲食", "テナント"],
    },
    {
        "date": "2026-03-21",
        "category": "地域の話題",
        "title": "浅草「まちづくりビジョン」策定：量から質へ、回遊動線を強化",
        "summary": (
            "台東区が2026年度から浅草駅・隅田川周辺の都市空間再編に着手。"
            "「もっと人を呼ぶ」から「人を受け止め回遊させる」への転換が核心。"
            "六区ブロードウェイのナイトタイム観光強化、隅田川沿い多機能空間化が柱。"
            "大規模再開発ではなく既存資産活用型。"
        ),
        "talkingPoint": "浅草エリアの再開発動向として、不動産・店舗オーナーへの訪問で「区の方針が変わってきましたね」と戦略的に話題提供",
        "area": "浅草",
        "sourceName": "健美家（kenbiya.com）",
        "sourceUrl": "https://www.kenbiya.com/ar/ns/region/tokyo/9946.html",
        "relatedTags": ["浅草", "再開発", "まちづくり", "観光政策", "インバウンド"],
    },
    {
        "date": "2026-06-01",
        "category": "地域の話題",
        "title": "台東区「EDO IT！」観光マナー啓発キャンペーン展開中",
        "summary": (
            "台東区がインバウンド急増に伴うオーバーツーリズム対策として「EDO IT！」スローガンを掲げ、"
            "地域住民・事業者と協働したマナー啓発活動を展開。"
            "観光客の満足度向上と区民生活の両立を目指す。"
        ),
        "talkingPoint": "浅草・上野の宿泊施設や商店への訪問で「外国人対応、大変ですよね」と共感ワードとして使える",
        "area": "浅草",
        "sourceName": "HotelBank",
        "sourceUrl": "https://hotelbank.jp/inbound/asakusa-tourism-sustainability-edo-it/",
        "relatedTags": ["台東区", "インバウンド", "観光政策", "マナー", "持続可能"],
    },
    {
        "date": "2026-06-06",
        "category": "季節・歳時",
        "title": "台東区の夏歳時記：7月ほおずき市（浅草寺）・酉の市（11月）",
        "summary": (
            "浅草寺ほおずき市は毎年7月9〜10日、功徳日に開わせて開催。"
            "境内に100軒以上のほおずき屋台が並び浴衣姿の参拝者で賑わう。"
            "11月の酉の市（鷲神社・花園神社）とともに台東区を代表する夏秋の歳時記。"
        ),
        "talkingPoint": "浅草エリア全般の訪問で「もうすぐほおずき市ですね」と季節感ある話題で雰囲気を作る",
        "area": "浅草",
        "sourceName": "hibiyakadan.com",
        "sourceUrl": "https://www.hibiyakadan.com/item/LIFESTYLE_Z_0041.html",
        "relatedTags": ["浅草", "ほおずき市", "浅草寺", "夏", "歳時記", "酉の市"],
    },
    {
        "date": "2026-06-06",
        "category": "地域の話題",
        "title": "上野桜木あたり：1938年築古民家3棟を再生した複合施設が観光スポットに",
        "summary": (
            "上野桜木1丁目の1938年築古民家3棟をリノベーションした複合施設「上野桜木あたり」。"
            "ビアホール・ベーカリー・塩と油の専門店・イベントスペース等で構成。"
            "谷根千散策の新拠点として国内外から注目を集めている。"
        ),
        "talkingPoint": "上野桜木・谷中エリアの訪問で「上野桜木あたり、行かれましたか？」と地域活性化の話題に",
        "area": "上野桜木",
        "sourceName": "上野桜木あたり公式",
        "sourceUrl": "https://www.uenosakuragiatari.jp/",
        "relatedTags": ["上野桜木", "古民家", "リノベ", "観光スポット", "谷根千"],
    },
    {
        "date": "2026-06-06",
        "category": "営業フック",
        "title": "【営業フック】蔵前・浅草橋エリア：問屋街×クリエイター集積の二層構造",
        "summary": (
            "蔵前・浅草橋は江戸時代からの人形・玩具・アクセサリー問屋街に、"
            "近年ハイセンスなカフェ・ギャラリー・クラフト工房が混在。"
            "「KURAMAE」ブランドで海外メディアにも取り上げられる台東区の成長エリア。"
            "广告营业（言問散歩）の訴求軸として「地域に根ざした老舗×新規クリエイター層」が有効。"
        ),
        "talkingPoint": "蔵前・浅草橋の訪問先に「この辺り、最近すごく注目されてますよね」と地域プライドをくすぐるアプローチ",
        "area": "蔵前",
        "sourceName": "TAITOおでかけナビ",
        "sourceUrl": "https://t-navi.city.taito.lg.jp/features/asakusabashi_kuramae",
        "relatedTags": ["蔵前", "浅草橋", "問屋街", "クリエイター", "営業フック"],
    },
    {
        "date": "2026-06-06",
        "category": "営業フック",
        "title": "【営業フック】谷根千エリア：古民家カフェ・銭湯ギャラリーで散策需要が拡大",
        "summary": (
            "谷中・根津・千駄木（谷根千）エリアは関東大震災・戦災を免れた木造家屋が残り、"
            "古民家カフェ・銭湯を改装したギャラリー・雑貨店が増加。"
            "国内リピーター＋インバウンドのディープトーキョー需要を取り込む地域として成長中。"
        ),
        "talkingPoint": "谷中・根岸エリアの店舗・施設に「谷根千、最近また人が増えてますね」と地域の成長を共有し関係を深める",
        "area": "谷中",
        "sourceName": "TAITOおでかけナビ",
        "sourceUrl": "https://t-navi.city.taito.lg.jp/features/yanaka_negishi_iriya_kanasugi",
        "relatedTags": ["谷中", "根岸", "谷根千", "古民家", "インバウンド", "散策"],
    },
    {
        "date": "2026-06-06",
        "category": "地域の話題",
        "title": "フィリピンエキスポ2026 上野公園噴水前広場で開催（6月5〜7日）",
        "summary": (
            "上野公園噴水前広場でフィリピンエキスポが2026年6月5〜7日に開催。"
            "台東区上野エリアのインバウンド多様化・東南アジア客増加の潮流を示す動向。"
        ),
        "talkingPoint": "上野エリアの飲食・ホテル訪問で「フィリピン系のお客さんも増えてきましたよね」と多様化トークに",
        "area": "上野",
        "sourceName": "台東区公式カレンダー",
        "sourceUrl": "https://www.city.taito.lg.jp/event/calendar/list_calendar.html",
        "relatedTags": ["上野", "インバウンド", "多様化", "東南アジア", "イベント"],
    },
]


# ─────────────────────────────────────────────
# Web収集（低コスト：2ソースのみフェッチ）
# ─────────────────────────────────────────────
WEB_SOURCES = [
    {
        "label": "号外NET台東区",
        "url": "https://taito.goguynet.jp/category/cat_openclose/",
        "extractor": extract_goguynet_taito,
    },
    {
        "label": "台東区公式カレンダー",
        "url": "https://www.city.taito.lg.jp/event/calendar/list_calendar.html",
        "extractor": extract_taito_calendar,
    },
]


def collect_from_web() -> list[dict]:
    results = []
    for src in WEB_SOURCES:
        print(f"  フェッチ中: {src['label']} ({src['url']})")
        html = fetch_html(src["url"])
        if not html:
            print(f"  スキップ（取得失敗）: {src['label']}")
            continue
        items = src["extractor"](html)
        print(f"  取得: {len(items)}件")
        results.extend(items)
        time.sleep(1)  # サーバー負荷軽減
    return results


# ─────────────────────────────────────────────
# メイン処理
# ─────────────────────────────────────────────
def load_existing() -> dict:
    if OUTPUT_FILE.exists():
        try:
            with open(OUTPUT_FILE, encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            pass
    return {"_meta": {}, "items": []}


def build_item(raw: dict) -> dict:
    title = raw.get("title", "").strip()
    source_url = raw.get("sourceUrl", "")
    item_id = make_id(title, source_url)
    return {
        "id": item_id,
        "date": raw.get("date", TODAY),
        "category": raw.get("category", "地域の話題"),
        "title": title,
        "summary": truncate(raw.get("summary", title), 200),
        "talkingPoint": raw.get("talkingPoint", ""),
        "area": raw.get("area", "台東区"),
        "sourceName": raw.get("sourceName", ""),
        "sourceUrl": source_url,
        "relatedTags": raw.get("relatedTags", []),
    }


def main():
    print("=== 台東区 街ネタ収集スクリプト 開始 ===")
    print(f"出力先: {OUTPUT_FILE}")

    # 既存データ読み込み
    existing = load_existing()
    existing_ids = {item["id"] for item in existing.get("items", [])}
    print(f"既存件数: {len(existing_ids)}件")

    # 新規収集
    print("\n--- シードデータ処理 ---")
    new_items = []
    for raw in SEEDED_ITEMS:
        item = build_item(raw)
        if item["id"] not in existing_ids:
            new_items.append(item)
            existing_ids.add(item["id"])

    print(f"シード新規: {len(new_items)}件")

    print("\n--- Web収集 ---")
    web_raws = collect_from_web()
    web_new = 0
    for raw in web_raws:
        item = build_item(raw)
        if item["id"] not in existing_ids:
            new_items.append(item)
            existing_ids.add(item["id"])
            web_new += 1
    print(f"Web新規: {web_new}件")

    if not new_items:
        print("\n新規アイテムなし。既存データを保持します。")
    else:
        print(f"\n合計新規: {len(new_items)}件 → 先頭に追加")

    # 新着を先頭に追記
    all_items = new_items + existing.get("items", [])

    # カテゴリ別集計
    cat_count: dict[str, int] = {}
    for it in all_items:
        c = it.get("category", "不明")
        cat_count[c] = cat_count.get(c, 0) + 1

    output = {
        "_meta": {
            "updated": datetime.datetime.now().isoformat(timespec="seconds"),
            "totalItems": len(all_items),
            "categorySummary": cat_count,
            "sources": [
                "号外NET台東区 (taito.goguynet.jp)",
                "TAITOおでかけナビ (t-navi.city.taito.lg.jp)",
                "台東区公式カレンダー (city.taito.lg.jp)",
                "入谷朝顔まつり公式 (event-checker.info)",
                "浅草観光連盟 (e-asakusa.jp)",
                "健美家 (kenbiya.com)",
                "HotelBank (hotelbank.jp)",
            ],
        },
        "items": all_items,
    }

    with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
        json.dump(output, f, ensure_ascii=False, indent=2)

    print("\n=== 完了 ===")
    print(f"総件数: {len(all_items)}")
    for cat, cnt in sorted(cat_count.items(), key=lambda x: -x[1]):
        print(f"  {cat}: {cnt}件")
    print(f"ファイル: {OUTPUT_FILE}")
    return len(all_items), cat_count


if __name__ == "__main__":
    main()
