<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * State machine for running through Yammer import.
 *
 * @package YammerImportPlugin
 * @author Brion Vibber <brion@status.net>
 */
class YammerRunner
{
    private $state;
    private $client;
    private $importer;

    /**
     * Normalize our singleton state and give us a YammerRunner object to play with!
     *
     * @return YammerRunner
     */
    public static function init()
    {
        $state = Yammer_state::getKV('id', 1);
        if (!$state) {
            $state = self::initState();
        }
        return new YammerRunner($state);
    }

    private static function initState()
    {
        $state = new Yammer_state();
        $state->id = 1;
        $state->state = 'init';
        $state->created = common_sql_now();
        $state->modified = common_sql_now();
        $state->insert();
        return $state;
    }

    private function __construct($state)
    {
        $this->state = $state;

        $this->client = new SNYammerClient(
            common_config('yammer', 'consumer_key'),
            common_config('yammer', 'consumer_secret'),
            $this->state->oauth_token,
            $this->state->oauth_secret);

        $this->importer = new YammerImporter($this->client);
    }

    /**
     * Check which state we're in
     *
     * @return string
     */
    public function state()
    {
        return $this->state->state;
    }

    /**
     * Is the import done, finished, complete, finito?
     *
     * @return boolean
     */
    public function isDone()
    {
        $workStates = array('import-users', 'import-groups', 'fetch-messages', 'save-messages');
        return ($this->state() == 'done');
    }

    /**
     * Check if we have work to do in iterate().
     *
     * @return boolean
     */
    public function hasWork()
    {
        $workStates = array('import-users', 'import-groups', 'fetch-messages', 'save-messages');
        return in_array($this->state(), $workStates);
    }

    /**
     * Blow away any current state!
     */
    public function reset()
    {
        $this->state->delete();
        $this->state = self::initState();
    }

    /**
     * Start the authentication process! If all goes well, we'll get back a URL.
     * Have the user visit that URL, log in on Yammer and verify the importer's
     * permissions. They'll get back a verification code, which needs to be passed
     * on to saveAuthToken().
     *
     * @return string URL
     */
    public function requestAuth()
    {
        if ($this->state->state != 'init') {
            // TRANS: Server exception thrown if a Yammer authentication request is already present.
            throw new ServerException(_m('Cannot request Yammer auth; already there!'));
        }

        $data = $this->client->requestToken();

        $old = clone($this->state);
        $this->state->state = 'requesting-auth';
        $this->state->oauth_token = $data['oauth_token'];
        $this->state->oauth_secret = $data['oauth_token_secret'];
        $this->state->modified = common_sql_now();
        $this->state->update($old);

        return $this->getAuthUrl();
    }

    /**
     * When already in requesting-auth state, grab the URL to send the user to
     * to complete OAuth setup.
     *
     * @return string URL
     */
    function getAuthUrl()
    {
        if ($this->state() == 'requesting-auth') {
            return $this->client->authorizeUrl($this->state->oauth_token);
        } else {
            // TRANS: Server exception thrown when requesting a Yammer authentication URL while in an incorrect state.
            throw new ServerException(_m('Cannot get Yammer auth URL when not in requesting-auth state!'));
        }
    }

    /**
     * Now that the user's given us this verification code from Yammer, we can
     * request a final OAuth token/secret pair which we can use to access the
     * API.
     *
     * After success here, we'll be ready to move on and run through iterate()
     * until the import is complete.
     *
     * @param string $verifier
     * @return boolean success
     */
    public function saveAuthToken($verifier)
    {
        if ($this->state->state != 'requesting-auth') {
            // TRANS: Server exception thrown if a Yammer authentication token could not be saved in a certain import state.
            // TRANS: %s is the import state in the which the error occurred.
            throw new ServerException(_m('Cannot save auth token in Yammer import state %s.',$this->state->state));
        }

        $data = $this->client->accessToken($verifier);

        $old = clone($this->state);
        $this->state->state = 'import-users';
        $this->state->oauth_token = $data['oauth_token'];
        $this->state->oauth_secret = $data['oauth_token_secret'];
        $this->state->modified = common_sql_now();
        $this->state->update($old);

        return true;
    }

    /**
     * Once authentication is complete, we need to call iterate() a bunch of times
     * until state() returns 'done'.
     *
     * @return boolean success
     */
    public function iterate()
    {
        switch($this->state())
        {
            case 'init':
            case 'requesting-auth':
                // Neither of these should reach our background state!
                common_log(LOG_ERR, "Non-background YammerImport state '$state->state' during import run!");
                return false;
            case 'import-users':
                return $this->iterateUsers();
            case 'import-groups':
                return $this->iterateGroups();
            case 'fetch-messages':
                return $this->iterateFetchMessages();
            case 'save-messages':
                return $this->iterateSaveMessages();
            default:
                common_log(LOG_ERR, "Invalid YammerImport state '$state->state' during import run!");
                return false;
        }
    }

    /**
     * Trundle through one 'page' return of up to 50 user accounts retrieved
     * from the Yammer API, importing them as we go.
     *
     * When we run out of users, move on to groups.
     *
     * @return boolean success
     */
    private function iterateUsers()
    {
        $old = clone($this->state);

        $page = intval($this->state->users_page) + 1;
        $data = $this->client->users(array('page' => $page));

        if (count($data) == 0) {
            common_log(LOG_INFO, "Finished importing Yammer users; moving on to groups.");
            $this->state->state = 'import-groups';
        } else {
            foreach ($data as $item) {
                $user = $this->importer->importUser($item);
                common_log(LOG_INFO, "Imported Yammer user " . $item['id'] . " as $user->nickname ($user->id)");
            }
            $this->state->users_page = $page;
        }
        $this->state->modified = common_sql_now();
        $this->state->update($old);
        return true;
    }

    /**
     * Trundle through one 'page' return of up to 20 user groups retrieved
     * from the Yammer API, importing them as we go.
     *
     * When we run out of groups, move on to messages.
     *
     * @return boolean success
     */
    private function iterateGroups()
    {
        $old = clone($this->state);

        $page = intval($this->state->groups_page) + 1;
        $data = $this->client->groups(array('page' => $page));

        if (count($data) == 0) {
            common_log(LOG_INFO, "Finished importing Yammer groups; moving on to messages.");
            $this->state->state = 'fetch-messages';
        } else {
            foreach ($data as $item) {
                $group = $this->importer->importGroup($item);
                common_log(LOG_INFO, "Imported Yammer group " . $item['id'] . " as $group->nickname ($group->id)");
            }
            $this->state->groups_page = $page;
        }
        $this->state->modified = common_sql_now();
        $this->state->update($old);
        return true;
    }

    /**
     * Trundle through one 'page' return of up to 20 public messages retrieved
     * from the Yammer API, saving them to our stub table for future import in
     * correct chronological order.
     *
     * When we run out of messages to fetch, move on to saving the messages.
     *
     * @return boolean success
     */
    private function iterateFetchMessages()
    {
        $old = clone($this->state);

        $oldest = intval($this->state->messages_oldest);
        if ($oldest) {
            $params = array('older_than' => $oldest);
        } else {
            $params = array();
        }
        $data = $this->client->messages($params);
        $messages = $data['messages'];

        if (count($messages) == 0) {
            common_log(LOG_INFO, "Finished fetching Yammer messages; moving on to save messages.");
            $this->state->state = 'save-messages';
        } else {
            foreach ($messages as $item) {
                $stub = Yammer_notice_stub::getKV($item['id']);
                if (!$stub) {
                    Yammer_notice_stub::record($item['id'], $item);
                }
                $oldest = $item['id'];
            }
            $this->state->messages_oldest = $oldest;
        }
        $this->state->modified = common_sql_now();
        $this->state->update($old);
        return true;
    }

    private function iterateSaveMessages()
    {
        $old = clone($this->state);

        $newest = intval($this->state->messages_newest);

        $stub = new Yammer_notice_stub();
        if ($newest) {
            $stub->whereAdd('id > ' . $newest);
        }
        $stub->limit(20);
        $stub->orderBy('id');
        $stub->find();

        if ($stub->N == 0) {
            common_log(LOG_INFO, "Finished saving Yammer messages; import complete!");
            $this->state->state = 'done';
        } else {
            while ($stub->fetch()) {
                $item = $stub->getData();
                $notice = $this->importer->importNotice($item);
                common_log(LOG_INFO, "Imported Yammer notice " . $item['id'] . " as $notice->id");
                $newest = $item['id'];
            }
            $this->state->messages_newest = $newest;
        }
        $this->state->modified = common_sql_now();
        $this->state->update($old);
        return true;
    }

    /**
     * Count the number of Yammer users we've mapped into our system!
     *
     * @return int
     */
    public function countUsers()
    {
        $map = new Yammer_user();
        return $map->count();
    }

    /**
     * Count the number of Yammer groups we've mapped into our system!
     *
     * @return int
     */
    public function countGroups()
    {
        $map = new Yammer_group();
        return $map->count();
    }

    /**
     * Count the number of Yammer notices we've pulled down for pending import...
     *
     * @return int
     */
    public function countFetchedNotices()
    {
        $map = new Yammer_notice_stub();
        return $map->count();
    }

    /**
     * Count the number of Yammer notices we've mapped into our system!
     *
     * @return int
     */
    public function countSavedNotices()
    {
        $map = new Yammer_notice();
        return $map->count();
    }

    /**
     * Start running import work in the background queues...
     */
    public function startBackgroundImport()
    {
        $qm = QueueManager::get();
        $qm->enqueue('YammerImport', 'yammer');
    }

    /**
     * Record an error condition from a background run, which we should
     * display in progress state for the admin.
     *
     * @param string $msg
     */
    public function recordError($msg)
    {
        // HACK HACK HACK
        try {
            $temp = new Yammer_state();
            $temp->query('ROLLBACK');
        } catch (Exception $e) {
            common_log(LOG_ERR, 'Exception while confirming rollback while recording error: ' . $e->getMessage());
        }
        $old = clone($this->state);
        $this->state->last_error = $msg;
        $this->state->update($old);
    }

    /**
     * Clear the error state.
     */
    public function clearError()
    {
        $this->recordError('');
    }

    /**
     * Get the last recorded background error message, if any.
     *
     * @return string
     */
    public function lastError()
    {
        return $this->state->last_error;
    }
}
