<?php
/**
 * Class represents records from table user
 * {autogenerated}
 * @property int $user_id 
 * @property string $login 
 * @property string $pass 
 * @property string $email 
 * @property string $name_f 
 * @property string $name_l 
 * @property string $street
 * @property string $street2
 * @property string $city 
 * @property string $state 
 * @property string $zip 
 * @property string $country 
 * @property string $phone 
 * @property datetime $added 
 * @property string $remote_addr 
 * @property int $status 
 * @property int $unsubscribed 
 * @property int $email_verified 
 * @property string $security_code 
 * @property datetime $securitycode_expire 
 * @property string $lang 
 * @property int $i_agree 
 * @property int $is_approved 
 * @property int $is_locked 
 * @property int $reseller_id 
 * @property string $comment 
 * @property int $aff_id 
 * @property int $is_affiliate 
 * @property string $aff_payout_type 
 * @see Am_Table
 */
class User extends Am_Record_WithData {
    /** @var string available after call to setPass() only */
    protected $_plaintextPass;
    /** @var bool if the password was changed, additional event handlers will be called after save */
    protected $_passwordChanged = false;

    const STATUS_PENDING = 0;
    const STATUS_ACTIVE = 1;
    const STATUS_EXPIRED = 2;
    const NEED_SESSION_REFRESH = 'need_session_refresh';

    function delete() 
    {
        if ($this->user_id <= 0) throw new Am_Exception_InternalError('Could not delete user, user_id is null');
        $this->getDi()->hook->call(Am_Event::USER_BEFORE_DELETE, array('user' => $this));
        $this->getDi()->invoiceTable->deleteByUserId($this->user_id);
        $this->getDi()->accessTable->deleteByUserId($this->user_id);
        $this->checkSubscriptions(false);
        $this->getDi()->hook->call(new Am_Event_SubscriptionRemoved($this));
        $this->getTable()->delete($this->pk());
        foreach (array('?_access_log', '?_access_cache') as $table)
            $this->getAdapter()->query("DELETE FROM $table WHERE user_id=?", $this->user_id);
        $this->getDi()->couponBatchTable->deleteByUserId($this->user_id);
        $this->getDi()->hook->call(new Am_Event_UserAfterDelete($this));
    }
    function insert($reload = true){
        if (!isset($this->is_approved))
            $this->is_approved = !$this->getDi()->config->get('manually_approve');
        if (empty($this->remote_addr))
            $this->remote_addr = htmlentities(@$_SERVER['REMOTE_ADDR']);
        if (empty($this->added))
            $this->added = $this->getDi()->sqlDateTime;

        $this->getDi()->hook->call(new Am_Event_UserBeforeInsert($this));
        $ret = parent::insert($reload);
        if ($this->_passwordChanged)
        {
            $event = new Am_Event_SetPassword($this, $this->getPlaintextPass());
            $this->getDi()->savedPassTable->setPass($event);
            $this->getDi()->hook->call($event);
        }
        $this->getDi()->hook->call(new Am_Event_UserAfterInsert($this));
        $this->_passwordChanged = false;
        return $ret;
    }
    function update(){
        $oldU = new stdclass; $oldU->user_id = null;
        if ($this->getDi()->hook->have(array(
            'userBeforeUpdate', 'userAfterUpdate', 'subscriptionUpdated')) || 
            $this->getDi()->config->get('manually_approve')    
            )
        { // do loading only if hooks are set
            $oldU = $this->getTable()->load($this->user_id, false);
            if (!$oldU) $oldU = new self; // avoid errors here
            $this->getDi()->hook->call(new Am_Event_UserBeforeUpdate($this, $oldU));
        }
        $ret = parent::update();
        
        if ($this->_passwordChanged)
        {
            $this->data()->set(self::NEED_SESSION_REFRESH, true)->update();
            $event = new Am_Event_SetPassword($this, $this->getPlaintextPass());
            $this->getDi()->savedPassTable->setPass($event);
            $this->getDi()->hook->call($event);
        }

        if ($oldU->user_id) {
            if ($this->is_approved && !$oldU->is_approved)
                $this->approve();
            
            $this->getDi()->hook->call(new Am_Event_UserAfterUpdate($this, $oldU));
        }
        if ($this->status && $oldU->user_id)
        {
            $this->getDi()->hook->call(new Am_Event_SubscriptionUpdated($this, $oldU));
        }
        $this->_passwordChanged = false;
        return $ret;
    }
    
    
    function approve()
    {
        foreach($this->getDi()->invoiceTable->findByUserId($this->pk()) as $invoice)
            $invoice->approve();
        
        if(!$this->is_approved) $this->updateQuick('is_approved', 1);
        
        $this->sendSignupEmailIfNecessary();
        
    }
    
    protected function _prepareForSet(&$vars)
    {
        if (isset($vars['pass']))
            unset($vars['pass']);
        return parent::_prepareForSet($vars);
    }
    /**
     * @param string submitted password
     * @return bool true if ok, false if not
     */
    function checkPassword($pass){
        if (!strlen($pass) || 
            !isset($this->pass) ||
            !strlen($this->pass)) {
            return false;
        }
        $ph = new PasswordHash(8, true);
        return $ph->CheckPassword($pass, $this->pass);
    }
    /**
     * Set new password
     * (important! - it does not save the password!)
     */
    function setPass($pass, $quick = false){
        $this->_plaintextPass = $pass;
        $this->_passwordChanged = true;
        $this->pass = self::cryptPass($pass, $quick);
        return $this;
    }
    static function cryptPass($pass, $quick = false)
    {
        $ph = new PasswordHash($quick ? 4 : 8, true);
        return $ph->HashPassword($pass);
    }
    /** It is only exists after call of @method setPass() */
    function getPlaintextPass()
    {
        return $this->_plaintextPass;
    }
    function getLoginCookie()
    {
        return sha1($this->user_id.$this->login.md5($this->pass));
    }
    function generateLogin() 
    {
        // usernames to try
        $try = array();
        if (!empty($this->email) && preg_match("/^([a-zA-Z0-9_]+)\@/", $this->email, $regs))
            $try[] = $regs[1];
        $fn = strtolower(preg_replace('/[^\w\d_]/', '', @$this->name_f));
        $ln = strtolower(preg_replace('/[^\w\d_]/', '', @$this->name_l));
        if ($fn || $ln)
        {
            if ($fn && $ln)
                $try[] = $fn.'_'.$ln;
            else
                $try[] = $fn . $ln;
            $try[] = $try[ count($try)-1 ] . rand(100, 999);
        }
        foreach ($try as $login)
        {
            if (strlen($login) > $this->getDi()->config->get('login_max_length'))
                $login = substr($login, 0, $this->getDi()->config->get('login_max_length'));
            if ((strlen($login)>=$this->getDi()->config->get('login_min_length')) && $this->getDi()->userTable->checkUniqLogin($login))
            {
                $this->login = $login;
                return $this;
            }
        }

        // will generate it
        // a bit of configuration
        $min_length=$this->getDi()->config->get('login_min_length') < 4 ? 4 : $this->getDi()->config->get('login_min_length');
        $max_length=$this->getDi()->config->get('login_max_length') > 10 ? 10 : $this->getDi()->config->get('login_max_length');
        /// let's go
        do {
            $pass = $this->getDi()->app->generateRandomString(rand($min_length, $max_length), 
                "qwertyuiopasdfghjklzxcvbnm");
        } while (!$this->getDi()->userTable->checkUniqLogin($pass));
        $this->login = $pass;
        return $this;
    }

    function generatePassword(){
        // a bit of configuration
        $min_length = max($this->getDi()->config->get('pass_min_length', 8), 8);
        $max_length = min($this->getDi()->config->get('pass_max_length', 12), 14);
        $all_g = "aeiyo";
        $all_gn = $all_g . "1234567890";
        $all_s = "bcdfghjkmnpqrstwxz";
        /// let's go
        $pass = "";
        $length = rand($min_length, $max_length);
        for($i=0;$i<$length;$i++) {
            if ($i % 2)
                if ($i < $min_length)
                    $pass .= $all_g[ rand(0, strlen($all_g) - 1) ];
                else
                    $pass .= $all_gn[ rand(0, strlen($all_gn) - 1) ];
            else
                $pass .= $all_s[ rand(0, strlen($all_s) - 1) ];
        }
        $this->setPass($pass);
        return $this;
    }

    private function _trueIfActive($v){    return $v == self::STATUS_ACTIVE;   }
    private function _trueIfExpired($v){    return $v == self::STATUS_EXPIRED;   }

    function checkSubscriptions($updateCache = false)
    {
        
        if (!$this->user_id)
            throw new Am_Exception_InternalError("Could not do User->checkSubscriptions() : user_id is empty");

        if ($updateCache)
            $this->getDi()->resourceAccessTable->updateCache($this->user_id);

        $newStatus = $this->getDi()->accessTable->getStatusByUserId($this->user_id);
        $active = array_keys(array_filter($newStatus, array($this, '_trueIfActive')));
        
        $oldStatus = $this->getProductsStatus();
        $saved = array_keys(array_filter($oldStatus, array($this, '_trueIfActive')));
        
        $merged = array_unique(array_merge($active, $saved));
        $added = array_diff($merged, $saved);
        $deleted = array_diff($merged, $active);
        foreach ($added as $product_id)
        {
            $e = new Am_Event_SubscriptionAdded($this, $this->getDi()->productTable->load($product_id));
            $this->getDi()->hook->call($e);
        }
        foreach ($deleted as $product_id)
        {
            $e = new Am_Event_SubscriptionDeleted($this, $this->getDi()->productTable->load($product_id));
            $this->getDi()->hook->call($e);
        }
        if ($active)
            $newUserStatus = self::STATUS_ACTIVE;
        elseif (array_filter($newStatus, array($this, '_trueIfExpired')))
            $newUserStatus = self::STATUS_EXPIRED;
        else
            $newUserStatus = self::STATUS_PENDING;
        if ($newUserStatus != @$this->status)
        {
            $this->sendSignupEmailIfNecessary();
            $this->updateQuick('status', $newUserStatus);
        }
        if ($added || $deleted || array_diff_assoc($newStatus, $oldStatus))
        {
            $this->data()->set(self::NEED_SESSION_REFRESH, true)->update();
            $this->getDi()->userStatusTable->setByUserId($this->user_id, $newStatus);
            $e = new Am_Event_SubscriptionChanged($this, $added, $deleted);
            $this->getDi()->hook->call($e);
        }
    }
    /**
     * This function is called upon completion of payment
     * If user signup e-mail was not set before, it will be sent from there
     * It checks if :
     *   - email is enabled in config
     *   - user has at least one completed payment
     *   - the e-mail was not sent before
     *   - customer was approved
     */
    function sendSignupEmailIfNecessary(InvoicePayment $p = null){
        if (!$this->getDi()->config->get('send_signup_mail')) return ;
        if ($this->data()->get('signup_email_sent')) return ; // was already sent
        if (!$this->isApproved()) return ; // is not yet approved
        $this->sendSignupEmail($p);
    }
    
    function sendSignupEmail(InvoicePayment $p = null)
    {
        if ($et = Am_Mail_Template::load('send_signup_mail', $this->lang))
        {
            $et->setUser($this);
            if (empty($p))
                $p = $this->getDi()->invoicePaymentTable->findFirstByUserId($this->user_id);
            if ($p)
                $et->setPayment($p);
            $et->send($this);
            $this->data()->set('signup_email_sent', 1)->update();
        }
    }
    function sendRegistrationEmail()
    {
        if ($et = Am_Mail_Template::load('registration_mail', $this->lang))
        {
            $et->setUser($this);
            $et->password = $this->getPlaintextPass();
            $et->send($this);
        }            
    }
    
    function sendNotApprovedEmail(){
        if($et = Am_Mail_Template::load('manually_approve', $this->lang))
        {
            $et->setUser($this);
            $et->send($this);
        }
        if($et = Am_Mail_Template::load('manually_approve_admin', $this->lang))
        {
            $et->setUser($this);
            $et->send(Am_Mail_Template::TO_ADMIN);
        }
    }
    
    /**
     * @return bool true if user have any active subscriptions
     */
    function isActive()
    {
        return $this->status == self::STATUS_ACTIVE;
    }
    /**
     * Return true if customer paid at least once
     * @return bool
     */
    function isPaid(){
        return (bool)$this->getAdapter()->selectCell(
           "SELECT SUM(amount)>0 
               FROM ?_invoice_payment p WHERE user_id=?d"
            , $this->user_id);
    }
    /**
     * @return true if user is approved
     */
    function isApproved(){
        return !empty($this->is_approved) && ($this->is_approved > 0);
    }
    function isLocked(){
        return !empty($this->is_locked) && ($this->is_locked>0);
    }
    function lock($flag = true)
    {
        $this->updateQuick('is_locked', (int)$flag);
    }

    /**
     * @return array of Access objects 
     */
    function getAccessRecords(){
        return $this->getDi()->accessTable->findByUserId($this->user_id);
    }
    function getActiveProducts()
    {
        return $this->getDi()->productTable->loadIds($this->getActiveProductIds());
    }
    function getExpiredProducs()
    {
        return $this->getDi()->productTable->loadIds($this->getExpiredProductIds());
    }
    /**
     * Returns max expiration date
     * if product or array of products are specified, returns it
     * for given products only
     */
    function getExpire($productIdOrIds = array())
    {
        $productIdOrIds = (array)$productIdOrIds;
        $productIdOrIds = array_filter(array_map('intval', $productIdOrIds));
        return $this->getDi()->db->selectCell("SELECT MAX(expire_date)
            FROM ?_access
            WHERE user_id=?d { AND product_id IN (?a) }",
            $this->pk(), $productIdOrIds ? $productIdOrIds : DBSIMPLE_SKIP
         );
    }
    
    function getActiveProductsExpiration(){
        $ret = array();
        foreach($this->getActiveProductIds() as $pid){
            $ret[$pid] = $this->getExpire($pid);
        }
        return $ret;
    }
    
    /** @return array of int active product# */
    function getActiveProductIds(){
        return $this->getAdapter()->selectCol("SELECT product_id FROM ?_user_status WHERE user_id=?d AND status=?d", $this->user_id, self::STATUS_ACTIVE);
    }
    /** @return array of int active product# */
    function getExpiredProductIds(){
        return $this->getAdapter()->selectCol("SELECT product_id FROM ?_user_status WHERE user_id=?d AND status=?d", $this->user_id, self::STATUS_EXPIRED);
    }
    /**
     * @return array product_id => status (@see self::STATUS_ACTIVE, ... constants)
     */
    function getProductsStatus()
    {
        return $this->getDi()->userStatusTable->getByUserId($this->user_id);
    }

    /// -- implementing IMailReceiver interface */
    public function getEmail() 
    {
        return $this->email;
    }
    public function getName() 
    {
        return @$this->name_f . ' ' . @$this->name_l;
    }
    public function isUnsubscribed() 
    {
        return (bool)$this->unsubscribed;
    }
    public function canUnsubscribe() 
    {
        return true;
    }

    /** param array $groups - array of id# */
    function setGroups(array $groups)
    {
        $this->getAdapter()->query("DELETE FROM ?_user_user_group 
            WHERE user_id=?d {AND user_group_id NOT IN (?a)}",
                $this->user_id, $groups ? $groups : DBSIMPLE_SKIP);
        if ($groups) 
        {
            $vals = array();
            foreach ($groups as $id)
                $vals[] = sprintf("(%d,%d)", $this->user_id, $id);
            $this->getAdapter()->query("INSERT IGNORE INTO ?_user_user_group
                (user_id, user_group_id)
                VALUES " . implode(", ", $vals));
        }
        return $this;
    }
    /** @return array of id# */
    function getGroups()
    {
        if (empty($this->user_id)) return array();
        return $this->getAdapter()->selectCol(
           "SELECT DISTINCT user_group_id 
            FROM ?_user_user_group 
            WHERE user_id=?d", $this->user_id);
    }    
    
}

/**
 * @method findFirstByLogin($login)
 * @method findFirstByEmail($login)
 */
class UserTable extends Am_Table_WithData
{
    protected $_key = 'user_id';
    protected $_table = '?_user';
    protected $_recordClass = 'User';

    public function init()
    {
        $this->customFields()->addCallback(array($this, 'addFieldsFromSavedConfig'));        
    }
    
    public function clearPending($date)
    {
        $q = $this->_db->queryResultOnly("SELECT u.* 
            FROM ?_user u
                LEFT JOIN ?_access a ON a.user_id = u.user_id
                LEFT JOIN ?_invoice_payment ip ON ip.user_id = u.user_id
            WHERE u.status = 0 
                AND IFNULL(u.is_affiliate,0)=0 
                AND a.access_id IS NULL
                AND ip.invoice_payment_id is NULL
                AND u.added < ?
            GROUP BY u.user_id", sqlTime($date));
        while ($r = $this->_db->fetchRow($q))
        {
            $u = $this->createRecord($r);
            $u->delete();
        }
    }
    
    public function clearExpired($date)
    {
        // this function deletes only 500 records at time, to do not work over limits
        $q = $this->_db->queryResultOnly("SELECT u.* 
            FROM ?_user u
                LEFT JOIN ?_access a ON a.user_id = u.user_id
                LEFT JOIN ?_invoice_payment ip ON ip.user_id = u.user_id
            WHERE u.status = 2 
                AND IFNULL(u.is_affiliate,0)=0 
            GROUP BY u.user_id
            HAVING GREATEST(
                IFNULL(MAX(ip.dattm), '2000-01-01'), 
                IFNULL(MAX(a.expire_date), '2000-01-01')) < ?
            LIMIT 500 
        ", sqlTime($date), sqlTime($date));
        while ($r = $this->_db->fetchRow($q))
        {
            $u = $this->createRecord($r);
            $u->delete();
        }
    }

    function addFieldsFromSavedConfig(){
        foreach ((array)$this->getDi()->config->get('member_fields') as $f){
            $this->customFields()->add($f['name'], $f['title'],
                $f['type'], $f['description'], $f['validate_func'],
                    (array)$f['additional_fields']+array('from_config'=>1));
        }
    }
    function getLoginRegex(){
        return $this->getDi()->config->get('login_disallow_spaces') ?
            '/^[0-9a-zA-Z_]+$/D' :
            '/^[0-9a-zA-Z_][0-9a-zA-Z_ ]+[0-9a-zA-Z_]$/D';
    }
    /**
     * Check for username iniqueness only
     * @param string $login
     * @return bool True if record unique (no such login exists), false if not-unique
     */
    function checkUniqLogin($login, $user_id = null){
        $u = $this->_db->selectCell("SELECT user_id
            FROM ?_user
            WHERE login=? { AND user_id <> ?d}",
            $login, $user_id ? $user_id : DBSIMPLE_SKIP);
        if ($u) return 0;
        
        $event = $this->getDi()->hook->call(new Am_Event_CheckUniqLogin(null, array('login' => $login, 'userId' => $user_id)));
        return $event->isUnique() ? -1 : false;
    }

    /**
     * Check for username iniqueness only
     * @param string $login
     * @return bool True if record unique (no such login exists), false if not-unique
     */
    function checkUniqEmail($email, $user_id = null){
        $u = $this->_db->selectCell("SELECT user_id
            FROM ?_user
            WHERE email=? { AND user_id <> ?d}",
            $email, $user_id ? $user_id : DBSIMPLE_SKIP);
        if ($u) return 0;
        $event = $this->getDi()->hook->call(new Am_Event_CheckUniqEmail(null, array('email' => $email, 'userId' => $user_id)));
        return $event->isUnique() ? -1 : false;
    }

    function checkAllSubscriptionsFindChanged()
    {
        $db = $this->_db;

        // update resource_access_cache
        $this->getDi()->resourceAccessTable->updateCache();
        
        $db->query("DROP TABLE IF EXISTS ?_user_status_temp");
        $db->query("CREATE TEMPORARY TABLE ?_user_status_temp (
            user_id int not null,
            product_id int not null,
            status tinyint not null,
            INDEX(user_id,product_id))
            ");
        // create table with calculated records from "access"
        $db->query("
            INSERT INTO ?_user_status_temp
            SELECT a.user_id,a.product_id,
        	CASE WHEN SUM(IF(a.expire_date>=?, 1, 0)) THEN 1
        		 WHEN SUM(IF(a.expire_date< ?, 1, 0)) THEN 2
				 ELSE 0
			END as status
            FROM ?_access a
            GROUP BY user_id,product_id
            ", $this->getDi()->sqlDate, $this->getDi()->sqlDate);
        // now select differences
        $ids0 = $db->selectCol("
            SELECT DISTINCT ms.user_id
            FROM ?_user_status ms
                LEFT JOIN ?_user_status_temp mst
                    ON  ms.user_id =mst.user_id
                    AND ms.product_id=mst.product_id
                    AND ms.status =mst.status
            WHERE mst.user_id IS NULL
            GROUP BY ms.user_id,ms.product_id");
        // select if there is no such a record in user_status table
        $ids1 = $db->selectCol("SELECT DISTINCT mst.user_id
            FROM ?_user_status_temp mst
                LEFT JOIN ?_user_status ms
                    ON  ms.user_id =mst.user_id
                    AND ms.product_id=mst.product_id
                    AND ms.status    =mst.status
            WHERE ms.user_id IS NULL
            GROUP BY mst.user_id,mst.product_id
            ");
        // select if user record has different "status" value
        $ids2 = $db->selectCol("SELECT DISTINCT m.user_id,
            CASE WHEN SUM(IF(mst.status=1, 1, 0)) THEN 1
        		 WHEN SUM(IF(mst.status=2, 1, 0)) THEN 2
				 ELSE 0
            END AS calcStatus, m.status
            FROM ?_user m
                LEFT JOIN ?_user_status_temp mst USING (user_id)
            GROUP BY m.user_id
            HAVING IFNULL(calcStatus,0) <> IFNULL(m.status, 0)
            ");
        return array_unique(array_merge($ids0, $ids1, $ids2));
    }
    
    /**
     * If this function changed, check also AdminRebuildController - it must be changed too
     */
    function checkAllSubscriptions(){
        $changed = $this->checkAllSubscriptionsFindChanged();
        foreach ($changed as $user_id)
        {
            $u = $this->load($user_id, false);
            if ($u)
                $u->checkSubscriptions(false); // checked in checkAllSubscriptionsFindChanged
        }
    }

    /**
     * Find user record by email (if $login looks like an email)
     * or by username
     * @param string $login e-mail or username
     * @return User|null
     */
    function getByLoginOrEmail($login){
        if (!strlen($login)) 
            return null;
        if (strpos($login, '@')!==false)
            return $this->findFirstByEmail($login);
        else
            return $this->findFirstByLogin($login);
    }
    /**
     * Find record by login, check password and return it if all OK
     * with login/password
     * sets $resultCode from Am_Auth_Result
     * @return User
     */
    function getAuthenticatedRow($login, $pass, & $code = null){
        if(empty($login) || empty($pass)) 
        {
            $code = Am_Auth_Result::INVALID_INPUT;
            return;
        }
        $u = $this->getByLoginOrEmail($login);
        if (!$u)
        {
            $code = Am_Auth_Result::USER_NOT_FOUND;
            return;
        }
        if (!$u->checkPassword($pass))
        {
            $code = Am_Auth_Result::WRONG_CREDENTIALS;
            return;
        }
        $code = Am_Auth_Result::SUCCESS;
        return $u;
    }
    function getAuthenticatedCookieRow($cLogin, $cPass, & $code = null){
        if (empty($cLogin) || empty($cPass)) 
        {
            $code = Am_Auth_Result::INVALID_INPUT;
            return null;
        }
        $u = $this->getByLoginOrEmail($cLogin);
        if (!$u)
        {
            $code = Am_Auth_Result::USER_NOT_FOUND;
            return;
        }
        if ($u->getLoginCookie() !== $cPass)
        {
            $code = Am_Auth_Result::WRONG_CREDENTIALS;
            return null;
        }
        $code = Am_Auth_Result::SUCCESS;
        return $u;
    }
    function selectLast($num)
    {
        return $this->selectObjects("SELECT m.*, SUM(p.amount) AS paid
            FROM ?_user m LEFT JOIN ?_invoice_payment p USING (user_id)
            GROUP BY m.user_id
            ORDER BY m.user_id DESC LIMIT ?d", $num);
    }
}
