<?php
/**
 * Invoice Ninja (https://invoiceninja.com).
 *
 * @link https://github.com/invoiceninja/invoiceninja source repository
 *
 * @copyright Copyright (c) 2026. Invoice Ninja LLC (https://invoiceninja.com)
 *
 * @license https://www.elastic.co/licensing/elastic-license
 */
namespace App\Jobs\SES;

use App\DataMapper\Analytics\Mail\EmailBounce;
use App\DataMapper\Analytics\Mail\EmailSpam;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation;
use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Models\SystemLog;
use App\Notifications\Ninja\EmailBounceNotification;
use App\Notifications\Ninja\EmailSpamNotification;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Turbo124\Beacon\Facades\LightLogs;

class SESWebhook implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public $tries = 1;

    public $invitation;

    private $entity;

    private array $default_response = [
        'recipients' => '',
        'subject' => 'Message not found.',
        'entity' => '',
        'entity_id' => '',
        'events' => [],
        'event_type' => 'unknown',
        'timestamp' => '',
        'message_id' => '',
    ];

    private ?Company $company = null;

    /**
     * Create a new job instance.
     *
     */
    public function __construct(private array $request) {}

    private function getSystemLog(string $message_id): ?SystemLog
    {
        return SystemLog::query()
            ->where('company_id', $this->invitation->company_id)
            ->where('type_id', SystemLog::TYPE_WEBHOOK_RESPONSE)
            ->where('category_id', SystemLog::CATEGORY_MAIL)
            ->where('log->MessageID', $message_id)
            ->orderBy('id', 'desc')
            ->first();
    }

    private function updateSystemLog(SystemLog $system_log, array $data): void
    {
        // Get existing log data or initialize empty array
        $existing_log = $system_log->log ?? [];
        $existing_history = $existing_log['history'] ?? [];
        $existing_events = $existing_history['events'] ?? [];

        // Get new event data from the current webhook
        $new_event = $this->extractEventData();

        // Check if this event type already exists in events array
        $event_type = $this->getCurrentEventType();
        $event_exists = false;

        foreach ($existing_events as $event) {
            if (isset($event['event_type']) && $event['event_type'] === $event_type) {
                $event_exists = true;
                break;
            }
        }

        // Only add new event if this event type doesn't already exist
        if (!$event_exists && !empty($new_event)) {
            $existing_events[] = $new_event;
        }

        // Update the history with existing events plus any new event
        $updated_history = array_merge($existing_history, [
            'events' => $existing_events,
        ]);

        // Update the log with existing data plus updated history
        $system_log->log = array_merge($existing_log, [
            'history' => $updated_history,
            'last_updated' => now()->toISOString(),
            'last_event_type' => $event_type,
        ]);

        $system_log->save();
    }

    /**
     * Get the current event type being processed
     */
    private function getCurrentEventType(): string
    {
        $notification_type = $this->request['eventType'] ?? $this->request['Type'] ?? $this->request['notificationType'] ?? '';

        switch ($notification_type) {
            case 'Delivery':
            case 'Received':
                return 'delivery';
            case 'Bounce':
                return 'bounce';
            case 'Complaint':
                return 'complaint';
            case 'Open':
            case 'Rendering Failure':
                return 'open';
            default:
                return 'unknown';
        }
    }

    /**
     * Extract company key from SES message tags or metadata
     */
    private function extractCompanyKey(): ?string
    {
        // Check various possible locations for company key

        // Check mail tags
        if (isset($this->request['mail']['tags']['company_key'])) {
            nlog("SESWebhook: Found company key in mail tags: " . $this->request['mail']['tags']['company_key']);
            return $this->request['mail']['tags']['company_key'];
        }

        // Check email headers - specifically X-Tag which contains the company key
        if (isset($this->request['mail']['headers'])) {
            nlog("SESWebhook: Checking mail headers for X-Tag", [
                'headers_count' => count($this->request['mail']['headers']),
                'headers' => $this->request['mail']['headers'],
            ]);

            foreach ($this->request['mail']['headers'] as $header) {
                if (isset($header['name']) && $header['name'] === 'X-Tag' && isset($header['value'])) {
                    nlog("SESWebhook: Found X-Tag header: " . $header['value']);
                    return $header['value'];
                }
            }

            nlog("SESWebhook: X-Tag header not found in mail headers");
        }

        // Check common headers
        if (isset($this->request['mail']['commonHeaders']['x-company-key'])) {
            nlog("SESWebhook: Found company key in common headers: " . $this->request['mail']['commonHeaders']['x-company-key']);
            return $this->request['mail']['commonHeaders']['x-company-key'];
        }

        // Check if company key is in the main request
        if (isset($this->request['company_key'])) {
            nlog("SESWebhook: Found company key in main request: " . $this->request['company_key']);
            return $this->request['company_key'];
        }

        nlog("SESWebhook: No company key found in any location", [
            'mail_headers_exists' => isset($this->request['mail']['headers']),
            'mail_common_headers_exists' => isset($this->request['mail']['commonHeaders']),
            'request_keys' => array_keys($this->request),
        ]);

        return null;
    }

    /**
     * Extract message ID from SES notification
     */
    private function extractMessageId(): ?string
    {
        // Check various possible locations for message ID
        if (isset($this->request['mail']['messageId'])) {
            nlog("SESWebhook: Found message ID in mail: " . $this->request['mail']['messageId']);
            return $this->request['mail']['messageId'];
        }

        if (isset($this->request['messageId'])) {
            nlog("SESWebhook: Found message ID in main request: " . $this->request['messageId']);
            return $this->request['messageId'];
        }

        nlog("SESWebhook: No message ID found in any location");
        return null;
    }

    /**
     * Execute the job.
     *
     */
    public function handle()
    {
        nlog("SESWebhook: Processing SES webhook data", ['request' => $this->request]);

        // Extract company key from SES message tags or metadata
        $company_key = $this->extractCompanyKey();

        if (!$company_key) {
            nlog("SESWebhook: No company key found in webhook data");
            return;
        }

        MultiDB::findAndSetDbByCompanyKey($company_key);
        $this->company = Company::query()->where('company_key', $company_key)->first();

        if (!$this->company) {
            nlog("SESWebhook: Company not found for key: " . $company_key);
            return;
        }

        // Extract message ID from SES notification
        $message_id = $this->extractMessageId();

        if (!$message_id) {
            nlog("SESWebhook: No message ID found in webhook data");
            return;
        }

        $this->invitation = $this->discoverInvitation($message_id);

        /** Free accounts do not have email delivery meta data stored. */
        if (!$this->invitation || (Ninja::isHosted() && $this->company->account->isFreeHostedClient())) {
            nlog("SESWebhook: No invitation found for message ID: " . $message_id);
            return;
        }

        // Handle different SES notification types
        $notification_type = $this->request['eventType'] ?? $this->request['Type'] ?? $this->request['notificationType'] ?? '';

        switch ($notification_type) {
            case 'Delivery':
            case 'Received':
                return $this->processDelivery();
            case 'Bounce':
                return $this->processBounce();
            case 'Complaint':
                return $this->processComplaint();
            case 'Open':
            case 'Rendering Failure':
                return $this->processOpen();
            default:
                nlog("SESWebhook: Unknown notification type: " . $notification_type);
                break;
        }
    }

    /**
     * Process email delivery confirmation
     */
    private function processDelivery()
    {
        $this->invitation->email_status = 'delivered';
        $this->invitation->saveQuietly();

        $this->request['MessageID'] = $this->extractMessageId();
        $data = array_merge($this->request, [
            'history' => $this->fetchMessage(),
            'MessageID' => $this->extractMessageId(),
        ]);

        $sl = $this->getSystemLog($this->extractMessageId());

        if ($sl) {
            $this->updateSystemLog($sl, $data);
            return;
        }

        (
            new SystemLogger(
                $data,
                SystemLog::CATEGORY_MAIL,
                SystemLog::EVENT_MAIL_DELIVERY,
                SystemLog::TYPE_WEBHOOK_RESPONSE,
                $this->invitation->contact->client,
                $this->invitation->company
            )
        )->handle();
    }

    /**
     * Process email bounce
     */
    private function processBounce()
    {
        $this->invitation->email_status = 'bounced';
        $this->invitation->saveQuietly();

        // Check if this is a confirmation email bounce
        $subject = $this->request['bounce']['bouncedRecipients'][0]['email'] ?? '';
        if ($subject == ctrans('texts.confirmation_subject')) {
            $this->company->notification(new EmailBounceNotification($subject))->ninja();
        }

        $bounce = new EmailBounce(
            $this->extractCompanyKey(),
            $this->request['mail']['source'] ?? '',
            $this->extractMessageId()
        );

        LightLogs::create($bounce)->send();

        $data = array_merge($this->request, [
            'history' => $this->fetchMessage(),
            'MessageID' => $this->extractMessageId(),
        ]);

        $sl = $this->getSystemLog($this->extractMessageId());

        if ($sl) {
            $this->updateSystemLog($sl, $data);
            return;
        }

        (
            new SystemLogger(
                $data,
                SystemLog::CATEGORY_MAIL,
                SystemLog::EVENT_MAIL_BOUNCED,
                SystemLog::TYPE_WEBHOOK_RESPONSE,
                $this->invitation->contact->client,
                $this->invitation->company
            )
        )->handle();
    }

    /**
     * Process spam complaint
     */
    private function processComplaint()
    {
        $this->invitation->email_status = 'spam';
        $this->invitation->saveQuietly();

        if (config('ninja.notification.slack')) {
            $this->company->notification(new EmailSpamNotification($this->company))->ninja();
        }

        $spam = new EmailSpam(
            $this->extractCompanyKey(),
            $this->request['mail']['source'] ?? '',
            $this->extractMessageId()
        );

        LightLogs::create($spam)->send();

        $data = array_merge($this->request, [
            'history' => $this->fetchMessage(),
            'MessageID' => $this->extractMessageId(),
        ]);

        $sl = $this->getSystemLog($this->extractMessageId());

        if ($sl) {
            $this->updateSystemLog($sl, $data);
            return;
        }

        (
            new SystemLogger(
                $data,
                SystemLog::CATEGORY_MAIL,
                SystemLog::EVENT_MAIL_SPAM_COMPLAINT,
                SystemLog::TYPE_WEBHOOK_RESPONSE,
                $this->invitation->contact->client,
                $this->invitation->company
            )
        )->handle();
    }

    /**
     * Process email open
     */
    private function processOpen()
    {
        $this->invitation->opened_date = now();
        $this->invitation->saveQuietly();

        $data = array_merge($this->request, [
            'history' => $this->fetchMessage(),
            'MessageID' => $this->extractMessageId(),
        ]);

        $sl = $this->getSystemLog($this->extractMessageId());

        if ($sl) {
            $this->updateSystemLog($sl, $data);
            return;
        }

        (
            new SystemLogger(
                $data,
                SystemLog::CATEGORY_MAIL,
                SystemLog::EVENT_MAIL_OPENED,
                SystemLog::TYPE_WEBHOOK_RESPONSE,
                $this->invitation->contact->client,
                $this->invitation->company
            )
        )->handle();
    }

    /**
     * Discover invitation by message ID
     */
    private function discoverInvitation($message_id)
    {
        $invitation = false;

        if ($invitation = InvoiceInvitation::where('message_id', $message_id)->first()) {
            $this->entity = 'invoice';
            return $invitation;
        } elseif ($invitation = QuoteInvitation::where('message_id', $message_id)->first()) {
            $this->entity = 'quote';
            return $invitation;
        } elseif ($invitation = RecurringInvoiceInvitation::where('message_id', $message_id)->first()) {
            $this->entity = 'recurring_invoice';
            return $invitation;
        } elseif ($invitation = CreditInvitation::where('message_id', $message_id)->first()) {
            $this->entity = 'credit';
            return $invitation;
        } elseif ($invitation = PurchaseOrderInvitation::where('message_id', $message_id)->first()) {
            $this->entity = 'purchase_order';
            return $invitation;
        } else {
            return $invitation;
        }
    }

    /**
     * Fetch message details and create response data
     */
    private function fetchMessage(): array
    {
        $message_id = $this->extractMessageId();

        if (strlen($message_id) < 1) {
            return $this->default_response;
        }

        try {
            // Extract information from SES webhook data
            $recipients = $this->extractRecipients();
            $subject = $this->extractSubject();
            $events = $this->extractEvents();
            $event_type = $this->getCurrentEventType();

            return [
                'recipients' => $recipients,
                'subject' => $subject,
                'entity' => $this->entity ?? '',
                'entity_id' => $this->invitation->{$this->entity}->hashed_id ?? '',
                'events' => [$this->extractEventData()], // Start with single event in array
                'event_type' => $event_type,
                'timestamp' => now()->toISOString(),
                'message_id' => $message_id,
            ];

        } catch (\Exception $e) {
            nlog("SESWebhook: Error fetching message: " . $e->getMessage());
            return $this->default_response;
        }
    }

    /**
     * Extract recipients from SES webhook data
     */
    private function extractRecipients(): string
    {
        if (isset($this->request['mail']['destination'])) {
            return is_array($this->request['mail']['destination'])
                ? implode(',', $this->request['mail']['destination'])
                : $this->request['mail']['destination'];
        }

        if (isset($this->request['bounce']['bouncedRecipients'])) {
            return collect($this->request['bounce']['bouncedRecipients'])
                ->pluck('emailAddress')
                ->implode(',');
        }

        if (isset($this->request['complaint']['complainedRecipients'])) {
            return collect($this->request['complaint']['complainedRecipients'])
                ->pluck('emailAddress')
                ->implode(',');
        }

        return '';
    }

    /**
     * Extract subject from SES webhook data
     */
    private function extractSubject(): string
    {
        return $this->request['mail']['commonHeaders']['subject']
               ?? $this->request['bounce']['bouncedRecipients'][0]['email']
               ?? '';
    }

    /**
     * Extract events from the webhook data
     */
    private function extractEvents(): array
    {
        $event_type = $this->getCurrentEventType();
        $message_id = $this->extractMessageId();

        switch ($event_type) {
            case 'delivery':
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Delivered',
                    'delivery_message' => $this->request['delivery']['smtpResponse'] ?? 'Successfully delivered',
                    'server' => $this->request['delivery']['processingTimeMillis'] ?? '',
                    'server_ip' => $this->request['delivery']['remoteMtaIp'] ?? '',
                    'date' => $this->request['delivery']['timestamp'] ?? now()->toISOString(),
                ];

            case 'bounce':
                $bounce_data = $this->request['bounce'] ?? [];
                return [
                    'bounce_id' => $bounce_data['bounceId'] ?? '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Bounced',
                    'bounce_type' => $bounce_data['bounceType'] ?? '',
                    'bounce_sub_type' => $bounce_data['bounceSubType'] ?? '',
                    'date' => $bounce_data['timestamp'] ?? now()->toISOString(),
                ];

            case 'complaint':
                $complaint_data = $this->request['complaint'] ?? [];
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Spam Complaint',
                    'complaint_type' => $complaint_data['complaintFeedbackType'] ?? '',
                    'date' => $complaint_data['timestamp'] ?? now()->toISOString(),
                ];

            case 'open':
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Opened',
                    'date' => now()->toISOString(),
                ];

            default:
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Unknown',
                    'date' => now()->toISOString(),
                ];
        }
    }

    /**
     * Extract event data for the current webhook
     */
    private function extractEventData(): array
    {
        $event_type = $this->getCurrentEventType();
        $message_id = $this->extractMessageId();

        switch ($event_type) {
            case 'delivery':
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Delivered',
                    'delivery_message' => $this->request['delivery']['smtpResponse'] ?? 'Successfully delivered',
                    'server' => $this->request['delivery']['processingTimeMillis'] ?? '',
                    'server_ip' => $this->request['delivery']['remoteMtaIp'] ?? '',
                    'date' => $this->request['delivery']['timestamp'] ?? now()->toISOString(),
                    'event_type' => $event_type,
                    'timestamp' => now()->toISOString(),
                    'message_id' => $message_id,
                ];

            case 'bounce':
                $bounce_data = $this->request['bounce'] ?? [];
                return [
                    'bounce_id' => $bounce_data['bounceId'] ?? '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Bounced',
                    'bounce_type' => $bounce_data['bounceType'] ?? '',
                    'bounce_sub_type' => $bounce_data['bounceSubType'] ?? '',
                    'date' => $bounce_data['timestamp'] ?? now()->toISOString(),
                    'event_type' => $event_type,
                    'timestamp' => now()->toISOString(),
                    'message_id' => $message_id,
                ];

            case 'complaint':
                $complaint_data = $this->request['complaint'] ?? [];
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Spam Complaint',
                    'complaint_type' => $complaint_data['complaintFeedbackType'] ?? '',
                    'date' => $complaint_data['timestamp'] ?? now()->toISOString(),
                    'event_type' => $event_type,
                    'timestamp' => now()->toISOString(),
                    'message_id' => $message_id,
                ];

            case 'open':
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Opened',
                    'date' => now()->toISOString(),
                    'event_type' => $event_type,
                    'timestamp' => now()->toISOString(),
                    'message_id' => $message_id,
                ];

            default:
                return [
                    'bounce_id' => '',
                    'recipient' => $this->extractRecipients(),
                    'status' => 'Unknown',
                    'date' => now()->toISOString(),
                    'event_type' => $event_type,
                    'timestamp' => now()->toISOString(),
                    'message_id' => $message_id,
                ];
        }
    }

    /**
     * Handle job failure
     */
    public function failed($exception = null)
    {
        if ($exception) {
            nlog("SESWebhook:: " . $exception->getMessage());
        }

        config(['queue.failed.driver' => null]);
    }
}
