<?php
/*********************************************************************
    class.ticket.php

    The most important class! Don't play with fire please.

    Peter Rotich <peter@osticket.com>
    Copyright (c)  2006-2013 osTicket
    http://www.osticket.com

    Released under the GNU General Public License WITHOUT ANY WARRANTY.
    See LICENSE.TXT for details.

    vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
include_once(INCLUDE_DIR.'class.thread.php');
include_once(INCLUDE_DIR.'class.staff.php');
include_once(INCLUDE_DIR.'class.client.php');
include_once(INCLUDE_DIR.'class.team.php');
include_once(INCLUDE_DIR.'class.email.php');
include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.topic.php');
include_once(INCLUDE_DIR.'class.lock.php');
include_once(INCLUDE_DIR.'class.file.php');
include_once(INCLUDE_DIR.'class.export.php');
include_once(INCLUDE_DIR.'class.attachment.php');
include_once(INCLUDE_DIR.'class.banlist.php');
include_once(INCLUDE_DIR.'class.template.php');
include_once(INCLUDE_DIR.'class.variable.php');
include_once(INCLUDE_DIR.'class.priority.php');
include_once(INCLUDE_DIR.'class.sla.php');
include_once(INCLUDE_DIR.'class.canned.php');
require_once(INCLUDE_DIR.'class.dynamic_forms.php');
require_once(INCLUDE_DIR.'class.user.php');
require_once(INCLUDE_DIR.'class.collaborator.php');
require_once(INCLUDE_DIR.'class.task.php');
require_once(INCLUDE_DIR.'class.faq.php');

class Ticket extends VerySimpleModel
implements RestrictedAccess, Threadable, Searchable {
    static $meta = array(
        'table' => TICKET_TABLE,
        'pk' => array('ticket_id'),
        'select_related' => array('topic', 'staff', 'user', 'team', 'dept',
            'sla', 'thread', 'child_thread', 'user__default_email', 'status'),
        'joins' => array(
            'user' => array(
                'constraint' => array('user_id' => 'User.id'),
                'null' => true,
            ),
            'status' => array(
                'constraint' => array('status_id' => 'TicketStatus.id')
            ),
            'lock' => array(
                'constraint' => array('lock_id' => 'Lock.lock_id'),
                'null' => true,
            ),
            'dept' => array(
                'constraint' => array('dept_id' => 'Dept.id'),
                'null' => true,
            ),
            'sla' => array(
                'constraint' => array('sla_id' => 'Sla.id'),
                'null' => true,
            ),
            'staff' => array(
                'constraint' => array('staff_id' => 'Staff.staff_id'),
                'null' => true,
            ),
            'tasks' => array(
                'reverse' => 'Task.ticket',
            ),
            'team' => array(
                'constraint' => array('team_id' => 'Team.team_id'),
                'null' => true,
            ),
            'topic' => array(
                'constraint' => array('topic_id' => 'Topic.topic_id'),
                'null' => true,
            ),
            'thread' => array(
                'reverse' => 'TicketThread.ticket',
                'list' => false,
                'null' => true,
            ),
            'child_thread' => array(
                'constraint' => array(
                    'ticket_id'  => 'TicketThread.object_id',
                    "'C'" => 'TicketThread.object_type',
                ),
                'searchable' => false,
                'null' => true,
            ),
            'cdata' => array(
                'reverse' => 'TicketCData.ticket',
                'list' => false,
            ),
            'entries' => array(
                'constraint' => array(
                    "'T'" => 'DynamicFormEntry.object_type',
                    'ticket_id' => 'DynamicFormEntry.object_id',
                ),
                'list' => true,
            ),
        )
    );

    const PERM_CREATE   = 'ticket.create';
    const PERM_EDIT     = 'ticket.edit';
    const PERM_ASSIGN   = 'ticket.assign';
    const PERM_RELEASE  = 'ticket.release';
    const PERM_TRANSFER = 'ticket.transfer';
    const PERM_REFER    = 'ticket.refer';
    const PERM_MERGE    = 'ticket.merge';
    const PERM_LINK     = 'ticket.link';
    const PERM_REPLY    = 'ticket.reply';
    const PERM_MARKANSWERED = 'ticket.markanswered';
    const PERM_CLOSE    = 'ticket.close';
    const PERM_DELETE   = 'ticket.delete';

    const FLAG_COMBINE_THREADS     = 0x0001;
    const FLAG_SEPARATE_THREADS    = 0x0002;
    const FLAG_LINKED              = 0x0008;
    const FLAG_PARENT              = 0x0010;

    static protected $perms = array(
            self::PERM_CREATE => array(
                'title' =>
                /* @trans */ 'Create',
                'desc'  =>
                /* @trans */ 'Ability to open tickets on behalf of users'),
            self::PERM_EDIT => array(
                'title' =>
                /* @trans */ 'Edit',
                'desc'  =>
                /* @trans */ 'Ability to edit tickets'),
            self::PERM_ASSIGN => array(
                'title' =>
                /* @trans */ 'Assign',
                'desc'  =>
                /* @trans */ 'Ability to assign tickets to agents or teams'),
            self::PERM_RELEASE => array(
                'title' =>
                /* @trans */ 'Release',
                'desc'  =>
                /* @trans */ 'Ability to release ticket assignment'),
            self::PERM_TRANSFER => array(
                'title' =>
                /* @trans */ 'Transfer',
                'desc'  =>
                /* @trans */ 'Ability to transfer tickets between departments'),
            self::PERM_REFER => array(
                'title' =>
                /* @trans */ 'Refer',
                'desc'  =>
                /* @trans */ 'Ability to manage ticket referrals'),
            self::PERM_MERGE => array(
                'title' =>
                /* @trans */ 'Merge',
                'desc'  =>
                /* @trans */ 'Ability to merge tickets'),
            self::PERM_LINK => array(
                'title' =>
                /* @trans */ 'Link',
                'desc'  =>
                /* @trans */ 'Ability to link tickets'),
            self::PERM_REPLY => array(
                'title' =>
                /* @trans */ 'Post Reply',
                'desc'  =>
                /* @trans */ 'Ability to post a ticket reply'),
            self::PERM_MARKANSWERED => array(
                'title' =>
                /* @trans */ 'Mark as Answered',
                'desc'  =>
                /* @trans */ 'Ability to mark a ticket as Answered/Unanswered'),
            self::PERM_CLOSE => array(
                'title' =>
                /* @trans */ 'Close',
                'desc'  =>
                /* @trans */ 'Ability to close tickets'),
            self::PERM_DELETE => array(
                'title' =>
                /* @trans */ 'Delete',
                'desc'  =>
                /* @trans */ 'Ability to delete tickets'),
            );

    // Ticket Sources
    static protected $sources =  array(
            'Phone' =>
            /* @trans */ 'Phone',
            'Email' =>
            /* @trans */ 'Email',

            'Web' =>
            /* @trans */ 'Web',
            'API' =>
            /* @trans */ 'API',
            'Other' =>
            /* @trans */ 'Other',
            );

    var $lastMsgId;
    var $last_message;

    var $owner;     // TicketOwner
    var $_user;      // EndUser
    var $_answers;
    var $collaborators;
    var $active_collaborators;
    var $recipients;
    var $lastrespondent;
    var $lastuserrespondent;
    var $_children;

    function loadDynamicData($force=false) {
        if (!isset($this->_answers) || $force) {
            $this->_answers = array();
            foreach (DynamicFormEntryAnswer::objects()
                ->filter(array(
                    'entry__object_id' => $this->getId(),
                    'entry__object_type' => 'T'
                )) as $answer
            ) {
                $tag = mb_strtolower($answer->field->name)
                    ?: 'field.' . $answer->field->id;
                    $this->_answers[$tag] = $answer;
            }
        }
        return $this->_answers;
    }

    function getAnswer($field, $form=null) {
        // TODO: Prefer CDATA ORM relationship if already loaded
        $this->loadDynamicData();
        return $this->_answers[$field];
    }

    function getId() {
        return $this->ticket_id;
    }

    function getPid() {
        return $this->ticket_pid;
    }

    function getChildren() {
        if (!isset($this->_children) && $this->isParent())
            $this->_children = self::getChildTickets($this->getId());

        return $this->_children ?: array();
    }

    function getMergeTypeByFlag($flag) {
        if (($flag & self::FLAG_COMBINE_THREADS) != 0)
            return 'combine';
        if (($flag & self::FLAG_SEPARATE_THREADS) != 0)
            return 'separate';
        else
            return 'visual';
        return 'visual';
    }

    function getMergeType() {
        if ($this->hasFlag(self::FLAG_COMBINE_THREADS))
            return 'combine';
        if ($this->hasFlag(self::FLAG_SEPARATE_THREADS))
            return 'separate';
        else
            return 'visual';
        return 'visual';
    }

    function isMerged() {
        if (!is_null($this->getPid()) || $this->isParent())
            return true;

        return false;
    }

    function isParent($flag=false) {
        if (is_numeric($flag) && ($flag & self::FLAG_PARENT) != 0)
            return true;
        elseif (!is_numeric($flag) && $this->hasFlag(self::FLAG_PARENT))
            return true;

        return false;
    }

    function hasFlag($flag) {
        return ($this->get('flags', 0) & $flag) != 0;
    }

    function isChild($pid=false) {
        return ($this->getPid() ? true : false);
    }

    function hasState($state) {
        return  strcasecmp($this->getState(), $state) == 0;
    }

    function isOpen() {
        return $this->hasState('open');
    }

    function isReopened() {
        return null !== $this->getReopenDate();
    }

    function isReopenable() {
        return ($this->getStatus()->isReopenable() && $this->getDept()->allowsReopen()
        && ($this->getTopic() ? $this->getTopic()->allowsReopen() : true));
    }

    function isClosed() {
         return $this->hasState('closed');
    }

    function isCloseable() {
        global $cfg;

        if ($this->isClosed())
            return true;

        $warning = null;
        if (self::getMissingRequiredFields($this)) {
            $warning = sprintf(
                    __( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'),
                    __('This ticket'),
                    '', '');
        } elseif (($num=$this->getNumOpenTasks())) {
            $warning = sprintf(__('%1$s has %2$d open tasks and cannot be closed'),
                    __('This ticket'), $num);
        } elseif ($cfg->requireTopicToClose() && !$this->getTopicId()) {
            $warning = sprintf(
                    __( '%1$s is missing a %2$s and cannot be closed'),
                    __('This ticket'), __('Help Topic'), '');
        }

        return $warning ?: true;
    }

    function isArchived() {
         return $this->hasState('archived');
    }

    function isDeleted() {
         return $this->hasState('deleted');
    }

    function isAssigned($to=null) {
        if (!$this->isOpen())
            return false;

        if (is_null($to))
            return ($this->getStaffId() || $this->getTeamId());

        switch (true) {
        case $to instanceof Staff:
            return ($to->getId() == $this->getStaffId() ||
                    $to->isTeamMember($this->getTeamId()));
            break;
        case $to instanceof Team:
            return ($to->getId() == $this->getTeamId());
            break;
        }

        return false;
    }

    function isOverdue() {
        return $this->ht['isoverdue'];
    }

    function isAnswered() {
       return $this->ht['isanswered'];
    }

    function isLocked() {
        return null !== $this->getLock();
    }

    function getRole($staff) {
        if (!$staff instanceof Staff)
            return null;

        return $staff->getRole($this->getDept(), $this->isAssigned($staff));
    }

    function checkStaffPerm($staff, $perm=null) {

        // Must be a valid staff
        if ((!$staff instanceof Staff) && !($staff=Staff::lookup($staff)))
            return false;

        // check department access first
        if (!$staff->canAccessDept($this->getDept())
                // check assignment
                && !$this->isAssigned($staff)
                // check referral
                && !$this->getThread()->isReferred($staff))
            return false;

        // At this point staff has view access unless a specific permission is
        // requested
        if ($perm === null)
            return true;

        // Permission check requested -- get role if any
        if (!($role=$this->getRole($staff)))
            return false;

        // Check permission based on the effective role
        return $role->hasPerm($perm);
    }

    function checkUserAccess($user) {
        if (!$user || !($user instanceof EndUser))
            return false;

        // Ticket Owner
        if ($user->getId() == $this->getUserId())
            return true;

        // Organization
        if ($user->canSeeOrgTickets()
            && ($U = $this->getUser())
            && ($U->getOrgId() == $user->getOrgId())
        ) {
            // The owner of this ticket is in the same organization as the
            // user in question, and the organization is configured to allow
            // the user in question to see other tickets in the
            // organization.
            return true;
        }

        // Collaborator?
        // 1) If the user was authorized via this ticket.
        if ($user->getTicketId() == $this->getId()
            && !strcasecmp($user->getUserType(), 'collaborator')
        ) {
            return true;
        }
        // 2) Query the database to check for expanded access...
        if (Collaborator::lookup(array(
            'user_id' => $user->getId(),
            'thread_id' => $this->getThreadId()))
        ) {
            return true;
        }
        // 3) If the ticket is a child of a merge
        if ($this->isParent() && $this->getMergeType() != 'visual') {
            $children = Ticket::objects()
                    ->filter(array('ticket_pid'=>$this->getId()))
                    ->order_by('sort');

            foreach ($children as $child)
                if ($child->checkUserAccess($user))
                    return true;
        }

        return false;
    }

    // Getters
    function getNumber() {
        return $this->number;
    }

    function getOwnerId() {
        return $this->user_id;
    }

    function getOwner() {
        if (!isset($this->owner)) {
            $this->owner = new TicketOwner(new EndUser($this->user), $this);
        }
        return $this->owner;
    }

    function getEmail() {
        if ($o = $this->getOwner()) {
            return $o->getEmail();
        }
        return null;
    }

    function getReplyToEmail() {
        //TODO: Determine the email to use (once we enable multi-email support)
        return $this->getEmail();
    }

    // Deprecated
    function getOldAuthToken() {
        # XXX: Support variable email address (for CCs)
        return md5($this->getId() . strtolower($this->getEmail()) . SECRET_SALT);
    }

    function getName(){
        if ($o = $this->getOwner()) {
            return $o->getName();
        }
        return null;
    }

    function getSubject() {
        return (string) $this->getAnswer('subject');
    }

    /* Help topic title  - NOT object -> $topic */
    function getHelpTopic() {
        if ($this->topic)
            return $this->topic->getFullName();
    }

    function getCreateDate() {
        return $this->created;
    }

    function getOpenDate() {
        return $this->getCreateDate();
    }

    function getReopenDate() {
        return $this->reopened;
    }

    function getUpdateDate() {
        return $this->updated;
    }

    function getEffectiveDate() {
        return $this->lastupdate;
    }

    function getDueDate() {
        return $this->duedate;
    }

    function getSLADueDate($recompute=false) {
        global $cfg;

        if (!$recompute && $this->est_duedate)
            return $this->est_duedate;

        if (($sla = $this->getSLA()) && $sla->isActive()) {
            $schedule = $this->getDept()->getSchedule();
            $tz = new DateTimeZone($cfg->getDbTimezone());
            $dt = new DateTime($this->getReopenDate() ?:
                    $this->getCreateDate(), $tz);
            $dt = $sla->addGracePeriod($dt, $schedule);
            // Make sure time is in DB timezone
            $dt->setTimezone($tz);
            return $dt->format('Y-m-d H:i:s');
        }
    }

    function updateEstDueDate($clearOverdue=true) {
        if ($this->isOverdue() && $clearOverdue)
            $this->clearOverdue(false);

        $this->est_duedate = $this->getSLADueDate(true) ?: null;

        return $this->save();
    }

    function getEstDueDate() {
        // Real due date or  sla due date (If ANY)
        return $this->getDueDate() ?: $this->getSLADueDate();
    }


    function getCloseDate() {
        return $this->closed;
    }

    function getStatusId() {
        return $this->status_id;
    }

    /**
     * setStatusId
     *
     * Forceably set the ticket status ID to the received status ID. No
     * checks are made. Use ::setStatus() to change the ticket status
     */
    // XXX: Use ::setStatus to change the status. This can be used as a
    //      fallback if the logic in ::setStatus fails.
    function setStatusId($id) {
        $this->status_id = $id;
        return $this->save();
    }

    function getStatus() {
        return $this->status;
    }

    function getState() {
        if (!$this->getStatus()) {
            return '';
        }
        return $this->getStatus()->getState();
    }

    function getDeptId() {
       return $this->dept_id;
    }

    function getDeptName() {
        if ($this->dept instanceof Dept)
            return $this->dept->getFullName();
    }

    function getPriorityId() {
        global $cfg;

        if (($priority = $this->getPriority()))
            return $priority->getId();

        return $cfg->getDefaultPriorityId();
    }

    function getPriority() {
        if (($a = $this->getAnswer('priority')))
            return $a->getValue();

        return null;
    }

    function getPriorityField() {
        if (($a = $this->getAnswer('priority')))
            return $a->getField();

        return TicketForm::getInstance()->getField('priority');
    }

    function getPhoneNumber() {
        return (string)$this->getOwner()->getPhoneNumber();
    }

    function getSource() {
        $sources = $this->getSources();
        return $sources[$this->source] ?: $this->source;
    }

    function getIP() {
        return $this->ip_address;
    }

    function getHashtable() {
        return $this->ht;
    }

    function getUpdateInfo() {
        global $cfg;

        return array(
            'source'    => $this->source,
            'topicId'   => $this->getTopicId(),
            'slaId'     => $this->getSLAId(),
            'user_id'   => $this->getOwnerId(),
            'duedate'   => Misc::db2gmtime($this->getDueDate()),
        );
    }

    function getLock() {
        $lock = $this->lock;
        if ($lock && !$lock->isExpired())
            return $lock;
    }

    function acquireLock($staffId, $lockTime=null) {
        global $cfg;

        if (!isset($lockTime))
            $lockTime = $cfg->getLockTime();

        if (!$staffId or !$lockTime) //Lockig disabled?
            return null;

        // Check if the ticket is already locked.
        if (($lock = $this->getLock()) && !$lock->isExpired()) {
            if ($lock->getStaffId() != $staffId) //someone else locked the ticket.
                return null;

            //Lock already exits...renew it
            $lock->renew($lockTime); //New clock baby.

            return $lock;
        }
        // No lock on the ticket or it is expired
        $this->lock = Lock::acquire($staffId, $lockTime); //Create a new lock..

        if ($this->lock) {
            $this->save();
        }

        // load and return the newly created lock if any!
        return $this->lock;
    }

    function releaseLock($staffId=false) {
        if (!($lock = $this->getLock()))
            return false;

        if ($staffId && $lock->staff_id != $staffId)
            return false;

        if (!$lock->delete())
            return false;

        $this->lock = null;
        return $this->save();
    }

    function getDept() {
        global $cfg;

        return $this->dept ?: $cfg->getDefaultDept();
    }

    function getUserId() {
        return $this->getOwnerId();
    }

    function getUser() {
        if (!isset($this->_user) && $this->user) {
            $this->_user = new EndUser($this->user);
        }
        return $this->_user;
    }

    function getStaffId() {
        return $this->staff_id;
    }

    function getStaff() {
        return $this->staff;
    }

    function getTeamId() {
        return $this->team_id;
    }

    function getTeam() {
        return $this->team;
    }

    function getAssigneeId() {

        if (!($assignee=$this->getAssignee()))
            return null;

        $id = '';
        if ($assignee instanceof Staff)
            $id = 's'.$assignee->getId();
        elseif ($assignee instanceof Team)
            $id = 't'.$assignee->getId();

        return $id;
    }

    function getAssignee() {

        if (!$this->isOpen() || !$this->isAssigned())
            return false;

        if ($this->staff)
            return $this->staff;

        if ($this->team)
            return $this->team;

        return null;
    }

    function getAssignees() {

        $assignees = array();
        if ($staff = $this->getStaff())
            $assignees[] = $staff->getName();

        if ($team = $this->getTeam())
            $assignees[] = $team->getName();

        return $assignees;
    }

    function getAssigned($glue='/') {
        $assignees = $this->getAssignees();
        return $assignees ? implode($glue, $assignees) : '';
    }

    function getTopicId() {
        return $this->topic_id;
    }

    function getTopic() {
        return $this->topic;
    }


    function getSLAId() {
        return $this->sla_id;
    }

    function getSLA() {
        return $this->sla;
    }

    function getLastRespondent() {
        if (!isset($this->lastrespondent)) {
            if (!$this->getThread() || !$this->getThread()->entries)
                return $this->lastrespondent = false;
            $this->lastrespondent = Staff::objects()
                ->filter(array(
                'staff_id' => $this->getThread()->entries
                    ->filter(array(
                        'type' => 'R',
                        'staff_id__gt' => 0,
                    ))
                    ->values_flat('staff_id')
                    ->order_by('-id')
                    ->limit('1,1')
                ))
                ->first()
                ?: false;
        }
        return $this->lastrespondent;
    }

    function getLastUserRespondent() {
        if (!isset($this->$lastuserrespondent)) {
            if (!$this->getThread() || !$this->getThread()->entries)
                return $this->$lastuserrespondent = false;
            $this->$lastuserrespondent = User::objects()
                ->filter(array(
                'id' => $this->getThread()->entries
                    ->filter(array(
                        'user_id__gt' => 0,
                    ))
                    ->values_flat('user_id')
                    ->order_by('-id')
                    ->limit(1)
                ))
                ->first()
                ?: false;
        }
        return $this->$lastuserrespondent;
    }

    function getLastMessageDate() {
        return $this->getThread()->lastmessage;
    }

    function getLastMsgDate() {
        return $this->getLastMessageDate();
    }

    function getLastResponseDate() {
        return $this->getThread()->lastresponse;
    }

    function getLastRespDate() {
        return $this->getLastResponseDate();
    }

    function getLastMsgId() {
        return $this->lastMsgId;
    }

    function getLastMessage() {
        if (!isset($this->last_message)) {
            if ($this->getLastMsgId())
                $this->last_message = MessageThreadEntry::lookup(
                    $this->getLastMsgId(), $this->getThreadId());

            if (!$this->last_message)
                $this->last_message = $this->getThread() ? $this->getThread()->getLastMessage() : '';
        }
        return $this->last_message;
    }

    function getNumTasks() {
        // FIXME: Implement this after merging Tasks
        return count($this->tasks);
    }

    function getNumOpenTasks() {
        return count($this->tasks->filter(array(
                        'flags__hasbit' => TaskModel::ISOPEN)));
    }


    function getThreadId() {
        if ($this->getThread())
            return $this->getThread()->getId();
    }

    function getThread() {
        if (is_null($this->thread) && $this->child_thread)
            return $this->child_thread;

        return $this->thread;
    }

    function getThreadCount() {
        return $this->getClientThread()->count();
    }

    function getNumMessages() {
        return $this->getThread()->getNumMessages();
    }

    function getNumResponses() {
        return $this->getThread()->getNumResponses();
    }

    function getNumNotes() {
        return $this->getThread()->getNumNotes();
    }

    function getMessages() {
        return $this->getThreadEntries(array('M'));
    }

    function getResponses() {
        return $this->getThreadEntries(array('R'));
    }

    function getNotes() {
        return $this->getThreadEntries(array('N'));
    }

    function getClientThread() {
        return $this->getThreadEntries(array('M', 'R'));
    }

    function getThreadEntry($id) {
        return $this->getThread()->getEntry($id);
    }

    function getThreadEntries($type=false) {
        if ($this->getThread()) {
            $entries = $this->getThread()->getEntries();
            if ($type && is_array($type))
                $entries->filter(array('type__in' => $type));
        }

        return $entries;
    }

    // MailingList of participants  (owner + collaborators)
    function getRecipients($who='all', $whitelist=array(), $active=true) {
        $list = new MailingList();
        switch (strtolower($who)) {
            case 'user':
                $list->addTo($this->getOwner());
                break;
            case 'all':
                $list->addTo($this->getOwner());
                // Fall-trough
            case 'collabs':
                if (($collabs = $active ?  $this->getActiveCollaborators() :
                    $this->getCollaborators())) {
                    foreach ($collabs as $c)
                        if (!$whitelist || in_array($c->getUserId(),
                                    $whitelist))
                            $list->addCc($c);
                }
                break;
            default:
                return null;
        }
        return $list;
    }

    function getCollaborators() {
        return $this->getThread() ? $this->getThread()->getCollaborators() : '';
    }

    function getNumCollaborators() {
        return $this->getThread() ? $this->getThread()->getNumCollaborators() : '';
    }

    function getActiveCollaborators() {
        return $this->getThread() ? $this->getThread()->getActiveCollaborators() : '';
    }

    function getNumActiveCollaborators() {
        return $this->getThread() ? $this->getThread()->getNumActiveCollaborators() : '';
    }

    function getAssignmentForm($source=null, $options=array()) {
        $prompt = $assignee = '';
        // Possible assignees
        $assignees = null;
        $dept = $this->getDept();
        switch (strtolower($options['target'])) {
            case 'agents':
                $assignees = array();
                foreach ($dept->getAssignees() as $member)
                    $assignees['s'.$member->getId()] = $member;

                if (!$source && $this->isOpen() && $this->staff)
                    $assignee = sprintf('s%d', $this->staff->getId());
                $prompt = __('Select an Agent');
                break;
            case 'teams':
                $assignees = array();
                if (($teams = Team::getActiveTeams()))
                    foreach ($teams as $id => $name)
                        $assignees['t'.$id] = $name;

                if (!$source && $this->isOpen() && $this->team)
                    $assignee = sprintf('t%d', $this->team->getId());
                $prompt = __('Select a Team');
                break;
        }

        // Default to current assignee if source is not set
        if (!$source)
            $source = array('assignee' => array($assignee));

        $form = AssignmentForm::instantiate($source, $options);

        if (isset($assignees))
            $form->setAssignees($assignees);

        if (($refer = $form->getField('refer'))) {
            if ($assignee) {
                $visibility = new VisibilityConstraint(
                        new Q(array()), VisibilityConstraint::HIDDEN);
                $refer->set('visibility', $visibility);
            } else {
                $refer->configure('desc', sprintf(__('Maintain referral access to %s'),
                        $this->getAssigned()));
            }
        }

        // Field configurations
        if ($f=$form->getField('assignee')) {
            if ($prompt)
                $f->configure('prompt', $prompt);
            $f->configure('dept', $dept);
        }

        return $form;
    }

    function getReferralForm($source=null, $options=array()) {

        $form = ReferralForm::instantiate($source, $options);
        $dept = $this->getDept();
        // Agents
        $staff = Staff::objects()->filter(array(
         'isactive' => 1,
         ))
         ->filter(Q::not(array('dept_id' => $dept->getId())));
        $staff = Staff::nsort($staff);
        $agents = array();
        foreach ($staff as $s)
          $agents[$s->getId()] = $s;
        $form->setChoices('agent', $agents);
        // Teams
        $form->setChoices('team', Team::getActiveTeams());
        // Depts
        $form->setChoices('dept', Dept::getDepartments());

        return $form;
    }

    function getClaimForm($source=null, $options=array()) {
        global $thisstaff;

        $id = sprintf('s%d', $thisstaff->getId());
        if(!$source)
            $source = array('assignee' => array($id));

        $form = ClaimForm::instantiate($source, $options);
        $form->setAssignees(array($id => $thisstaff->getName()));

        return $form;

    }

    function getTransferForm($source=null) {

        if (!$source)
            $source = array('dept' => array($this->getDeptId()),
                    'refer' => false);

        return TransferForm::instantiate($source);
    }

    function getField($fid) {

        if (is_numeric($fid))
            return $this->getDynamicFieldById($fid);

        // Special fields
        switch ($fid) {
        case 'priority':
            return $this->getPriorityField();
            break;
        case 'sla':
            return SLAField::init(array(
                        'id' => $fid,
                        'name' => "{$fid}_id",
                        'label' => __('SLA Plan'),
                        'default' => $this->getSLAId(),
                        'choices' => SLA::getSLAs()
                        ));
            break;
        case 'topic':
            $current = array();
            if ($topic = $this->getTopic())
                $current = array($topic->getId());
            $choices = Topic::getHelpTopics(false, $topic ? (Topic::DISPLAY_DISABLED) : false, true, $current);
            return TopicField::init(array(
                        'id' => $fid,
                        'name' => "{$fid}_id",
                        'label' => __('Help Topic'),
                        'default' => $this->getTopicId(),
                        'choices' => $choices
                        ));
            break;
        case 'source':
            return ChoiceField::init(array(
                        'id' => $fid,
                        'name' => 'source',
                        'label' => __('Ticket Source'),
                        'default' => $this->source,
                        'choices' => Ticket::getSources()
                        ));
            break;
        case 'duedate':

            $hint = sprintf(__('Setting a %s will override %s'),
                    __('Due Date'), __('SLA Plan'));
            return DateTimeField::init(array(
                'id' => $fid,
                'name' => $fid,
                'default' => Misc::db2gmtime($this->getDueDate()),
                'label' => __('Due Date'),
                'hint' => $hint,
                'configuration' => array(
                    'min' => Misc::gmtime(),
                    'time' => true,
                    'gmt' => false,
                    'future' => true,
                    )
                ));
        }
    }

    function getDynamicFieldById($fid) {
        foreach (DynamicFormEntry::forTicket($this->getId()) as $form) {
            foreach ($form->getFields() as $field)
                if ($field->getId() == $fid) {
                    // This is to prevent SimpleForm using index name as
                    // field name when one is not set.
                    if (!$field->get('name'))
                        $field->set('name', "field_$fid");

                    return $field;
                }
        }
    }

    function getDynamicFields($criteria=array()) {

        $fields = DynamicFormField::objects()->filter(array(
                    'id__in' => $this->entries
                    ->filter($criteria)
                ->values_flat('answers__field_id')));

        return ($fields && count($fields)) ? $fields : array();
    }

    function hasClientEditableFields() {
        $forms = DynamicFormEntry::forTicket($this->getId());
        foreach ($forms as $form) {
            foreach ($form->getFields() as $field) {
                if ($field->isEditableToUsers())
                    return true;
            }
        }
    }

    //if ids passed, function returns only the ids of fields disabled by help topic
    static function getMissingRequiredFields($ticket, $ids=false) {
        // Check for fields disabled by Help Topic
        $disabled = array();
        foreach (($ticket->getTopic() ? $ticket->getTopic()->forms : $ticket->entries) as $f) {
            $extra = JsonDataParser::decode($f->extra);

            if (!empty($extra['disable']))
                $disabled[] = $extra['disable'];
        }

        $disabled = !empty($disabled) ? call_user_func_array('array_merge', $disabled) : NULL;

        if ($ids)
          return $disabled;

        $criteria = array(
                    'answers__field__flags__hasbit' => DynamicFormField::FLAG_ENABLED,
                    'answers__field__flags__hasbit' => DynamicFormField::FLAG_CLOSE_REQUIRED,
                    'answers__value__isnull' => true,
                    );

        // If there are disabled fields then exclude them
        if ($disabled)
            array_push($criteria, Q::not(array('answers__field__id__in' => $disabled)));

        return $ticket->getDynamicFields($criteria);
    }

    function getMissingRequiredField() {
        $fields = self::getMissingRequiredFields($this);
        return $fields ? $fields[0] : null;
    }

    function addCollaborator($user, $vars, &$errors, $event=true) {

        if ($user && $user->getId() == $this->getOwnerId())
            $errors['err'] = __('Ticket Owner cannot be a Collaborator');

        if ($user && !$errors
                && ($c = $this->getThread()->addCollaborator($user, $vars,
                        $errors, $event))) {
            $c->setCc($c->active);
            $this->collaborators = null;
            $this->recipients = null;
            return $c;
        }

        return null;
    }

    function addCollaborators($users, $vars, &$errors, $event=true) {

        if (!$users || !is_array($users))
            return null;

        $collabs = $this->getCollaborators();
        $new = array();
        foreach ($users as $user) {
            if (!($user instanceof User)
                    && !($user = User::lookup($user)))
                continue;
            if ($collabs->findFirst(array('user_id' => $user->getId())))
                continue;
            if ($user->getId() == $this->getOwnerId())
                continue;

            if ($c=$this->addCollaborator($user, $vars, $errors, $event))
                $new[] = $c;
        }
        return $new;
    }

    //XXX: Ugly for now
    function updateCollaborators($vars, &$errors) {
        global $thisstaff;

        if (!$thisstaff) return;

        //Deletes
        if($vars['del'] && ($ids=array_filter($vars['del']))) {
            $collabs = array();
            foreach ($ids as $k => $cid) {
                if (($c=Collaborator::lookup($cid))
                        && $c->getTicketId() == $this->getId()
                        && $c->delete())
                     $collabs[] = (string) $c;
            }
            $this->logEvent('collab', array('del' => $collabs));
        }

        //statuses
        $cids = null;
        if($vars['cid'] && ($cids=array_filter($vars['cid']))) {
            $this->getThread()->collaborators->filter(array(
                'thread_id' => $this->getThreadId(),
                'id__in' => $cids
            ))->update(array(
                'updated' => SqlFunction::NOW(),
            ));
        }

        if ($cids) {
            $this->getThread()->collaborators->filter(array(
                'thread_id' => $this->getThreadId(),
                Q::not(array('id__in' => $cids))
            ))->update(array(
                'updated' => SqlFunction::NOW(),
            ));
        }

        unset($this->active_collaborators);
        $this->collaborators = null;

        return true;
    }

    function getAuthToken($user, $algo=1) {

        //Format: // <user type><algo id used>x<pack of uid & tid><hash of the algo>
        $authtoken = sprintf('%s%dx%s',
                ($user->getId() == $this->getOwnerId() ? 'o' : 'c'),
                $algo,
                Base32::encode(pack('VV',$user->getId(), $this->getId())));

        switch($algo) {
            case 1:
                $authtoken .= substr(base64_encode(
                            md5($user->getId().$this->getCreateDate().$this->getId().SECRET_SALT, true)), 8);
                break;
            default:
                return null;
        }

        return $authtoken;
    }

    function sendAccessLink($user) {
        global $ost;

        if (!($email = $ost->getConfig()->getDefaultEmail())
            || !($content = Page::lookupByType('access-link')))
            return;

        $vars = array(
            'url' => $ost->getConfig()->getBaseUrl(),
            'ticket' => $this,
            'user' => $user,
            'recipient' => $user,
            // Get ticket link, with authcode, directly to bypass collabs
            // check
            'recipient.ticket_link' => $user->getTicketLink(),
        );

        $lang = $user->getLanguage(UserAccount::LANG_MAILOUTS);
        $msg = $ost->replaceTemplateVariables(array(
            'subj' => $content->getLocalName($lang),
            'body' => $content->getLocalBody($lang),
        ), $vars);

        $email->send($user, Format::striptags($msg['subj']),
            $msg['body']);
    }


    /* -------------------- Setters --------------------- */
    public function setFlag($flag, $val) {

        if ($val)
            $this->flags |= $flag;
        else
            $this->flags &= ~$flag;
    }

    function setMergeType($combine=false, $parent=false) {
        //for $combine, 0 = separate, 1 = combine, 2 = link, 3 = regular ticket
        $flags = array(Ticket::FLAG_SEPARATE_THREADS, Ticket::FLAG_COMBINE_THREADS, Ticket::FLAG_LINKED);
        foreach ($flags as $key => $flag) {
            if ($combine == $key)
                $this->setFlag($flag, true);
            else
                $this->setFlag($flag, false);
        }
        if ($parent)
            $this->setFlag(Ticket::FLAG_PARENT, true);
        else
            $this->setFlag(Ticket::FLAG_PARENT, false);

        $this->save();
    }

    function setPid($pid) {
        return $this->ticket_pid = $this->getId() != $pid ? $pid : NULL;
    }

    function setSort($sort) {
        return $this->sort=$sort;
    }

    function setLastMsgId($msgid) {
        return $this->lastMsgId=$msgid;
    }
    function setLastMessage($message) {
        $this->last_message = $message;
        $this->setLastMsgId($message->getId());
    }

    //DeptId can NOT be 0. No orphans please!
    function setDeptId($deptId) {
        // Make sure it's a valid department
        if ($deptId == $this->getDeptId() || !($dept=Dept::lookup($deptId))) {
            return false;
        }
        $this->dept = $dept;
        return $this->save();
    }

    // Set staff ID...assign/unassign/release (id can be 0)
    function setStaffId($staffId) {
        if (!is_numeric($staffId))
            return false;

        $this->staff = Staff::lookup($staffId);
        return $this->save();
    }

    function setSLAId($slaId) {
        if ($slaId == $this->getSLAId())
            return true;

        $sla = null;
        if ($slaId && !($sla = Sla::lookup($slaId)))
            return false;

        $this->sla = $sla;
        return $this->save();
    }
    /**
     * Selects the appropriate service-level-agreement plan for this ticket.
     * When tickets are transfered between departments, the SLA of the new
     * department should be applied to the ticket. This would be useful,
     * for instance, if the ticket is transferred to a different department
     * which has a shorter grace period, the ticket should be considered
     * overdue in the shorter window now that it is owned by the new
     * department.
     *
     * $trump - if received, should trump any other possible SLA source.
     *          This is used in the case of email filters, where the SLA
     *          specified in the filter should trump any other SLA to be
     *          considered.
     */
    function selectSLAId($trump=null) {
        global $cfg;
        # XXX Should the SLA be overridden if it was originally set via an
        #     email filter? This method doesn't consider such a case
        if ($trump && is_numeric($trump)) {
            $slaId = $trump;
        } elseif ($this->getDept() && $this->getDept()->getSLAId()) {
            $slaId = $this->getDept()->getSLAId();
        } elseif ($this->getTopic() && $this->getTopic()->getSLAId()) {
            $slaId = $this->getTopic()->getSLAId();
        } else {
            $slaId = $cfg->getDefaultSLAId();
        }

        return ($slaId && $this->setSLAId($slaId)) ? $slaId : false;
    }

    //Set team ID...assign/unassign/release (id can be 0)
    function setTeamId($teamId) {
        if (!is_numeric($teamId))
            return false;

        $this->team = Team::lookup($teamId);
        return $this->save();
    }

    // Ticket Status helper.
    function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true, $force_close=false) {
        global $cfg, $thisstaff;

        if ($thisstaff && !($role=$this->getRole($thisstaff)))
            return false;

        if ((!$status instanceof TicketStatus)
                && !($status = TicketStatus::lookup($status)))
            return false;

        // Double check permissions (when changing status)
        if ($role && $this->getStatusId()) {
            switch ($status->getState()) {
            case 'closed':
                if (!($role->hasPerm(Ticket::PERM_CLOSE)))
                    return false;
                break;
            case 'deleted':
                // XXX: intercept deleted status and do hard delete TODO: soft deletes
                if ($role->hasPerm(Ticket::PERM_DELETE))
                    return $this->delete($comments);
                // Agent doesn't have permission to delete  tickets
                return false;
                break;
            }
        }

        $hadStatus = $this->getStatusId();
        if ($this->getStatusId() == $status->getId())
            return true;

        // Perform checks on the *new* status, _before_ the status changes
        $ecb = $refer = null;
        switch ($status->getState()) {
            case 'closed':
                // Check if ticket is closeable
                $closeable = $force_close ? true : $this->isCloseable();
                if ($closeable !== true)
                    $errors['err'] = $closeable ?: sprintf(__('%s cannot be closed'), __('This ticket'));

                if ($errors)
                    return false;

                $refer = $this->staff ?: $thisstaff;
                $this->closed = $this->lastupdate = SqlFunction::NOW();
                if ($thisstaff && $set_closing_agent)
                    $this->staff = $thisstaff;
                // Clear overdue flags & due dates
                $this->clearOverdue(false);

                $ecb = function($t) use ($status) {
                    $t->logEvent('closed', array('status' => array($status->getId(), $status->getName())), null, 'closed');
                    $type = array('type' => 'closed');
                    Signal::send('object.edited', $t, $type);
                    $t->deleteDrafts();
                };
                break;
            case 'open':
                if ($this->isClosed() && $this->isReopenable()) {
                    // Auto-assign to closing staff or the last respondent if the
                    // agent is available and has access. Otherwise, put the ticket back
                    // to unassigned pool.
                    $dept = $this->getDept();
                    $staff = $this->getStaff() ?: $this->getLastRespondent();
                    $autoassign = (!$dept->disableReopenAutoAssign());
                    if ($autoassign
                            && $staff
                            // Is agent on vacation ?
                            && $staff->isAvailable()
                            // Does the agent have access to dept?
                            && $staff->canAccessDept($dept))
                        $this->setStaffId($staff->getId());
                    else
                        $this->setStaffId(0); // Clear assignment
                }

                if ($this->isClosed()) {
                    $this->closed = null;
                    $this->lastupdate = $this->reopened = SqlFunction::NOW();
                    $ecb = function ($t) {
                        $t->logEvent('reopened', false, null, 'closed');
                        // Set new sla duedate if any
                        $t->updateEstDueDate();
                    };
                }

                // If the ticket is not open then clear answered flag
                if (!$this->isOpen())
                    $this->isanswered = 0;
                break;
            default:
                return false;

        }

        $this->status = $status;
        if (!$this->save(true))
            return false;

        // Refer thread to previously assigned or closing agent
        if ($refer && $cfg->autoReferTicketsOnClose())
            $this->getThread()->refer($refer);

        // Log status change b4 reload — if currently has a status. (On new
        // ticket, the ticket is opened and thereafter the status is set to
        // the requested status).
        if ($hadStatus) {
            $alert = false;
            if ($comments = ThreadEntryBody::clean($comments)) {
                // Send out alerts if comments are included
                $alert = true;
                $this->logNote(__('Status Changed'), $comments, $thisstaff, $alert);
            }
        }
        // Log events via callback
        if ($ecb)
            $ecb($this);
        elseif ($hadStatus)
            // Don't log the initial status change
            $this->logEvent('edited', array('status' => $status->getId()));

        return true;
    }

    function setState($state, $alerts=false) {
        switch (strtolower($state)) {
        case 'open':
            return $this->setStatus('open');
        case 'closed':
            return $this->setStatus('closed');
        case 'answered':
            return $this->setAnsweredState(1);
        case 'unanswered':
            return $this->setAnsweredState(0);
        case 'overdue':
            return $this->markOverdue();
        case 'notdue':
            return $this->clearOverdue();
        case 'unassined':
            return $this->unassign();
        }
        // FIXME: Throw and excception and add test cases
        return false;
    }

    function setAnsweredState($isanswered) {
        $this->isanswered = $isanswered;
        return $this->save();
    }

    function reopen() {
        global $cfg;

        if (!$this->isClosed())
            return false;

        // Set status to open based on current closed status settings
        // If the closed status doesn't have configured "reopen" status then use the
        // the default ticket status.
        if (!($status=$this->getStatus()->getReopenStatus()))
            $status = $cfg->getDefaultTicketStatusId();

        return $status ? $this->setStatus($status) : false;
    }

    function onNewTicket($message, $autorespond=true, $alertstaff=true) {
        global $cfg;

        //Log stuff here...

        if (!$autorespond && !$alertstaff)
            return true; //No alerts to send.

        /* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/

        if(!$cfg
            || !($dept=$this->getDept())
            || !($tpl = $dept->getTemplate())
            || !($email=$dept->getAutoRespEmail())
        ) {
            return false;  //bail out...missing stuff.
        }

        $options = array();
        if (($message instanceof ThreadEntry)
                && $message->getEmailMessageId()) {
            $options += array(
                'inreplyto'=>$message->getEmailMessageId(),
                'references'=>$message->getEmailReferences(),
                'thread'=>$message
            );
        }
        else {
            $options += array(
                'thread' => $this->getThread(),
            );
        }

        //Send auto response - if enabled.
        if ($autorespond
            && $cfg->autoRespONNewTicket()
            && $dept->autoRespONNewTicket()
            && ($msg = $tpl->getAutoRespMsgTemplate())
        ) {
            $msg = $this->replaceVars(
                $msg->asArray(),
                array('message' => $message,
                      'recipient' => $this->getOwner(),
                      'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''
                )
            );
            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'],
                null, $options);
        }

        // Send alert to out sleepy & idle staff.
        if ($alertstaff
            && $cfg->alertONNewTicket()
            && ($email=$dept->getAlertEmail())
            && ($msg=$tpl->getNewTicketAlertMsgTemplate())
        ) {
            $msg = $this->replaceVars($msg->asArray(), array('message' => $message));
            $recipients = $sentlist = array();
            // Exclude the auto responding email just incase it's from staff member.
            if ($message instanceof ThreadEntry && $message->isAutoReply())
                $sentlist[] = $this->getEmail();

            if ($dept->getNumMembersForAlerts()) {
                // Only alerts dept members if the ticket is NOT assigned.
                $manager = $dept->getManager();
                if ($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()
                    && ($members = $dept->getMembersForAlerts())
                ) {
                    foreach ($members as $M)
                        if ($M != $manager)
                            $recipients[] = $M;
                }

                if ($cfg->alertDeptManagerONNewTicket() && $manager) {
                    $recipients[] = $manager;
                }

                // Account manager
                if ($cfg->alertAcctManagerONNewTicket()
                    && ($org = $this->getOwner()->getOrganization())
                    && ($acct_manager = $org->getAccountManager())
                ) {
                    if ($acct_manager instanceof Team)
                        $recipients = array_merge($recipients, $acct_manager->getMembersForAlerts());
                    else
                        $recipients[] = $acct_manager;
                }

                foreach ($recipients as $k=>$staff) {
                    if (!is_object($staff)
                        || !$staff->isAvailable()
                        || in_array($staff->getEmail(), $sentlist)
                    ) {
                        continue;
                    }
                    $alert = $this->replaceVars($msg, array('recipient' => $staff));
                    $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                    $sentlist[] = $staff->getEmail();
                }
            }

            // Alert admin ONLY if not already a staff??
            if ($cfg->alertAdminONNewTicket()
                    && !in_array($cfg->getAdminEmail(), $sentlist)
                    && ($dept->isGroupMembershipEnabled() != Dept::ALERTS_DISABLED)) {
                $options += array('utype'=>'A');
                $alert = $this->replaceVars($msg, array('recipient' => 'Admin'));
                $email->sendAlert($cfg->getAdminEmail(), $alert['subj'],
                        $alert['body'], null, $options);
            }

        }
        return true;
    }

    function onOpenLimit($sendNotice=true) {
        global $ost, $cfg;

        //Log the limit notice as a warning for admin.
        $msg=sprintf(_S('Maximum open tickets (%1$d) reached for %2$s'),
            $cfg->getMaxOpenTickets(), $this->getEmail());
        $ost->logWarning(sprintf(_S('Maximum Open Tickets Limit (%s)'),$this->getEmail()),
            $msg);

        if (!$sendNotice || !$cfg->sendOverLimitNotice())
            return true;

        //Send notice to user.
        if (($dept = $this->getDept())
            && ($tpl=$dept->getTemplate())
            && ($msg=$tpl->getOverlimitMsgTemplate())
            && ($email=$dept->getAutoRespEmail())
        ) {
            $msg = $this->replaceVars(
                $msg->asArray(),
                array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
            );

            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body']);
        }

        $user = $this->getOwner();

        // Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
        $alert=sprintf(__('Maximum open tickets reached for %s.'), $this->getEmail())."\n"
              .sprintf(__('Open tickets: %d'), $user->getNumOpenTickets())."\n"
              .sprintf(__('Max allowed: %d'), $cfg->getMaxOpenTickets())
              ."\n\n".__("Notice sent to the user.");

        $ost->alertAdmin(__('Overlimit Notice'), $alert);

        return true;
    }

    function onResponse($response, $options=array()) {
        $this->isanswered = 1;
        $this->save();

        $vars = array_merge($options,
            array(
                'activity' => _S('New Response'),
                'threadentry' => $response
            )
        );
        $this->onActivity($vars);
    }

    /*
     * Notify collaborators on response or new message
     *
     */
    function notifyCollaborators($entry, $vars = array()) {
        global $cfg;

        if (!$entry instanceof ThreadEntry
            || !($recipients=$this->getRecipients())
            || !($dept=$this->getDept())
            || !($tpl=$dept->getTemplate())
            || !($msg=$tpl->getActivityNoticeMsgTemplate())
            || !($email=$dept->getEmail())
        ) {
            return;
        }

        $poster = User::lookup($entry->user_id);
        $posterEmail = $poster->getEmail()->address;

        $vars = array_merge($vars, array(
            'message' => (string) $entry,
            'poster' => $poster ?: _S('A collaborator'),
            )
        );

        $msg = $this->replaceVars($msg->asArray(), $vars);

        $attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
        $options = array('thread' => $entry);

        if ($vars['from_name'])
            $options += array('from_name' => $vars['from_name']);

        $skip = array();
        if ($entry instanceof MessageThreadEntry) {
          foreach ($entry->getAllEmailRecipients() as $R) {
                $skip[] = $R->mailbox.'@'.$R->host;
            }
        }

        foreach ($recipients as $key => $recipient) {
            $recipient = $recipient->getContact();

            if(get_class($recipient) == 'TicketOwner')
                $owner = $recipient;

            if ((get_class($recipient) == 'Collaborator' ? $recipient->getUserId() : $recipient->getId()) == $entry->user_id)
                unset($recipients[$key]);
         }

        if (!count($recipients))
            return true;

        //see if the ticket user is a recipient
        if ($owner->getEmail()->address != $poster->getEmail()->address && !in_array($owner->getEmail()->address, $skip))
          $owner_recip = $owner->getEmail()->address;

        //say dear collaborator if the ticket user is not a recipient
        if (!$owner_recip) {
            $nameFormats = array_keys(PersonsName::allFormats());
            $names = array();
            foreach ($nameFormats as $key => $value) {
              $names['recipient.name.' . $value] = __('Collaborator');
            }
            $names = array_merge($names, array('recipient' => $recipient));
            $cnotice = $this->replaceVars($msg, $names);
        }
        //otherwise address email to ticket user
        else
            $cnotice = $this->replaceVars($msg, array('recipient' => $owner));

        $email->send($recipients, $cnotice['subj'], $cnotice['body'], $attachments,
            $options);
    }

    function onMessage($message, $autorespond=true, $reopen=true) {
        global $cfg;

        $this->isanswered = 0;
        $this->lastupdate = SqlFunction::NOW();
        $this->save();


        // Reopen if closed AND reopenable
        // We're also checking autorespond flag because we don't want to
        // reopen closed tickets on auto-reply from end user. This is not to
        // confused with autorespond on new message setting
        if ($reopen && $this->isClosed() && $this->isReopenable())
            $this->reopen();

        if (!$autorespond)
            return;

        // Figure out the user
        if ($this->getOwnerId() == $message->getUserId())
            $user = new TicketOwner(
                    User::lookup($message->getUserId()), $this);
        else
            $user = Collaborator::lookup(array(
                    'user_id' => $message->getUserId(),
                    'thread_id' => $this->getThreadId()));

        /**********   double check auto-response  ************/
        if (!$user)
            $autorespond = false;
        elseif ((Email::getIdByEmail($user->getEmail())))
            $autorespond = false;
        elseif (($dept=$this->getDept()))
            $autorespond = $dept->autoRespONNewMessage();

        if (!$autorespond
            || !$cfg->autoRespONNewMessage()
            || !$message
        ) {
            return;  //no autoresp or alerts.
        }

        $dept = $this->getDept();
        $email = $dept->getAutoRespEmail();

        // If enabled...send confirmation to user. ( New Message AutoResponse)
        if ($email
            && ($tpl=$dept->getTemplate())
            && ($msg=$tpl->getNewMessageAutorepMsgTemplate())
        ) {
            $msg = $this->replaceVars($msg->asArray(),
                array(
                    'recipient' => $user,
                    'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''
                )
            );
            $options = array('thread' => $message);
            if ($message->getEmailMessageId()) {
                $options += array(
                        'inreplyto' => $message->getEmailMessageId(),
                        'references' => $message->getEmailReferences()
                        );
            }

            $email->sendAutoReply($user, $msg['subj'], $msg['body'],
                null, $options);
        }
    }

    function onActivity($vars, $alert=true) {
        global $cfg, $thisstaff;

        //TODO: do some shit
        if (!$alert // Check if alert is enabled
            || !$cfg->alertONNewActivity()
            || !($dept=$this->getDept())
            || !$dept->getNumMembersForAlerts()
            || !($email=$cfg->getAlertEmail())
            || !($tpl = $dept->getTemplate())
            || !($msg=$tpl->getNoteAlertMsgTemplate())
        ) {
            return;
        }

        // Alert recipients
        $recipients = array();

        //Last respondent.
        if ($cfg->alertLastRespondentONNewActivity())
            $recipients[] = $this->getLastRespondent();

        // Assigned staff / team
        if ($cfg->alertAssignedONNewActivity()) {
            if (isset($vars['assignee'])
                    && $vars['assignee'] instanceof Staff)
                 $recipients[] = $vars['assignee'];
            elseif ($this->isOpen() && ($assignee = $this->getStaff()))
                $recipients[] = $assignee;

            if ($team = $this->getTeam())
                $recipients = array_merge($recipients, $team->getMembersForAlerts());
        }

        // Dept manager
        if ($cfg->alertDeptManagerONNewActivity() && $dept && $dept->getManagerId())
            $recipients[] = $dept->getManager();

        $options = array();
        $staffId = $thisstaff ? $thisstaff->getId() : 0;
        if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) {
            $options = array('thread' => $vars['threadentry']);

            // Activity details
            if (!$vars['comments'])
                $vars['comments'] = $vars['threadentry'];

            // Staff doing the activity
            $staffId = $vars['threadentry']->getStaffId() ?: $staffId;
        }

        $msg = $this->replaceVars($msg->asArray(),
                array(
                    'note' => $vars['threadentry'], // For compatibility
                    'activity' => $vars['activity'],
                    'comments' => $vars['comments']));

        $isClosed = $this->isClosed();
        $sentlist=array();
        foreach ($recipients as $k=>$staff) {
            if (!is_object($staff)
                // Don't bother vacationing staff.
                || !$staff->isAvailable()
                // No need to alert the poster!
                || $staffId == $staff->getId()
                // No duplicates.
                || isset($sentlist[$staff->getEmail()])
                // Make sure staff has access to ticket
                || ($isClosed && !$this->checkStaffPerm($staff))
            ) {
                continue;
            }
            $alert = $this->replaceVars($msg, array('recipient' => $staff));
            $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
            $sentlist[$staff->getEmail()] = 1;
        }
    }

    function onAssign($assignee, $comments, $alert=true) {
        global $cfg, $thisstaff;

        if ($this->isClosed())
            $this->reopen(); //Assigned tickets must be open - otherwise why assign?

        // Assignee must be an object of type Staff or Team
        if (!$assignee || !is_object($assignee))
            return false;

        $user_comments = (bool) $comments;
        $assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)');

        //Log an internal note - no alerts on the internal note.
        if ($user_comments) {
            if ($assignee instanceof Staff
                    && $thisstaff
                    // self assignment
                    && $assignee->getId() == $thisstaff->getId())
                $title = sprintf(_S('Ticket claimed by %s'),
                    $thisstaff->getName());
            else
                $title = sprintf(_S('Ticket Assigned to %s'),
                        $assignee->getName());

            $note = $this->logNote($title, $comments, $assigner, false);
        }
        $dept = $this->getDept();
        // See if we need to send alerts
        if (!$alert || !$cfg->alertONAssignment() || !$dept->getNumMembersForAlerts())
            return true; //No alerts!

        if (!$dept
            || !($tpl = $dept->getTemplate())
            || !($email = $dept->getAlertEmail())
        ) {
            return true;
        }

        // Recipients
        $recipients = array();
        if ($assignee instanceof Staff) {
            if ($cfg->alertStaffONAssignment())
                $recipients[] = $assignee;
        } elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) {
            if ($cfg->alertTeamMembersONAssignment() && ($members=$assignee->getMembersForAlerts()))
                $recipients = array_merge($recipients, $members);
            elseif ($cfg->alertTeamLeadONAssignment() && ($lead=$assignee->getTeamLead()))
                $recipients[] = $lead;
        }

        // Get the message template
        if ($recipients
            && ($msg=$tpl->getAssignedAlertMsgTemplate())
        ) {
            $msg = $this->replaceVars($msg->asArray(),
                array('comments' => $comments ?: '',
                      'assignee' => $assignee,
                      'assigner' => $assigner
                )
            );
            // Send the alerts.
            $sentlist = array();
            $options = $note instanceof ThreadEntry
                ? array('thread'=>$note)
                : array();
            foreach ($recipients as $k=>$staff) {
                if (!is_object($staff)
                    || !$staff->isAvailable()
                    || in_array($staff->getEmail(), $sentlist)
                ) {
                    continue;
                }
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                $sentlist[] = $staff->getEmail();
            }
        }
        return true;
    }

   function onOverdue($whine=true, $comments="") {
        global $cfg;

        if ($whine && ($sla = $this->getSLA()) && !$sla->alertOnOverdue())
            $whine = false;

        // Check if we need to send alerts.
        if (!$whine
            || !$cfg->alertONOverdueTicket()
            || !($dept = $this->getDept())
            || !$dept->getNumMembersForAlerts()
        ) {
            return true;
        }
        // Get the message template
        if (($tpl = $dept->getTemplate())
            && ($msg=$tpl->getOverdueAlertMsgTemplate())
            && ($email = $dept->getAlertEmail())
        ) {
            $msg = $this->replaceVars($msg->asArray(),
                array('comments' => $comments)
            );
            // Recipients
            $recipients = array();
            // Assigned staff or team... if any
            if ($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) {
                if ($this->getStaffId()) {
                    $recipients[]=$this->getStaff();
                }
                elseif ($this->getTeamId()
                    && ($team = $this->getTeam())
                    && ($members = $team->getMembersForAlerts())
                ) {
                    $recipients=array_merge($recipients, $members);
                }
            }
            elseif ($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
                // Only alerts dept members if the ticket is NOT assigned.
                foreach ($dept->getMembersForAlerts() as $M)
                    $recipients[] = $M;
            }
            // Always alert dept manager??
            if ($cfg->alertDeptManagerONOverdueTicket()
                && $dept && ($manager=$dept->getManager())
            ) {
                $recipients[]= $manager;
            }
            $sentlist = array();
            foreach ($recipients as $k=>$staff) {
                if (!is_object($staff)
                    || !$staff->isAvailable()
                    || in_array($staff->getEmail(), $sentlist)
                ) {
                    continue;
                }
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null);
                $sentlist[] = $staff->getEmail();
            }
        }
        return true;
    }

    // TemplateVariable interface
    function asVar() {
       return $this->getNumber();
    }

    function getVar($tag) {
        global $cfg;

        switch(mb_strtolower($tag)) {
        case 'phone':
        case 'phone_number':
            return $this->getPhoneNumber();
            break;
        case 'auth_token':
            return $this->getOldAuthToken();
            break;
        case 'client_link':
            return sprintf('%s/view.php?t=%s',
                    $cfg->getBaseUrl(), $this->getNumber());
            break;
        case 'staff_link':
            return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId());
            break;
        case 'create_date':
            return new FormattedDate($this->getCreateDate());
            break;
         case 'due_date':
            if ($due = $this->getEstDueDate())
                return new FormattedDate($due);
            break;
        case 'close_date':
            if ($this->isClosed())
                return new FormattedDate($this->getCloseDate());
            break;
        case 'last_update':
            return new FormattedDate($this->lastupdate);
        case 'user':
            return $this->getOwner();
        default:
            if ($a = $this->getAnswer($tag))
                // The answer object is retrieved here which will
                // automatically invoke the toString() method when the
                // answer is coerced into text
                return $a;
        }
    }

    static function getVarScope() {
        $base = array(
            'assigned' => __('Assigned Agent / Team'),
            'close_date' => array(
                'class' => 'FormattedDate', 'desc' => __('Date Closed'),
            ),
            'create_date' => array(
                'class' => 'FormattedDate', 'desc' => __('Date Created'),
            ),
            'dept' => array(
                'class' => 'Dept', 'desc' => __('Department'),
            ),
            'due_date' => array(
                'class' => 'FormattedDate', 'desc' => __('Due Date'),
            ),
            'email' => __('Default email address of ticket owner'),
            'id' => __('Ticket ID (internal ID)'),
            'name' => array(
                'class' => 'PersonsName', 'desc' => __('Name of ticket owner'),
            ),
            'number' => __('Ticket Number'),
            'phone' => __('Phone number of ticket owner'),
            'priority' => array(
                'class' => 'Priority', 'desc' => __('Priority'),
            ),
            'recipients' => array(
                'class' => 'UserList', 'desc' => __('List of all recipient names'),
            ),
            'source' => __('Source'),
            'status' => array(
                'class' => 'TicketStatus', 'desc' => __('Status'),
            ),
            'staff' => array(
                'class' => 'Staff', 'desc' => __('Assigned/closing agent'),
            ),
            'subject' => 'Subject',
            'team' => array(
                'class' => 'Team', 'desc' => __('Assigned/closing team'),
            ),
            'thread' => array(
                'class' => 'TicketThread', 'desc' => __('Ticket Thread'),
            ),
            'topic' => array(
                'class' => 'Topic', 'desc' => __('Help Topic'),
            ),
            // XXX: Isn't lastreponse and lastmessage more useful
            'last_update' => array(
                'class' => 'FormattedDate', 'desc' => __('Time of last update'),
            ),
            'user' => array(
                'class' => 'User', 'desc' => __('Ticket Owner'),
            ),
        );

        $extra = VariableReplacer::compileFormScope(TicketForm::getInstance());
        return $base + $extra;
    }

    // Searchable interface
    static function getSearchableFields() {
        $base = array(
            'number' => new TextboxField(array(
                'label' => __('Ticket Number')
            )),
            'created' => new DatetimeField(array(
                'label' => __('Create Date'),
                'configuration' => array(
                    'fromdb' => true, 'time' => true,
                    'format' => 'y-MM-dd HH:mm:ss'),
            )),
            'duedate' => new DatetimeField(array(
                'label' => __('Due Date'),
                'configuration' => array(
                    'fromdb' => true, 'time' => true,
                    'format' => 'y-MM-dd HH:mm:ss'),
            )),
            'est_duedate' => new DatetimeField(array(
                'label' => __('SLA Due Date'),
                'configuration' => array(
                    'fromdb' => true, 'time' => true,
                    'format' => 'y-MM-dd HH:mm:ss'),
            )),
            'reopened' => new DatetimeField(array(
                'label' => __('Reopen Date'),
                'configuration' => array(
                    'fromdb' => true, 'time' => true,
                    'format' => 'y-MM-dd HH:mm:ss'),
            )),
            'closed' => new DatetimeField(array(
                'label' => __('Close Date'),
                'configuration' => array(
                    'fromdb' => true, 'time' => true,
                    'format' => 'y-MM-dd HH:mm:ss'),
            )),
            'lastupdate' => new DatetimeField(array(
                'label' => __('Last Update'),
                'configuration' => array(
                    'fromdb' => true, 'time' => true,
                    'format' => 'y-MM-dd HH:mm:ss'),
            )),
            'assignee' => new AssigneeChoiceField(array(
                'label' => __('Assignee'),
            )),
            'staff_id' => new AgentSelectionField(array(
                'label' => __('Assigned Staff'),
            )),
            'team_id' => new TeamSelectionField(array(
                'label' => __('Assigned Team'),
            )),
            'dept_id' => new DepartmentChoiceField(array(
                'label' => __('Department'),
            )),
            'topic_id' => new HelpTopicChoiceField(array(
                'label' => __('Help Topic'),
            )),
            'source' => new TicketSourceChoiceField(array(
                'label' => __('Ticket Source'),
            )),
            'isoverdue' => new BooleanField(array(
                'label' => __('Overdue'),
                'descsearchmethods' => array(
                    'set' => '%s',
                    'nset' => 'Not %s'
                    ),
            )),
            'isanswered' => new BooleanField(array(
                'label' => __('Answered'),
                'descsearchmethods' => array(
                    'set' => '%s',
                    'nset' => 'Not %s'
                    ),
            )),
            'isassigned' => new AssignedField(array(
                        'label' => __('Assigned'),
            )),
            'merged' => new MergedField(array(
                'label' => __('Merged'),
            )),
            'linked' => new LinkedField(array(
                'label' => __('Linked'),
            )),
            'thread_count' => new TicketThreadCountField(array(
                        'label' => __('Thread Count'),
            )),
            'attachment_count' => new ThreadAttachmentCountField(array(
                        'label' => __('Attachment Count'),
            )),
            'collaborator_count' => new ThreadCollaboratorCountField(array(
                        'label' => __('Collaborator Count'),
            )),
            'task_count' => new TicketTasksCountField(array(
                        'label' => __('Task Count'),
            )),
            'reopen_count' => new TicketReopenCountField(array(
                        'label' => __('Reopen Count'),
            )),
            'ip_address' => new TextboxField(array(
                'label' => __('IP Address'),
                'configuration' => array('validator' => 'ip'),
            )),
        );
        $tform = TicketForm::getInstance();
        foreach ($tform->getFields() as $F) {
            $fname = $F->get('name') ?: ('field_'.$F->get('id'));
            if (!$F->hasData() || $F->isPresentationOnly() || !$F->isEnabled())
                continue;
            if (!$F->isStorable())
                $base[$fname] = $F;
            else
                $base["cdata__{$fname}"] = $F;
        }
        return $base;
    }

    static function supportsCustomData() {
        return true;
    }

    //Replace base variables.
    function replaceVars($input, $vars = array()) {
        global $ost;

        $vars = array_merge($vars, array('ticket' => $this));
        return $ost->replaceTemplateVariables($input, $vars);
    }

    function markUnAnswered() {
        return (!$this->isAnswered() || $this->setAnsweredState(0));
    }

    function markAnswered() {
        return ($this->isAnswered() || $this->setAnsweredState(1));
    }

    function markOverdue($whine=true) {
        global $cfg;

        // Only open tickets can be marked overdue
        if (!$this->isOpen())
            return false;

        if ($this->isOverdue())
            return true;

        $this->isoverdue = 1;
        if (!$this->save())
            return false;

        $this->logEvent('overdue');
        $this->onOverdue($whine);

        return true;
    }

    function clearOverdue($save=true) {

        //NOTE: Previously logged overdue event is NOT annuled.
        if ($this->isOverdue())
            $this->isoverdue = 0;

        // clear due date if it's in the past
        if ($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime())
            $this->duedate = null;

        // Clear SLA if est. due date is in the past
        if ($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime())
            $this->est_duedate = null;

        return $save ? $this->save() : true;
    }

    function unlinkChild($parent) {
        $this->setPid(NULL);
        $this->setSort(1);
        $this->setFlag(Ticket::FLAG_LINKED, false);
        $this->save();
        $this->logEvent('unlinked', array('ticket' => sprintf('Ticket #%s', $parent->getNumber()), 'id' => $parent->getId()));
        $parent->logEvent('unlinked', array('ticket' => sprintf('Ticket #%s', $this->getNumber()), 'id' => $this->getId()));
    }

    function unlink() {
        $pid = $this->isChild() ? $this->getPid() : $this->getId();
        $parent = $this->isParent() ? $this : (Ticket::lookup($pid));
        $child = $this->isChild() ? $this : '';
        $children = $this->getChildren();

        if ($children) {
            foreach ($children as $child) {
                $child = Ticket::lookup($child[0]);
                $child->unlinkChild($parent);
            }
        } elseif ($child)
            $child->unlinkChild($parent);

        if (count($children) == 0) {
            $parent->setFlag(Ticket::FLAG_LINKED, false);
            $parent->setFlag(Ticket::FLAG_PARENT, false);
            $parent->save();
        }

        return true;
    }

    function manageMerge($tickets) {
        global $thisstaff;

        $permission = ($tickets['title'] && $tickets['title'] == 'link') ? (Ticket::PERM_LINK) : (Ticket::PERM_MERGE);
        $eventName = ($tickets['title'] && $tickets['title'] == 'link') ? 'linked' : 'merged';
        //see if any tickets should be unlinked
        if ($tickets['dtids']) {
            foreach($tickets['dtids'] as $key => $value) {
                if (is_numeric($key) && $ticket = Ticket::lookup($value))
                    $ticket->unlink();
            }
            return true;
        } elseif ($tickets['tids']) { //see if any tickets should be merged
            $ticketObjects = array();
            foreach($tickets['tids'] as $key => $value) {
                if ($ticket = Ticket::lookupByNumber($value)) {
                    $ticketObjects[] = $ticket;
                    if (!$ticket->checkStaffPerm($thisstaff, $permission) && !$ticket->getThread()->isReferred())
                       return false;

                    if ($key == 0)
                        $parent = $ticket;
                    //changing from link to merge
                    if (($ticket->isParent() || $ticket->isChild()) &&
                         $ticket->getMergeType() == 'visual' && $tickets['combine'] != 2 ||
                        ($tickets['combine'] == 2 && !$parent->isParent() && $parent->isChild())) { //changing link parent
                            $ticket->unlink();
                            $changeParent = true;
                    }

                    if ($ticket->getMergeType() == 'visual') {
                        $ticket->setSort($key);
                        $ticket->save();
                    }

                    if ($parent && $parent->getId() != $ticket->getId()) {
                        if (($changeParent) || ($parent->isParent() && $ticket->getMergeType() == 'visual' && !$ticket->isChild()) || //adding to link/merge
                           (!$parent->isParent() && !$ticket->isChild())) { //creating fresh link/merge
                               $parent->logEvent($eventName, array('ticket' => sprintf('Ticket #%s', $ticket->getNumber()),  'id' => $ticket->getId()));
                               $ticket->logEvent($eventName, array('ticket' => sprintf('Ticket #%s', $parent->getNumber()),  'id' => $parent->getId()));
                               if ($ticket->getPid() != $parent->getId())
                                   $ticket->setPid($parent->getId());
                               $parent->setMergeType($tickets['combine'], true);
                               $ticket->setMergeType($tickets['combine']);

                               //referrals for merged tickets
                               if ($parent->getDeptId() != ($ticketDeptId = $ticket->getDeptId()) && $tickets['combine'] != 2) {
                                   $refDept = $ticket->getDept();
                                   $parent->getThread()->refer($refDept);
                                   $evd = array('dept' => $ticketDeptId);
                                   $parent->logEvent('referred', $evd);
                               }
                        }
                    //switch between combine and separate
                    } elseif ($parent->isParent() && $ticket->getMergeType() != 'visual' && $parent->getId() != $ticket->getId()) {
                        $ticket->setMergeType($tickets['combine']);
                    } elseif ($parent->isParent() && $ticket->getMergeType() != 'visual' && $parent->getId() == $ticket->getId())
                        $parent->setMergeType($tickets['combine'], true);
                }
            }
        }
        return $ticketObjects;
    }

    function merge($tickets) {
        $options = $tickets;
        if (!$tickets = self::manageMerge($tickets))
            return false;
        if (is_bool($tickets))
            return true;

        $children = array();
        foreach ($tickets as $ticket) {
            if ($ticket->isParent())
                $parent = $ticket;
            else
                $children[] = $ticket;
        }

        if ($parent && $parent->getMergeType() != 'visual') {
            $errors = array();
            foreach ($children as $child) {
                if ($options['participants'] == 'all' && $collabs = $child->getCollaborators()) {
                    foreach ($collabs as $collab) {
                        $collab = $collab->getUser();
                        if ($collab->getId() != $parent->getOwnerId())
                            $parent->addCollaborator($collab, array(), $errors);
                    }
                }
                $cUser = $child->getUser();
                if ($cUser->getId() != $parent->getOwnerId())
                    $parent->addCollaborator($cUser, array(), $errors);
                $parentThread = $parent->getThread();

                $deletedChild = Thread::objects()
                    ->filter(array('extra__contains'=>'"ticket_id":'.$child->getId()))
                    ->values_flat('id', 'extra')
                    ->first();
                if ($deletedChild) {
                    $extraThread = Thread::lookup($deletedChild[0]);
                    $extraThread->setExtra($parentThread, array('extra' => $deletedChild[1], 'threadId' => $extraThread->getId()));
                }

                if ($child->getThread())
                    $child->getThread()->setExtra($parentThread);

                $child->setMergeType($options['combine']);
                $child->setStatus(intval($options['childStatusId']), false, $errors, true, true); //force close status for children

                if ($options['parentStatusId'])
                    $parent->setStatus(intval($options['parentStatusId']));

                if ($options['delete-child'] || $options['move-tasks']) {
                    if ($tasks = Task::objects()
                        ->filter(array('object_id' => $child->getId()))
                        ->values_flat('id')) {
                        foreach ($tasks as $key => $tid) {
                            $task = Task::lookup($tid[0]);
                            $task->object_id = $parent->getId();
                            $task->save();
                        }
                    }
                }

                if ($options['delete-child'])
                     $child->delete();
            }
            return $parent;
        }
        return false;
    }

    function getRelatedTickets() {
        return sprintf('<tr>
            <td width="8px">&nbsp;</td>
            <td>
                <a class="Icon strtolower(%s) Ticket preview"
                   data-preview="#tickets/%d/preview"
                   href="tickets.php?id=%d">%s</a>
            </td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
            <td>%s</td>
        </tr>',
        strtolower($this->getSource()), $this->getId(), $this->getId(), $this->getNumber(), $this->getSubject(),
            $this->getDeptName(), $this->getAssignee(), Format::datetime($this->getCreateDate()));
    }

    function hasReferral($object, $type) {
        if (($referral=$this->getThread()->getReferral($object->getId(), $type)))
            return $referral;

        return false;
    }

    //Dept Transfer...with alert.. done by staff
    function transfer(TransferForm $form, &$errors, $alert=true) {
        global $thisstaff, $cfg;

        // Check if staff can do the transfer
        if (!$this->checkStaffPerm($thisstaff, Ticket::PERM_TRANSFER))
            return false;

        $cdept = $this->getDept(); // Current department
        $dept = $form->getDept(); // Target department
        if (!$dept || !($dept instanceof Dept))
            $errors['dept'] = __('Department selection required');
        elseif ($dept->getid() == $this->getDeptId())
            $errors['dept'] = sprintf(
                    __('%s already in the department'), __('Ticket'));
        else {
            $this->dept_id = $dept->getId();

            // Make sure the new department allows assignment to the
            // currently assigned agent (if any)
            if ($this->isAssigned()
                && ($staff=$this->getStaff())
                && $dept->assignMembersOnly()
                && !$dept->isMember($staff)
            ) {
                $this->staff_id = 0;
            }
        }

        if ($errors || !$this->save(true))
            return false;

        // Reopen ticket if closed
        if ($this->isClosed())
            $this->reopen();

        // Set SLA of the new department
        if (!$this->getSLAId() || $this->getSLA()->isTransient())
            if (($slaId=$this->getDept()->getSLAId()))
                $this->selectSLAId($slaId);

        // Log transfer event
        $this->logEvent('transferred', array('dept' => $dept->getName()));

        if (($referral=$this->hasReferral($dept,ObjectModel::OBJECT_TYPE_DEPT)))
            $referral->delete();

        // Post internal note if any
        $note = null;
        $comments = $form->getField('comments')->getClean();
        if ($comments) {
            $title = sprintf(__('%1$s transferred from %2$s to %3$s'),
                    __('Ticket'),
                   $cdept->getName(),
                    $dept->getName());

            $_errors = array();
            $note = $this->postNote(
                    array('note' => $comments, 'title' => $title),
                    $_errors, $thisstaff, false);
        }

        if ($form->refer() && $cdept)
            $this->getThread()->refer($cdept);

        //Send out alerts if enabled AND requested
        if (!$alert || !$cfg->alertONTransfer() || !$dept->getNumMembersForAlerts())
            return true; //no alerts!!

         if (($email = $dept->getAlertEmail())
             && ($tpl = $dept->getTemplate())
             && ($msg=$tpl->getTransferAlertMsgTemplate())
         ) {
            $msg = $this->replaceVars($msg->asArray(),
                array('comments' => $note, 'staff' => $thisstaff));
            // Recipients
            $recipients = array();
            // Assigned staff or team... if any
            if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
                if($this->getStaffId())
                    $recipients[] = $this->getStaff();
                elseif ($this->getTeamId()
                    && ($team=$this->getTeam())
                    && ($members=$team->getMembersForAlerts())
                ) {
                    $recipients = array_merge($recipients, $members);
                }
            }
            elseif ($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
                // Only alerts dept members if the ticket is NOT assigned.
                foreach ($dept->getMembersForAlerts() as $M)
                    $recipients[] = $M;
            }

            // Always alert dept manager??
            if ($cfg->alertDeptManagerONTransfer()
                && $dept
                && ($manager=$dept->getManager())
            ) {
                $recipients[] = $manager;
            }
            $sentlist = $options = array();
            if ($note) {
                $options += array('thread'=>$note);
            }
            foreach ($recipients as $k=>$staff) {
                if (!is_object($staff)
                    || !$staff->isAvailable()
                    || in_array($staff->getEmail(), $sentlist)
                ) {
                    continue;
                }
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                $sentlist[] = $staff->getEmail();
            }
         }

         return true;
    }

    function claim(ClaimForm $form, &$errors) {
        global $thisstaff;

        $dept = $this->getDept();
        $assignee = $form->getAssignee();
        if (!($assignee instanceof Staff)
                || !$thisstaff
                || $thisstaff->getId() != $assignee->getId()) {
            $errors['err'] = __('Unknown assignee');
        } elseif (!$assignee->isAvailable()) {
            $errors['err'] = __('Agent is unavailable for assignment');
        } elseif (!$dept->canAssign($assignee)) {
            $errors['err'] = __('Permission denied');
        }

        if ($errors)
            return false;

        return $this->assignToStaff($assignee, $form->getComments(), false);
    }

    function assignToStaff($staff, $note, $alert=true, $user=null) {

        if(!is_object($staff) && !($staff = Staff::lookup($staff)))
            return false;

        if (!$staff->isAvailable() || !$this->setStaffId($staff->getId()))
            return false;

        $this->onAssign($staff, $note, $alert);

        global $thisstaff;
        $data = array();
        if ($thisstaff && $staff->getId() == $thisstaff->getId())
            $data['claim'] = true;
        else
            $data['staff'] = $staff->getId();

        $this->logEvent('assigned', $data, $user);

        $key = $data['claim'] ? 'claim' : 'auto';
        $type = array('type' => 'assigned', $key => true);
        Signal::send('object.edited', $this, $type);

        if (($referral=$this->hasReferral($staff,ObjectModel::OBJECT_TYPE_STAFF)))
            $referral->delete();

        return true;
    }

    function assignToTeam($team, $note, $alert=true, $user=null) {

        if(!is_object($team) && !($team = Team::lookup($team)))
            return false;

        if (!$team->isActive() || !$this->setTeamId($team->getId()))
            return false;

        //Clear - staff if it's a closed ticket
        //  staff_id is overloaded -> assigned to & closed by.
        if ($this->isClosed())
            $this->setStaffId(0);

        $this->onAssign($team, $note, $alert);
        $this->logEvent('assigned', array('team' => $team->getId()), $user);

        if (($referral=$this->hasReferral($team,ObjectModel::OBJECT_TYPE_TEAM)))
            $referral->delete();

        return true;
    }

    function assign(AssignmentForm $form, &$errors, $alert=true) {
        global $thisstaff;

        $evd = array();
        $audit = array();
        $refer = null;
        $dept = $this->getDept();
        $assignee = $form->getAssignee();
        if ($assignee instanceof Staff) {
            if ($this->getStaffId() == $assignee->getId()) {
                $errors['assignee'] = sprintf(__('%s already assigned to %s'),
                        __('Ticket'),
                        __('the agent')
                        );
            } elseif (!$assignee->isAvailable()) {
                $errors['assignee'] = __('Agent is unavailable for assignment');
            } elseif (!$dept->canAssign($assignee)) {
                $errors['err'] = __('Permission denied');
            } else {
                $refer = $this->staff ?: null;
                $this->staff_id = $assignee->getId();
                if ($thisstaff && $thisstaff->getId() == $assignee->getId()) {
                    $alert = false;
                    $evd['claim'] = true;
                    $audit = array('staff' => $assignee->getName()->name,'claim' => true);
                } else {
                    $evd['staff'] = array($assignee->getId(), (string) $assignee->getName()->getOriginal());
                    $audit = array('staff' => $assignee->getName()->name);
                }

                if (($referral=$this->hasReferral($assignee,ObjectModel::OBJECT_TYPE_STAFF)))
                    $referral->delete();
            }
        } elseif ($assignee instanceof Team) {
            if ($this->getTeamId() == $assignee->getId()) {
                $errors['assignee'] = sprintf(__('%s already assigned to %s'),
                        __('Ticket'),
                        __('the team')
                        );
            } elseif (!$dept->canAssign($assignee)) {
                $errors['err'] = __('Permission denied');
            } else {
                $refer = $this->team ?: null;
                $this->team_id = $assignee->getId();
                $evd = array('team' => $assignee->getId());
                $audit = array('team' => $assignee->getName());
                if (($referral=$this->hasReferral($assignee,ObjectModel::OBJECT_TYPE_TEAM)))
                    $referral->delete();
            }
        } else {
            $errors['assignee'] = __('Unknown assignee');
        }

        if ($errors || !$this->save(true))
            return false;

        $this->logEvent('assigned', $evd);

        $type = array('type' => 'assigned');
        $type += $audit;
        Signal::send('object.edited', $this, $type);

        $this->onAssign($assignee, $form->getComments(), $alert);

        if ($refer && $form->refer())
            $this->getThread()->refer($refer);

        return true;
    }

    // Unassign primary assignee
    function unassign() {
        // We can't release what is not assigned buddy!
        if (!$this->isAssigned())
            return true;

        // We can only unassigned OPEN tickets.
        if ($this->isClosed())
            return false;

        // Unassign staff (if any)
        if ($this->getStaffId() && !$this->setStaffId(0))
            return false;

        // Unassign team (if any)
        if ($this->getTeamId() && !$this->setTeamId(0))
            return false;

        return true;
    }

    function release($info=array(), &$errors) {
        if ($info['sid'] && $info['tid'])
            return $this->unassign();
        elseif ($info['sid'] && $this->setStaffId(0))
            return true;
        elseif ($info['tid'] && $this->setTeamId(0))
            return true;

        return false;
    }

    function refer(ReferralForm $form, &$errors, $alert=true) {
        global $thisstaff;

        $evd = array();
        $audit = array();
        $referee = $form->getReferee();
        switch (true) {
        case $referee instanceof Staff:
            $dept = $this->getDept();
            if ($this->getStaffId() == $referee->getId()) {
                $errors['agent'] = sprintf(__('%s is assigned to %s'),
                        __('Ticket'),
                        __('the agent')
                        );
            } elseif(!$referee->isAvailable()) {
                $errors['agent'] = sprintf(__('Agent is unavailable for %s'),
                        __('referral'));
            } else {
                $evd['staff'] = array($referee->getId(), (string) $referee->getName()->getOriginal());
                $audit = array('staff' => $referee->getName()->name);
            }
            break;
        case $referee instanceof Team:
            if ($this->getTeamId() == $referee->getId()) {
                $errors['team'] = sprintf(__('%s is assigned to %s'),
                        __('Ticket'),
                        __('the team')
                        );
            } else {
                //TODO::
                $evd = array('team' => $referee->getId());
                $audit = array('team' => $referee->getName());
            }
            break;
        case $referee instanceof Dept:
            if ($this->getDeptId() == $referee->getId()) {
                $errors['dept'] = sprintf(__('%s is already in %s'),
                        __('Ticket'),
                        __('the department')
                        );
            } else {
                //TODO::
                $evd = array('dept' => $referee->getId());
                $audit = array('dept' => $referee->getName());
            }
            break;
        default:
            $errors['target'] = __('Unknown referral');
        }

        if (!$errors && !$this->getThread()->refer($referee))
            $errors['err'] = __('Unable to refer ticket');

        if ($errors)
            return false;

        $this->logEvent('referred', $evd);

        $type = array('type' => 'referred');
        $type += $audit;
        Signal::send('object.edited', $this, $type);

        return true;
    }

    function systemReferral($emails) {

        if (!$this->getThread())
            return;

        foreach ($emails as $id) {
            if ($id != $this->email_id
                    && ($email=Email::lookup($id))
                    && $this->getDeptId() != $email->getDeptId()
                    && ($dept=Dept::lookup($email->getDeptId()))
                    && $this->getThread()->refer($dept)
                    )
                $this->logEvent('referred',
                            array('dept' => $dept->getId()));
        }

    }

    //Change ownership
    function changeOwner($user) {
        global $thisstaff;

        if (!$user
            || ($user->getId() == $this->getOwnerId())
            || !($this->checkStaffPerm($thisstaff,
                Ticket::PERM_EDIT))
        ) {
            return false;
        }

        $this->user_id = $user->getId();
        if (!$this->save())
            return false;

        unset($this->user);
        $this->collaborators = null;
        $this->recipients = null;

        // Remove the new owner from list of collaborators
        $c = Collaborator::lookup(array(
            'user_id' => $user->getId(),
            'thread_id' => $this->getThreadId()
        ));
        if ($c)
            $c->delete();

        $this->logEvent('edited', array('owner' => $user->getId(), 'fields' => array('Ticket Owner' => $user->getName()->name)));

        return true;
    }

    // Insert message from client
    function postMessage($vars, $origin='', $alerts=true) {
        global $cfg;

        if ($origin)
            $vars['origin'] = $origin;
        if (isset($vars['ip']))
            $vars['ip_address'] = $vars['ip'];
        elseif (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];

        //see if message should go to a parent ticket
        if ($this->isChild() && $this->getMergeType() != 'visual')
            $parent = self::lookup($this->getPid());

        $ticket = $parent ?: $this;
        $errors = array();
        if ($vars['userId'] != $ticket->user_id) {
            if ($vars['userId']) {
                $user = User::lookup($vars['userId']);
             } elseif ($vars['header']
                    && ($hdr= Mail_parse::splitHeaders($vars['header'], true))
                    && $hdr['From']
                    && ($addr= Mail_Parse::parseAddressList($hdr['From']))) {
                $info = array(
                        'name' => $addr[0]->personal,
                        'email' => $addr[0]->mailbox.'@'.$addr[0]->host);
                if ($user=User::fromVars($info))
                    $vars['userId'] = $user->getId();
            }

            if ($user) {
                $c = $ticket->getThread()->addCollaborator($user,array(),
                        $errors);
            }
       }

      // Get active recipients of the response
      // Initial Message from Tickets created by Agent
      if ($vars['reply-to'])
          $recipients = $ticket->getRecipients($vars['reply-to'], $vars['ccs']);
      // Messages from Web Portal
      elseif (strcasecmp($origin, 'email')) {
          $recipients = $ticket->getRecipients('all');
          foreach ($recipients as $key => $recipient) {
              if (!$recipientContact = $recipient->getContact())
                  continue;

              $userId = $recipientContact->getUserId() ?: $recipientContact->getId();
              // Do not list the poster as a recipient
              if ($userId == $vars['userId'])
                unset($recipients[$key]);
          }
      }
      if ($recipients && $recipients instanceof MailingList)
          $vars['thread_entry_recipients'] = $recipients->getEmailAddresses();

        if (!($message = $ticket->getThread()->addMessage($vars, $errors)))
            return null;

        $ticket->setLastMessage($message);

        // Add email recipients as collaborators...
        if ($vars['recipients']
            && (strtolower($origin) != 'email' || ($cfg && $cfg->addCollabsViaEmail()))
            //Only add if we have a matched local address
            && $vars['to-email-id']
        ) {
            //New collaborators added by other collaborators are disable --
            // requires staff approval.
            $info = array(
                'isactive' => ($message->getUserId() == $ticket->getUserId())? 1: 0);
            $collabs = array();
            foreach ($vars['recipients'] as $recipient) {
                // Skip virtual delivered-to addresses
                if (strcasecmp($recipient['source'], 'delivered-to') === 0)
                    continue;

                if (($cuser=User::fromVars($recipient))) {
                  if (!$existing = Collaborator::getIdByUserId($cuser->getId(), $ticket->getThreadId())) {
                    $_errors = array();
                    if ($c=$ticket->addCollaborator($cuser, $info, $_errors, false)) {
                      $c->setCc($c->active);

                      // FIXME: This feels very unwise — should be a
                      // string indexed array for future
                      $collabs[$c->user_id] = array(
                          'name' => $c->getName()->getOriginal(),
                          'src' => $recipient['source'],
                      );
                    }
                  }

                }

            }
            // TODO: Can collaborators add others?
            if ($collabs) {
                $ticket->logEvent('collab', array('add' => $collabs), $message->user);
                $type = array('type' => 'collab', 'add' => $collabs);
                Signal::send('object.created', $ticket, $type);
            }
        }

        // Do not auto-respond to bounces and other auto-replies
        $autorespond = isset($vars['mailflags'])
                ? !$vars['mailflags']['bounce'] && !$vars['mailflags']['auto-reply']
                : true;
        $reopen = $autorespond; // Do not reopen bounces
        if ($autorespond && $message->isBounceOrAutoReply())
            $autorespond = $reopen= false;
        elseif ($autorespond && isset($vars['autorespond']))
            $autorespond = $vars['autorespond'];

        $ticket->onMessage($message, ($autorespond && $alerts), $reopen); //must be called b4 sending alerts to staff.

        if ($autorespond && $alerts
            && $cfg && $cfg->notifyCollabsONNewMessage()
            && strcasecmp($origin, 'email')) {
          //when user replies, this is where collabs notified
          $ticket->notifyCollaborators($message, array('signature' => ''));
        }

        if (!($alerts && $autorespond))
            return $message; //Our work is done...

        $dept = $ticket->getDept();
        $variables = array(
            'message' => $message,
            'poster' => ($vars['poster'] ? $vars['poster'] : $ticket->getName())
        );

        $options = array('thread'=>$message);
        // If enabled...send alert to staff (New Message Alert)
        if ($cfg->alertONNewMessage()
            && ($email = $dept->getAlertEmail())
            && ($tpl = $dept->getTemplate())
            && ($msg = $tpl->getNewMessageAlertMsgTemplate())
        ) {
            $msg = $ticket->replaceVars($msg->asArray(), $variables);
            // Build list of recipients and fire the alerts.
            $recipients = array();
            //Last respondent.
            if ($cfg->alertLastRespondentONNewMessage() && ($lr = $ticket->getLastRespondent()))
                $recipients[] = $lr;

            //Assigned staff if any...could be the last respondent
            if ($cfg->alertAssignedONNewMessage() && $ticket->isAssigned()) {
                if ($staff = $ticket->getStaff())
                    $recipients[] = $staff;
                elseif ($team = $ticket->getTeam())
                    $recipients = array_merge($recipients, $team->getMembersForAlerts());
            }

            // Dept manager
            if ($cfg->alertDeptManagerONNewMessage()
                && $dept
                && ($manager = $dept->getManager())
            ) {
                $recipients[]=$manager;
            }

            // Account manager
            if ($cfg->alertAcctManagerONNewMessage()
                    && ($org = $this->getOwner()->getOrganization())
                    && ($acct_manager = $org->getAccountManager())) {
                if ($acct_manager instanceof Team)
                    $recipients = array_merge($recipients, $acct_manager->getMembersForAlerts());
                else
                    $recipients[] = $acct_manager;
            }

            $sentlist = array(); //I know it sucks...but..it works.
            foreach ($recipients as $k=>$staff) {
                if (!$staff || !$staff->getEmail()
                    || !$staff->isAvailable()
                    || in_array($staff->getEmail(), $sentlist)
                ) {
                    continue;
                }
                $alert = $this->replaceVars($msg, array('recipient' => $staff));
                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                $sentlist[] = $staff->getEmail();
            }
        }
        $type = array('type' => 'message', 'uid' => $vars['userId']);
        Signal::send('object.created', $this, $type);

        return $message;
    }

    function postCannedReply($canned, $message, $alert=true) {
        global $ost, $cfg;

        if ((!is_object($canned) && !($canned=Canned::lookup($canned)))
            || !$canned->isEnabled()
        ) {
            return false;
        }
        $files = array();
        foreach ($canned->attachments->getAll() as $att) {
            $files[] = array('id' => $att->file_id, 'name' => $att->getName());
            $_SESSION[':cannedFiles'][$att->file_id] = $att->getName();
        }

        if ($cfg->isRichTextEnabled())
            $response = new HtmlThreadEntryBody(
                $this->replaceVars($canned->getHtml()));
        else
            $response = new TextThreadEntryBody(
                $this->replaceVars($canned->getPlainText()));

        $info = array('msgId' => $message instanceof ThreadEntry ? $message->getId() : 0,
                      'poster' => __('SYSTEM (Canned Reply)'),
                      'response' => $response,
                      'files' => $files
        );
        $errors = array();
        if (!($response=$this->postReply($info, $errors, false, false)))
            return null;

        $this->markUnAnswered();

        if (!$alert)
            return $response;

        $dept = $this->getDept();

        if (($email=$dept->getEmail())
            && ($tpl = $dept->getTemplate())
            && ($msg=$tpl->getAutoReplyMsgTemplate())
        ) {
            if ($dept && $dept->isPublic())
                $signature=$dept->getSignature();
            else
                $signature='';

            $msg = $this->replaceVars($msg->asArray(),
                array(
                    'response' => $response,
                    'signature' => $signature,
                    'recipient' => $this->getOwner(),
                )
            );
            $attachments = ($cfg->emailAttachments() && $files)
                ? $response->getAttachments() : array();

            $options = array('thread' => $response);
            if (($message instanceof ThreadEntry)
                    && $message->getUserId() == $this->getUserId()
                    && ($mid=$message->getEmailMessageId())) {
                $options += array(
                        'inreplyto' => $mid,
                        'references' => $message->getEmailReferences()
                        );
            }

            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'], $attachments,
                $options);
        }
        return $response;
    }

    /* public */
    function postReply($vars, &$errors, $alert=true, $claim=true) {
        global $thisstaff, $cfg;

        if (!$vars['poster'] && $thisstaff)
            $vars['poster'] = $thisstaff;

        if (!$vars['staffId'] && $thisstaff)
            $vars['staffId'] = $thisstaff->getId();

        if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];

        // clear db cache
        $this->getThread()->_collaborators = null;

        // Get active recipients of the response
        $recipients = $this->getRecipients($vars['reply-to'], $vars['ccs']);
        if ($recipients instanceof MailingList)
            $vars['thread_entry_recipients'] = $recipients->getEmailAddresses();

        if (!($response = $this->getThread()->addResponse($vars, $errors)))
            return null;

        $dept = $this->getDept();
        $assignee = $this->getStaff();
        // Set status if new is selected
        if ($vars['reply_status_id']
                && ($status = TicketStatus::lookup($vars['reply_status_id']))
                && $status->getId() != $this->getStatusId())
            $this->setStatus($status);

        // Claim on response bypasses the department assignment restrictions
        $claim = ($claim
                && $cfg->autoClaimTickets()
                && !$dept->disableAutoClaim());
        if ($claim && $thisstaff && $this->isOpen() && !$this->getStaffId()) {
            $this->setStaffId($thisstaff->getId()); //direct assignment;
        }

        $this->onResponse($response, array('assignee' => $assignee)); //do house cleaning..

        $this->lastrespondent = $response->staff;

        $type = array('type' => 'message');
        Signal::send('object.created', $this, $type);

        /* email the user??  - if disabled - then bail out */
        if (!$alert)
            return $response;

        //allow agent to send from different dept email
        if (!$vars['from_email_id']
                ||  !($email = Email::lookup($vars['from_email_id'])))
            $email = $dept->getEmail();

        $options = array('thread'=>$response);
        $signature = $from_name = '';
        if ($thisstaff && $vars['signature']=='mine')
            $signature=$thisstaff->getSignature();
        elseif ($vars['signature']=='dept' && $dept->isPublic())
            $signature=$dept->getSignature();

        if ($thisstaff && ($type=$thisstaff->getReplyFromNameType())) {
            switch ($type) {
                case 'mine':
                    if (!$cfg->hideStaffName())
                        $from_name = (string) $thisstaff->getName();
                    break;
                case 'dept':
                    if ($dept->isPublic())
                        $from_name = $dept->getName();
                    break;
                case 'email':
                default:
                    $from_name =  $email->getName();
            }
            if ($from_name)
                $options += array('from_name' => $from_name);
        }

        $variables = array(
            'response' => $response,
            'signature' => $signature,
            'staff' => $thisstaff,
            'poster' => $thisstaff
        );

        if ($email
                && $recipients
                && ($tpl = $dept->getTemplate())
                && ($msg=$tpl->getReplyMsgTemplate())) {

            // Add ticket link (possibly with authtoken) if the ticket owner
            // is the only recipient on a ticket with collabs
            if (count($recipients) == 1
                    && $this->getNumCollaborators()
                    && ($contact = $recipients->offsetGet(0)->getContact())
                    && ($contact instanceof TicketOwner))
                $variables['recipient.ticket_link'] =
                    $contact->getTicketLink();

            $msg = $this->replaceVars($msg->asArray(),
                $variables + array('recipient' => $this->getOwner())
            );

            // Attachments
            $attachments = $cfg->emailAttachments() ?
                $response->getAttachments() : array();

            //Send email to recepients
            $email->send($recipients, $msg['subj'], $msg['body'],
                    $attachments, $options);
        }

        return $response;
    }

    //Activity log - saved as internal notes WHEN enabled!!
    function logActivity($title, $note) {
        return $this->logNote($title, $note, 'SYSTEM', false);
    }

    // History log -- used for statistics generation (pretty reports)
    function logEvent($state, $data=null, $user=null, $annul=null) {
        switch ($state) {
            case 'collab':
            case 'transferred':
                $type = $data;
                $type['type'] = $state;
                break;
            case 'edited':
                $type = array('type' => $state, 'fields' => $data['fields'] ? $data['fields'] : $data);
                break;
            case 'assigned':
            case 'referred':
                break;
            default:
                $type = array('type' => $state);
                break;
        }
        if ($type)
            Signal::send('object.created', $this, $type);
        if ($this->getThread())
            $this->getThread()->getEvents()->log($this, $state, $data, $user, $annul);
    }

    //Insert Internal Notes
    function logNote($title, $note, $poster='SYSTEM', $alert=true) {
        // Unless specified otherwise, assume HTML
        if ($note && is_string($note))
            $note = new HtmlThreadEntryBody($note);

        $errors = array();
        return $this->postNote(
            array(
                'title' => $title,
                'note' => $note,
            ),
            $errors,
            $poster,
            $alert
        );
    }

    function postNote($vars, &$errors, $poster=false, $alert=true) {
        global $cfg, $thisstaff;

        //Who is posting the note - staff or system? or user?
        if ($vars['staffId'] && !$poster)
            $poster = Staff::lookup($vars['staffId']);

        $vars['staffId'] = $vars['staffId'] ?: 0;
        if ($poster && is_object($poster) && !$vars['userId']) {
            $vars['staffId'] = $poster->getId();
            $vars['poster'] = $poster->getName();
        }
        elseif ($poster) { //string
            $vars['poster'] = $poster;
        }
        elseif (!isset($vars['poster'])) {
            $vars['poster'] = 'SYSTEM';
        }
        if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];

        if (!($note=$this->getThread()->addNote($vars, $errors)))
            return null;

        $alert = $alert && (
            isset($vars['mailflags'])
            // No alerts for bounce and auto-reply emails
            ? !$vars['mailflags']['bounce'] && !$vars['mailflags']['auto-reply']
            : true
        );

        // Get assigned staff just in case the ticket is closed.
        $assignee = $this->getStaff();

        if ($vars['note_status_id']
            && ($status=TicketStatus::lookup($vars['note_status_id']))
        ) {
            $this->setStatus($status);
        }

        $activity = $vars['activity'] ?: _S('New Internal Note');
        $this->onActivity(array(
            'activity' => $activity,
            'threadentry' => $note,
            'assignee' => $assignee
        ), $alert);

        $type = array('type' => 'note');
        Signal::send('object.created', $this, $type);

        return $note;
    }

    // Threadable interface
    function postThreadEntry($type, $vars, $options=array()) {
        $errors = array();
        switch ($type) {
        case 'M':
            return $this->postMessage($vars, $vars['origin']);
        case 'N':
            return $this->postNote($vars, $errors);
        case 'R':
            return $this->postReply($vars, $errors);
        }
    }

    // Print ticket... export the ticket thread as PDF.
    function pdfExport($psize='Letter', $notes=false, $events=false) {
        global $thisstaff;

        require_once(INCLUDE_DIR.'class.pdf.php');
        if (!is_string($psize)) {
            if ($_SESSION['PAPER_SIZE'])
                $psize = $_SESSION['PAPER_SIZE'];
            elseif (!$thisstaff || !($psize = $thisstaff->getDefaultPaperSize()))
                $psize = 'Letter';
        }

        $pdf = new Ticket2PDF($this, $psize, $notes, $events);
        $name = 'Ticket-'.$this->getNumber().'.pdf';
        Http::download($name, 'application/pdf', $pdf->output($name, 'S'));
        //Remember what the user selected - for autoselect on the next print.
        $_SESSION['PAPER_SIZE'] = $psize;
        exit;
    }

    function zipExport($notes=true, $tasks=false) {
        $exporter = new TicketZipExporter($this);
        $exporter->download(['notes'=>$notes, 'tasks'=>$tasks]);
        exit;
    }

    function delete($comments='') {
        global $ost, $thisstaff;

        //delete just orphaned ticket thread & associated attachments.
        // Fetch thread prior to removing ticket entry
        $t = $this->getThread();

        if (!parent::delete())
            return false;

        //deleting parent ticket
        if ($children = $this->getChildren()) {
            foreach ($children as $childId) {
                if (!($child = Ticket::lookup($childId[0])))
                    continue;

                $child->setPid(NULL);
                $child->setMergeType(3);
                $child->save();
                $childThread = $child->getThread();
                $childThread->object_type = 'T';
                $childThread->save();
            }
        }

        //deleting child ticket
        if ($this->isChild()) {
            $parent = Ticket::lookup($this->ticket_pid);
            if ($parent->isParent() && count($parent->getChildren()) == 0) {
                $parent->setMergeType(3);
                $parent->save();
            }
        } else
            $t->delete();

        $this->logEvent('deleted');

        foreach (DynamicFormEntry::forTicket($this->getId()) as $form)
            $form->delete();

        $this->deleteDrafts();

        if ($this->cdata)
            $this->cdata->delete();

        // Log delete
        $log = sprintf(__('Ticket #%1$s deleted by %2$s'),
            $this->getNumber(),
            $thisstaff ? $thisstaff->getName() : __('SYSTEM')
        );
        if ($comments)
            $log .= sprintf('<hr>%s', $comments);

        $ost->logDebug(
            sprintf( __('Ticket #%s deleted'), $this->getNumber()),
            $log
        );
        return true;
    }

    function deleteDrafts() {
        Draft::deleteForNamespace('ticket.%.' . $this->getId());
    }

    function save($refetch=false) {
        if ($this->dirty) {
            $this->updated = SqlFunction::NOW();
            if (isset($this->dirty['status_id']) && PHP_SAPI !== 'cli')
                // Refetch the queue counts
                SavedQueue::clearCounts();
        }
        return parent::save($this->dirty || $refetch);
    }

    function update($vars, &$errors) {
        global $cfg, $thisstaff;

        if (!$cfg
            || !($this->checkStaffPerm($thisstaff,
                Ticket::PERM_EDIT))
        ) {
            return false;
        }

        $fields = array();
        $fields['topicId']  = array('type'=>'int',      'required'=>1, 'error'=>__('Help topic selection is required'));
        $fields['slaId']    = array('type'=>'int',      'required'=>0, 'error'=>__('Select a valid SLA'));
        $fields['duedate']  = array('type'=>'date',     'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY'));

        $fields['user_id']  = array('type'=>'int',      'required'=>0, 'error'=>__('Invalid user-id'));

        if (!Validator::process($fields, $vars, $errors) && !$errors['err'])
            $errors['err'] = sprintf('%s — %s',
                __('Missing or invalid data'),
                __('Correct any errors below and try again'));

        $vars['note'] = ThreadEntryBody::clean($vars['note']);

        if ($vars['duedate']) {
            if ($this->isClosed())
                $errors['duedate']=__('Due date can NOT be set on a closed ticket');
            elseif (strtotime($vars['duedate']) === false)
                $errors['duedate']=__('Invalid due date');
            elseif (Misc::user2gmtime($vars['duedate']) <= Misc::user2gmtime())
                $errors['duedate']=__('Due date must be in the future');
        }

        if (isset($vars['source']) // Check ticket source if provided
                && !array_key_exists($vars['source'], Ticket::getSources()))
            $errors['source'] = sprintf( __('Invalid source given - %s'),
                    Format::htmlchars($vars['source']));

        $topic = Topic::lookup($vars['topicId']);
        if($topic && !$topic->isActive())
          $errors['topicId']= sprintf(__('%s selected must be active'), __('Help Topic'));

        // Validate dynamic meta-data
        $forms = DynamicFormEntry::forTicket($this->getId());
        foreach ($forms as $form) {
            // Don't validate deleted forms
            if (!in_array($form->getId(), $vars['forms']))
                continue;
            $form->filterFields(function($f) { return !$f->isStorable(); });
            $form->setSource($_POST);
            if (!$form->isValid(function($f) {
                return $f->isVisibleToStaff() && $f->isEditableToStaff();
            })) {
                $errors = array_merge($errors, $form->errors());
            }
        }

        if ($errors)
            return false;

        // Decide if we need to keep the just selected SLA
        $keepSLA = ($this->getSLAId() != $vars['slaId']);

        $this->topic_id = $vars['topicId'];
        $this->sla_id = $vars['slaId'];
        $this->source = $vars['source'];
        $this->duedate = $vars['duedate']
            ? date('Y-m-d H:i:s',Misc::dbtime($vars['duedate']))
            : null;

        if ($vars['user_id'])
            $this->user_id = $vars['user_id'];
        if ($vars['duedate'])
            // We are setting new duedate...
            $this->isoverdue = 0;

        $changes = array();
        foreach ($this->dirty as $F=>$old) {
            switch ($F) {
            case 'topic_id':
            case 'user_id':
            case 'source':
            case 'duedate':
            case 'sla_id':
                $changes[$F] = array($old, $this->{$F});
            }
        }

        if (!$this->save())
            return false;

        $vars['note'] = ThreadEntryBody::clean($vars['note']);
        if ($vars['note'])
            $this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff);

        // Update dynamic meta-data
        foreach ($forms as $form) {
            if ($C = $form->getChanges())
                $changes['fields'] = ($changes['fields'] ?: array()) + $C;
            // Drop deleted forms
            $idx = array_search($form->getId(), $vars['forms']);
            if ($idx === false) {
                $form->delete();
            }
            else {
                $form->set('sort', $idx);
                $form->saveAnswers(function($f) {
                        return $f->isVisibleToStaff()
                        && $f->isEditableToStaff(); }
                        );
            }
        }

        if ($changes) {
          $this->logEvent('edited', $changes);
        }


        // Reselect SLA if transient
        if (!$keepSLA
            && (!$this->getSLA() || $this->getSLA()->isTransient())
        ) {
            $this->selectSLAId();
        }

        if (!$this->save())
            return false;

        $this->updateEstDueDate();
        Signal::send('model.updated', $this);

        return true;
   }

   function updateField($form, &$errors) {
       global $thisstaff, $cfg;

       if (!($field = $form->getField('field')))
           return null;

       $updateDuedate = false;
       if (!($changes = $field->getChanges()))
           $errors['field'] = sprintf(__('%s is already assigned this value'),
                   __($field->getLabel()));
       else {
           if ($field->answer) {
               if (!$field->isEditableToStaff())
                   $errors['field'] = sprintf(__('%s can not be edited'),
                           __($field->getLabel()));
               elseif (!$field->save(true))
                   $errors['field'] =  __('Unable to update field');

               // Strip tags from TextareaFields to ensure event data is not
               // truncated
               if ($field instanceof TextareaField)
                   foreach ($changes as $k=>$v)
                       $changes[$k] = Format::truncate(Format::striptags($v), 200);

               $changes['fields'] = array($field->getId() => $changes);
           } else {
               $val =  $field->getClean();
               $fid = $field->get('name');

               // Convert duedate to DB timezone.
               if ($fid == 'duedate') {
                   if (empty($val))
                       $val = null;
                   elseif ($dt = Format::parseDateTime($val)) {
                     // Make sure the due date is valid
                     if (Misc::user2gmtime($val) <= Misc::user2gmtime())
                         $errors['field']=__('Due date must be in the future');
                     else {
                         $dt->setTimezone(new DateTimeZone($cfg->getDbTimezone()));
                         $val = $dt->format('Y-m-d H:i:s');
                     }
                  }
               } elseif (is_object($val))
                   $val = $val->getId();

               $changes = array();
               $this->{$fid} = $val;
               foreach ($this->dirty as $F=>$old) {
                   switch ($F) {
                   case 'sla_id':
                   case 'duedate':
                        $updateDuedate = true;
                   case 'topic_id':
                   case 'user_id':
                   case 'source':
                       $changes[$F] = array($old, $this->{$F});
                   }
               }

               if (!$errors && !$this->save())
                   $errors['field'] =  __('Unable to update field');
           }
       }

       if ($errors)
           return false;

       // Record the changes
       $this->logEvent('edited', $changes);

       // Log comments (if any)
       if (($comments = $form->getField('comments')->getClean())) {
           $title = sprintf(__('%s updated'), __($field->getLabel()));
           $_errors = array();
           $this->postNote(
                   array('note' => $comments, 'title' => $title),
                   $_errors, $thisstaff, false);
       }

       $this->lastupdate = SqlFunction::NOW();

       if ($updateDuedate)
           $this->updateEstDueDate();

       $this->save();

       Signal::send('model.updated', $this);

       return true;
   }

   /*============== Static functions. Use Ticket::function(params); =============nolint*/
    static function getIdByNumber($number, $email=null, $ticket=false) {

        if (!$number)
            return 0;

        $query = static::objects()
            ->filter(array('number' => $number));

        if ($email)
            $query->filter(Q::any(array(
                'user__emails__address' => $email,
                'thread__collaborators__user__emails__address' => $email
            )));


        if (!$ticket) {
            $query = $query->values_flat('ticket_id');
            if ($row = $query->first())
                return $row[0];
        }
        else {
            return $query->first();
        }
    }

    static function lookupByNumber($number, $email=null) {
        return static::getIdByNumber($number, $email, true);
    }

    static function isTicketNumberUnique($number) {
        $num = static::objects()
            ->filter(array('number' => $number))
        ->count();

    return ($num === 0);
    }

    static function getChildTickets($pid) {
        return Ticket::objects()
                ->filter(array('ticket_pid'=>$pid))
                ->values_flat('ticket_id', 'number', 'ticket_pid', 'sort', 'thread__id', 'user_id', 'cdata__subject', 'user__name', 'flags')
                ->annotate(array('tasks' => SqlAggregate::COUNT('tasks__id', true),
                                 'collaborators' => SqlAggregate::COUNT('thread__collaborators__id'),
                                 'entries' => SqlAggregate::COUNT('thread__entries__id'),))
                ->order_by('sort');
    }

    /* Quick client's tickets stats
       @email - valid email.
     */
    function getUserStats($user) {
        if(!$user || !($user instanceof EndUser))
            return null;

        $sql='SELECT count(open.ticket_id) as open, count(closed.ticket_id) as closed '
            .' FROM '.TICKET_TABLE.' ticket '
            .' LEFT JOIN '.TICKET_TABLE.' open
                ON (open.ticket_id=ticket.ticket_id AND open.status=\'open\') '
            .' LEFT JOIN '.TICKET_TABLE.' closed
                ON (closed.ticket_id=ticket.ticket_id AND closed.status=\'closed\')'
            .' WHERE ticket.user_id = '.db_input($user->getId());

        return db_fetch_array(db_query($sql));
    }

    protected function filterTicketData($origin, $vars, $forms, $user=false, $postCreate=false) {
        global $cfg;

        // Unset all the filter data field data in case things change
        // during recursive calls
        foreach ($vars as $k=>$v)
            if (strpos($k, 'field.') === 0)
                unset($vars[$k]);

        foreach ($forms as $F) {
            if ($F) {
                $vars += $F->getFilterData();
            }
        }

        if (!$user) {
            $interesting = array('name', 'email');
            $user_form = UserForm::getUserForm()->getForm($vars);
            // Add all the user-entered info for filtering
            foreach ($interesting as $F) {
                if ($field = $user_form->getField($F))
                    $vars[$F] = $field->toString($field->getClean());
            }
            // Attempt to lookup the user and associated data
            $user = User::lookupByEmail($vars['email']);
        }

        // Add in user and organization data for filtering
        if ($user) {
            $vars += $user->getFilterData();
            $vars['email'] = $user->getEmail();
            $vars['name'] = $user->getName()->getOriginal();
            if ($org = $user->getOrganization()) {
                $vars += $org->getFilterData();
            }
        }
        // Don't include org information based solely on email domain
        // for existing user instances
        else {
            // Unpack all known user info from the request
            foreach ($user_form->getFields() as $f) {
                $vars['field.'.$f->get('id')] = $f->toString($f->getClean());
            }
            // Add in organization data if one exists for this email domain
            list($mailbox, $domain) = explode('@', $vars['email'], 2);
            if ($org = Organization::forDomain($domain)) {
                $vars += $org->getFilterData();
            }
        }

        try {
            // Make sure the email address is not banned
            if (($filter=Banlist::isBanned($vars['email']))) {
                throw new RejectedException($filter, $vars);
            }

            // Init ticket filters...
            $ticket_filter = new TicketFilter($origin, $vars);
            $ticket_filter->apply($vars, $postCreate);
        }
        catch (FilterDataChanged $ex) {
            // Don't pass user recursively, assume the user has changed
            return self::filterTicketData($origin, $ex->getData(), $forms);
        }
        return $vars;
    }

    /*
     * The mother of all functions...You break it you fix it!
     *
     *  $autorespond and $alertstaff overrides config settings...
     */
    static function create($vars, &$errors, $origin, $autorespond=true,
            $alertstaff=true) {
        global $ost, $cfg, $thisstaff;

        // Don't enforce form validation for email
        $field_filter = function($type) use ($origin) {
            return function($f) use ($origin, $type) {
                // Ultimately, only offer validation errors for web for
                // non-internal fields. For email, no validation can be
                // performed. For other origins, validate as usual
                switch (strtolower($origin)) {
                case 'email':
                    return false;
                case 'staff':
                    // Required 'Contact Information' fields aren't required
                    // when staff open tickets
                    return $f->isVisibleToStaff();
                case 'web':
                    return $f->isVisibleToUsers();
                default:
                    return true;
                }
            };
        };

        $reject_ticket = function($message) use (&$errors) {
            global $ost;
            $errors = array(
                'errno' => 403,
                'err' => __('This help desk is for use by authorized users only'));
            $ost->logWarning(_S('Ticket denied'), $message, false);
            return 0;
        };

        Signal::send('ticket.create.before', null, $vars);

        // Create and verify the dynamic form entry for the new ticket
        $form = TicketForm::getNewInstance();
        $form->setSource($vars);

        // If submitting via email or api, ensure we have a subject and such
        if (!in_array(strtolower($origin), array('web', 'staff'))) {
            foreach ($form->getFields() as $field) {
                $fname = $field->get('name');
                if ($fname && isset($vars[$fname]) && !$field->value)
                    $field->value = $field->parse($vars[$fname]);
            }
        }

        if ($vars['uid'])
            $user = User::lookup($vars['uid']);

        $id=0;
        $fields=array();
        switch (strtolower($origin)) {
            case 'web':
                $fields['topicId']  = array('type'=>'int',  'required'=>1, 'error'=>__('Select a Help Topic'));
                break;
            case 'staff':
                $fields['deptId']   = array('type'=>'int',  'required'=>0, 'error'=>__('Department selection is required'));
                $fields['topicId']  = array('type'=>'int',  'required'=>1, 'error'=>__('Help topic selection is required'));
                $fields['duedate']  = array('type'=>'date', 'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY'));
            case 'api':
                $fields['source']   = array('type'=>'string', 'required'=>1, 'error'=>__('Indicate ticket source'));
                break;
            case 'email':
                $fields['emailId']  = array('type'=>'int',  'required'=>1, 'error'=>__('Unknown system email'));
                break;
            default:
                # TODO: Return error message
                $errors['err']=$errors['origin'] = __('Invalid ticket origin given');
        }

        if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
            $errors['err'] = sprintf('%s — %s',
                __('Missing or invalid data'),
                __('Correct any errors below and try again'));

        // Make sure the due date is valid
        if ($vars['duedate']) {
            if (strtotime($vars['duedate']) === false)
                $errors['duedate']=__('Invalid due date');
            elseif (Misc::user2gmtime($vars['duedate']) <= Misc::user2gmtime())
                $errors['duedate']=__('Due date must be in the future');
        }

        $topic_forms = array();
        if (!$errors) {

            // Handle the forms associate with the help topics. Instanciate the
            // entries, disable and track the requested disabled fields.
            if ($vars['topicId']) {
                if ($__topic=Topic::lookup($vars['topicId'])) {
                    foreach ($__topic->getForms() as $idx=>$__F) {
                        $disabled = array();
                        foreach ($__F->getFields() as $field) {
                            if (!$field->isEnabled() && $field->hasFlag(DynamicFormField::FLAG_ENABLED))
                                $disabled[] = $field->get('id');
                        }
                        // Special handling for the ticket form — disable fields
                        // requested to be disabled as per the help topic.
                        if ($__F->get('type') == 'T') {
                            foreach ($form->getFields() as $field) {
                                if (false !== array_search($field->get('id'), $disabled))
                                    $field->disable();
                            }
                            $form->sort = $idx;
                            $__F = $form;
                        }
                        else {
                            $__F = $__F->instanciate($idx);
                            $__F->setSource($vars);
                            $topic_forms[] = $__F;
                        }
                        // Track fields currently disabled
                        $__F->extra = JsonDataEncoder::encode(array(
                            'disable' => $disabled
                        ));
                    }
                }
            }

            try {
                $vars = self::filterTicketData($origin, $vars,
                    array_merge(array($form), $topic_forms), $user, false);
            }
            catch (RejectedException $ex) {
                return $reject_ticket(
                    sprintf(_S('Ticket rejected (%s) by filter "%s"'),
                    $ex->vars['email'], $ex->getRejectingFilter()->getName())
                );
            }

            //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
            if ($cfg->getMaxOpenTickets() > 0
                    && strcasecmp($origin, 'staff')
                    && ($_user=TicketUser::lookupByEmail($vars['email']))
                    && ($openTickets=$_user->getNumOpenTickets())
                    && ($openTickets>=$cfg->getMaxOpenTickets()) ) {

                $errors = array('err' => __("You've reached the maximum open tickets allowed."));
                $ost->logWarning(sprintf(_S('Ticket denied - %s'), $vars['email']),
                        sprintf(_S('Max open tickets (%1$d) reached for %2$s'),
                            $cfg->getMaxOpenTickets(), $vars['email']),
                        false);

                return 0;
            }

            // Allow vars to be changed in ticket filter and applied to the user
            // account created or detected
            if (!$user && $vars['email'])
                $user = User::lookupByEmail($vars['email']);

            if (!$user) {
                // Reject emails if not from registered clients (if
                // configured)
                if (strcasecmp($origin, 'email') === 0
                        && !$cfg->acceptUnregisteredEmail()) {
                    list($mailbox, $domain) = explode('@', $vars['email'], 2);
                    // Users not yet created but linked to an organization
                    // are still acceptable
                    if (!Organization::forDomain($domain)) {
                        return $reject_ticket(
                            sprintf(_S('Ticket rejected (%s) (unregistered client)'),
                                $vars['email']));
                    }
                }

                $user_form = UserForm::getUserForm()->getForm($vars);
                $can_create = !$thisstaff || $thisstaff->hasPerm(User::PERM_CREATE);
                if (!$user_form->isValid($field_filter('user'))
                    || !($user=User::fromVars($user_form->getClean(), $can_create))
                ) {
                    $errors['user'] = $can_create
                        ? __('Incomplete client information')
                        : __('You do not have permission to create users.');
                }
            }
        }

        if (!$form->isValid($field_filter('ticket')))
            $errors += $form->errors();

        if ($vars['topicId']) {
            if (($topic=Topic::lookup($vars['topicId']))
                    && $topic->isActive()) {
                foreach ($topic_forms as $topic_form) {
                    $TF = $topic_form->getForm($vars);
                    if (!$TF->isValid($field_filter('topic')))
                        $errors = array_merge($errors, $TF->errors());
                }
            } else  {
                $vars['topicId'] = 0;
            }
        }



        // Any errors above are fatal.
        if ($errors)
            return 0;

        Signal::send('ticket.create.validated', null, $vars);

        # Some things will need to be unpacked back into the scope of this
        # function
        if (isset($vars['autorespond']))
            $autorespond = $vars['autorespond'];

        # Apply filter-specific priority
        if ($vars['priorityId'])
            $form->setAnswer('priority', null, $vars['priorityId']);

        // If the filter specifies a help topic which has a form associated,
        // and there was previously either no help topic set or the help
        // topic did not have a form, there's no need to add it now as (1)
        // validation is closed, (2) there may be a form already associated
        // and filled out from the original  help topic, and (3) staff
        // members can always add more forms now

        // OK...just do it.
        $statusId = $vars['statusId'];
        $deptId = $vars['deptId']; //pre-selected Dept if any.
        $source = ucfirst($vars['source']);

        // Apply email settings for emailed tickets. Email settings should
        // trump help topic settins if the email has an associated help
        // topic
        if ($vars['emailId'] && ($email=Email::lookup($vars['emailId']))) {
            $deptId = $deptId ?: $email->getDeptId();
            $priority = $form->getAnswer('priority');
            if (!$priority || !$priority->getIdValue())
                $form->setAnswer('priority', null, $email->getPriorityId());
            if ($autorespond)
                $autorespond = $email->autoRespond();
            if (!isset($topic)
                    && ($T = $email->getTopic())
                    && ($T->isActive())) {
                $topic = $T;
            }
            $email = null;
            $source = 'Email';
        }

        if (!isset($topic)) {
            // This may return NULL, no big deal
            $topic = $cfg->getDefaultTopic();
        }

        // Intenal mapping magic...see if we need to override anything
        if (isset($topic)) {
            $deptId = $deptId ?: $topic->getDeptId();
            $statusId = $statusId ?: $topic->getStatusId();
            $priority = $form->getAnswer('priority');
            if (!$priority || !$priority->getIdValue())
                $form->setAnswer('priority', null, $topic->getPriorityId());
            if ($autorespond)
                $autorespond = $topic->autoRespond();

            //Auto assignment.
            if (!isset($vars['staffId']) && $topic->getStaffId())
                $vars['staffId'] = $topic->getStaffId();
            elseif (!isset($vars['teamId']) && $topic->getTeamId())
                $vars['teamId'] = $topic->getTeamId();

            // Unset slaId if 0 to use the Help Topic SLA or Default SLA
            if ($vars['slaId'] == 0)
                unset($vars['slaId']);

            //set default sla.
            if (isset($vars['slaId']))
                $vars['slaId'] = $vars['slaId'] ?: $cfg->getDefaultSLAId();
            elseif ($topic && $topic->getSLAId())
                $vars['slaId'] = $topic->getSLAId();
        }

        // Auto assignment to organization account manager
        if (($org = $user->getOrganization())
                && $org->autoAssignAccountManager()
                && ($code = $org->getAccountManagerId())) {
            if (!isset($vars['staffId']) && $code[0] == 's')
                $vars['staffId'] = substr($code, 1);
            elseif (!isset($vars['teamId']) && $code[0] == 't')
                $vars['teamId'] = substr($code, 1);
        }

        // Last minute checks
        $priority = $form->getAnswer('priority');
        if (!$priority || !$priority->getIdValue())
            $form->setAnswer('priority', null, $cfg->getDefaultPriorityId());
        $deptId = $deptId ?: $cfg->getDefaultDeptId();
        $statusId = $statusId ?: $cfg->getDefaultTicketStatusId();
        $topicId = isset($topic) ? $topic->getId() : 0;
        $ipaddress = $vars['ip'] ?: $_SERVER['REMOTE_ADDR'];
        $source = $source ?: 'Web';

        //We are ready son...hold on to the rails.
        $number = $topic ? $topic->getNewTicketNumber() : $cfg->getNewTicketNumber();
        $ticket = new static(array(
            'created' => SqlFunction::NOW(),
            'lastupdate' => SqlFunction::NOW(),
            'number' => $number,
            'user' => $user,
            'dept_id' => $deptId,
            'topic_id' => $topicId,
            'ip_address' => $ipaddress,
            'source' => $source,
        ));

        if (isset($vars['emailId']) && $vars['emailId'])
            $ticket->email_id = $vars['emailId'];

        //Make sure the origin is staff - avoid firebug hack!
        if ($vars['duedate'] && !strcasecmp($origin,'staff'))
            $ticket->duedate = date('Y-m-d G:i',
                Misc::dbtime($vars['duedate']));


        if (!$ticket->save())
            return null;
        if (!($thread = TicketThread::create($ticket->getId())))
            return null;

        /* -------------------- POST CREATE ------------------------ */
        $vars['ticket'] = $ticket;
        self::filterTicketData($origin, $vars,
            array_merge(array($form), $topic_forms), $user, true);

        // Save the (common) dynamic form
        // Ensure we have a subject
        $subject = $form->getAnswer('subject');
        if ($subject && !$subject->getValue() && $topic)
            $subject->setValue($topic->getFullName());

        $form->setTicketId($ticket->getId());
        $form->save();

        // Save the form data from the help-topic form, if any
        foreach ($topic_forms as $topic_form) {
            $topic_form->setTicketId($ticket->getId());
            $topic_form->save();
        }

        $ticket->loadDynamicData(true);

        $dept = $ticket->getDept();

        // Start tracking ticket lifecycle events (created should come first!)
        $ticket->logEvent('created', null, $thisstaff ?: $user);

        // Set default ticket status (if none) for Thread::getObject()
        // in addCollaborators()
        if ($ticket->getStatusId() <= 0)
            $ticket->setStatusId($cfg->getDefaultTicketStatusId());

        // Add collaborators (if any)
        if (isset($vars['ccs']) && count($vars['ccs']))
          $ticket->addCollaborators($vars['ccs'], array(), $errors);

        // Add organizational collaborators
        if ($org && $org->autoAddCollabs()) {
            $pris = $org->autoAddPrimaryContactsAsCollabs();
            $members = $org->autoAddMembersAsCollabs();
            $settings = array('isactive' => true);
            $collabs = array();
            foreach ($org->allMembers() as $u) {
                $_errors = array();
                if ($members || ($pris && $u->isPrimaryContact())) {
                    if ($c = $ticket->addCollaborator($u, $settings, $_errors)) {
                        $collabs[] = (string) $c;
                    }
                }
            }
            //TODO: Can collaborators add others?
            if ($collabs) {
                $ticket->logEvent('collab', array('org' => $org->getId()));
            }
        }

        //post the message.
        $vars['title'] = $vars['subject']; //Use the initial subject as title of the post.
        $vars['userId'] = $ticket->getUserId();
        $message = $ticket->postMessage($vars , $origin, false);

        // If a message was posted, flag it as the orignal message. This
        // needs to be done on new ticket, so as to otherwise separate the
        // concept from the first message entry in a thread.
        if ($message instanceof ThreadEntry) {
            $message->setFlag(ThreadEntry::FLAG_ORIGINAL_MESSAGE);
            $message->save();
        }

        //check to see if ticket was created from a thread
        if ($_SESSION[':form-data']['ticketId'] || $_SESSION[':form-data']['taskId']) {
          $oldTicket = Ticket::lookup($_SESSION[':form-data']['ticketId']);
          $oldTask = Task::lookup($_SESSION[':form-data']['taskId']);

          //add internal note to new ticket.
          //New ticket should have link to old task/ticket:
          $link = sprintf('<a href="%s.php?id=%d"><b>#%s</b></a>',
              $oldTicket ? 'tickets' : 'tasks',
              $oldTicket ? $oldTicket->getId() : $oldTask->getId(),
              $oldTicket ? $oldTicket->getNumber() : $oldTask->getNumber());

          $note = array(
                  'title' => __('Ticket Created From Thread Entry'),
                  'body' => sprintf(__(
                        // %1$s is the word Ticket or Task, %2$s will be a link to it
                        'This Ticket was created from %1$s %2$s'),
                        $oldTicket ? __('Ticket') : __('Task'), $link)
                  );

          $ticket->logNote($note['title'], $note['body'], $thisstaff);

          //add internal note to referenced ticket/task
          // Old ticket/task should have link to new ticket
          $ticketLink = sprintf('<a href="tickets.php?id=%d"><b>#%s</b></a>',
              $ticket->getId(),
              $ticket->getNumber());

          $entryLink = sprintf('<a href="#entry-%d"><b>%s</b></a>',
              $_SESSION[':form-data']['eid'],
              Format::datetime($_SESSION[':form-data']['timestamp']));

          $ticketNote = array(
              'title' => __('Ticket Created From Thread Entry'),
              'body' => sprintf(__('Ticket %1$s<br/> Thread Entry: %2$s'),
                $ticketLink, $entryLink)
          );

          $taskNote = array(
              'title' => __('Ticket Created From Thread Entry'),
              'note' => sprintf(__('Ticket %1$s<br/> Thread Entry: %2$s'),
                $ticketLink, $entryLink)
          );

          if ($oldTicket)
            $oldTicket->logNote($ticketNote['title'], $ticketNote['body'], $thisstaff);
          elseif ($oldTask)
            $oldTask->postNote($taskNote, $errors, $thisstaff);
        }

        // Configure service-level-agreement for this ticket
        $ticket->selectSLAId($vars['slaId']);

        // Set status
        $status = TicketStatus::lookup($statusId);
        if (!$status || !$ticket->setStatus($status, false, $errors,
                    !strcasecmp($origin, 'staff'))) {
            // Tickets _must_ have a status. Forceably set one here
            $ticket->setStatusId($cfg->getDefaultTicketStatusId());
        }

        // Only do assignment if the ticket is in an open state
        if ($ticket->isOpen()) {
            // Assign ticket to staff or team (new ticket by staff)
            if ($vars['assignId']) {
                $asnform = $ticket->getAssignmentForm(array(
                            'assignee' => $vars['assignId'],
                            'comments' => $vars['note'])
                        );
                $e = array();
                $ticket->assign($asnform, $e);
            }
            else {
                // Auto assign staff or team - auto assignment based on filter
                // rules. Both team and staff can be assigned
                $username = __('Ticket Filter');
                if ($vars['staffId'])
                     $ticket->assignToStaff($vars['staffId'], false, true, $username);
                if ($vars['teamId'])
                    // No team alert if also assigned to an individual agent
                    $ticket->assignToTeam($vars['teamId'], false, !$vars['staffId'], $username);
            }
        }

        // Update the estimated due date in the database
        $ticket->updateEstDueDate();

        /**********   double check auto-response  ************/
        //Override auto responder if the FROM email is one of the internal emails...loop control.
        if($autorespond && (Email::getIdByEmail($ticket->getEmail())))
            $autorespond=false;

        # Messages that are clearly auto-responses from email systems should
        # not have a return 'ping' message
        if (isset($vars['mailflags']) && $vars['mailflags']['bounce'])
            $autorespond = false;
        if ($autorespond && $message instanceof ThreadEntry && $message->isAutoReply())
            $autorespond = false;

        // Post canned auto-response IF any (disables new ticket auto-response).
        if ($vars['cannedResponseId']
            && $ticket->postCannedReply($vars['cannedResponseId'], $message, $autorespond)) {
                $ticket->markUnAnswered(); //Leave the ticket as unanswred.
                $autorespond = false;
        }


        if ($vars['system_emails'])
            $ticket->systemReferral($vars['system_emails']);

        // Check department's auto response settings
        // XXX: Dept. setting doesn't affect canned responses.
        if ($autorespond && $dept && !$dept->autoRespONNewTicket())
            $autorespond=false;

        // Don't send alerts to staff when the message is a bounce
        // this is necessary to avoid possible loop (especially on new ticket)
        if ($alertstaff && $message instanceof ThreadEntry && $message->isBounce())
            $alertstaff = false;

        /***** See if we need to send some alerts ****/
        $ticket->onNewTicket($message, $autorespond, $alertstaff);

        /************ check if the user JUST reached the max. open tickets limit **********/
        if ($cfg->getMaxOpenTickets()>0
            && ($user=$ticket->getOwner())
            && ($user->getNumOpenTickets()==$cfg->getMaxOpenTickets())
        ) {
            $ticket->onOpenLimit($autorespond && strcasecmp($origin, 'staff'));
        }

        // Fire post-create signal (for extra email sending, searching)
        Signal::send('ticket.created', $ticket);

        /* Phew! ... time for tea (KETEPA) */

        return $ticket;
    }

    /* routine used by staff to open a new ticket */
    static function open($vars, &$errors) {
        global $thisstaff, $cfg;

        if (!$thisstaff)
            return false;

        if ($vars['deptId']
            && ($dept=Dept::lookup($vars['deptId']))
            && ($role = $thisstaff->getRole($dept))
            && !$role->hasPerm(Ticket::PERM_CREATE)
        ) {
            $errors['err'] = sprintf(__('You do not have permission to create a ticket in %s'), __('this department'));
            return false;
        }

        if (isset($vars['source']) // Check ticket source if provided
                && !array_key_exists($vars['source'], Ticket::getSources()))
            $errors['source'] = sprintf( __('Invalid source given - %s'),
                    Format::htmlchars($vars['source']));


        if (!$vars['uid']) {
            // Special validation required here
            if (!$vars['email'] || !Validator::is_email($vars['email']))
                $errors['email'] = __('Valid email address is required');

            if (!$vars['name'])
                $errors['name'] = __('Name is required');
        }

        // Ensure agent has rights to make assignment in the cited
        // department
        if ($vars['assignId'] && !(
            $role
            ? ($role->hasPerm(Ticket::PERM_ASSIGN) || $role->__new__)
            : $thisstaff->hasPerm(Ticket::PERM_ASSIGN, false)
        )) {
            $errors['assignId'] = __('Action Denied. You are not allowed to assign/reassign tickets.');
        }

        // TODO: Deny action based on selected department.
        $vars['response'] = ThreadEntryBody::clean($vars['response']);
        $vars['note'] = ThreadEntryBody::clean($vars['note']);
        $create_vars = $vars;
        $tform = TicketForm::objects()->one()->getForm($create_vars);
        $mfield = $tform->getField('message');
        $create_vars['message'] = $mfield->getClean();
        $create_vars['files'] = $mfield->getWidget()->getAttachments()->getFiles();

        if (!($ticket=self::create($create_vars, $errors, 'staff', false)))
            return false;

        $vars['msgId']=$ticket->getLastMsgId();

        // Effective role for the department
        $role = $ticket->getRole($thisstaff);

        $alert = strcasecmp('none', $vars['reply-to']);
        // post response - if any
        $response = null;
        if ($vars['response'] && $role->hasPerm(Ticket::PERM_REPLY)) {
            $vars['response'] = $ticket->replaceVars($vars['response']);
            // $vars['cannedatachments'] contains the attachments placed on
            // the response form.
            $response = $ticket->postReply($vars, $errors, ($alert &&
                        !$cfg->notifyONNewStaffTicket()));
        }

        // Not assigned...save optional note if any
        if (!$vars['assignId'] && $vars['note']) {
            if (!$cfg->isRichTextEnabled())
                $vars['note'] = new TextThreadEntryBody($vars['note']);
            $ticket->logNote(_S('New Ticket'), $vars['note'], $thisstaff, false);
        }

        if (!$cfg->notifyONNewStaffTicket()
            || !$alert
            || !($dept=$ticket->getDept())
        ) {
            return $ticket; //No alerts.
        }

        // Notice Recipients
        $recipients = $ticket->getRecipients($vars['reply-to']);

        // Send Notice to user --- if requested AND enabled!!
        if (($tpl=$dept->getTemplate())
            && ($msg=$tpl->getNewTicketNoticeMsgTemplate())
            && ($email=$dept->getEmail())
        ) {
           $attachments = array();
           $message = $ticket->getLastMessage();
           if ($cfg->emailAttachments()) {
               if ($message && $message->getNumAttachments()) {
                 foreach ($message->getAttachments() as $attachment)
                     $attachments[] = $attachment;
               }
               if ($response && $response->getNumAttachments()) {
                 foreach ($response->getAttachments() as $attachment)
                     $attachments[] = $attachment;
               }
           }

            if ($vars['signature']=='mine')
                $signature=$thisstaff->getSignature();
            elseif ($vars['signature']=='dept' && $dept && $dept->isPublic())
                $signature=$dept->getSignature();
            else
                $signature='';

            $msg = $ticket->replaceVars($msg->asArray(),
                array(
                    'message'   => $message ?: '',
                    'response'  => $response ?: '',
                    'signature' => $signature,
                    'recipient' => $ticket->getOwner(), //End user
                    'staff'     => $thisstaff,
                )
            );
            $message = $ticket->getLastMessage();
            $options = array(
                'thread' => $message ?: $ticket->getThread(),
            );

            //ticket created on user's behalf
            $email->send($recipients, $msg['subj'], $msg['body'], $attachments,
                $options);
        }
        return $ticket;
    }

    static function checkOverdue() {
        $overdue = static::objects()
            ->filter(array(
                'isoverdue' => 0,
                'status__state' => 'open',
                Q::any(array(
                    Q::all(array(
                        'duedate__isnull' => true,
                        'est_duedate__isnull' => false,
                        'est_duedate__lt' => SqlFunction::NOW())
                        ),
                    Q::all(array(
                        'duedate__isnull' => false,
                        'duedate__lt' => SqlFunction::NOW())
                        )
                    ))
                ))
            ->limit(100);

        foreach ($overdue as $ticket)
            $ticket->markOverdue();

    }

    static function agentActions($agent, $options=array()) {
        if (!$agent)
            return;

        require STAFFINC_DIR.'templates/tickets-actions.tmpl.php';
    }

    static function getLink($id) {
        global $thisstaff;

        switch (true) {
        case ($thisstaff instanceof Staff):
            return ROOT_PATH . sprintf('scp/tickets.php?id=%s', $id);
        }
    }

    static function getPermissions() {
        return self::$perms;
    }

    static function getSources() {
        static $translated = false;
        if (!$translated) {
            foreach (static::$sources as $k=>$v)
                static::$sources[$k] = __($v);
        }

        return static::$sources;
    }

    // TODO: Create internal Form for internal fields
    static function duedateField($name, $default='', $hint='') {
        return DateTimeField::init(array(
            'id' => $name,
            'name' => $name,
            'default' => $default ?: false,
            'label' => __('Due Date'),
            'hint' => $hint,
            'configuration' => array(
                'min' => Misc::gmtime(),
                'time' => true,
                'gmt' => false,
                'future' => true,
                )
            ));
    }

    static function registerCustomData(DynamicForm $form) {
        if (!isset(static::$meta['joins']['cdata+'.$form->id])) {
            $cdata_class = <<<EOF
class DynamicForm{$form->id} extends DynamicForm {
    static function getInstance() {
        static \$instance;
        if (!isset(\$instance))
            \$instance = static::lookup({$form->id});
        return \$instance;
    }
}
class TicketCdataForm{$form->id}
extends VerySimpleModel {
    static \$meta = array(
        'view' => true,
        'pk' => array('ticket_id'),
        'joins' => array(
            'ticket' => array(
                'constraint' => array('ticket_id' => 'Ticket.ticket_id'),
            ),
        )
    );
    static function getQuery(\$compiler) {
        return '('.DynamicForm{$form->id}::getCrossTabQuery('T', 'ticket_id').')';
    }
}
EOF;
            eval($cdata_class);
            $join = array(
                'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'),
                'list' => true,
            );
            // This may be necessary if the model has already been inspected
            if (static::$meta instanceof ModelMeta)
                static::$meta->addJoin('cdata+'.$form->id, $join);
            else {
                static::$meta['joins']['cdata+'.$form->id] = array(
                    'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'),
                    'list' => true,
                );
            }
        }
    }
}
RolePermission::register(/* @trans */ 'Tickets', Ticket::getPermissions(), true);

class TicketCData extends VerySimpleModel {
    static $meta = array(
        'pk' => array('ticket_id'),
        'joins' => array(
            'ticket' => array(
                'constraint' => array('ticket_id' => 'Ticket.ticket_id'),
            ),
            ':priority' => array(
                'constraint' => array('priority' => 'Priority.priority_id'),
                'null' => true,
            ),
        ),
    );
}
TicketCData::$meta['table'] = TABLE_PREFIX . 'ticket__cdata';
