"""Thin Moodle REST Web Services client.

Used by the ``pull_moodle_outcomes`` task to read learning outcomes (course
completions, enrolments, grades, feedback) from a Moodle instance and land them
in MEL. Configure ``MOODLE_BASE_URL`` + ``MOODLE_WS_TOKEN`` (see
setup/moodle_bootstrap.md for creating the token).
"""
from __future__ import annotations

import logging
from typing import Any

import requests
from django.conf import settings

logger = logging.getLogger(__name__)


class MoodleError(RuntimeError):
    """Raised when Moodle returns a Web Services exception envelope."""


class MoodleClient:
    def __init__(self, base_url: str | None = None, token: str | None = None, *, timeout: int | None = None):
        self.base_url = (base_url or getattr(settings, "MOODLE_BASE_URL", "") or "").rstrip("/")
        self.token = token or getattr(settings, "MOODLE_WS_TOKEN", "") or ""
        self.timeout = timeout or int(getattr(settings, "MOODLE_WS_TIMEOUT", 20))
        # TLS verify: real cert in prod; dev disables it for Caddy's internal CA.
        self.verify = getattr(settings, "MOODLE_WS_VERIFY", True)

    @property
    def configured(self) -> bool:
        return bool(self.base_url and self.token)

    def call(self, wsfunction: str, **params: Any) -> Any:
        """Invoke a Moodle WS function and return the decoded JSON.

        Moodle returns HTTP 200 even for errors, encoding them as
        ``{"exception": ..., "errorcode": ..., "message": ...}`` — those are
        raised as :class:`MoodleError`.
        """
        if not self.configured:
            raise MoodleError("Moodle client not configured (MOODLE_BASE_URL / MOODLE_WS_TOKEN)")

        data = {
            "wstoken": self.token,
            "wsfunction": wsfunction,
            "moodlewsrestformat": "json",
        }
        data.update(_flatten_params(params))
        resp = requests.post(
            f"{self.base_url}/webservice/rest/server.php",
            data=data,
            timeout=self.timeout,
            verify=self.verify,
        )
        resp.raise_for_status()
        payload = resp.json() if resp.content else None
        if isinstance(payload, dict) and payload.get("exception"):
            raise MoodleError(
                f"{wsfunction}: {payload.get('errorcode')} — {payload.get('message')}"
            )
        return payload


def _flatten_params(params: dict[str, Any], prefix: str = "") -> dict[str, Any]:
    """Flatten nested lists/dicts into Moodle's ``key[0][field]=value`` form."""
    flat: dict[str, Any] = {}
    for key, value in params.items():
        name = f"{prefix}[{key}]" if prefix else str(key)
        if isinstance(value, dict):
            flat.update(_flatten_params(value, name))
        elif isinstance(value, (list, tuple)):
            for i, item in enumerate(value):
                if isinstance(item, dict):
                    flat.update(_flatten_params(item, f"{name}[{i}]"))
                else:
                    flat[f"{name}[{i}]"] = item
        elif isinstance(value, bool):
            flat[name] = 1 if value else 0
        else:
            flat[name] = value
    return flat
