<?php

namespace App\Actions\Communications;

use App\Enum\Contact\MessageChannel;
use App\Enum\Contact\MessageDirection;
use App\Enum\Contact\MessageProvider;
use App\Enum\Contact\MessageStatus;
use App\Jobs\Contact\SendContactMessageJob;
use App\Models\Client\Client;
use App\Models\Contact\Hub\ContactMessage;
use App\Models\Contact\Hub\ContactMessageAttachment;
use App\Models\Document\Document;
use App\Models\Solicitor\Solicitor;
use App\Models\Solicitor\SolicitorEmailSender;
use App\Models\Solicitor\SolicitorSmsSender;
use App\Services\Communications\Placeholders\TemplateRenderer;
use App\Services\HistoryService;
use App\ValueObjects\Communications\DispatchRequest;
use App\ValueObjects\Communications\DispatchResult;
use App\ValueObjects\Communications\PlaceholderContext;
use App\ValueObjects\Communications\RenderedMessage;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\RateLimiter;

class DispatchContactMessageAction
{
    public function __construct(
        protected TemplateRenderer $renderer,
        protected ValidateRecipientAction $validateRecipient,
        protected HistoryService $history,
    ) {}

    public function __invoke(DispatchRequest $request): DispatchResult
    {
        $template = $request->template;
        $channel = $template?->channel ?? $request->channel;

        if (!$channel) {
            return DispatchResult::invalidRecipient('No channel specified.');
        }

        if ($template !== null) {
            if (!$template->is_active) {
                return DispatchResult::invalidRecipient('Template is inactive.');
            }
            if ($request->origin->isAutomatic() && !$template->allow_automatic_send) {
                return DispatchResult::invalidRecipient('Template does not allow automatic sending.');
            }
            if (!$request->origin->isAutomatic() && !$template->allow_manual_send) {
                return DispatchResult::invalidRecipient('Template does not allow manual sending.');
            }
        }

        // --- Sender resolution ---
        $sender = $this->resolveSender($request, $template, $channel);
        if (!$sender) {
            return DispatchResult::invalidRecipient('No sender identity configured for this channel.');
        }

        // --- Recipient validation ---
        // The send-to recipient may differ from the messageable (e.g. scheme, staff
        // user, institution contact, or redress case worker). Placeholders, documents,
        // history and dedupe all stay keyed on $messageable below.
        $solicitor = app(Solicitor::class);
        $recipientSnapshot = ($this->validateRecipient)($channel, $request->recipient ?? $request->messageable, $solicitor);

        if (!$recipientSnapshot->canSend() && !$request->force) {
            if ($request->origin->isAutomatic()) {
                $message = $this->createRow($request, $template, $channel, $sender, $recipientSnapshot, null, MessageStatus::SUPPRESSED);
                $message->update([
                    'failure_reason' => $recipientSnapshot->reason,
                    'failed_at' => now(),
                ]);
                return DispatchResult::suppressed($message, $recipientSnapshot->reason ?? 'Recipient not contactable');
            }
            return DispatchResult::invalidRecipient($recipientSnapshot->reason ?? 'Recipient not contactable');
        }

        // --- Dedupe (automatic template sends only) ---
        $dedupeHash = $this->dedupeHash($request);

        if ($request->origin->isAutomatic() && $template !== null) {
            $windowMinutes = (int) ($request->originDetail['dedupe_window_minutes'] ?? config('contact.dedupe_window_minutes', 60));

            // Scope the dedupe to the resolved recipient as well as the messageable so a
            // template that broadcasts to several recipients (e.g. recipient_type = user
            // or institution) queues one message per recipient instead of collapsing to
            // the first. Recipients with no contactable value fall back to messageable-level
            // dedupe.
            $recipientValue = $recipientSnapshot->value();
            $recipientColumn = $channel === MessageChannel::EMAIL ? 'recipient_email' : 'recipient_phone';

            $existing = ContactMessage::query()
                ->where('template_id', $template->id)
                ->where('messageable_type', $request->messageable->getMorphClass())
                ->where('messageable_id', $request->messageable->getKey())
                ->where('origin_type', $request->origin->value)
                ->when(
                    $recipientValue !== null,
                    fn ($q) => $q->where($recipientColumn, $recipientValue),
                )
                ->where('created_at', '>=', now()->subMinutes($windowMinutes))
                ->first();

            if ($existing) {
                return DispatchResult::deduped($existing, "Duplicate send within {$windowMinutes} minute window.");
            }
        }

        // --- Rate limit per recipient ---
        $recipientKey = $recipientSnapshot->value() ?? 'unknown';
        $rateKey = "contact:{$channel->value}:{$recipientKey}";
        $attempts = (int) config('contact.rate_limit.attempts', 5);
        $decay = (int) config('contact.rate_limit.decay_seconds', 60);

        if (RateLimiter::tooManyAttempts($rateKey, $attempts)) {
            Log::warning('Contact rate limit hit', ['key' => $rateKey]);
            return DispatchResult::rateLimited("Rate limit hit: {$attempts} per {$decay}s");
        }
        RateLimiter::hit($rateKey, $decay);

        // --- Render ---
        if ($template !== null && $request->reviewBodyText !== null) {
            $rendered = $this->buildReviewRendered($request, $channel, $sender);
        } elseif ($template !== null) {
            $ctx = PlaceholderContext::fromMessageable(
                messageable: $request->messageable,
                related: $request->related,
                solicitor: $solicitor,
                user: $request->user ?? auth()->user(),
                sender: $sender,
                extra: $request->extraContext,
            );
            // Automatic sends prefer to fail loud on unknown placeholders, but an
            // unrecognised token must never silently swallow the whole send (the
            // trigger evaluator only logs the exception). Fall back to a graceful
            // render so the message still goes out, and log the offending tokens.
            try {
                $rendered = $this->renderer->render($template, $ctx, strict: $request->origin->isAutomatic());
            } catch (\App\Exceptions\Communications\UnknownPlaceholderException $e) {
                Log::warning('Contact template rendered with unknown placeholders; sending anyway', [
                    'template_id'     => $template->id,
                    'origin'          => $request->origin->value,
                    'unknown_tokens'  => $e->unknownTokens,
                ]);
                $rendered = $this->renderer->render($template, $ctx, strict: false);
            }
        } else {
            $rendered = $this->buildFreeTextRendered($request, $channel, $sender);
        }

        // --- Persist + enqueue ---
        $message = DB::transaction(function () use ($request, $template, $channel, $sender, $recipientSnapshot, $rendered, $dedupeHash) {
            return $this->createRow(
                $request, $template, $channel, $sender, $recipientSnapshot, $rendered,
                status: MessageStatus::QUEUED,
                dedupeHash: $dedupeHash,
            );
        });

        $this->attachDocuments($request, $message);

        SendContactMessageJob::dispatch($message->id);

        $this->logHistory($request, $message, $channel);

        return DispatchResult::queued($message);
    }

    private function resolveSender(DispatchRequest $request, $template, MessageChannel $channel)
    {
        if ($channel === MessageChannel::EMAIL) {
            if ($request->senderEmailId) {
                return SolicitorEmailSender::query()->find($request->senderEmailId);
            }
            if ($template?->default_sender_email_id) {
                return $template->defaultSenderEmail;
            }
            $solicitor = app(Solicitor::class);
            return $solicitor?->emailSenderFor($template?->config['sender_purpose'] ?? 'general');
        }

        if ($request->smsSenderId) {
            return SolicitorSmsSender::query()->find($request->smsSenderId);
        }
        if ($template?->default_sms_sender_id) {
            return $template->defaultSmsSender;
        }
        return SolicitorSmsSender::query()
            ->active()
            ->orderByDesc('is_default')
            ->first();
    }

    private function buildReviewRendered(DispatchRequest $request, MessageChannel $channel, $sender): RenderedMessage
    {
        $bodyText = $request->reviewBodyText ?? '';

        if ($channel === MessageChannel::EMAIL) {
            $bodyHtml = nl2br(htmlspecialchars($bodyText, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
            if ($sender->signatureHtml()) {
                $bodyHtml .= config('contact.signature.html_separator', '<br/><br/>') . $sender->signatureHtml();
            }
            if ($sender->signatureText()) {
                $bodyText .= config('contact.signature.text_separator', "\n\n") . $sender->signatureText();
            }
        } else {
            $bodyHtml = null;
        }

        return new RenderedMessage(
            subject: $request->reviewSubject ?: null,
            bodyText: $bodyText,
            bodyHtml: $bodyHtml,
        );
    }

    private function buildFreeTextRendered(DispatchRequest $request, MessageChannel $channel, $sender): RenderedMessage
    {
        $bodyText = $request->freeBodyText ?? '';

        if ($channel === MessageChannel::EMAIL) {
            $bodyHtml = nl2br(htmlspecialchars($bodyText, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
            if ($sender->signatureHtml()) {
                $bodyHtml .= config('contact.signature.html_separator', '<br/><br/>') . $sender->signatureHtml();
            }
            if ($sender->signatureText()) {
                $bodyText .= config('contact.signature.text_separator', "\n\n") . $sender->signatureText();
            }
        } else {
            $bodyHtml = null;
        }

        return new RenderedMessage(
            subject: $request->freeSubject ?: null,
            bodyText: $bodyText,
            bodyHtml: $bodyHtml,
        );
    }

    private function createRow(
        DispatchRequest $request,
                        $template,
        MessageChannel $channel,
                        $sender,
                        $recipientSnapshot,
                        $rendered,
        MessageStatus $status,
        ?string $dedupeHash = null,
    ): ContactMessage {
        $now = now();

        return ContactMessage::create([
            // Set the tenant explicitly rather than relying on the BelongsToSolicitor
            // auto-stamp: that hook is skipped in console/queue context, so dispatching
            // from a job (e.g. bulk sends) would otherwise insert a null solicitor_id.
            'solicitor_id' => app(Solicitor::class)?->id,
            'channel' => $channel,
            'direction' => MessageDirection::OUTBOUND,
            'provider' => $channel === MessageChannel::EMAIL
                ? MessageProvider::SENDGRID
                : MessageProvider::TWILIO,
            'status' => $status,
            'origin_type' => $request->origin,
            'origin_detail' => $request->originDetail,
            'messageable_type' => $request->messageable->getMorphClass(),
            'messageable_id' => $request->messageable->getKey(),
            'related_type' => $request->related?->getMorphClass(),
            'related_id' => $request->related?->getKey(),
            'template_id' => $template?->id,
            'sender_email_id' => $sender instanceof SolicitorEmailSender ? $sender->id : null,
            'sms_sender_id' => $sender instanceof SolicitorSmsSender ? $sender->id : null,
            'recipient_name' => $recipientSnapshot->name,
            'recipient_email' => $recipientSnapshot->email,
            'recipient_phone' => $recipientSnapshot->phone,
            'sender_display' => $sender->label(),
            'sender_value' => $sender->value(),
            'reply_to' => $sender->replyTo(),
            'subject' => $rendered?->subject,
            'body_text' => $rendered?->bodyText,
            'body_html' => $rendered?->bodyHtml,
            'rendered_placeholders' => $rendered?->placeholdersUsed,
            'dedupe_hash' => $dedupeHash,
            'queued_at' => $status === MessageStatus::QUEUED ? $now : null,
            'created_by' => auth()->id() ?? $request->user?->id,
        ]);
    }

    private function dedupeHash(DispatchRequest $request): string
    {
        return md5(implode('|', [
            $request->template?->id,
            $request->messageable->getMorphClass(),
            $request->messageable->getKey(),
            $request->origin->value,
            json_encode($request->originDetail),
        ]));
    }

    private function attachDocuments(DispatchRequest $request, ContactMessage $message): void
    {
        $docTypes = $request->template?->documentTypes()->orderBy('sort_order')->get();
        if (($docTypes && $docTypes->isEmpty()) || !$request->template) {
            return;
        }

        $overrides = $request->extraContext['attachment_overrides'] ?? [];

        // Resolve the client model to look up documents.
        // The messageable might be the Client directly, or another model (Scheme, User, etc.)
        // in which case the ComposeMessage component passes client context in extraContext.
        $client = null;
        if ($request->messageable instanceof Client) {
            $client = $request->messageable;
        } elseif (isset($request->extraContext['document_client_id'], $request->extraContext['document_client_morph'])) {
            $client = Client::find($request->extraContext['document_client_id']);
        } elseif (method_exists($request->messageable, 'client')) {
            $client = $request->messageable->client;
        }

        if (!$client) {
            return;
        }

        foreach ($docTypes as $docType) {
            $overrideId = $overrides[$docType->id] ?? null;

            $documents = Document::where('owner_type', $client->getMorphClass())
                ->where('owner_id', $client->getKey())
                ->where('type_id', $docType->id)
                ->when(
                    $overrideId,
                    fn ($query) => $query->whereKey($overrideId),
                )
                ->latest()
                ->get();

            foreach ($documents as $document) {
                $storagePath = $client->getDocumentStoragePath() . $document->file_name;

                ContactMessageAttachment::create([
                    'contact_message_id' => $message->id,
                    'disk'               => 'public',
                    'path'               => $storagePath,
                    'filename'           => $document->file_name,
                    'mime'               => $document->file_type,
                    'size'               => (int) $document->getRawOriginal('file_size'),
                ]);
            }
        }
    }

    private function logHistory(DispatchRequest $request, ContactMessage $message, MessageChannel $channel): void
    {
        try {
            if ($request->messageable instanceof \App\Interface\HistoryOwner) {
                $label = $channel === MessageChannel::EMAIL ? 'Email' : 'SMS';
                $subject = $message->subject ?: 'message';
                $this->history->add(
                    $request->messageable,
                    "{$label} queued: \"{$subject}\" via {$message->sender_display}",
                    ['contact_message_id' => $message->id],
                );
            }
        } catch (\Throwable $e) {
            Log::debug('History log skipped for contact message', ['error' => $e->getMessage()]);
        }
    }
}
