<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

namespace mod_bigbluebuttonbn\local\proxy;

use cache;
use cache_helper;
use SimpleXMLElement;

/**
 * The recording proxy.
 *
 * This class acts as a proxy between Moodle and the BigBlueButton API server,
 * and deals with all requests relating to recordings.
 *
 * @package   mod_bigbluebuttonbn
 * @copyright 2021 onwards, Blindside Networks Inc
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 * @author    Laurent David  (laurent [at] call-learning [dt] fr)
 * @author    Jesus Federico  (jesus [at] blindsidenetworks [dt] com)
 */
class recording_proxy extends proxy_base {

    /**
     * Invalidate the MUC cache for the specified recording.
     *
     * @param string $recordid
     */
    protected static function invalidate_cache_for_recording(string $recordid): void {
        cache_helper::invalidate_by_event('mod_bigbluebuttonbn/recordingchanged', [$recordid]);
    }

    /**
     * Perform deleteRecordings on BBB.
     *
     * @param string $recordid a recording id
     * @return bool
     */
    public static function delete_recording(string $recordid): bool {
        $result = self::fetch_endpoint_xml('deleteRecordings', ['recordID' => $recordid]);
        if (!$result || $result->returncode != 'SUCCESS') {
            return false;
        }
        return true;
    }

    /**
     * Perform publishRecordings on BBB.
     *
     * @param string $recordid
     * @param string $publish
     * @return bool
     */
    public static function publish_recording(string $recordid, string $publish = 'true'): bool {
        $result = self::fetch_endpoint_xml('publishRecordings', [
            'recordID' => $recordid,
            'publish' => $publish,
        ]);

        self::invalidate_cache_for_recording($recordid);

        if (!$result || $result->returncode != 'SUCCESS') {
            return false;
        }

        return true;
    }

    /**
     * Perform publishRecordings on BBB.
     *
     * @param string $recordid
     * @param string $protected
     * @return bool
     */
    public static function protect_recording(string $recordid, string $protected = 'true'): bool {
        global $CFG;

        // Ignore action if recording_protect_editable is set to false.
        if (empty($CFG->bigbluebuttonbn_recording_protect_editable)) {
            return false;
        }

        $result = self::fetch_endpoint_xml('updateRecordings', [
            'recordID' => $recordid,
            'protect' => $protected,
        ]);

        self::invalidate_cache_for_recording($recordid);

        if (!$result || $result->returncode != 'SUCCESS') {
            return false;
        }

        return true;
    }

    /**
     * Perform updateRecordings on BBB.
     *
     * @param string $recordid a single record identifier
     * @param array $params ['key'=>param_key, 'value']
     */
    public static function update_recording(string $recordid, array $params): bool {
        $result = self::fetch_endpoint_xml('updateRecordings', array_merge([
            'recordID' => $recordid,
        ], $params));

        self::invalidate_cache_for_recording($recordid);

        return $result ? $result->returncode == 'SUCCESS' : false;
    }

    /**
     * Helper function to fetch a single recording from a BigBlueButton server.
     *
     * @param string $recordingid
     * @return null|array
     */
    public static function fetch_recording(string $recordingid): ?array {
        $result = self::fetch_recordings([$recordingid]);
        $data = $result['recordings'];
        if (array_key_exists($recordingid, $data)) {
            return $data[$recordingid];
        }

        return null;
    }

    /**
     * Check whether the current recording is a protected recording and purge the cache if necessary.
     *
     * @param string $recordingid
     */
    public static function purge_protected_recording(string $recordingid): void {
        $cache = cache::make('mod_bigbluebuttonbn', 'recordings');

        $recording = $cache->get($recordingid);
        if (empty($recording)) {
            // This value was not cached to begin with.
            return;
        }

        $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch');
        if ($currentfetchcache->has($recordingid)) {
            // This item was fetched in the current request.
            return;
        }

        if (array_key_exists('protected', $recording) && $recording['protected'] === 'true') {
            // This item is protected. Purge it from the cache.
            $cache->delete($recordingid);
            return;
        }
    }

    /**
     * Helper function to fetch recordings from a BigBlueButton server.
     *
     * We use a cache to store recording indexed by keyids/recordingID.
     * @param array $keyids list of recordingids
     * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array.
     */
    public static function fetch_recordings(array $keyids = []): array {
        $recordings = [];

        // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
        if (empty($keyids)) {
            return ['recordings' => $recordings, 'unfetchedids' => []];
        }
        $cache = cache::make('mod_bigbluebuttonbn', 'recordings');
        $currentfetchcache = cache::make('mod_bigbluebuttonbn', 'currentfetch');
        $recordings = array_filter($cache->get_many($keyids));
        $missingkeys = array_diff(array_values($keyids), array_keys($recordings));

        $result = self::do_fetch_recordings($missingkeys);
        $recordings += $result['recordings'];
        $cache->set_many($recordings);
        $currentfetchcache->set_many(array_flip(array_keys($recordings)));
        return ['recordings' => $recordings, 'unfetchedids' => $result['unfetchedids']];
    }

    /**
     * Helper function to fetch recordings from a BigBlueButton server.
     *
     * @param array $keyids list of meetingids
     * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array.
     */
    public static function fetch_recording_by_meeting_id(array $keyids = []): array {
        $recordings = [];

        // If $ids is empty return array() to prevent a getRecordings with meetingID and recordID set to ''.
        if (empty($keyids)) {
            return ['recordings' => $recordings, 'unfetchedids' => []];
        }
        return self::do_fetch_recordings($keyids, 'meetingID');
    }

    /**
     * Helper function to fetch recordings from a BigBlueButton server.
     *
     * @param array $keyids list of meetingids or recordingids
     * @param string $key the param name used for the BBB request (<recordID>|meetingID)
     * @return array (associative) with recordings indexed by recordID, each recording is a non sequential array.
     */
    private static function do_fetch_recordings(array $keyids = [], string $key = 'recordID'): array {
        $recordings = [];
        $unfetchedids = [];
        $pagesize = 25;
        while ($ids = array_splice($keyids, 0, $pagesize)) {
            $result = self::fetch_recordings_page($ids, $key);
            if (!$result['success']) {
                $unfetchedids = array_merge($unfetchedids, $ids);
                continue; // Skip this page as the response is not valid but we will keep record of all unfetched ids.
            }
            $recordings += $result['recordings'];
        }
        return ['recordings' => $recordings, 'unfetchedids' => $unfetchedids];
    }
    /**
     * Helper function to fetch a page of recordings from the remote server.
     *
     * @param array $ids
     * @param string $key
     * @return array an associative array containing recordings list and fetch success.
     */
    private static function fetch_recordings_page(array $ids, $key = 'recordID'): array {
        // The getRecordings call is executed using a method GET (supported by all versions of BBB).
        $xml = self::fetch_endpoint_xml('getRecordings', [$key => implode(',', $ids), 'state' => 'any']);

        // Response from the server must be valid.
        if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) {
            $recordings = [];
            // If there were recordings already created.
            foreach ($xml->recordings->recording as $recordingxml) {
                $recording = self::parse_recording($recordingxml);
                $recordings[$recording['recordID']] = $recording;
                // Check if there are any child.
                if (isset($recordingxml->breakoutRooms->breakoutRoom)) {
                    $breakoutrooms = [];
                    foreach ($recordingxml->breakoutRooms->breakoutRoom as $breakoutroom) {
                        $breakoutrooms[] = trim((string) $breakoutroom);
                    }
                    if ($breakoutrooms) {
                        $xml = self::fetch_endpoint_xml('getRecordings', ['recordID' => implode(',', $breakoutrooms)]);
                        if ($xml && $xml->returncode == 'SUCCESS' && isset($xml->recordings)) {
                            // If there were already created meetings.
                            foreach ($xml->recordings->recording as $subrecordingxml) {
                                $recording = self::parse_recording($subrecordingxml);
                                $recordings[$recording['recordID']] = $recording;
                            }
                        }
                    }
                }
            }
            return ['recordings' => $recordings, 'success' => true];
        }
        debugging('getRecordings API call failed', DEBUG_DEVELOPER);
        return ['recordings' => [], 'success' => false];
    }

    /**
     * Helper function to parse an xml recording object and produce an array in the format used by the plugin.
     *
     * @param SimpleXMLElement $recording
     *
     * @return array
     */
    public static function parse_recording(SimpleXMLElement $recording): array {
        // Add formats.
        $playbackarray = [];
        foreach ($recording->playback->format as $format) {
            $playbackarray[(string) $format->type] = [
                'type' => (string) $format->type,
                'url' => trim((string) $format->url), 'length' => (string) $format->length,
            ];
            // Add preview per format when existing.
            if ($format->preview) {
                $playbackarray[(string) $format->type]['preview'] =
                    self::parse_preview_images($format->preview);
            }
        }
        // Add the metadata to the recordings array.
        $metadataarray =
            self::parse_recording_meta(get_object_vars($recording->metadata));
        $recordingarray = [
            'recordID' => (string) $recording->recordID,
            'meetingID' => (string) $recording->meetingID,
            'meetingName' => (string) $recording->name,
            'published' => (string) $recording->published,
            'state' => (string) $recording->state,
            'startTime' => (string) $recording->startTime,
            'endTime' => (string) $recording->endTime,
            'playbacks' => $playbackarray,
        ];
        if (isset($recording->protected)) {
            $recordingarray['protected'] = (string) $recording->protected;
        }
        return $recordingarray + $metadataarray;
    }

    /**
     * Helper function to convert an xml recording metadata object to an array in the format used by the plugin.
     *
     * @param array $metadata
     *
     * @return array
     */
    public static function parse_recording_meta(array $metadata): array {
        $metadataarray = [];
        foreach ($metadata as $key => $value) {
            if (is_object($value)) {
                $value = '';
            }
            $metadataarray['meta_' . $key] = $value;
        }
        return $metadataarray;
    }

    /**
     * Helper function to convert an xml recording preview images to an array in the format used by the plugin.
     *
     * @param SimpleXMLElement $preview
     *
     * @return array
     */
    public static function parse_preview_images(SimpleXMLElement $preview): array {
        $imagesarray = [];
        foreach ($preview->images->image as $image) {
            $imagearray = ['url' => trim((string) $image)];
            foreach ($image->attributes() as $attkey => $attvalue) {
                $imagearray[$attkey] = (string) $attvalue;
            }
            array_push($imagesarray, $imagearray);
        }
        return $imagesarray;
    }
}
