<?php

namespace App\Jobs;

use App\Models\Campaign;
use App\Models\CampaignEvent;
use App\Models\Contact;
use App\Models\SmtpConfig;
use App\Services\BusinessSettings;
use App\Services\EmailDeliverabilityChecker;
use App\Services\EmailRenderer;
use App\Services\SmtpConfigService;
use App\Services\SequenceEngine;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Mail\Message;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class ProcessSequenceStepJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public int $timeout = 120;
    public bool $isPausedRelease = false;

    public function __construct(
        public readonly int $sequenceId,
        public readonly int $contactId,
        public readonly string $stepId
    ) {}

    public function handle(EmailRenderer $renderer): void
    {
        $sequence = Campaign::find($this->sequenceId);
        $contact = Contact::find($this->contactId);

        \Illuminate\Support\Facades\Log::info("ProcessSequenceStepJob execution started", [
            'sequence_id' => $this->sequenceId,
            'contact_id' => $this->contactId,
            'step_id' => $this->stepId,
            'sequence_exists' => (bool)$sequence,
            'contact_exists' => (bool)$contact,
            'sequence_status' => $sequence?->status,
        ]);

        if (!$sequence || !$contact) {
            return;
        }

        if ($sequence->status === 'paused') {
            \Illuminate\Support\Facades\Log::info("ProcessSequenceStepJob paused - sequence is paused, releasing back to queue", [
                'sequence_id' => $this->sequenceId,
                'contact_id' => $this->contactId,
            ]);
            $this->isPausedRelease = true;
            
            if ($this->job && method_exists($this->job, 'getJobId')) {
                try {
                    $jobId = $this->job->getJobId();
                    $dbJob = \Illuminate\Support\Facades\DB::table('jobs')->find($jobId);
                    if ($dbJob) {
                        $payload = json_decode($dbJob->payload, true);
                        $payload['data']['command'] = serialize($this);
                        \Illuminate\Support\Facades\DB::table('jobs')->where('id', $jobId)->update([
                            'payload' => json_encode($payload)
                        ]);
                    }
                } catch (\Exception $e) {
                    \Illuminate\Support\Facades\Log::error("Failed to update payload on release: " . $e->getMessage());
                }
            }
            
            $this->release(3600); // Check again in 1 hour
            return;
        }

        if ($sequence->status !== 'active') {
            \Illuminate\Support\Facades\Log::warning("ProcessSequenceStepJob exited early - sequence status not active", [
                'sequence_id' => $this->sequenceId,
                'contact_id' => $this->contactId,
                'status' => $sequence->status,
            ]);
            return;
        }

        if ($contact->status === 'unsubscribed') {
            return;
        }

        $steps = $sequence->settings['steps'] ?? [];
        $stepIdx = null;
        foreach ($steps as $i => $s) {
            if (($s['id'] ?? null) === $this->stepId) {
                $stepIdx = $i;
                break;
            }
        }
        $step = $stepIdx !== null ? $steps[$stepIdx] : null;

        // Step not found - either it was deleted from the builder after this
        // job was queued, or the sequence was edited and steps reshuffled.
        // Addressing by stable id (rather than array position) means an
        // edit never sends the WRONG step; at worst the contact's journey
        // ends a little early here.
        if (!$step) {
            SequenceEngine::complete($sequence, $contact);
            return;
        }

        // Record start / processing state for this step
        CampaignEvent::updateOrCreate(
            [
                'campaign_id' => $sequence->id,
                'contact_id'  => $contact->id,
                'node_id'     => $step['id'],
            ],
            [
                'event_type'  => 'node_processing',
                'reason'      => 'Step is currently executing.',
            ]
        );

        try {
            if ($this->outsideSendWindow($step, $sequence, $contact)) {
                $this->release(3600); // Try again in an hour
                return;
            }

            $sent = $this->sendEmail($sequence, $contact, $step, $renderer);

            if (!$sent) {
                // Domain-level bounce: sendEmail() already recorded the
                // `bounced` event and marked the contact. Every later step
                // would fail the same way, so the journey ends here rather
                // than wasting the remaining steps' delays on a dead address.
                SequenceEngine::completeAsTerminal($sequence, $contact);
                return;
            }

            CampaignEvent::updateOrCreate(
                [
                    'campaign_id' => $sequence->id,
                    'contact_id'  => $contact->id,
                    'node_id'     => $step['id'],
                ],
                [
                    'event_type'  => 'node_success',
                    'reason'      => 'Email sent successfully.',
                ]
            );
        } catch (\Exception $e) {
            CampaignEvent::updateOrCreate(
                [
                    'campaign_id' => $sequence->id,
                    'contact_id'  => $contact->id,
                    'node_id'     => $step['id'],
                ],
                [
                    'event_type'  => 'node_error',
                    'reason'      => 'Error: ' . $e->getMessage(),
                ]
            );
            throw $e;
        }

        // Dispatch next step if one exists
        $nextStep = $steps[$stepIdx + 1] ?? null;
        if ($nextStep) {
            $delaySeconds = SequenceEngine::parseDelay($nextStep['delay'] ?? []);
            ProcessSequenceStepJob::dispatch($this->sequenceId, $this->contactId, $nextStep['id'])
                ->delay(now()->addSeconds($delaySeconds));
        } else {
            SequenceEngine::complete($sequence, $contact);
        }
    }

    /**
     * Called after every attempt (success or final failure) exhausts
     * Laravel's retry budget for this job. Without this, a permanently
     * failing step (bad SMTP creds, etc.) just vanishes into `failed_jobs`
     * with no trace on the sequence/contact - `emails_failed` never
     * increments and the contact never reaches a terminal state, which
     * also means the sequence can never auto-complete.
     */
    public function failed(\Throwable $e): void
    {
        $sequence = Campaign::find($this->sequenceId);
        $contact = Contact::find($this->contactId);
        if (!$sequence || !$contact) {
            return;
        }

        $sequence->increment('emails_failed');

        // A distinct event_type from the per-attempt `node_error` rows
        // written on each retry - this one specifically means "retries are
        // exhausted, this contact's journey through the sequence is over",
        // which is what SequenceEngine::completeAsTerminal() looks for.
        CampaignEvent::updateOrCreate(
            [
                'campaign_id' => $sequence->id,
                'contact_id'  => $contact->id,
                'node_id'     => $this->stepId,
            ],
            [
                'event_type'  => 'failed',
                'reason'      => 'Permanently failed after retries: ' . $e->getMessage(),
            ]
        );

        SequenceEngine::completeAsTerminal($sequence, $contact);
    }

    private function outsideSendWindow(array $step, Campaign $sequence, Contact $contact): bool
    {
        $timezone = 'UTC';
        $seqTz = $sequence->settings['timezone'] ?? 'UTC';
        if ($seqTz === "Contact's local time") {
            $timezone = $contact->timezone ?? BusinessSettings::timezone($sequence->user_id);
        } elseif ($seqTz !== 'UTC') {
            $timezone = $seqTz;
        }

        $now = now($timezone);
        $dayOfWeek = $now->dayOfWeek; // 0 (Sun) .. 6 (Sat)

        // "Enable Specific Days Only" - an explicit list of allowed days of
        // the week takes priority over the legacy any/weekdays/weekends
        // enum, which only remains as a fallback for steps saved before
        // this field existed.
        $specificDays = $step['send_specific_days'] ?? null;
        if (is_array($specificDays) && !empty($specificDays)) {
            if (!in_array($dayOfWeek, $specificDays, true)) {
                return true;
            }
        } else {
            $sendDays = $step['send_days'] ?? 'any';
            if ($sendDays === 'weekdays' && ($dayOfWeek === 0 || $dayOfWeek === 6)) {
                return true;
            }
            if ($sendDays === 'weekends' && ($dayOfWeek > 0 && $dayOfWeek < 6)) {
                return true;
            }
        }

        $startStr = $step['send_hours_start'] ?? '9:00 AM';
        $endStr = $step['send_hours_end'] ?? '6:00 PM';

        if ($startStr === 'Any time' || $startStr === 'Any hour' || $endStr === 'Any time') {
            return false;
        }

        try {
            $start = Carbon::createFromFormat('g:i A', $startStr, $timezone)->setDate($now->year, $now->month, $now->day);
            $end = Carbon::createFromFormat('g:i A', $endStr, $timezone)->setDate($now->year, $now->month, $now->day);

            if ($now->lt($start) || $now->gt($end)) {
                return true;
            }
        } catch (\Exception $e) {
        }

        return false;
    }

    /**
     * @return bool false when the send was blocked by a domain-level bounce
     *              (recorded here as a `bounced` event, not a true send).
     */
    private function sendEmail(Campaign $sequence, Contact $contact, array $step, EmailRenderer $renderer): bool
    {
        if ($reason = EmailDeliverabilityChecker::reason($contact->email)) {
            $this->recordBounce($sequence, $contact, $step, $reason);
            return false;
        }

        $blocks = $step['blocks'] ?? [];
        $mergeData = [
            'contact.first_name' => $contact->first_name ?? '',
            'contact.last_name'  => $contact->last_name  ?? '',
            'contact.email'      => $contact->email       ?? '',
            'contact.full_name'  => trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')),
            'first_name'         => $contact->first_name ?? '',
            'last_name'          => $contact->last_name  ?? '',
            'email'              => $contact->email       ?? '',
            'full_name'          => trim(($contact->first_name ?? '') . ' ' . ($contact->last_name ?? '')),
        ];

        if (!empty($step['track_utm'])) {
            $renderer->setUtmData([
                'utm_source'   => $step['utm_source'] ?? '',
                'utm_medium'   => $step['utm_medium'] ?? '',
                'utm_campaign' => $step['utm_campaign'] ?? '',
                'utm_term'     => $step['utm_term'] ?? '',
                'utm_content'  => $step['utm_content'] ?? '',
            ]);
        }

        $html = $renderer->render($blocks, $mergeData, $sequence->id, $contact->id, $step['pre_header'] ?? null);
        $subject = $step['subject'] ?: $sequence->title;

        foreach ($mergeData as $k => $v) {
            $subject = str_replace('{{' . $k . '}}', $v, $subject);
        }

        $smtp = SmtpConfigService::resolveForCampaign($sequence);
        $fromEmail = $smtp?->from_email ?: null;
        $fromName  = $smtp?->from_name  ?: null;

        if (!empty($step['from_email'])) {
            $fromEmail = $step['from_email'];
        }
        if (!empty($step['from_name'])) {
            $fromName = $step['from_name'];
        }

        $emailSettings = BusinessSettings::emailSettings($sequence->id);
        if (empty($fromEmail) && !empty($emailSettings['from_email'])) {
            $fromEmail = $emailSettings['from_email'];
        }
        if (empty($fromName) && !empty($emailSettings['from_name'])) {
            $fromName = $emailSettings['from_name'];
        }

        $replyEmail = $step['reply_to_email'] ?? ($emailSettings['reply_to_email'] ?: null);
        $replyName  = $emailSettings['reply_to_name'] ?: null;
        $returnPath = ($smtp?->return_path_match && $fromEmail) ? $fromEmail : null;

        $mailer = $this->resolveMailer($smtp);

        $mailer->html($html, function (Message $msg) use ($sequence, $contact, $subject, $fromEmail, $fromName, $replyEmail, $replyName, $returnPath) {
            $msg->to($contact->email)->subject($subject);

            if ($fromEmail) {
                $msg->from($fromEmail, $fromName);
            }
            if ($replyEmail) {
                $msg->replyTo($replyEmail, $replyName);
            }
            if ($returnPath) {
                $msg->returnPath($returnPath);
            }
            $msg->getHeaders()->addTextHeader('X-Entity-Ref-ID', 'seq_' . $sequence->id . '_' . $contact->id . '_' . time());
        });

        $sequence->increment('emails_sent');

        CampaignEvent::create([
            'campaign_id' => $sequence->id,
            'contact_id'  => $contact->id,
            'node_id'     => $step['id'],
            'event_type'  => 'sent',
        ]);

        $contact->logActivity('email_sent', "Sequence email sent: \"{$subject}\"");

        return true;
    }

    /**
     * Mirrors SendCampaignEmailJob::recordBounce() so sequence bounces show
     * up the same way Campaign bounces do: a real `bounced` event (not a
     * generic error), the contact flipped to `bounced` so every other
     * campaign/sequence resolver excludes them going forward, and an
     * activity log entry.
     */
    private function recordBounce(Campaign $sequence, Contact $contact, array $step, string $reason): void
    {
        $sequence->increment('emails_bounced');

        CampaignEvent::updateOrCreate(
            [
                'campaign_id' => $sequence->id,
                'contact_id'  => $contact->id,
                'node_id'     => $step['id'],
            ],
            [
                'event_type' => 'bounced',
                'reason'     => $reason,
            ]
        );

        if ($contact->status !== 'bounced') {
            $contact->update(['status' => 'bounced']);
        }

        $contact->logActivity('email_bounced', "Sequence email bounced: \"{$sequence->title}\" - {$reason}");
    }

    private function resolveMailer(?SmtpConfig $smtp): \Illuminate\Mail\Mailer
    {
        if ($smtp) {
            return Mail::mailer(SmtpConfigService::configureRuntime($smtp));
        }
        return Mail::mailer();
    }
}
