#!/usr/bin/env python3
"""
live_chat_counter.py — YouTube canlı yayın sohbetindeki TEKİL kişileri sayar.

Kurulum:
    pip install chat-downloader

Kullanım:
    python live_chat_counter.py https://www.youtube.com/watch?v=VIDEO_ID
    python live_chat_counter.py https://www.youtube.com/@KANALADI/live
    python live_chat_counter.py URL --cikti kisiler.csv --aralik 30

Ne yapar:
    * Sohbeti canlı okur, kişileri author.id (kanal kimliği) ile tekilleştirir
      (nick değişebilir, kimlik sabittir).
    * Yeni kişi görününce: toplam tekil kişi, nick ve kanal kimliği yazılır.
    * users.csv: id, nick, mesaj_sayısı, ilk_mesaj, son_mesaj
      Çalışırken her 60 sn'de güncellenir (yayın yarıda kesilse veri kaybolmaz).
    * Mesajlardaki kelimeleri de sayar → kelimeler.csv (kelime, adet).
    * Ctrl+C ya da yayın bitince özet basar (toplam kişi, mesaj, en çok yazan 20).
    * NOT: YouTube canlı yayında geçmiş sohbeti vermez; bağlandığında yalnızca
      son ~70 mesajı ve sonrasını görürsünüz. Yayının başından beri sayım için
      aracı yayın başlarken çalıştırın.
    * Bitmiş yayınlarda sohbet kaydı baştan sona okunur (hepsi sayılır).
"""
import argparse
import csv
import json
import os
import re
import sys
import threading
import time
import urllib.request
from math import ceil
from collections import Counter
from datetime import datetime, timezone

CSV_BASLIK = ["id", "nick", "mesaj_sayısı", "ilk_mesaj", "son_mesaj"]
# Kelime sayımında atlanan çok yaygın Türkçe sözcükler (--tum-kelimeler ile açılır)
YAYGIN = set("""ve bir bu da de mi mı mu mü ne o çok ama için ile gibi var yok ben sen biz siz şu ki daha en hiç
her ya yani ise bile kadar ben bana sana ona bunu şunu onu ne ki hem ya ha he hı bi""".split())
MESAJ_TURLERI = ["text_message", "paid_message", "paid_sticker", "membership_item"]


class Sayac:
    """Kişileri kimliğe göre tutar. Birden çok iş parçacığından güvenle kullanılır."""

    def __init__(self):
        self.kisiler = {}
        self.toplam_mesaj = 0
        self.kelimeler = Counter()
        self.dakika = Counter()        # dakika -> mesaj
        self.yeni_dakika = Counter()   # dakika -> ilk kez yazan kişi
        self.emoji = Counter()
        self.anma = Counter()          # @anmalar
        self.odemeli = 0               # Super Chat / sticker
        self.uye = 0                   # üyelik mesajı
        self.kilit = threading.Lock()

    def ekle(self, msg):
        """Mesajı işler. Yeni kişiyse kişi sözlüğünü, değilse None döndürür."""
        yazar = msg.get("author") or {}
        kimlik = yazar.get("id")
        if not kimlik:
            return None
        nick = yazar.get("name") or "?"
        an = zaman_yaz(msg)
        metin = msg.get("message") if isinstance(msg.get("message"), str) else ""
        kelimeler = kelime_ayir(metin)
        emojiler = EMOJI_RE.findall(metin)
        anmalar = [tr_kucuk(a) for a in re.findall(r"(?<![\w@])@([^\W\d_][\w.\-]*)", metin)]
        tur = msg.get("tur") or {"paid_message": "odemeli", "paid_sticker": "odemeli",
                                 "membership_item": "uye"}.get(msg.get("message_type"))
        ts = msg.get("timestamp")
        dk = int(ts // 60_000_000) if isinstance(ts, (int, float)) and ts > 0 else int(time.time() // 60)
        with self.kilit:
            self.toplam_mesaj += 1
            self.kelimeler.update(kelimeler)
            self.emoji.update(emojiler)
            self.anma.update(anmalar)
            self.dakika[dk] += 1
            if tur == "odemeli":
                self.odemeli += 1
            elif tur == "uye":
                self.uye += 1
            k = self.kisiler.get(kimlik)
            yeni = k is None
            if yeni:
                self.yeni_dakika[dk] += 1
                k = self.kisiler[kimlik] = {
                    "id": kimlik, "nick": nick, "mesaj": 0, "ilk": an, "son": an,
                }
            k["nick"] = nick
            k["mesaj"] += 1
            k["son"] = an
            return dict(k) if yeni else None

    def say(self):
        with self.kilit:
            return len(self.kisiler), self.toplam_mesaj

    def liste(self):
        with self.kilit:
            return sorted((dict(k) for k in self.kisiler.values()), key=lambda k: -k["mesaj"])

    def csv_yaz(self, yol):
        """Önce geçici dosyaya yazıp yerine koyar; yarım dosya kalmaz."""
        gecici = yol + ".tmp"
        with open(gecici, "w", newline="", encoding="utf-8-sig") as f:
            w = csv.writer(f)
            w.writerow(CSV_BASLIK)
            for k in self.liste():
                w.writerow([k["id"], k["nick"], k["mesaj"], k["ilk"], k["son"]])
        os.replace(gecici, yol)

    def kelime_liste(self, n=None, yaygin_gizle=False):
        with self.kilit:
            ogeler = self.kelimeler.most_common()
        if yaygin_gizle:
            ogeler = [(k, c) for k, c in ogeler if k not in YAYGIN]
        return ogeler[:n]

    def kelime_csv_yaz(self, yol):
        gecici = yol + ".tmp"
        with open(gecici, "w", newline="", encoding="utf-8-sig") as f:
            w = csv.writer(f)
            w.writerow(["kelime", "adet"])
            w.writerows(self.kelime_liste())
        os.replace(gecici, yol)

    def kelime_csv_metin(self):
        import io
        tampon = io.StringIO()
        w = csv.writer(tampon)
        w.writerow(["kelime", "adet"])
        w.writerows(self.kelime_liste())
        return tampon.getvalue()

    def analiz(self):
        """Katılım dağılımı, dakika dakika hareket, emoji ve anma istatistikleri."""
        with self.kilit:
            adetler = sorted((k["mesaj"] for k in self.kisiler.values()), reverse=True)
            toplam = self.toplam_mesaj
            dakika, yeni = dict(self.dakika), dict(self.yeni_dakika)
            emoji, anma = self.emoji.most_common(12), self.anma.most_common(12)
            odemeli, uye = self.odemeli, self.uye
        kisi = len(adetler)
        if not kisi:
            return {"kisi": 0}
        kovalar = [("1 mesaj", 1, 1), ("2–5", 2, 5), ("6–20", 6, 20), ("21+", 21, 10**9)]
        dagilim = [{"ad": ad, "kisi": sum(1 for a in adetler if lo <= a <= hi)} for ad, lo, hi in kovalar]
        orta = adetler[kisi // 2] if kisi % 2 else (adetler[kisi // 2 - 1] + adetler[kisi // 2]) / 2
        ilk10 = sum(adetler[:10])
        bas, son = min(dakika), max(dakika)
        adim = max(1, ceil((son - bas + 1) / 120))
        n = (son - bas) // adim + 1
        seri, yseri = [0] * n, [0] * n
        for d, c in dakika.items():
            seri[(d - bas) // adim] += c
        for d, c in yeni.items():
            yseri[(d - bas) // adim] += c
        zirve = max(range(n), key=lambda i: seri[i])
        sure_dk = son - bas + 1
        return {
            "kisi": kisi, "mesaj": toplam,
            "ortalama": round(toplam / kisi, 1), "medyan": orta,
            "tek_mesaj_yuzde": round(100 * dagilim[0]["kisi"] / kisi, 1),
            "ilk10_yuzde": round(100 * ilk10 / toplam, 1) if toplam else 0,
            "dagilim": dagilim,
            "mesaj_dk": round(toplam / sure_dk, 1), "sure_dk": sure_dk,
            "seri": {"bas": bas * 60, "adim": adim * 60, "mesaj": seri, "yeni": yseri},
            "zirve": {"zaman": (bas + zirve * adim) * 60, "mesaj": seri[zirve], "adim": adim},
            "emoji": emoji, "anma": anma, "odemeli": odemeli, "uye": uye,
        }

    def csv_metin(self):
        import io
        tampon = io.StringIO()
        w = csv.writer(tampon)
        w.writerow(CSV_BASLIK)
        for k in self.liste():
            w.writerow([k["id"], k["nick"], k["mesaj"], k["ilk"], k["son"]])
        return tampon.getvalue()


EMOJI_RE = re.compile("[\U0001F000-\U0001FAFF\u2600-\u27BF\u2B50\u2B06\u2764]|:[\\w-]+:")


def tr_baslik(metin):
    """İlk harfi Türkçe büyük yapar (izmir → İzmir)."""
    return (metin[:1].replace("i", "İ").upper() + metin[1:]) if metin else metin


def tr_kucuk(metin):
    """Türkçe küçük harfe çevirir (İ→i, I→ı)."""
    return metin.replace("İ", "i").replace("I", "ı").lower()


def kelime_ayir(metin):
    """Mesajdaki kelimeleri çıkarır: bağlantı, @anma ve :emoji: kodları atılır, en az 2 harf."""
    if not metin or not isinstance(metin, str):
        return []
    metin = re.sub(r"https?://\S+|www\.\S+|@\S+|:[\w-]+:", " ", metin)
    return [k for k in re.findall(r"[^\W\d_]+", tr_kucuk(metin)) if len(k) >= 2]


def zaman_yaz(msg):
    """Mesaj zamanı (UTC, ISO). Zaman yoksa şu an."""
    ts = msg.get("timestamp")
    if isinstance(ts, (int, float)) and ts > 0:
        try:
            return datetime.fromtimestamp(ts / 1_000_000, timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
        except (OverflowError, OSError, ValueError):
            pass
    return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")


class SohbetHatasi(Exception):
    """Yedek okuyucunun anlaşılır (Türkçe) hata mesajı taşıyan hatası."""


def hata_cevir(e):
    """chat-downloader hatalarını anlaşılır Türkçe mesaja çevirir."""
    if isinstance(e, SohbetHatasi):
        return str(e)
    from chat_downloader import errors as h

    sozluk = [
        (h.InvalidURL, "Geçersiz YouTube adresi. Video linki ya da https://www.youtube.com/@KANAL/live verin."),
        (h.URLNotProvided, "Adres verilmedi."),
        (h.SiteNotSupported, "Bu site desteklenmiyor; yalnızca YouTube adresi verin."),
        (h.ChatDisabled, "Bu yayında sohbet kapalı."),
        (h.NoChatReplay, "Bu yayının sohbet kaydı yok (canlı sohbet kapalıydı ya da kayıt silinmiş)."),
        (h.VideoUnavailable, "Video bulunamadı ya da gizli/silinmiş."),
        (h.VideoUnplayable, "Video oynatılamıyor (bölge/yaş kısıtı olabilir)."),
        (h.LoginRequired, "Bu yayın giriş gerektiriyor (üyelere özel ya da yaş kısıtlı)."),
        (h.NoVideos, "Bu kanalın şu an canlı yayını yok."),
        (h.NoContinuation, "Sohbete bağlanılamadı; yayın canlı değil ya da sohbet kapalı olabilir."),
        (h.ParsingError, "YouTube sayfası okunamadı (YouTube yapısı değişmiş olabilir)."),
    ]
    for tur, metin in sozluk:
        if isinstance(e, tur):
            return metin
    return None


# ---- Yedek okuyucu --------------------------------------------------------
# chat-downloader YouTube'un sayfa yapısı değişince sohbeti açamayabiliyor
# (ParsingError). O durumda canlı sohbet doğrudan YouTube'un kendi sohbet
# penceresi uç noktasından okunur. Yalnızca CANLI yayınlarda çalışır.

_TARAYICI = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
    "Cookie": "SOCS=CAI",
}


def _http(url, veri=None, baslik=None):
    istek = urllib.request.Request(url, data=veri, headers={**_TARAYICI, **(baslik or {})})
    return urllib.request.urlopen(istek, timeout=20).read().decode("utf-8", "replace")


def mesaj_metni(r):
    """Sohbet satırındaki düz yazıyı toplar (emoji parçaları atlanır)."""
    parcalar = []
    for p in (r.get("message") or {}).get("runs") or []:
        e = p.get("emoji")
        if e:  # standart emoji → karakterin kendisi; özel emoji → :kısayol:
            parcalar.append(" " + ((e.get("shortcuts") or [""])[0] if e.get("isCustomEmoji") else e.get("emojiId", "")) + " ")
        else:
            parcalar.append(p.get("text", ""))
    return "".join(parcalar)


def video_kimligi(url):
    """Video linkinden ya da @kanal/live adresinden 11 karakterlik video kimliğini çıkarır."""
    m = re.search(r"(?:v=|youtu\.be/|/live/|/shorts/|/embed/)([\w-]{11})(?![\w-])", url)
    if m:
        return m.group(1)
    if not re.match(r"https?://(?:www\.|m\.)?youtube\.com/", url):
        raise SohbetHatasi("Geçersiz YouTube adresi. Video linki ya da https://www.youtube.com/@KANAL/live verin.")
    try:
        sayfa = _http(url)
    except Exception as e:
        raise SohbetHatasi(f"Adrese ulaşılamadı: {e}")
    m = re.search(r'<link rel="canonical" href="https://www\.youtube\.com/watch\?v=([\w-]{11})"', sayfa)
    if m and '"isLiveNow":true' in sayfa:
        return m.group(1)
    raise SohbetHatasi("Bu kanalın şu an canlı yayını yok.")


def yedek_sohbet(url):
    """chat-downloader biçiminde ({'author':{'id','name'},'timestamp'}) mesaj üretir."""
    vid = video_kimligi(url)
    try:
        yield from _yedek_canli(vid)
    except SohbetHatasi:
        yield from yedek_kayit(vid)  # canlı değilse: bitmiş yayının sohbet kaydı


def _yedek_canli(vid):
    try:
        sayfa = _http(f"https://www.youtube.com/live_chat?is_popout=1&v={vid}")
        veri = json.loads(re.search(r'ytInitialData"?\]?\s*=\s*({.+?})\s*;\s*</script', sayfa).group(1))
        anahtar = re.search(r'"INNERTUBE_API_KEY":"([^"]+)"', sayfa).group(1)
        baglam = json.loads(re.search(r'"INNERTUBE_CONTEXT":({.+?}),"INNERTUBE_CONTEXT_CLIENT_NAME"', sayfa).group(1))
    except Exception:
        raise SohbetHatasi("Sohbete bağlanılamadı; video yok, yayın canlı değil ya da sohbet kapalı olabilir.")
    sohbet = (veri.get("contents") or {}).get("liveChatRenderer")
    if not sohbet or not sohbet.get("continuations"):
        raise SohbetHatasi("Bu yayında sohbet kapalı ya da yayın canlı değil.")
    belirteç = list(sohbet["continuations"][0].values())[0]["continuation"]
    ilk = sohbet.get("actions") or []
    hata = 0
    while True:
        eylemler = ilk
        bekle = 1.0
        if not ilk:
            try:
                govde = json.dumps({"context": baglam, "continuation": belirteç}).encode()
                yanit = json.loads(_http(
                    f"https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key={anahtar}",
                    govde, {"Content-Type": "application/json"}))
                hata = 0
            except Exception:
                hata += 1
                if hata >= 5:
                    return
                time.sleep(3)
                continue
            icerik = (yanit.get("continuationContents") or {}).get("liveChatContinuation")
            if not icerik or not icerik.get("continuations"):
                return  # yayın bitti
            eylemler = icerik.get("actions") or []
            devam = list(icerik["continuations"][0].values())[0]
            belirteç = devam.get("continuation")
            if not belirteç:
                return
            bekle = min(max(devam.get("timeoutMs", 2000), 500), 5000) / 1000
        ilk = []
        for a in eylemler:
            oge = (a.get("addChatItemAction") or {}).get("item") or {}
            for tur, r in oge.items():
                if "authorExternalChannelId" in r and tur in YEDEK_TURLER:
                    ad = (r.get("authorName") or {}).get("simpleText") or "?"
                    yield {
                        "author": {"id": r["authorExternalChannelId"], "name": ad},
                        "timestamp": int(r.get("timestampUsec") or 0),
                        "message": mesaj_metni(r),
                        "tur": YEDEK_TURLER[tur],
                    }
        time.sleep(bekle)


def yedek_kayit(vid):
    """Bitmiş yayının sohbet kaydını baştan sona okur (sohbet tekrarı)."""
    try:
        sayfa = _http(f"https://www.youtube.com/watch?v={vid}")
        veri = json.loads(re.search(r'ytInitialData\s*=\s*({.+?})\s*;\s*</script', sayfa).group(1))
        anahtar = re.search(r'"INNERTUBE_API_KEY":"([^"]+)"', sayfa).group(1)
        baglam = json.loads(re.search(r'"INNERTUBE_CONTEXT":({.+?}),"INNERTUBE_CONTEXT_CLIENT_NAME"', sayfa).group(1))
    except Exception:
        raise SohbetHatasi("Video bulunamadı ya da gizli/silinmiş.")
    sohbet = ((veri.get("contents") or {}).get("twoColumnWatchNextResults") or {}) \
        .get("conversationBar", {}).get("liveChatRenderer")
    if not sohbet or not sohbet.get("isReplay") or not sohbet.get("continuations"):
        raise SohbetHatasi("Bu videonun sohbet kaydı yok (canlı yayın değil ya da sohbet kapalıydı).")
    belirteç = list(sohbet["continuations"][0].values())[0]["continuation"]
    konum, hata = 0, 0
    while belirteç:
        try:
            govde = json.dumps({"context": baglam, "continuation": belirteç,
                                "currentPlayerState": {"playerOffsetMs": str(konum)}}).encode()
            yanit = json.loads(_http(
                f"https://www.youtube.com/youtubei/v1/live_chat/get_live_chat_replay?key={anahtar}",
                govde, {"Content-Type": "application/json"}))
            hata = 0
        except Exception:
            hata += 1
            if hata >= 5:
                return
            time.sleep(2)
            continue
        icerik = (yanit.get("continuationContents") or {}).get("liveChatContinuation") or {}
        for a in icerik.get("actions") or []:
            r = a.get("replayChatItemAction") or {}
            if r.get("videoOffsetTimeMsec"):
                konum = int(r["videoOffsetTimeMsec"])
            for x in r.get("actions") or []:
                for tur, o in ((x.get("addChatItemAction") or {}).get("item") or {}).items():
                    if "authorExternalChannelId" in o and tur in YEDEK_TURLER:
                        yield {
                            "author": {"id": o["authorExternalChannelId"],
                                       "name": (o.get("authorName") or {}).get("simpleText") or "?"},
                            "timestamp": int(o.get("timestampUsec") or 0),
                            "message": mesaj_metni(o),
                            "tur": YEDEK_TURLER[tur],
                        }
        belirteç = None
        for c in icerik.get("continuations") or []:
            d = c.get("liveChatReplayContinuationData")
            if d and d.get("continuation"):
                belirteç = d["continuation"]


YEDEK_TURLER = {
    "liveChatTextMessageRenderer": None, "liveChatPaidMessageRenderer": "odemeli",
    "liveChatPaidStickerRenderer": "odemeli", "liveChatMembershipItemRenderer": "uye",
}


def video_bilgi(url):
    """Başlık ve izleyici sayısı. Canlıda: şu an izleyen; bitmişte: toplam izlenme."""
    vid = video_kimligi(url)
    try:
        sayfa = _http(f"https://www.youtube.com/watch?v={vid}")
        oyuncu = json.loads(re.search(r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;\s*(?:var\s|</script)', sayfa).group(1))
        veri = json.loads(re.search(r'ytInitialData\s*=\s*({.+?})\s*;\s*</script', sayfa).group(1))
    except Exception:
        return None
    ayrinti = oyuncu.get("videoDetails") or {}
    bilgi = {"id": vid, "baslik": ayrinti.get("title", ""), "kanal": ayrinti.get("author", ""),
             "canli": bool(ayrinti.get("isLive")), "izleyici": None, "toplam_izlenme": None,
             "sure_sn": int(ayrinti.get("lengthSeconds") or 0)}
    try:
        for c in veri["contents"]["twoColumnWatchNextResults"]["results"]["results"]["contents"]:
            vc = (c.get("videoPrimaryInfoRenderer") or {}).get("viewCount", {}).get("videoViewCountRenderer")
            if vc and vc.get("originalViewCount"):
                n = int(vc["originalViewCount"])
                if vc.get("isLive") and bilgi["canli"]:
                    bilgi["izleyici"] = n
                else:
                    bilgi["toplam_izlenme"] = n
    except (KeyError, TypeError, ValueError):
        pass
    if bilgi["toplam_izlenme"] is None and ayrinti.get("viewCount"):
        bilgi["toplam_izlenme"] = int(ayrinti["viewCount"])
    return bilgi


# ---- Transkript: en çok geçen isimler ---------------------------------------

def transkript_temizle(metin):
    """Yapıştırılmış transkript/SRT/VTT metninden zaman damgalarını ve etiketleri atar."""
    satirlar = []
    for s in metin.splitlines():
        s = s.strip()
        if not s or s == "WEBVTT" or re.fullmatch(r"\d+", s) or re.fullmatch(r"(?:\d{1,2}:)?\d{1,2}:\d{2}(?:[.,]\d+)?(?:\s*-->.*)?", s):
            continue
        satirlar.append(re.sub(r"<[^>]+>|\[[^\]]*\]", " ", s))
    return " ".join(satirlar)


def isimleri_bul(metin, en_cok=30):
    """(isimler, kelimeler): isim = cümle ortasında büyük harfle yazılan sözcük.
    Büyük harf yazan transkriptlerde çalışır; hepsi küçük harfse isim bulunamaz."""
    toplam, orta, orta_buyuk, buyuk = Counter(), Counter(), Counter(), Counter()
    for m in re.finditer(r"[^\W\d_]+", metin):
        kel = m.group()
        if len(kel) < 2:
            continue
        onceki = metin[:m.start()].rstrip()[-1:]
        if onceki in ("'", "’") and metin[m.start() - 1:m.start()] in ("'", "’"):
            continue  # ek: Hamza'nın → "nın"
        k = tr_kucuk(kel)
        toplam[k] += 1
        cumle_basi = onceki in ("", ".", "?", "!", "…")
        buyuk_mu = kel[0].isupper()
        if buyuk_mu:
            buyuk[k] += 1
        if not cumle_basi:
            orta[k] += 1
            if buyuk_mu:
                orta_buyuk[k] += 1
    isimler = [(k, buyuk[k]) for k in orta if k not in YAYGIN and len(k) >= 3
               and orta[k] >= 2 and orta_buyuk[k] / orta[k] >= 0.8]
    isimler.sort(key=lambda x: -x[1])
    adlar = {k for k, _ in isimler}
    kelimeler = [(k, c) for k, c in toplam.most_common() if k not in YAYGIN and k not in adlar][:en_cok]
    return isimler[:en_cok], kelimeler


def transkript_al(url):
    """Videonun altyazı/transkript metnini alır (kendi bilgisayarınızda; sunucu IP'lerine YouTube vermez)."""
    vid = video_kimligi(url)
    try:
        sayfa = _http(f"https://www.youtube.com/watch?v={vid}")
        anahtar = re.search(r'"INNERTUBE_API_KEY":"([^"]+)"', sayfa).group(1)
        yanit = json.loads(_http(
            f"https://www.youtube.com/youtubei/v1/player?key={anahtar}",
            json.dumps({"context": {"client": {"clientName": "IOS", "clientVersion": "20.10.4", "hl": "tr"}},
                        "videoId": vid, "contentCheckOk": True, "racyCheckOk": True}).encode(),
            {"Content-Type": "application/json",
             "User-Agent": "com.google.android.youtube/20.10.38 (Linux; U; Android 14) gzip"}))
    except Exception:
        raise SohbetHatasi("Transkript alınamadı (YouTube'a ulaşılamadı).")
    if (yanit.get("playabilityStatus") or {}).get("status") == "LOGIN_REQUIRED":
        raise SohbetHatasi("YouTube bu bilgisayardan transkripti vermedi (sunucu/VPN IP'si olabilir).")
    izler = ((yanit.get("captions") or {}).get("playerCaptionsTracklistRenderer") or {}).get("captionTracks") or []
    if not izler:
        raise SohbetHatasi("Bu videoda altyazı/transkript yok.")
    izler.sort(key=lambda t: t.get("languageCode", "") != "tr")  # Türkçe öncelikli
    try:
        veri = json.loads(_http(izler[0]["baseUrl"] + "&fmt=json3"))
    except Exception:
        raise SohbetHatasi("Transkript indirilemedi.")
    return " ".join("".join(g.get("utf8", "") for g in e["segs"]) for e in veri.get("events", []) if e.get("segs"))


def mesajlar(url):
    """Önce chat-downloader; sohbeti açamazsa (ParsingError/NoContinuation) yedek okuyucu."""
    from chat_downloader import ChatDownloader, errors as h

    gelen = 0
    try:
        for msg in ChatDownloader().get_chat(url, message_types=MESAJ_TURLERI, retry_timeout=-1):
            gelen += 1
            yield msg
        return
    except (h.ParsingError, h.NoContinuation):
        if gelen:
            raise
    yield from yedek_sohbet(url)


def ana():
    ap = argparse.ArgumentParser(description="YouTube canlı sohbetinde tekil kişi sayacı")
    ap.add_argument("url", help="YouTube video linki ya da https://www.youtube.com/@KANAL/live")
    ap.add_argument("--cikti", default="users.csv", help="CSV dosyası (varsayılan: users.csv)")
    ap.add_argument("--kelimeler", default="kelimeler.csv", help="Kelime sayımı CSV'si (varsayılan: kelimeler.csv)")
    ap.add_argument("--tum-kelimeler", action="store_true", help="Özette 've, bir, bu…' gibi yaygın kelimeleri de göster")
    ap.add_argument("--transkript", action="store_true", help="Videonun transkriptinden en çok geçen isimleri de çıkar (isimler.csv)")
    ap.add_argument("--aralik", type=int, default=60, help="CSV güncelleme aralığı sn (varsayılan 60)")
    arg = ap.parse_args()

    try:
        import chat_downloader  # noqa: F401
    except ImportError:
        sys.exit("chat-downloader kurulu değil. Kurmak için:  pip install chat-downloader")

    sayac = Sayac()
    dur = threading.Event()

    def kaydedici():
        while not dur.wait(arg.aralik):
            try:
                sayac.csv_yaz(arg.cikti)
                sayac.kelime_csv_yaz(arg.kelimeler)
            except OSError as e:
                print(f"[uyarı] CSV yazılamadı: {e}", file=sys.stderr)

    threading.Thread(target=kaydedici, daemon=True).start()

    print(f"Bağlanılıyor: {arg.url}")
    sebep = "yayın bitti"
    try:
        print("Bağlandı. Sohbet izleniyor (durdurmak için Ctrl+C)...\n")
        for msg in mesajlar(arg.url):
            yeni = sayac.ekle(msg)
            if yeni:
                toplam, _ = sayac.say()
                print(f"[{toplam:>5}] {yeni['nick']}  ({yeni['id']})")
    except KeyboardInterrupt:
        sebep = "Ctrl+C"
    except Exception as e:  # chat-downloader ve yedek okuyucu hataları
        anlasilir = hata_cevir(e)
        if anlasilir is None:
            anlasilir = f"Beklenmeyen hata: {type(e).__name__}: {e}"
        if sayac.say()[1] == 0:
            sys.exit(f"HATA: {anlasilir}")
        sebep = f"hata — {anlasilir}"
    finally:
        dur.set()

    try:
        sayac.csv_yaz(arg.cikti)
        sayac.kelime_csv_yaz(arg.kelimeler)
    except OSError as e:
        print(f"[uyarı] CSV yazılamadı: {e}", file=sys.stderr)

    toplam_kisi, toplam_mesaj = sayac.say()
    print(f"\n===== ÖZET ({sebep}) =====")
    print(f"Toplam tekil kişi : {toplam_kisi}")
    print(f"Toplam mesaj      : {toplam_mesaj}")
    print("En çok yazan 20 kişi:")
    for i, k in enumerate(sayac.liste()[:20], 1):
        print(f"  {i:>2}. {k['mesaj']:>5}  {k['nick']}  ({k['id']})")
    print("\nEn çok kullanılan 20 kelime:")
    for i, (k, c) in enumerate(sayac.kelime_liste(20, not arg.tum_kelimeler), 1):
        print(f"  {i:>2}. {c:>5}  {k}")
    a = sayac.analiz()
    if a.get("kisi"):
        print(f"\nAnaliz: kişi başı ortalama {a['ortalama']} mesaj, kişilerin %{a['tek_mesaj_yuzde']}'i tek mesaj yazdı, "
              f"en çok yazan 10 kişi mesajların %{a['ilk10_yuzde']}'ini yazdı, dakikada ortalama {a['mesaj_dk']} mesaj.")
    try:
        bilgi = video_bilgi(arg.url)
    except SohbetHatasi:
        bilgi = None
    if bilgi and toplam_kisi:
        if bilgi["izleyici"]:
            print(f"Şu an izleyen {bilgi['izleyici']} → sohbete katılım ≈ %{100 * toplam_kisi / bilgi['izleyici']:.1f}")
        elif bilgi["toplam_izlenme"]:
            print(f"Toplam izlenme {bilgi['toplam_izlenme']} → yazan kişi / izlenme ≈ %{100 * toplam_kisi / bilgi['toplam_izlenme']:.1f}")
    if arg.transkript:
        try:
            isimler, kel = isimleri_bul(transkript_al(arg.url))
            print("\nTranskriptte en çok geçen isimler:")
            for i, (k, c) in enumerate(isimler[:20], 1):
                print(f"  {i:>2}. {c:>5}  {tr_baslik(k)}")
            if not isimler:
                print("  (isim bulunamadı: transkript büyük harf kullanmıyor olabilir)")
            with open("isimler.csv", "w", newline="", encoding="utf-8-sig") as f:
                w = csv.writer(f)
                w.writerow(["isim", "adet"])
                w.writerows(isimler)
        except SohbetHatasi as e:
            print(f"\n[transkript] {e}")
    print(f"\nSonuçlar kaydedildi: {arg.cikti}, {arg.kelimeler}")


if __name__ == "__main__":
    ana()
