<?php
/**
 * Whups external API interface.
 *
 * This file defines Whups' external API interface. Other applications
 * can interact with Whups through this API.
 *
 * @package Whups
 */
class Whups_Api extends Horde_Registry_Api
{
    /**
     * Links.
     *
     * @var array
     */
    protected $_links = array(
        'show' => '%application%/ticket/?id=|ticket|'
    );

    /**
     * Browse through Whups' object tree.
     *
     * @param string $path  The level of the tree to browse.
     *
     * @return array  The contents of $path
     */
    public function browse($path = '')
    {
        global $whups_driver, $registry;

        if (substr($path, 0, 5) == 'whups') {
            $path = substr($path, 5);
        }
        $path = trim($path, '/');

        if (empty($path)) {
            $results = array(
                'whups/queue' => array(
                    'name' => _("Queues"),
                    'icon' => Horde_Themes::img('whups.png'),
                    'browseable' => count(
                        Whups::permissionsFilter($whups_driver->getQueues(),
                                                 'queue', Horde_Perms::READ))));
        } else {
            $path = explode('/', $path);
            $results = array();

            switch ($path[0]) {
            case 'queue':
                $queues = Whups::permissionsFilter($whups_driver->getQueues(),
                                                   'queue', Horde_Perms::SHOW);
                if (count($path) == 1) {
                    foreach ($queues as $queue => $name) {
                        $results['whups/queue/' . $queue] = array(
                            'name' => sprintf(_("Tickets from %s"), $name),
                            'browseable' => true);
                    }
                } else {
                    if (!Whups::hasPermission($queues[$path[1]], 'queue',
                                              Horde_Perms::READ)) {
                        throw new Horde_Exception_PermissionDenied();
                    }

                    $tickets = $whups_driver->getTicketsByProperties(
                        array('queue' => $path[1]));
                    foreach ($tickets as $ticket) {
                        $results['whups/queue/' . $path[1] . '/' . $ticket['id']] = array(
                            'name' => $ticket['summary'],
                            'browseable' => false);
                    }
                }
                break;
            }
        }

        return $results;
    }

    /**
     * Create a new queue.
     *
     * @param string $name The queue's name.
     *
     * @return integer  The new queue id.
     */
    public function addQueue($name)
    {
        if ($GLOBALS['registry']->isAdmin(array('permission' => 'whups:admin'))) {
            return $GLOBALS['whups_driver']->addQueue($name, '');
        }
        throw new Horde_Exception_PermissionDenied('You must be an administrator to perform this action.');
    }

    /**
     * Return the ids of all open tickets assigned to the current user.
     *
     * @return array  Array of ticket ids.
     */
    public function getAssignedTicketIds()
    {
        global $whups_driver;

        $info = array('owner' => 'user:' . $GLOBALS['registry']->getAuth(), 'nores' => true);
        $tickets = $whups_driver->getTicketsByProperties($info);
        $result = array();
        foreach ($tickets as $ticket) {
            $result[] = $ticket['id'];
        }
        return $result;
    }

    /**
     * Return the ids of all open tickets that the current user created.
     *
     * @return array  Array of ticket ids.
     */
    public function getRequestedTicketIds()
    {
        global $whups_driver;

        $info = array('requester' => $GLOBALS['registry']->getAuth(), 'nores' => true);
        $tickets = $whups_driver->getTicketsByProperties($info);
        $result = array();
        foreach ($tickets as $ticket) {
            $result[] = (int) $ticket['id'];
        }
        return $result;
    }

    /**
     * Create a new ticket.
     *
     * @param array $ticket_info An array of form variables representing all of the
     * data collected by CreateStep1Form, CreateStep2Form, CreateStep3Form, and
     * optionally CreateStep4Form.
     *
     * @return integer The new ticket id.
     */
    public function addTicket($ticket_info)
    {
        global $whups_driver;

        if (!is_array($ticket_info)) {
            throw new Whups_Exception('Invalid arguments');
        }

        $vars = new Horde_Variables($ticket_info);

        $form1 = new Whups_Form_Ticket_CreateStepOne($vars);
        $form2 = new Whups_Form_Ticket_CreateStepTwo($vars);
        $form3 = new Whups_Form_Ticket_CreateStepThree($vars);

        // FIXME: This is an almighty hack, but we can't have form
        // tokens in rpc calls.
        $form1->useToken(false);
        $form2->useToken(false);
        $form3->useToken(false);

        // Complain if we've been given bad parameters.
        if (!$form1->validate($vars, true)) {
            $f1 = var_export($form1->getErrors(), true);
            throw new Whups_Exception("Invalid arguments ($f1)");
        }
        if (!$form2->validate($vars, true)) {
            $f2 = var_export($form2->getErrors(), true);
            throw new Whups_Exception("Invalid arguments ($f2)");
        }
        if (!$form3->validate($vars, true)) {
            $f3 = var_export($form3->getErrors(), true);
            throw new Whups_Exception("Invalid arguments ($f3)");
        }

        $form1->getInfo($vars, $info);
        $form2->getInfo($vars, $info);
        $form3->getInfo($vars, $info);

        // More checks if we're assigning the ticket at create-time.
        if ($GLOBALS['registry']->getAuth() && $whups_driver->isCategory('assigned', $vars->get('state'))) {
            $form4 = new Whups_Form_Ticket_CreateStepFour($vars);
            $form4->useToken(false);
            if (!$form4->validate($vars, true)) {
                throw new Whups_Exception('Invalid arguments (' . var_export($form4->getErrors(), true) . ')');
            }

            $form4->getInfo($vars, $info);
        }

        $ticket = Whups_Ticket::newTicket($info, $GLOBALS['registry']->getAuth());

        return $ticket->getId();
    }

    /**
     * Update a ticket's properties.
     *
     * @param integer $ticket_id  The id of the id to changes.
     * @param array $ticket_info  The attributes to set, from
     *                            Whups_Form_Ticket_Edit.
     *
     * @return boolean  True
     */
    public function updateTicket($ticket_id, $ticket_info)
    {
        global $whups_driver;

        // Cast as an int for safety.
        $ticket = Whups_Ticket::makeTicket((int)$ticket_id);

        // Check that we have permission to update the ticket
        if (!$GLOBALS['registry']->getAuth() ||
            !Whups::hasPermission($ticket->get('queue'), 'queue', 'update')) {
            throw new Whups_Exception_PermissionDenied(_('You do not have permission to update this ticket.'));
        }

        // Populate $vars with existing ticket details.
        $vars = new Horde_Variables();
        $ticket->setDetails($vars, true);

        // Copy new ticket details in.
        foreach ($ticket_info as $detail => $newval) {
            $vars->set($detail, $newval);
        }

        // Create and populate the EditTicketForm for validation. API calls can't
        // use form tokens and aren't the result of the EditTicketForm being
        // submitted.
        $editform = new Whups_Form_Ticket_Edit($vars, $ticket);
        $editform->useToken(false);
        $editform->setSubmitted(true);

        // Attempt to validate and update the ticket.
        if (!$editform->validate($vars)) {
             $form_errors = var_export($editform->getErrors(), true);
             throw new Whups_Exception(sprintf(_("Invalid ticket data supplied: %s"), $form_errors));
        }

        $editform->getInfo($vars, $info);

        $ticket->change('summary', $info['summary']);
        $ticket->change('state', $info['state']);
        $ticket->change('due', $info['due']);
        $ticket->change('priority', $info['priority']);
        if (!empty($info['newcomment'])) {
            $ticket->change('comment', $info['newcomment']);
        }
        if (!empty($info['due'])) {
            $ticket->change('due', $info['due']);
        }

        // Update attributes.
        $whups_driver->setAttributes($info, $ticket);

        // Add attachment if one was uploaded.
        if (!empty($info['newattachment']['name'])) {
            $ticket->change('attachment',
                            array('name' => $info['newattachment']['name'],
                                  'tmp_name' => $info['newattachment']['tmp_name']));
        }

        // If there was a new comment and permissions were specified on
        // it, set them.
        if (!empty($info['group'])) {
            $ticket->change('comment-perms', $info['group']);
        }
        $ticket->commit();

        // Ticket updated successfully
        return true;
    }

    /**
     * Add a comment to a ticket.
     *
     * @param integer $ticket_id  The id of the ticket to comment on.
     * @param string  $comment    The comment text to add.
     * @param string  $group      (optional) Restrict this comment to a specific group.
     *
     * @return boolean  True
     */
    public function addComment($ticket_id, $comment, $group = null)
    {
        $ticket_id = (int)$ticket_id;
        if (empty($ticket_id)) {
            throw new Whups_Exception('Invalid ticket id');
        }

        $ticket = Whups_Ticket::makeTicket($ticket_id);
        if (empty($comment)) {
            throw new Whups_Exception('Empty comments are not allowed');
        }

        // Add comment.
        $ticket->change('comment', $comment);

        // Add comment permissions, if specified.
        // @TODO: validate the user is allowed to specify this group
        if (!empty($group)) {
            $ticket->change('comment-perms', $group);
        }
        $ticket->commit();

        return true;
    }

    /**
     * Adds an attachment to a ticket.
     *
     * @param integer $ticket_id  The ticket number.
     * @param string $name        The name of the attachment.
     * @param string $data        The attachment data.
     *
     * @throws Whups_Exception
     */
    public function addAttachment($ticket_id, $name, $data)
    {
        $ticket_id = (int)$ticket_id;
        if (empty($ticket_id)) {
            throw new Whups_Exception(_("Invalid Ticket Id"));
        }

        $ticket = Whups_Ticket::makeTicket($ticket_id);
        if (!strlen($name) || !strlen($data)) {
            throw new Whups_Exception(_("Empty attachment"));
        }

        $tmp_name = Horde_Util::getTempFile('whups', true, $GLOBALS['conf']['tmpdir']);
        $fp = fopen($tmp_name, 'wb');
        fwrite($fp, $data);
        fclose($fp);

        $ticket->change('attachment', array('name' => $name, 'tmp_name' => $tmp_name));
        $ticket->commit();
    }

    /**
     * Set attributes for a ticket
     *
     * @TODO fold this into the updateTicket method
     */
    public function setTicketAttributes($info)
    {
        global $whups_driver;

        if (!isset($info['ticket_id']) || !isset($info['attributes'])) {
            throw new Whups_Exception(_("Invalid arguments: Must supply a ticket number and new attributes."));
        }

        // Convert the RPC parameters into what we'd expect if we were
        // posting the EditAttributes form.
        $ainfo = array();
        foreach ($info['attributes'] as $attrib) {
            if (!isset($attrib['id']) || !isset($attrib['value'])) {
                throw new InvalidArgumentException(_("Invalid argument: Missing attribute name or value."));
            }
            $ainfo['a' . $attrib['id']] = $attrib['value'];
        }
        $ainfo['id'] = $info['ticket_id'];

        return $whups_driver->setAttributes($ainfo);
    }

    /**
     * Get the types that Whups items can be listed as.
     *
     * @return array  Array of list types.
     */
    public function getListTypes()
    {
        return array('taskHash' => true);
    }

    /**
     * Get a list of items from whups as type $type.
     *
     * @param string $type  The list type to use (@see getListTypes). Currently supported: 'taskHash'
     *
     * @return array  An array of tickets.
     */
    public function listAs($type)
    {
        switch ($type) {
        case 'taskHash':
            global $whups_driver;
            $info = array(
              'owner' => 'user:' . $GLOBALS['registry']->getAuth(),
              'nores' => true);
            $tickets = $whups_driver->getTicketsByProperties($info);
            $result = array();
            foreach ($tickets as $ticket) {
                $view_link = Whups::urlFor('ticket', $ticket['id'], true);
                $delete_link = Whups::urlFor('ticket_action', array('delete', $ticket['id']), true);
                $complete_link = Whups::urlFor('ticket_action', array('update', $ticket['id']), true);

                $result['whups/' . $ticket['id']] = array(
                    'task_id'           => $ticket['id'],
                    'completed'         => ($ticket['state_category'] == 'resolved'),
                    'name'              => '[' . _("Ticket") . ' #' . $ticket['id'] . '] ' . $ticket['summary'],
                    'desc'              => null,
                    'due'               => $ticket['due'],
                    'view_link'         => $view_link,
                    'delete_link'       => $delete_link,
                    'edit_link'         => $view_link,
                    'complete_link'     => $complete_link
                    );
            }
            break;

        default:
            $result = array();
            break;
        }

        return $result;
    }

    /**
     * Return a list of queues that the current user has read permissions for
     *
     * @return array  Array of queue details
     */
    public function listQueues()
    {
        return Whups::permissionsFilter($GLOBALS['whups_driver']->getQueuesInternal(), 'queue', Horde_Perms::SHOW);
    }

    /**
     * Return a list of slugs that the current user has read permissions for
     *
     * @return array  Array of queue details
     */
    public function listSlugs()
    {
        return Whups::permissionsFilter($GLOBALS['whups_driver']->getSlugs(), 'queue', Horde_Perms::SHOW);
    }

    /**
     * Get details for a queue
     *
     * @param array | integer $queue  Either an array of queue ids or a single queue id.
     *
     * @return array  An array of queue information (or an array of arrays, if multiple queues were passed).
     */
    public function getQueueDetails($queue)
    {
        if (is_array($queue)) {
            $queues = Whups::permissionsFilter($queue, 'queue_id');
            $details = array();
            foreach ($queues as $id) {
                $details[$id] = $GLOBALS['whups_driver']->getQueueInternal($id);
            }
            return $details;
        }

        $queues = Whups::permissionsFilter(array($queue), 'queue_id');
        if ($queues) {
            return $GLOBALS['whups_driver']->getQueueInternal($queue);
        }

        return array();
    }

    /**
     * List the versions associated with a queue
     *
     * @param integer $queue  The queue id to get versions for.
     *
     * @return array  Array of queue versions
     */
    public function listVersions($queue)
    {
        $queues = Whups::permissionsFilter(array($queue), 'queue_id');
        if (!$queues) {
            return array();
        }

        $versions = array();
        $version_list = $GLOBALS['whups_driver']->getVersionInfoInternal($queue);
        foreach ($version_list as $version) {
            $versions[] = array('id' => $version['version_id'],
                                'name' => $version['version_name'],
                                'description' => $version['version_description'],
                                'active' => !empty($version['version_active']),
                                'readonly' => false);
        }

        usort($versions, array($this, '_sortVersions'));

        return $versions;
    }

    private function _sortVersions($a, $b)
    {
        $a_number = (string)(int)$a['name'][0] === $a['name'][0];
        $b_number = (string)(int)$b['name'][0] === $b['name'][0];

        if ($a_number && $b_number) {
            return version_compare($b['name'], $a['name']);
        }
        if (!$a_number && !$b_number) {
            return strcoll($b['name'], $a['name']);
        }
        return $a_number ? 1 : -1;
    }

    /**
     * Add a version to a queue
     *
     * @param integer $queue       The queue id to add the version to.
     * @param string $name         The name of the new version.
     * @param string $description  The descriptive text for the new version.
     * @param boolean $active      Whether the version is still active.
     */
    public function addVersion($queue, $name, $description, $active = true)
    {
        return $GLOBALS['whups_driver']->addVersion($queue, $name, $description, $active);
    }

    /**
     * Return the details for a queue version
     *
     * @param integer $version_id  The version to fetch
     *
     * @return array  Array of version details
     */
    public function getVersionDetails($version_id)
    {
        return $GLOBALS['whups_driver']->getVersionInternal($version_id);
    }

    /**
     * Get the all tickets for a queue, optionally with a specific state.
     *
     * @param integer $queue_id  The queue to get tickets for
     * @param string  $state     The state filter, if any.
     *
     * @return array  Array of tickets
     */
    public function getTicketDetails($queue_id, $state = null)
    {
        global $whups_driver;

        $info['queue_id'] = $queue_id;
        if (!empty($state)) {
            $info['category'] = $state;
        }
        $tickets = $whups_driver->getTicketsByProperties($info);

        for ($i = 0; $i < count($tickets); $i++) {
            $view_link = Whups::urlFor('ticket', $tickets[$i]['id'], true);
            $delete_link = Whups::urlFor('ticket_action', array('delete', $tickets[$i]['id']), true);
            $complete_link = Whups::urlFor('ticket_action', array('update', $tickets[$i]['id']), true);

            $tickets[$i] = array(
                    'ticket_id'         => $tickets[$i]['id'],
                    'completed'         => ($tickets[$i]['state_category'] == 'resolved'),
                    'assigned'          => ($tickets[$i]['state_category'] == 'assigned'),
                    'name'              => $tickets[$i]['queue_name'] . ' #' .
                                           $tickets[$i]['id'] . ' - ' . $tickets[$i]['summary'],
                    'state'             => $tickets[$i]['state_name'],
                    'type'              => $tickets[$i]['type_name'],
                    'priority'          => $tickets[$i]['priority_name'],
                    'desc'              => null,
                    'due'               => $tickets[$i]['due'],
                    'category'          => null,
                    'view_link'         => $view_link,
                    'delete_link'       => $delete_link,
                    'edit_link'         => $view_link,
                    'complete_link'     => $complete_link
                    );
        }

        return $tickets;
    }

    /**
     * List cost objects
     *
     * @param array $criteria  The list criteria
     *
     * @return array  Tickets (as cost objects) matching $criteria
     */
    public function listCostObjects($criteria)
    {
        global $whups_driver;

        $info = array();
        if (!empty($criteria['user'])) {
            if (is_array($criteria['user'])) {
                $info['owner'] = array_map(
                    function($input) { return 'user:' . $input; },
                    $criteria['user']
                );
            } else {
                $info['owner'] = 'user:' . $criteria['user'];
            }
        }
        if (!empty($criteria['active'])) {
            $info['nores'] = true;
        }
        if (!empty($criteria['id'])) {
            $info['id'] = $criteria['id'];
        }

        $tickets = $whups_driver->getTicketsByProperties($info, true, false, false);
        $result = array();
        foreach ($tickets as $ticket) {
            $result[$ticket['id']] = array(
              'id'     => $ticket['id'],
              'active' => ($ticket['state_category'] != 'resolved'),
              'name'   => sprintf(
                _("Ticket %s - %s"), $ticket['id'], $ticket['summary']));
        }
        /* If the user has an estimate attribute, use that for cost object
         * hour estimates. */
        $ticket_ids = array_map(function($item) {
            return $item['id'];
        }, $tickets);
        $att_list = $whups_driver->getTicketAttributesWithNames($ticket_ids);
        foreach ($att_list as $attributes) {
            foreach ($attributes as $k => $v) {
                if (strtolower($v['attribute_name']) == _("estimated time")) {
                    if (!empty($v['attribute_value'])) {
                        $result[$k['id']]['estimate'] = (double) $v['attribute_value'];
                    }
                }
            }
        }
        ksort($result);
        if (count($result) == 0) {
            return array();
        } else {
            return array(
              array(
                'category' => _("Tickets"),
                'objects'  => array_values($result)));
        }
    }

    /**
     * List the ways that tickets can be treated as time objects
     *
     * @return array  Array of time object types
     */
    public function listTimeObjectCategories()
    {
        return array('created' => array('title' => _("My tickets by creation date"), 'type' => 'single'),
                     'assigned' => array('title' => _("My tickets by assignment date"), 'type' => 'single'),
                     'due' => array('title' => _("My tickets by due date"), 'type' => 'single'),
                     'resolved' => array('title' => _("My tickets by resolution date"), 'type' => 'single')
                );
    }

    /**
     * Lists tickets with due dates as time objects.
     *
     * @param array $categories  The time categories (from listTimeObjectCategories) to list.
     * @param mixed $start       The start date of the period.
     * @param mixed $end         The end date of the period.
     */
    public function listTimeObjects($categories, $start, $end)
    {
        global $whups_driver;

        $start = new Horde_Date($start);
        $start_ts = $start->timestamp();
        $end = new Horde_Date($end);
        $end_ts = $end->timestamp();

        $criteria['owner'] = Whups::getOwnerCriteria($GLOBALS['registry']->getAuth());

        /* @TODO Use $categories */
        $category = 'due';
        switch ($category) {
        case 'assigned':
            $label = _("Assigned");
            $criteria['ass'] = true;
            break;

        case 'created':
            $label = _("Created");
            break;

        case 'due':
            $label = _("Due");
            $criteria['nores'] = true;
            break;

        case 'resolved':
            $label = _("Resolved");
            $criteria['res'] = true;
            break;
        }

        try {
            $tickets = $whups_driver->getTicketsByProperties($criteria);
        } catch (Whups_Exception $e) {
          return array();
        }
        $objects = array();
        foreach ($tickets as $ticket) {
            switch ($category) {
            case 'assigned':
                $t_start = $ticket['date_assigned'];
                break;

            case 'created':
                $t_start = $ticket['timestamp'];
                break;

            case 'due':
                if (empty($ticket['due'])) {
                    continue 2;
                }
                $t_start = $ticket['due'];
                break;

            case 'resolved':
                $t_start = $ticket['date_resolved'];
                break;
            }

            if ($t_start + 1 < $start_ts || $t_start > $end_ts) {
                continue;
            }
            $t = new Whups_Ticket($ticket['id'], $ticket);
            $objects[$ticket['id']] = array(
                'title' => sprintf('%s: [#%s] %s', $label, $ticket['id'], $ticket['summary']),
                'description' => $t->toString(),
                'id' => $ticket['id'],
                'start' => date('Y-m-d\TH:i:s', $t_start),
                'end' => date('Y-m-d\TH:i:s', $t_start + 1),
                'params' => array('id' => $ticket['id']),
                'link' => Whups::urlFor('ticket', $ticket['id'], true)
            );
        }

        return $objects;
    }

}
