"""WS4.1 / FRREP-LD003 — BigBlueButton API client.

Implements the subset of BBB calls we need: ``create``, ``join`` (URL only),
``isMeetingRunning``, ``end``, ``getRecordings``. Each call builds a canonical
query string and SHA-1 checksum per the BBB spec:

  checksum = sha1(callName + queryString + sharedSecret)

We use ``urllib.request`` so tests can patch it the same way the
``apps/rep/courses/tasks._mel_push_one`` pattern does. BBB's responses are
XML; we parse just enough to surface the meeting / recording status.
"""
from __future__ import annotations

import hashlib
import hmac
import logging
import urllib.parse
import urllib.request
from typing import Iterable
from xml.etree import ElementTree as ET

from django.conf import settings

logger = logging.getLogger(__name__)


def _checksum(call_name: str, query: str, secret: str) -> str:
    return hashlib.sha1(f"{call_name}{query}{secret}".encode()).hexdigest()


def _canonical_query(params: Iterable[tuple[str, str]]) -> str:
    return urllib.parse.urlencode(params, quote_via=urllib.parse.quote)


def build_url(call_name: str, params: list[tuple[str, str]]) -> str:
    base = settings.BBB_URL.rstrip("/")
    secret = settings.BBB_SHARED_SECRET
    query = _canonical_query(params)
    checksum = _checksum(call_name, query, secret)
    return f"{base}/api/{call_name}?{query}&checksum={checksum}"


def _do_get(url: str, *, timeout: int = 30) -> str:
    req = urllib.request.Request(url, method="GET")
    with urllib.request.urlopen(req, timeout=timeout) as resp:  # noqa: S310
        return resp.read().decode("utf-8", errors="replace")


def _parse(xml_text: str) -> ET.Element:
    return ET.fromstring(xml_text)


# ────────────────────────────────────────────────────────────────────────


def create_meeting(*, meeting_id: str, name: str, attendee_pw: str, moderator_pw: str,
                   record: bool, duration_minutes: int) -> dict:
    params = [
        ("name", name[:255]),
        ("meetingID", str(meeting_id)),
        ("attendeePW", attendee_pw),
        ("moderatorPW", moderator_pw),
        ("record", "true" if record else "false"),
        ("duration", str(int(duration_minutes))),
    ]
    url = build_url("create", params)
    xml_text = _do_get(url)
    root = _parse(xml_text)
    return {
        "returncode": (root.findtext("returncode") or "").upper(),
        "meetingID": root.findtext("meetingID") or str(meeting_id),
        "internal_meeting_id": root.findtext("internalMeetingID") or "",
        "raw": xml_text,
    }


def join_url(*, meeting_id: str, full_name: str, password: str, user_id: str) -> str:
    params = [
        ("fullName", full_name[:128]),
        ("meetingID", str(meeting_id)),
        ("password", password),
        ("userID", str(user_id)),
        ("redirect", "true"),
    ]
    return build_url("join", params)


def is_meeting_running(meeting_id: str) -> bool:
    url = build_url("isMeetingRunning", [("meetingID", str(meeting_id))])
    root = _parse(_do_get(url))
    return (root.findtext("running") or "false").lower() == "true"


def end_meeting(*, meeting_id: str, moderator_pw: str) -> bool:
    params = [("meetingID", str(meeting_id)), ("password", moderator_pw)]
    url = build_url("end", params)
    root = _parse(_do_get(url))
    return (root.findtext("returncode") or "").upper() == "SUCCESS"


def verify_bbb_checksum(
    *,
    callback_url: str,
    payload: bytes,
    signature: str,
    secret: str,
    algo: str = "sha256",
) -> bool:
    """Verify a bbb-webhooks delivery signature.

    BBB signs deliveries with ``hash(callback_url + payload + secret)``
    using the same checksum scheme as the BBB API itself, NOT plain HMAC
    of the body. This is intentional symmetry with how the LMS *calls*
    BBB (``_checksum`` above).

    ``algo`` defaults to sha256 but accepts sha1 for older bbb-webhooks
    deployments. Constant-time comparison via ``hmac.compare_digest``.
    """
    if not secret or not signature:
        return False
    hasher = getattr(hashlib, algo, None)
    if hasher is None:
        return False
    body_str = payload.decode("utf-8", errors="replace")
    digest = hasher(f"{callback_url}{body_str}{secret}".encode()).hexdigest()
    return hmac.compare_digest(digest, signature)


def get_recordings(meeting_id: str) -> list[dict]:
    url = build_url("getRecordings", [("meetingID", str(meeting_id))])
    root = _parse(_do_get(url))
    out = []
    for rec in root.findall(".//recording"):
        playback_url = ""
        playback = rec.find(".//playback/format/url")
        if playback is not None and playback.text:
            playback_url = playback.text
        out.append({
            "record_id": rec.findtext("recordID") or "",
            "state": (rec.findtext("state") or "processing").lower(),
            "url": playback_url,
            "duration_seconds": int(float(rec.findtext("duration") or "0") / 1000) if rec.findtext("duration") else 0,
            "start_time": rec.findtext("startTime") or "",
        })
    return out
