"""Build insta_mapping.json from insta_meta.json.

Input: insta_meta.json — list of {thread_id, thread_name (YYYY/MM/DD),
created_at_ts, caption, image_urls[]}.

Output: insta_mapping.json — keyed by week (YYMMDD_YYMMDD), then by day
(MON-FRI), with lunch/dinner arrays of {thread_id, caption, image_urls}.

Lunch/dinner classification:
1. Drop near-duplicate posts (same caption + created_at within 5s) — Discord
   bot occasionally double-publishes on RESUME / connection wobble.
2. Classify each post by caption keywords (English Discord translations +
   Korean originals). Posts tagged 'lunch' go to lunch[]; 'dinner' go to
   dinner[]. Unknown captions fall back to created_at ordering against the
   remaining empty slot.
3. After all entries placed, any 3rd+ entry per day goes to 'extra'.
"""
import json
import re
import sys
from collections import defaultdict
from datetime import datetime, timedelta
from pathlib import Path

if hasattr(sys.stdout, 'reconfigure'):
    sys.stdout.reconfigure(encoding='utf-8')

HERE = Path(__file__).resolve().parent
META = HERE / 'insta_meta.json'
OUT = HERE / 'insta_mapping.json'

DAY_NAMES = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']

# Caption keyword patterns. The Discord bot publishes DeepL English; Playwright
# fetches the Korean originals. Both vocabularies are matched here.
LUNCH_KW = re.compile(
    r"today'?s lunch|lunch menu|lunchtime|today'?s lunch special"
    r"|오늘 점심|점심 메뉴|점심 스페셜|오늘의 점심",
    re.IGNORECASE,
)
DINNER_KW = re.compile(
    r"tonight'?s|dinner|evening menu"
    r"|오늘 저녁|저녁 메뉴|저녁 스페셜|오늘의 저녁|today'?s dinner",
    re.IGNORECASE,
)

# Two entries within this window with identical caption are treated as the
# same post (Discord bot RESUME-driven double publish has been observed at
# ~300-700ms apart; 5s leaves comfortable margin without merging genuine
# back-to-back posts which are spaced minutes/hours apart).
DUP_WINDOW_MS = 5_000


def classify_meal(caption: str) -> str | None:
    """Return 'lunch' / 'dinner' / None based on caption text."""
    if not caption:
        return None
    if LUNCH_KW.search(caption):
        return 'lunch'
    if DINNER_KW.search(caption):
        return 'dinner'
    return None


def dedupe_caption_dupes(entries: list[dict]) -> tuple[list[dict], int]:
    """Drop later entries that share caption with an earlier entry within DUP_WINDOW_MS.

    Returns (kept, dropped_count). Entries assumed sorted by created_at_ts asc.
    """
    kept: list[dict] = []
    dropped = 0
    for e in entries:
        cap = (e.get('caption') or '').strip()
        ts = int(e.get('created_at_ts') or 0)
        is_dup = False
        for prev in kept:
            if (prev.get('caption') or '').strip() == cap and abs(int(prev.get('created_at_ts') or 0) - ts) <= DUP_WINDOW_MS:
                is_dup = True
                break
        if is_dup:
            dropped += 1
            continue
        kept.append(e)
    return kept, dropped


def date_to_week(date_str: str) -> tuple[str, str] | None:
    """'2026/04/22' → ('260420_260424', 'WED'). Returns None if Sat/Sun."""
    d = datetime.strptime(date_str, '%Y/%m/%d').date()
    weekday = d.weekday()  # Mon=0..Sun=6
    if weekday >= 5:
        return None  # Skip weekend menus (kitchen closed)
    monday = d - timedelta(days=weekday)
    friday = monday + timedelta(days=4)
    week_key = f'{monday.strftime("%y%m%d")}_{friday.strftime("%y%m%d")}'
    day_name = DAY_NAMES[weekday]
    return week_key, day_name


def _entry_payload(e: dict) -> dict:
    return {
        'thread_id': e['thread_id'],
        'caption': e['caption'],
        'image_urls': e['image_urls'],
        'video_urls': e.get('video_urls') or [],
        'posted_at': e['created_at_kst'],
    }


def main():
    if not META.is_file():
        print(f'not found: {META}', file=sys.stderr)
        sys.exit(1)
    data = json.loads(META.read_text(encoding='utf-8'))
    print(f'loaded {len(data)} entries from {META.name}')

    # Dedupe across-source: Playwright entries (thread_id starts with "insta_")
    # and Discord-forum entries (numeric thread_id) can describe the same
    # Instagram post on the same date. Discord entries are DeepL reuploads by
    # insta_bot; dropping them when the Playwright original exists keeps
    # lunch/dinner assignment correct.
    pw_dates = {e['thread_name'] for e in data
                if str(e.get('thread_id', '')).startswith('insta_')}
    before = len(data)
    data = [e for e in data
            if not (not str(e.get('thread_id', '')).startswith('insta_')
                    and e.get('thread_name') in pw_dates)]
    cross_dropped = before - len(data)
    if cross_dropped:
        print(f'dedupe (cross-source): dropped {cross_dropped} Discord entries that duplicate Playwright originals')

    by_date = defaultdict(list)
    for e in data:
        by_date[e['thread_name']].append(e)

    mapping: dict = {}
    weekend_skipped: list[str] = []
    extras_log: list[tuple[str, int]] = []
    same_caption_drops = 0
    fallback_log: list[tuple[str, str, str]] = []  # (date, slot, caption-prefix)

    for date_str, entries in sorted(by_date.items()):
        wk_day = date_to_week(date_str)
        if not wk_day:
            weekend_skipped.append(date_str)
            continue
        week_key, day_name = wk_day

        entries.sort(key=lambda x: x['created_at_ts'])
        # Same-caption near-duplicates within the day (insta_bot RESUME bug etc.)
        entries, dropped = dedupe_caption_dupes(entries)
        same_caption_drops += dropped

        if week_key not in mapping:
            mapping[week_key] = {}
        if day_name not in mapping[week_key]:
            mapping[week_key][day_name] = {'lunch': [], 'dinner': [], 'extra': []}
        day_slot = mapping[week_key][day_name]

        # Two-pass placement: caption-keyword classify first (high confidence),
        # then place unknowns into the remaining empty slot by time order.
        unknowns: list[dict] = []
        for e in entries:
            meal = classify_meal(e.get('caption') or '')
            if meal in ('lunch', 'dinner') and not day_slot[meal]:
                day_slot[meal].append(_entry_payload(e))
            elif meal in ('lunch', 'dinner'):
                # Same meal already filled — additional same-meal entry goes to extra
                day_slot['extra'].append(_entry_payload(e))
                extras_log.append((date_str, len(day_slot['extra'])))
            else:
                unknowns.append(e)

        for e in unknowns:
            if not day_slot['lunch']:
                day_slot['lunch'].append(_entry_payload(e))
                fallback_log.append((date_str, 'lunch', (e.get('caption') or '')[:40]))
            elif not day_slot['dinner']:
                day_slot['dinner'].append(_entry_payload(e))
                fallback_log.append((date_str, 'dinner', (e.get('caption') or '')[:40]))
            else:
                day_slot['extra'].append(_entry_payload(e))
                extras_log.append((date_str, len(day_slot['extra'])))

    OUT.write_text(json.dumps(mapping, ensure_ascii=False, indent=2), encoding='utf-8')

    print(f'\n=== summary ===')
    print(f'  weeks covered: {len(mapping)}')
    print(f'  weekend dates skipped: {len(weekend_skipped)}', weekend_skipped[:5])
    print(f'  same-caption duplicates dropped: {same_caption_drops}')
    print(f'  unknown-caption fallback placements: {len(fallback_log)}', fallback_log[:5])
    print(f'  extras (3rd+ on same day): {len(extras_log)}', extras_log[:5])
    print(f'  output: {OUT}')

    print('\n=== per-week ===')
    for wk in sorted(mapping):
        day_stats = []
        for d in DAY_NAMES[:5]:
            slots = mapping[wk].get(d, {})
            l = len(slots.get('lunch') or [])
            di = len(slots.get('dinner') or [])
            day_stats.append(f'{d}:{l}/{di}')
        print(f'  {wk}  {" ".join(day_stats)}')


if __name__ == '__main__':
    main()
