"""
Проверка подписи вебхука Тэрил на Python. Без зависимостей.

Два правила, на которых всё держится:

  1. подпись считается по СЫРОМУ телу, до разбора JSON: любая перекодировка
     (json.loads + json.dumps, смена пробелов) её ломает;
  2. отвечать надо 2xx сразу — мы ждём «принял», а не «обработал».

Пример для Flask внизу файла.
"""

import hashlib
import hmac
import re
import time

SIGNATURE = re.compile(r"^t=(\d+),v1=([0-9a-f]+)$")
MAX_AGE_SEC = 300


def verify(secret: str, raw_body: bytes, header: str, max_age_sec: int = MAX_AGE_SEC) -> bool:
    """Проверить заголовок `X-Teril-Signature: t=<unix>,v1=<hex>`.

    Проверяются ОБЕ вещи: что подпись сходится и что она свежая. Без второго
    перехваченный запрос пересылается через сутки с той же подписью, и отличить
    его от повторной доставки нечем.
    """
    m = SIGNATURE.match((header or "").strip())
    if not m:
        return False

    at = int(m.group(1))
    if abs(time.time() - at) > max_age_sec:
        return False

    signed = f"{at}.".encode("utf-8") + raw_body
    mine = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
    # compare_digest, а не ==: побайтовое сравнение отдаёт подпись посимвольно
    # тому, кто умеет мерить время ответа.
    return hmac.compare_digest(mine, m.group(2))


# -- Пример на Flask ---------------------------------------------------------
#
# from flask import Flask, request
#
# app = Flask(__name__)
# SECRET = os.environ["TERIL_WEBHOOK_SECRET"]
# seen = set()
#
# @app.post("/hooks/teril")
# def teril_hook():
#     raw = request.get_data()  # именно сырое тело
#     if not verify(SECRET, raw, request.headers.get("X-Teril-Signature")):
#         return "", 401
#
#     event = json.loads(raw)
#     if event["id"] in seen:          # доставка at-least-once
#         return "", 204
#     seen.add(event["id"])
#
#     queue.put(event)                 # работа — в очередь, ответ — сразу
#     return "", 204
#
# Решение принимайте по event["result"]["status"] (built / parsed / question /
# rejected / failed), а не по имени события: turn.completed приходит и тогда,
# когда ассистент законно ответил словами и ничего не собрал.
