#!/usr/bin/env python3
"""BizHub → Helpdesk bridge (P7 sample): Usedesk / Bitrix24 ticket from review.created.

Not a full marketplace connector — maps BizHub webhook → ticket create, then optional
reply via BizHub API (same HMAC as crm-webhook-receiver.py).

Env:
  BIZHUB_WEBHOOK_SECRET  — required (кабинет → Уведомления → Webhooks)
  BIZHUB_API_KEY         — optional bh_live_… for auto-reply on 5★
  BIZHUB_APP_URL         — default https://bizhub.com.ru
  HELPDESK_MODE          — usedesk | bitrix | log (default log)
  USEDESK_API_URL        — e.g. https://api.usedesk.ru/create/ticket
  USEDESK_API_TOKEN      — Usedesk API token
  BITRIX_WEBHOOK_URL     — incoming webhook …/rest/1/xxx/crm.timeline.comment.add.json
                           or task.item.add.json base (см. docs Bitrix24 REST)
  PORT                   — default 8788

Run:
  pip install flask
  BIZHUB_WEBHOOK_SECRET=whsec_… HELPDESK_MODE=log python3 helpdesk-webhook-receiver.py
"""

from __future__ import annotations

import hashlib
import hmac
import json
import os
import time
import urllib.error
import urllib.request

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ.get("BIZHUB_WEBHOOK_SECRET", "").encode()
API_KEY = os.environ.get("BIZHUB_API_KEY", "")
APP_URL = os.environ.get("BIZHUB_APP_URL", "https://bizhub.com.ru").rstrip("/")
MODE = (os.environ.get("HELPDESK_MODE") or "log").strip().lower()
MAX_SKEW = 300


def verify(secret: bytes, timestamp: str, body: bytes, header: str) -> bool:
    if not secret or not timestamp or not header:
        return False
    try:
        ts = int(timestamp)
    except ValueError:
        return False
    if abs(int(time.time()) - ts) > MAX_SKEW:
        return False
    sig = header.removeprefix("sha256=").strip()
    mac = hmac.new(secret, f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, sig)


def http_json(url: str, payload: dict, headers: dict | None = None) -> dict:
    data = json.dumps(payload).encode()
    hdrs = {"Content-Type": "application/json", "Accept": "application/json"}
    if headers:
        hdrs.update(headers)
    req = urllib.request.Request(url, data=data, headers=hdrs, method="POST")
    with urllib.request.urlopen(req, timeout=25) as res:
        raw = res.read().decode() or "{}"
        try:
            return json.loads(raw)
        except json.JSONDecodeError:
            return {"raw": raw}


def reply_review(review_id: str, text: str) -> dict:
    return http_json(
        f"{APP_URL}/api/v1/reviews/{review_id}/reply",
        {"text": text},
        {"Authorization": f"Bearer {API_KEY}"},
    )


def ticket_title(review: dict) -> str:
    plat = review.get("platform") or "review"
    rating = review.get("rating")
    author = review.get("author") or "Клиент"
    return f"BizHub · {plat} · ★{rating} · {author}"


def ticket_body(review: dict, event: str) -> str:
    text = (review.get("text") or "").strip()
    rid = review.get("id") or ""
    org = review.get("organization_id") or ""
    lines = [
        f"event={event}",
        f"review_id={rid}",
        f"organization_id={org}",
        f"platform={review.get('platform')}",
        f"rating={review.get('rating')}",
        f"author={review.get('author')}",
        "",
        text or "(без текста)",
        "",
        f"Ответ в BizHub: POST {APP_URL}/api/v1/reviews/{rid}/reply",
    ]
    return "\n".join(lines)


def create_usedesk_ticket(review: dict, event: str) -> dict:
    url = (os.environ.get("USEDESK_API_URL") or "").strip()
    token = (os.environ.get("USEDESK_API_TOKEN") or "").strip()
    if not url or not token:
        raise RuntimeError("USEDESK_API_URL and USEDESK_API_TOKEN required")
    # Usedesk create/ticket — поля зависят от аккаунта; минимальный набор.
    payload = {
        "api_token": token,
        "subject": ticket_title(review),
        "message": ticket_body(review, event),
        "client_name": review.get("author") or "BizHub review",
        "priority": 2 if int(review.get("rating") or 5) <= 3 else 1,
    }
    return http_json(url, payload)


def create_bitrix_timeline(review: dict, event: str) -> dict:
    """Bitrix24 incoming webhook: crm.timeline.comment.add (нужен ENTITY_ID в env).

    BITRIX_WEBHOOK_URL — полный URL метода, напр.
      https://portal.bitrix24.ru/rest/1/xxx/crm.timeline.comment.add.json
    BITRIX_ENTITY_TYPE / BITRIX_ENTITY_ID — опц. (default company / 0 → только log если 0)
    """
    url = (os.environ.get("BITRIX_WEBHOOK_URL") or "").strip()
    if not url:
        raise RuntimeError("BITRIX_WEBHOOK_URL required")
    entity_type = (os.environ.get("BITRIX_ENTITY_TYPE") or "company").strip()
    entity_id = int(os.environ.get("BITRIX_ENTITY_ID") or "0")
    comment = ticket_body(review, event)
    if entity_id <= 0:
        # Без привязки к CRM-сущности — пишем как log-only payload для отладки.
        return {"ok": True, "mode": "bitrix_dry", "comment": comment[:500]}
    payload = {
        "fields": {
            "ENTITY_ID": entity_id,
            "ENTITY_TYPE": entity_type,
            "COMMENT": f"{ticket_title(review)}\n\n{comment}",
        }
    }
    return http_json(url, payload)


def forward_helpdesk(review: dict, event: str) -> dict:
    if MODE == "usedesk":
        return create_usedesk_ticket(review, event)
    if MODE == "bitrix":
        return create_bitrix_timeline(review, event)
    print("helpdesk_log", ticket_title(review))
    print(ticket_body(review, event))
    return {"ok": True, "mode": "log"}


@app.post("/bizhub/webhook")
def webhook():
    body = request.get_data()
    if not verify(
        SECRET,
        request.headers.get("X-BizHub-Timestamp", ""),
        body,
        request.headers.get("X-BizHub-Signature", ""),
    ):
        abort(401)
    payload = json.loads(body.decode() or "{}")
    event = payload.get("event") or request.headers.get("X-BizHub-Event")
    review = (payload.get("data") or {}).get("review") or {}
    print("event", event, "review", review.get("id"), "rating", review.get("rating"), "mode", MODE)

    ticket_out = None
    if event == "review.created" and review.get("id"):
        try:
            ticket_out = forward_helpdesk(review, event)
            print("helpdesk", ticket_out)
        except Exception as exc:  # noqa: BLE001
            print("helpdesk_failed", exc)

    # Demo: auto-ack only 5★ if API key set (never for ≤3★).
    if (
        event == "review.created"
        and API_KEY
        and int(review.get("rating") or 0) >= 5
        and not review.get("replied")
    ):
        try:
            out = reply_review(
                review["id"],
                "Спасибо за высокую оценку! Будем рады видеть вас снова.",
            )
            print("reply", out.get("status"), out.get("published"))
        except (urllib.error.URLError, Exception) as exc:  # noqa: BLE001
            print("reply_failed", exc)

    return {"ok": True, "helpdesk": ticket_out, "mode": MODE}


if __name__ == "__main__":
    if not SECRET:
        raise SystemExit("Set BIZHUB_WEBHOOK_SECRET")
    app.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8788")))
