"""h-lab 댓글 수집 + 타임스탬프 매칭 — 자동 탭용.

h-lab(https://h-lab.tolag.shop)의 comment-cards API에서 영상 전체 댓글을 받아,
본문 속 mm:ss / h:mm:ss 언급을 초로 파싱한다. 정규식·평문 변환 규칙은
h-lab comment-cards.js(TS_RE, toPlainText)와 동일하게 맞춘다.
"""
from __future__ import annotations

import json
import re
import urllib.request
from typing import Dict, List, Optional

H_LAB = "https://h-lab.tolag.shop"

# h-lab comment-cards.js 의 TS_RE 와 동일 규칙
TS_RE = re.compile(r"(?<!\d)(\d{1,2}):([0-5]\d)(?::([0-5]\d))?(?!\d)")
_BR_RE = re.compile(r"<br\s*/?>", re.I)
_TAG_RE = re.compile(r"<[^>]+>")

# 분:초를 이만큼 넘게 나열한 댓글 = '목차 댓글'(하이라이트 모음). 카드로 못 쓴다.
# ⚠ 이게 없으면 모든 구간에 매칭돼서 **어느 ID를 열어도 맨 위에 이 댓글이 뜬다.**
# 후보(candidates)는 분:초가 아예 없는 댓글만 쓰므로, 여기만 막으면 화면에서 완전히 빠진다.
MAX_TIMES = 3

# 광고/홍보 댓글 판별 — 좋아요를 조작한 광고가 ➕좋아요 채우기·후보 상위에 올라와
# 드래프트 카드로 뽑히는 사고 방지. 오탐이 더 아프므로 보수적으로:
# URL, "n% 할인", "최저가"만 본다("할인"·"%" 단독인 일상 댓글은 통과).
_AD_RE = re.compile(
    r"https?://|www\.|tinyurl\.|bit\.ly|"
    r"\d+\s*%\s*할인|최저가"
)


def _is_ad(text: str) -> bool:
    """광고/홍보 댓글이면 True — fetch_comments 가 수집 단계에서 걸러낸다."""
    return bool(_AD_RE.search(plain_text(text)))


def plain_text(html: str) -> str:
    """YouTube textDisplay(HTML) → 평문. <br>→줄바꿈, 나머지 태그 제거."""
    s = _BR_RE.sub("\n", str(html or ""))
    return _TAG_RE.sub("", s)


def parse_times(text: str) -> List[float]:
    """댓글 본문 속 mm:ss / h:mm:ss → 초 리스트(중복 제거, 등장 순)."""
    out: List[float] = []
    seen = set()
    for m in TS_RE.finditer(plain_text(text)):
        if m.group(3) is not None:
            sec = int(m.group(1)) * 3600 + int(m.group(2)) * 60 + int(m.group(3))
        else:
            sec = int(m.group(1)) * 60 + int(m.group(2))
        if sec not in seen:
            seen.add(sec)
            out.append(float(sec))
    return out


def fetch_comments(url: str, *, timeout: float = 180.0) -> List[Dict]:
    """h-lab에서 전체 댓글 수집. 원본 순서 유지, idx = 배열 인덱스(식별자).

    실패는 예외 그대로 던진다(URLError/RuntimeError) — 호출부(자동 탭 스트림)가
    "댓글 없이 진행" 폴백을 담당한다.
    """
    req = urllib.request.Request(
        f"{H_LAB}/api/comment-cards/fetch",
        data=json.dumps({"url": url}).encode("utf-8"),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        data = json.loads(resp.read().decode("utf-8"))
    if not data.get("success"):
        raise RuntimeError(f"h-lab 응답 실패: {data.get('message')}")
    out: List[Dict] = []
    for c in data.get("data") or []:
        text = str(c.get("text") or "")
        if _is_ad(text):   # 광고는 수집 단계에서 제외 — 어떤 추천 경로로도 카드가 못 된다
            continue
        out.append({
            "idx": len(out),
            "authorName": str(c.get("authorName") or ""),
            "text": text,
            "likeCount": int(c.get("likeCount") or 0),
            "replyCount": int(c.get("replyCount") or 0),
            "publishedAt": str(c.get("publishedAt") or ""),
            "profileImageUrl": str(c.get("profileImageUrl") or ""),
            "times": parse_times(c.get("text") or ""),
        })
    return out


def match_window(comments: List[Dict], start: float, end: float) -> List[int]:
    """[start, end] 안의 시각을 하나라도 언급한 댓글 idx — 좋아요 내림차순."""
    return match_ranges(comments, [(start, end)])


def match_ranges(comments: List[Dict], ranges) -> List[int]:
    """여러 구간 중 어느 하나라도 언급한 댓글 idx — 좋아요 내림차순 (유튜브 구간 탭용).

    분:초를 MAX_TIMES 개보다 많이 나열한 목차 댓글은 제외(§MAX_TIMES 주석 참고).
    """
    hit = [c for c in comments
           if len(c["times"]) <= MAX_TIMES
           and any(any(s <= t <= e for s, e in ranges) for t in c["times"])]
    hit.sort(key=lambda c: -c["likeCount"])
    return [c["idx"] for c in hit]


def top_liked(comments: List[Dict], exclude: set, n: int = 20) -> List[int]:
    """exclude(idx 집합) 제외 좋아요 상위 n개 idx."""
    rest = [c for c in comments if c["idx"] not in exclude]
    rest.sort(key=lambda c: -c["likeCount"])
    return [c["idx"] for c in rest[:n]]


def match_slots(comments: List[Dict], start: float, total: float, n: int,
                exclude=None) -> List[Optional[int]]:
    """구간을 n개 슬롯으로 잘라 슬롯마다 댓글 1개씩 배정 — 통짜 모드용.

    슬롯 k = 원본 [start + k·total/n, start + (k+1)·total/n).
    통짜 모드는 구간을 그대로 이어붙여 **타임라인 시각 = 원본 시각**이라 이 계산이 성립한다
    (컷 있는 모드는 컷 순서를 섞어 재배치하므로 쓰면 안 된다 — 내용 기반 추천을 쓸 것).

    각 슬롯은 그 시간대를 언급한 댓글 중 좋아요 최다 1개. 없으면 None.
    한 댓글이 두 슬롯에 겹치지 않는다. exclude(idx 집합)는 처음부터 제외.
    Returns: 길이 n 리스트 (n<=0 이거나 total<=0 이면 빈 리스트)
    """
    if n <= 0 or total <= 0:
        return []
    used = set(exclude or ())
    step = total / n
    out: List[Optional[int]] = []
    for k in range(n):
        s = start + k * step
        pick = None
        for idx in match_ranges(comments, [(s, s + step)]):
            if idx not in used:
                pick = idx
                used.add(idx)
                break
        out.append(pick)
    return out
