<?php

namespace dokuwiki\Subscriptions;

use dokuwiki\ChangeLog\PageChangeLog;
use dokuwiki\Extension\AuthPlugin;
use dokuwiki\Input\Input;
use Exception;

class BulkSubscriptionSender extends SubscriptionSender
{
    /**
     * Send digest and list subscriptions
     *
     * This sends mails to all subscribers that have a subscription for namespaces above
     * the given page if the needed $conf['subscribe_time'] has passed already.
     *
     * This function is called form lib/exe/indexer.php
     *
     * @param string $page
     * @return int number of sent mails
     * @throws Exception
     */
    public function sendBulk($page)
    {
        $subscriberManager = new SubscriberManager();
        if (!$subscriberManager->isenabled()) {
            return 0;
        }

        /** @var AuthPlugin $auth */
        global $auth;
        global $conf;
        global $USERINFO;
        /** @var Input $INPUT */
        global $INPUT;
        $count = 0;

        $subscriptions = $subscriberManager->subscribers($page, null, ['digest', 'list']);

        // remember current user info
        $olduinfo = $USERINFO;
        $olduser = $INPUT->server->str('REMOTE_USER');

        foreach ($subscriptions as $target => $users) {
            if (!$this->lock($target)) {
                continue;
            }

            foreach ($users as $user => $info) {
                [$style, $lastupdate] = $info;

                $lastupdate = (int)$lastupdate;
                if ($lastupdate + $conf['subscribe_time'] > time()) {
                    // Less than the configured time period passed since last
                    // update.
                    continue;
                }

                // Work as the user to make sure ACLs apply correctly
                $USERINFO = $auth->getUserData($user);
                $INPUT->server->set('REMOTE_USER', $user);
                if ($USERINFO === false) {
                    continue;
                }
                if (!$USERINFO['mail']) {
                    continue;
                }

                if (str_ends_with($target, ':')) {
                    // subscription target is a namespace, get all changes within
                    $changes = getRecentsSince($lastupdate, null, getNS($target));
                } else {
                    // single page subscription, check ACL ourselves
                    if (auth_quickaclcheck($target) < AUTH_READ) {
                        continue;
                    }
                    $meta = p_get_metadata($target);
                    $changes = [$meta['last_change']];
                }

                // Filter out pages only changed in small and own edits
                $change_ids = [];
                foreach ($changes as $rev) {
                    $n = 0;
                    $pagelog = new PageChangeLog($rev['id']);
                    while (
                        !is_null($rev) && $rev['date'] >= $lastupdate &&
                        ($INPUT->server->str('REMOTE_USER') === $rev['user'] ||
                            $rev['type'] === DOKU_CHANGE_TYPE_MINOR_EDIT)
                    ) {
                        $revisions = $pagelog->getRevisions($n++, 1);
                        $rev = ($revisions !== []) ? $pagelog->getRevisionInfo($revisions[0]) : null;
                    }

                    if (!is_null($rev) && $rev['date'] >= $lastupdate) {
                        // Some change was not a minor one and not by myself
                        $change_ids[] = $rev['id'];
                    }
                }

                // send it
                if ($style === 'digest') {
                    foreach ($change_ids as $change_id) {
                        $this->sendDigest(
                            $USERINFO['mail'],
                            $change_id,
                            $lastupdate
                        );
                        $count++;
                    }
                } elseif ($style === 'list') {
                    $this->sendList($USERINFO['mail'], $change_ids, $target, $lastupdate);
                    $count++;
                }
                // TODO: Handle duplicate subscriptions.

                // Update notification time.
                $subscriberManager->add($target, $user, $style, time());
            }
            $this->unlock($target);
        }

        // restore current user info
        $USERINFO = $olduinfo;
        $INPUT->server->set('REMOTE_USER', $olduser);
        return $count;
    }

    /**
     * Lock subscription info
     *
     * We don't use io_lock() her because we do not wait for the lock and use a larger stale time
     *
     * @param string $id The target page or namespace, specified by id; Namespaces
     *                   are identified by appending a colon.
     *
     * @return bool true, if you got a succesful lock
     * @author Adrian Lang <lang@cosmocode.de>
     */
    protected function lock($id)
    {
        global $conf;

        $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';

        if (is_dir($lock) && time() - @filemtime($lock) > 60 * 5) {
            // looks like a stale lock - remove it
            @rmdir($lock);
        }

        // try creating the lock directory
        if (!@mkdir($lock)) {
            return false;
        }

        if ($conf['dperm']) {
            chmod($lock, $conf['dperm']);
        }
        return true;
    }

    /**
     * Unlock subscription info
     *
     * @param string $id The target page or namespace, specified by id; Namespaces
     *                   are identified by appending a colon.
     *
     * @return bool
     * @author Adrian Lang <lang@cosmocode.de>
     */
    protected function unlock($id)
    {
        global $conf;
        $lock = $conf['lockdir'] . '/_subscr_' . md5($id) . '.lock';
        return @rmdir($lock);
    }

    /**
     * Return the last revision before lastupdate.
     *
     * @param string $id         The target page or namespace, specified by id; Namespaces
     *                           are identified by appending a colon.
     * @param int    $lastupdate Time of the last notification
     *
     * @return int|null The revision timestamp, or null if no earlier revision exists
     * @author Michael Stapelberg <stapelberg+dokuwiki@google.com>
     */
    protected function lastRevBefore($id, $lastupdate)
    {
        $pagelog = new PageChangeLog($id);
        $n = 0;
        do {
            $rev = $pagelog->getRevisions($n++, 1);
            $rev = ($rev !== []) ? $rev[0] : null;
        } while (!is_null($rev) && $rev > $lastupdate);
        return $rev;
    }

    /**
     * Send a digest mail
     *
     * Sends a digest mail showing a bunch of changes of a single page. Basically the same as sendPageDiff()
     * but determines the last known revision first
     *
     * @param string $subscriber_mail The target mail address
     * @param string $id              The ID
     * @param int    $lastupdate      Time of the last notification
     *
     * @return bool
     * @author Adrian Lang <lang@cosmocode.de>
     *
     */
    protected function sendDigest($subscriber_mail, $id, $lastupdate)
    {
        $rev = $this->lastRevBefore($id, $lastupdate);

        // TODO I'm not happy with the following line and passing $this->mailer around. Not sure how to solve it better
        $pageSubSender = new PageSubscriptionSender($this->mailer);
        return $pageSubSender->sendPageDiff(
            $subscriber_mail,
            'subscr_digest',
            $id,
            $rev
        );
    }

    /**
     * Send a list mail
     *
     * Sends a list mail showing a list of changed pages.
     *
     * @param string $subscriber_mail The target mail address
     * @param array  $ids             Array of ids
     * @param string $ns_id           The id of the namespace
     * @param int    $lastupdate      Time of the last notification
     *
     * @return bool true if a mail was sent
     * @author Adrian Lang <lang@cosmocode.de>
     *
     */
    protected function sendList($subscriber_mail, $ids, $ns_id, $lastupdate)
    {
        global $lang;

        if ($ids === []) {
            return false;
        }

        $tlist = '';
        $hlist = '<ul>';
        foreach ($ids as $id) {
            $last = $this->lastRevBefore($id, $lastupdate);
            $link = wl($id, [], true);
            $difflink = $last ? wl($id, ['do' => 'diff', 'rev' => $last], true) : null;
            $difflinktext = $last ? wl($id, ['do' => 'diff', 'rev' => $last], true, '&') : null;

            $tlist .= '* ' . $link;
            if ($difflinktext) {
                $tlist .= ' (' . $lang['diff'] . ': ' . $difflinktext . ')';
            }
            $tlist .= NL;

            $hlist .= '<li>';
            $hlist .= '<a href="' . $link . '">' . hsc($id) . '</a>';
            if ($difflink) {
                $hlist .= ' (<a href="' . $difflink . '">' . $lang['diff'] . '</a>)';
            }
            $hlist .= '</li>' . NL;
        }
        $hlist .= '</ul>';

        $id = prettyprint_id($ns_id);
        $trep = [
            'DIFF' => rtrim($tlist),
            'PAGE' => $id,
            'SUBSCRIBE' => wl($id, ['do' => 'subscribe'], true, '&'),
        ];
        $hrep = [
            'DIFF' => $hlist,
        ];

        return $this->send(
            $subscriber_mail,
            'subscribe_list',
            $ns_id,
            'subscr_list',
            $trep,
            $hrep
        );
    }
}
