<?php

namespace Velis\Notification;

use Exception;
use InvalidArgumentException;
use Velis\App;
use Velis\Debug;
use Velis\Exception as VelisException;
use Velis\Model\BaseModel;
use Velis\Mutex\Semaphore;
use Velis\Notification;
use Velis\Output;

/**
 * Notification queue model
 * @author Olek Procki <olo@velis.pl>
 */
class Queue extends BaseModel
{

    /**
     * Default limit of messages sent by cron at one pass
     */
    const QUEUE_DEFAULT_LIMIT = 5;


    /**
     * Default TTL for resque scheduled message
     * (message is moved to cron queue after TTL is exceeded)
     */
    const RESQUE_DEFAULT_TTL  = 5;


    /**
     * Delay between each queue loop iteration
     */
    const DEFAULT_ITERATION_DELAY  = 0.2;


    /**
     * Rescue restart mutex id
     */
    const MUTEX_ID = 'resqueRestartSemaphore';


    /**
     * Test mode
     * @var bool
     */
    protected $_test = false;


    /**
     * Errors handled
     * @var Exception[]
     */
    protected $_errors = [];


    /**
     * Rescue restart mutex
     */
    protected $_mutex;


    /**
     * Constructor
     * @param bool $test
     */
    public function __construct($test = false)
    {
        $this->_test = $test;
        $this->_mutex = new Semaphore();
    }


    /**
     * Turns test mode on/off
     *
     * @param bool $test
     * @return Queue
     */
    public function setTestMode($test = true)
    {
        $this->_test = $test;

        return $this;
    }


    /**
     * Removes attachments of sent messages
     * @return int
     */
    public function clearSentAttachments()
    {
        $result = self::$_db->getAll(
            "SELECT notification_attachment_id FROM (
                SELECT
                    na.notification_attachment_id,
                    count(*) AS count,
                    sum(case when n.is_sent = 1 then 1 else 0 end) AS sum

                    FROM app.notification_attachment_tab na
                    JOIN app.notification_log_attachment_tab nla USING(notification_attachment_id)
                    JOIN app.notification_log_tab n USING(notification_log_id)
                    WHERE n.send_date < CURRENT_TIMESTAMP - INTERVAL '14 days'
                    GROUP BY na.notification_attachment_id
                    LIMIT 500
                ) AS v
                WHERE count = sum"
        );

        foreach ($result as $row) {
            /** @var Attachment $attachment */
            $attachment = Attachment::instance($row['notification_attachment_id']);
            if (!$this->_test) {
                $attachment->remove();
            }
        }

        return count($result);
    }


    /**
     * Check if there are any resque processes running
     * @return bool
     */
    public function checkResque()
    {
        exec('ps -A -o pid,command | grep [r]esque', $cmdOutput);

        foreach ($cmdOutput as $line) {
            $queueName = 'notification';
            if (App::$config->settings->instanceAcro) {
                $queueName .= '-' . App::$config->settings->instanceAcro;
            }

            if (preg_match('/' . $queueName . '(,|$|\))/i', $line) || preg_match('/' . $queueName . '\s/i', $line)) {
                return true;
            }
        }
        return false;
    }


    /**
     * Sends scheduled messages
     *
     * @param int $queueMode
     * @return int
     *
     * @throws VelisException
     */
    public function sendScheduled($queueMode = Notification::QUEUE_CRON)
    {
        $params = array();
        $resqueOverloaded = false;

        if ($queueMode == Notification::QUEUE_CRON) {
            $queryParams = array(
                'resque_ttl' => App::$config->resque->ttl ?: self::RESQUE_DEFAULT_TTL
            );

            // check if there is anything skipped by php resque
            $resqueSkipped = self::$_db->getAll(
                "SELECT notification_log_id,
                        round(extract(EPOCH from now()) - extract(EPOCH from send_date)) AS waiting_time

                 FROM app.notification_log_tab
                 WHERE is_sent=-1
                   AND send_date + (:resque_ttl || ' min')::interval < now()",
                $queryParams
            );

            if ($resqueSkipped) {
                if (!$this->checkResque()) {
                    $this->_moveExceeded();
                    $message = 'PHP-Resque notification worker is not working (no active process is running). '
                                . count($resqueSkipped) . ' message(s) moved to cron, trying to restart worker...';
                    $this->_restartWorker();
                    if (!$this->_initMutex()) {
                        VelisException::raise($message);
                    }
                } else {
                    $resqueOverloaded = true;
                    $message = 'PHP-Resque notification worker is overloaded. Moving '
                               . count($resqueSkipped) . ' message(s) to the next cron iteration';
                    VelisException::raise($message);
                }
            } else {
                $this->_unlinkMutex();
            }

            $params['cron_pending'] = 1;
        } elseif ($queueMode == Notification::QUEUE_RESQUE) {
            $params['resque_pending'] = 1;
        } else {
            throw new InvalidArgumentException('Unsupported queue mode!');
        }

        $logs = Log::getList(
            null,
            $params,
            'notification_log_id',
            App::$config->notifications->cronQueueLimit ?: self::QUEUE_DEFAULT_LIMIT
        );

        $result = $this->_send($logs);

        if ($resqueOverloaded) {
            $this->_moveExceeded();
        }

        return $result;
    }


    /**
     * Restarts worker
     */
    protected function _restartWorker()
    {
        register_shutdown_function(function () {
            chdir(ROOT_PATH . 'public');
            $pid = shell_exec('nohup ' . PHP_BINDIR . '/php index.php resque run all > /dev/null 2>/dev/null &');
        });
    }


    /**
     * Sends single message
     *
     * @param mixed $item
     * @param bool $force
     *
     * @return bool
     */
    public function sendItem($item, $force = false)
    {
        if (!$item instanceof Log) {
            $item = Log::instance($item, true);
        }

        if ($item) {
            $result = $this->_send(array($item), $force);
        }
        return $result != 0;
    }


    /**
     * Sends messages
     * @param Log[] $items
     * @param bool $force
     * @return int
     */
    protected function _send(array $items, $force = false)
    {
        $this->_errors = array();
        $messagesSent  = 0;

        foreach ($items as $row) {
            if ($row['is_sent'] == 1 && !$force) {
                continue;
            }

            try {
                $startTime = microtime(true);
                $mail = $row->getMail();

                if (!$this->_test) {
                    $mail->send();
                    $log = new Log(array(
                        'notification_log_id' => $row['notification_log_id'],
                        'is_sent'             => 1,
                        'send_date'           => date('Y-m-d H:i:s'),
                        'exec_time'           => microtime(true) - $startTime,
                    ));

                    if (Log::hasField('additional_headers')) {
                        $log['additional_headers'] = Output::jsonEncode($mail->getAdditionalHeaders());
                    }

                    $log->modify();
                    sleep(App::$config->notifications->cronIterationDelay ?: self::DEFAULT_ITERATION_DELAY);
                }
                $messagesSent++;
            } catch (Exception $e) {
                $this->_errors[] = $e;

                self::$_db->execDML(
                    'UPDATE app.notification_log_tab
                        SET unsuccessful_attempts = (CASE WHEN is_sent = -1 THEN 0 ELSE COALESCE(unsuccessful_attempts, 0) + 1 END),
                            is_sent = 0

                        WHERE notification_log_id = :notification_log_id',
                    array(
                       'notification_log_id' => $row['notification_log_id']
                    )
                );
                if ($row->unsuccessful_attempts >= Notification::QUEUE_MAX_SEND_ATTEMPTS - 1) {
                    Debug::reportMailException($row, $e);
                }
            }
        }

        return $messagesSent;
    }


    /**
     * Move all unsent resque messages to cron queue after ttl is exceeded
     */
    protected function _moveExceeded()
    {
        self::$_db->execDML(
            "UPDATE app.notification_log_tab
                SET is_sent=0

             WHERE is_sent=-1
               AND send_date + (:resque_ttl || ' min')::interval < now()",
            array(
                'resque_ttl' => App::$config->resque->ttl ?: self::RESQUE_DEFAULT_TTL
            )
        );
    }


    /**
     * Returns errors array
     * @return Exception[]
     */
    public function getErrors()
    {
        return $this->_errors;
    }


    /**
     * Returns true if error occured during last execution
     * @return bool
     */
    public function hasErrors()
    {
        return count($this->_errors) != 0;
    }


    /**
     * Returns last execution error count
     * @return int
     */
    public function getErrorCount()
    {
        return count($this->_errors);
    }


    /**
     * Creates resque restart mutex
     */
    protected function _initMutex()
    {
        try {
            $this->_mutex->init(self::MUTEX_ID);
            return $this->_mutex->acquire();
        } catch (Exception $e) {
            return false;
        }
    }


    /**
     * Deletes resque restart mutex
     */
    protected function _unlinkMutex()
    {
        $path =  DATA_PATH . 'semaphore' . DIRECTORY_SEPARATOR . self::MUTEX_ID . '.sem';

        try {
            if (file_exists($path)) {
                unlink($path);
            }
        } catch (Exception $e) {
            return false;
        }
        return true;
    }
}
