<?php
/**
 * @author: Uldis Zvirbulis <uldis.zvirbulis@mtcmedia.co.uk>
 * @version: 01.08.2018
 */

namespace Mtc\Core;

use App\Casts\OptionalEncrypted;
use Exception;
use Illuminate\Database\Eloquent\Model;


class EmailsQueued extends Model
{
    /**
     * @var string Model table name
     */
    protected $table = 'emails_queued';

    protected $guarded = ['id'];

    protected $casts = [
        'email' => OptionalEncrypted::class,
        'content' => OptionalEncrypted::class,
    ];

    const MAX_ATTEMPTS = 3;
    const CONSECUTIVE_FAIL_BAIL = 2;

    /**
     * Sends out queued emails
     *
     * @return void
     * @throws Exception
     */
    public static function send(): void
    {
        // Max execution time in seconds after cron is killed (in cases it gets stuck)
        // Give 1 hour for the process to complete, then kill it
        $lock_kill_age = 60 * 60;
        $lock_name = 'emails_queued_cron.lock';
        // If lock file exists
        if (file_exists($lock_name)) {
            // Determine how old it is
            $lock_age = time() - filemtime($lock_name);

            // This indicates the previous cron is still running
            if ($lock_age < $lock_kill_age) {
                echo 'Previous cron still running';
                return;
            }
            // If the lock age exceeds limit, we remove it
            unlink($lock_name);
        }
        // Create new lock file
        touch($lock_name);


        echo 'Deleting old emails (1 month or older)...' . PHP_EOL;

        // Remove any emails sent that are older than 1 month
        EmailsQueued::query()
            ->whereNotNull('sent_at')
            ->where('sent_at', '<', (new \Carbon\Carbon())->subMonth()->format('Y-m-d 00:00:00'))
            ->delete();

        echo 'Sending queued emails...' . PHP_EOL;

        $rows = EmailsQueued::query()
            ->whereNull('sent_at')
            ->whereNull('failed_at')
            ->get();

        $consecutiveFails = 0;
        $transportProvenAlive = false;
        $isFirstRow = true;

        foreach ($rows as $queued_email) {
            /** @var EmailsQueued $queued_email */
            echo 'Sending email: ' . $queued_email->email . PHP_EOL;

            $wasFirstRow = $isFirstRow;
            $isFirstRow = false;

            if (!filter_var($queued_email->email, FILTER_VALIDATE_EMAIL)) {
                $queued_email->markFailed('Invalid recipient address');
                continue;
            }

            $threwReason = null;
            try {
                $params = $queued_email->toArray();
                $sent = email($queued_email->email, $queued_email->subject, $queued_email->content, $params);
            } catch (\Throwable $t) {
                $sent = false;
                $threwReason = $t->getMessage();
                error_log('EmailsQueued row ' . $queued_email->id . ' threw: ' . $threwReason);
            }

            if ($sent) {
                $queued_email->sent_at = \Carbon\Carbon::now();
                $queued_email->save();
                $consecutiveFails = 0;
                $transportProvenAlive = true;
                continue;
            }

            // Tear down the shared singleton transport so the next row gets a fresh socket.
            self::resetTransport();

            $reason = $threwReason ?? 'email() returned false';

            if ($transportProvenAlive || $wasFirstRow) {
                // First row of the run, or transport is proven working — count the attempt.
                $queued_email->recordAttempt($reason);
            } else {
                // Can't tell row-fault from transport-fault yet; record diagnostic only.
                $queued_email->failure_reason = $reason;
                $queued_email->save();
            }

            $consecutiveFails++;
            if ($consecutiveFails >= self::CONSECUTIVE_FAIL_BAIL) {
                break;
            }
        }

        echo 'Queued email send complete.' . PHP_EOL;
        // Remove lock file when the scheduled process is complete.
        unlink($lock_name);
    }

    /**
     * Stop the shared singleton SMTP transport so the next send opens a fresh socket.
     */
    protected static function resetTransport(): void
    {
        try {
            $transport = app(\Swift_Transport::class);
            if ($transport && $transport->isStarted()) {
                $transport->stop();
            }
        } catch (\Throwable $t) {
            // best effort, ignore
        }
    }

    /**
     * Mark a row as permanently failed so it stops being retried.
     */
    public function markFailed(string $reason): void
    {
        $this->failed_at = \Carbon\Carbon::now();
        $this->failure_reason = $reason;
        $this->save();
    }

    /**
     * Record a soft failure. Marks the row failed once attempts hits MAX_ATTEMPTS.
     */
    public function recordAttempt(string $reason): void
    {
        $this->attempts = (int) $this->attempts + 1;
        $this->failure_reason = $reason;
        if ($this->attempts >= self::MAX_ATTEMPTS) {
            $this->failed_at = \Carbon\Carbon::now();
        }
        $this->save();
    }
}
