"""Webhook alıcısı (Python, yalnızca standart kitaplık): "dürt ve çek" deseni.

Ajansın webhook'u yalnızca bir dürtüdür: alıcı imzayı ve zaman damgasını doğrular, `webhook-id`'yi saklar, hemen 2xx
döner ve içeriği akıştan çeker (başvuru bağlayıcısı). Yükteki içerik doğrudan yayına dönüştürülmez.

Alıcı denetimi (Abone Terminali · webhook çıkışı · "Alıcıyı denetle") dört istek gönderir; bu alıcı dördünü de geçer:
  1. doğru imza → 202        2. bozuk imza → 401
  3. 10 dakika eski zaman damgası → 401        4. aynı webhook-id → 200 (sessizce kabul, iş yok)

Kullanım: YHA_WEBHOOK_SIRRI=whsec_... [YHA_ANAHTAR=yha_deneme_...] python webhook_alici.py
Ajans yalnızca https adrese gönderir: önüne TLS sonlandıran bir vekil koyun.
Sır döndürmede iki sır virgülle verilebilir (YHA_WEBHOOK_SIRRI=whsec_yeni,whsec_eski).
"""
from __future__ import annotations

import base64
import hashlib
import hmac
import json
import os
import sqlite3
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

TOLERANS_SANIYE = 300
SAKLAMA_SANIYE = 24 * 3600
EN_BUYUK_GOVDE = 64 * 1024


def imza_gecerli(sirlar: list[str], basliklar, govde: bytes, simdi: float | None = None) -> bool:
    """Standard Webhooks: v1,base64(HMAC-SHA256(sır, "<id>.<zaman>.<gövde>")); zaman ±5 dakika; sabit zamanlı karşılaştırma."""
    wid, zaman, imzalar = basliklar.get("webhook-id"), basliklar.get("webhook-timestamp"), basliklar.get("webhook-signature")
    if not wid or not zaman or not imzalar or not zaman.isdigit():
        return False
    if abs((simdi if simdi is not None else time.time()) - int(zaman)) > TOLERANS_SANIYE:
        return False
    for sir in sirlar:
        anahtar = base64.b64decode(sir.removeprefix("whsec_"))
        beklenen = base64.b64encode(hmac.new(anahtar, f"{wid}.{zaman}.".encode() + govde, hashlib.sha256).digest())
        for parca in imzalar.split():
            if parca.startswith("v1,") and hmac.compare_digest(parca[3:].encode(), beklenen):
                return True
    return False


class Kayit:
    """İşlenmiş webhook-id'ler (24 saat). Birden çok süreçte paylaşılsın diye SQLite."""

    def __init__(self, yol: str = ":memory:"):
        self.db = sqlite3.connect(yol, check_same_thread=False)
        self.kilit = threading.Lock()
        self.db.execute("create table if not exists webhook (id text primary key, zaman real)")

    def ilk_kez(self, wid: str) -> bool:
        with self.kilit:
            self.db.execute("delete from webhook where zaman < ?", (time.time() - SAKLAMA_SANIYE,))
            c = self.db.execute("insert or ignore into webhook values (?, ?)", (wid, time.time()))
            self.db.commit()
            return c.rowcount == 1


def alici_sinifi(sirlar: list[str], kayit: Kayit, durt):
    class Alici(BaseHTTPRequestHandler):
        def log_message(self, *a):  # noqa: D401
            pass

        def yanit(self, durum: int, govde: dict | None = None):
            ham = json.dumps(govde or {}).encode()
            self.send_response(durum)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(ham)))
            self.end_headers()
            self.wfile.write(ham)

        def do_POST(self):  # noqa: N802
            uzunluk = int(self.headers.get("Content-Length", "0"))
            if uzunluk > EN_BUYUK_GOVDE:
                return self.yanit(413, {"hata": "govde_cok_buyuk"})
            govde = self.rfile.read(uzunluk)
            if not imza_gecerli(sirlar, self.headers, govde):
                return self.yanit(401, {"hata": "imza_gecersiz"})  # imzasız isteğe hiçbir iş yapılmaz
            if not kayit.ilk_kez(self.headers["webhook-id"]):
                return self.yanit(200, {"tekrar": True})  # aynı bildirim: sessizce kabul
            try:
                g = json.loads(govde)
            except ValueError:
                return self.yanit(400, {"hata": "json"})
            if g.get("olay_turu") == "sinama.ping":
                return self.yanit(200, {"alici": "yha-ornek-webhook-alici", "surum": "1.0.0"})
            if g.get("olay_turu") == "sinama.denetim" or g.get("deneme"):
                return self.yanit(202, {"alindi": True})  # sınama: yayına dönüştürülmez
            threading.Thread(target=durt, daemon=True).start()  # yanıt beklemeden akıştan çek
            return self.yanit(202, {"alindi": True})

        def do_GET(self):  # noqa: N802
            self.yanit(405, {"hata": "yontem"})

    return Alici


def baglayiciyla_durt():
    """Başvuru bağlayıcısını (../baglayici) tek sefer çalıştırır; aynı anda tek çalışma."""
    import pathlib
    import sys

    sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "baglayici"))
    from baglayici import Api, Baglayici  # noqa: PLC0415

    kilit = threading.Lock()

    def durt():
        if not kilit.acquire(blocking=False):
            return  # süren çekme yeni olayları da alır
        try:
            api = Api(os.environ.get("YHA_KOK", "https://deneme.<alan>"), os.environ["YHA_ANAHTAR"])
            Baglayici(api, sqlite3.connect(os.environ.get("YHA_VERI", "baglayici.db")),
                      pathlib.Path(os.environ.get("YHA_MEDYA", "medya"))).cek()
        finally:
            kilit.release()

    return durt


def main() -> None:
    sirlar = [s.strip() for s in os.environ["YHA_WEBHOOK_SIRRI"].split(",") if s.strip()]
    durt = baglayiciyla_durt() if os.environ.get("YHA_ANAHTAR") else (lambda: print("dürtü alındı"))
    sunucu = ThreadingHTTPServer(("0.0.0.0", int(os.environ.get("PORT", "8080"))),
                                 alici_sinifi(sirlar, Kayit(os.environ.get("YHA_WEBHOOK_KAYIT", "webhook.db")), durt))
    sunucu.serve_forever()


if __name__ == "__main__":
    main()
