<?php

namespace Modules\MedNotify\Services;

use App\Business;
use App\Contact;
use App\User;
use Carbon\Carbon;
use Modules\MedNotify\Entities\MednotifyDigestQueue;
use Modules\MedNotify\Entities\MednotifyEvent;
use Modules\MedNotify\Entities\MednotifyLog;
use Modules\MedNotify\Entities\MednotifyPreference;
use Modules\MedNotify\Entities\MednotifyTemplate;
use Modules\MedNotify\Services\Channels\EmailChannel;
use Modules\MedNotify\Services\Channels\InAppChannel;
use Modules\MedNotify\Services\Channels\SmsChannel;
use Modules\MedNotify\Services\Channels\WhatsAppChannel;

/**
 * MedNotify — the single public notification entry point (SIFA plan P2-T1).
 *
 * Other modules call:
 *
 *   app(\Modules\MedNotify\Services\MedNotify::class)->notify('appointment.created', [
 *       'business_id' => 5,
 *       'users'       => [12, 34],          // ERP users.id (optional)
 *       'contacts'    => [88],              // ERP contacts.id (optional)
 *       'variables'   => ['appointment_date' => '2026-07-05 10:00', 'link' => url(...)],
 *       'channels'    => ['in_app', 'sms'], // optional override of the event default
 *   ]);
 *
 * FLOW: resolve event → for each channel: resolve template (business override
 * else system default; skip if disabled) → for each recipient: preference
 * check (disabled → skip; quiet hours / digest mode → queue) → adapter send →
 * log. Every step is business-scoped by $context['business_id'].
 *
 * Reuse-first: email/SMS go through the SAME pathways NotificationUtil uses,
 * WhatsApp through WaSender (only if installed), in-app through the Laravel
 * notifications table. No delivery mechanism is re-implemented here.
 */
class MedNotify
{
    /** channel key => adapter instance (lazy). */
    protected array $adapters = [];

    /**
     * Fire a notification.
     *
     * @return array Per-channel per-recipient outcome summary
     *               [['channel'=>, 'recipient_type'=>, 'recipient_id'=>, 'status'=>], …]
     */
    public function notify(string $eventKey, array $context): array
    {
        $results = [];

        $businessId = (int) ($context['business_id'] ?? 0);
        if ($businessId <= 0) {
            \Log::warning("MedNotify::notify called without a business_id for event {$eventKey}");

            return $results;
        }

        $event = MednotifyEvent::resolve($businessId, $eventKey);
        if (! $event || ! $event->is_active) {
            // Unknown / inactive event — nothing to send (fail-soft, no throw).
            return $results;
        }

        $business = Business::find($businessId);

        // Channels: explicit override → event default → all configured channels.
        $channels = $context['channels']
            ?? $event->default_channels
            ?? config('mednotify.channels', ['in_app', 'email', 'sms', 'whatsapp']);

        $recipients = $this->resolveRecipients($businessId, $context);
        if (empty($recipients)) {
            return $results;
        }

        $variables = (array) ($context['variables'] ?? []);

        foreach ($channels as $channel) {
            $template = MednotifyTemplate::resolve($businessId, $eventKey, $channel);
            if (! $template || ! $template->enabled) {
                continue; // no template for this channel, or explicitly disabled
            }

            foreach ($recipients as $recipient) {
                $results[] = $this->deliverToRecipient(
                    $businessId,
                    $eventKey,
                    $channel,
                    $template,
                    $recipient,
                    $variables,
                    $business
                );
            }
        }

        return $results;
    }

    /**
     * Resolve one recipient on one channel: apply preferences, quiet hours /
     * digest routing, then send via the adapter and log the outcome.
     */
    protected function deliverToRecipient(
        int $businessId,
        string $eventKey,
        string $channel,
        MednotifyTemplate $template,
        array $recipient,
        array $variables,
        ?Business $business
    ): array {
        $outcome = ['channel' => $channel, 'recipient_type' => $recipient['type'], 'recipient_id' => $recipient['id']];

        // Preferences apply only to ERP users (contacts have no in-app account).
        $pref = $recipient['type'] === 'user'
            ? PreferenceResolver::resolve($businessId, (int) $recipient['id'], $eventKey, $channel)
            : null;

        if ($pref && $pref->enabled === false) {
            $this->log($businessId, $eventKey, $channel, $recipient, null, 'skipped', 'user opted out');

            return $outcome + ['status' => 'skipped'];
        }

        // Interpolate the rendered subject/body for this recipient.
        $rendered = $this->render($template, $recipient, $variables, $business);

        // Quiet hours or a non-immediate digest → defer to the digest queue.
        $digestMode = $this->digestModeFor($pref, $recipient['type']);
        if ($digestMode !== 'immediate') {
            $this->queueDigest($businessId, $recipient, $eventKey, $channel, $rendered, $variables, $digestMode);
            $this->log($businessId, $eventKey, $channel, $recipient, $rendered['subject'], 'digested', null);

            return $outcome + ['status' => 'digested'];
        }

        // Immediate send via the channel adapter (fail-soft).
        $result = $this->adapter($channel)->send($rendered, $recipient, [
            'business_id' => $businessId,
            'event_key'   => $eventKey,
            'business'    => $business,
            'variables'   => $variables,
        ]);

        $this->log(
            $businessId,
            $eventKey,
            $channel,
            $recipient,
            $rendered['subject'],
            $result['status'],
            $result['error'],
            $result['status'] === 'sent' ? now() : null
        );

        return $outcome + ['status' => $result['status']];
    }

    /**
     * Decide the effective digest mode for this recipient right now:
     *   - a non-immediate preference digest → hourly|daily
     *   - inside the preference quiet-hours window → hourly (batch until later)
     *   - otherwise immediate
     */
    protected function digestModeFor(?MednotifyPreference $pref, string $recipientType): string
    {
        if (! $pref || $recipientType !== 'user') {
            return 'immediate';
        }

        if (in_array($pref->digest, ['hourly', 'daily'], true)) {
            return $pref->digest;
        }

        if ($this->inQuietHours($pref)) {
            return 'hourly';
        }

        return 'immediate';
    }

    /** True when "now" (business tz-agnostic local server time) is within quiet hours. */
    protected function inQuietHours(?MednotifyPreference $pref): bool
    {
        if (! $pref || empty($pref->quiet_from) || empty($pref->quiet_to)) {
            return false;
        }

        $now = Carbon::now()->format('H:i:s');
        $from = Carbon::parse($pref->quiet_from)->format('H:i:s');
        $to = Carbon::parse($pref->quiet_to)->format('H:i:s');

        if ($from <= $to) {
            // Same-day window, e.g. 22:00 → 23:00.
            return $now >= $from && $now <= $to;
        }

        // Overnight window, e.g. 22:00 → 07:00.
        return $now >= $from || $now <= $to;
    }

    /** Push a deferred send onto the digest queue. */
    protected function queueDigest(int $businessId, array $recipient, string $eventKey, string $channel, array $rendered, array $variables, string $digestMode): void
    {
        if ($recipient['type'] !== 'user') {
            return; // digests are user-inbox summaries; contacts always send immediately
        }

        MednotifyDigestQueue::create([
            'business_id' => $businessId,
            'user_id'     => (int) $recipient['id'],
            'event_key'   => $eventKey,
            'channel'     => $channel,
            'payload'     => [
                'subject'        => $rendered['subject'],
                'body'           => $rendered['body'],
                'variables'      => $variables,
                'recipient_type' => $recipient['type'],
                'recipient_id'   => $recipient['id'],
            ],
            'digest'    => $digestMode,
            'queued_at' => now(),
        ]);
    }

    /**
     * Interpolate {var} placeholders in the template subject + body from the
     * context variables plus the resolvable {business_name}, {user_name} and
     * {contact_name}.
     *
     * @return array{subject:?string, body:string}
     */
    protected function render(MednotifyTemplate $template, array $recipient, array $variables, ?Business $business): array
    {
        $replacements = [];
        foreach ($variables as $key => $value) {
            $replacements['{' . $key . '}'] = is_scalar($value) ? (string) $value : '';
        }

        $replacements['{business_name}'] = $business ? (string) $business->name : '';
        $replacements['{user_name}'] = $recipient['type'] === 'user' ? (string) ($recipient['name'] ?? '') : '';
        $replacements['{contact_name}'] = $recipient['type'] === 'contact' ? (string) ($recipient['name'] ?? '') : '';

        $subject = $template->subject !== null ? strtr($template->subject, $replacements) : null;
        $body = strtr((string) $template->body, $replacements);

        return ['subject' => $subject, 'body' => $body];
    }

    /**
     * Expand the ['users'=>[], 'contacts'=>[]] context into a flat recipient
     * list with the delivery coordinates each adapter needs. Business-scoped:
     * users/contacts are only loaded within the given business.
     */
    protected function resolveRecipients(int $businessId, array $context): array
    {
        $recipients = [];

        $userIds = array_values(array_filter(array_map('intval', (array) ($context['users'] ?? []))));
        if (! empty($userIds)) {
            $users = User::where('business_id', $businessId)->whereIn('id', $userIds)->get();
            foreach ($users as $user) {
                $recipients[] = [
                    'type'   => 'user',
                    'id'     => $user->id,
                    'name'   => $user->user_full_name,
                    'email'  => $user->email,
                    'mobile' => $user->contact_number ?? null,
                    'model'  => $user,
                ];
            }
        }

        $contactIds = array_values(array_filter(array_map('intval', (array) ($context['contacts'] ?? []))));
        if (! empty($contactIds)) {
            $contacts = Contact::where('business_id', $businessId)->whereIn('id', $contactIds)->get();
            foreach ($contacts as $contact) {
                $recipients[] = [
                    'type'   => 'contact',
                    'id'     => $contact->id,
                    'name'   => $contact->name,
                    'email'  => $contact->email,
                    'mobile' => $contact->mobile,
                    'model'  => $contact,
                ];
            }
        }

        return $recipients;
    }

    /** Write one audit row (never throws; log-and-swallow on DB error). */
    protected function log(int $businessId, string $eventKey, string $channel, array $recipient, ?string $subject, string $status, ?string $error, ?Carbon $sentAt = null): void
    {
        try {
            MednotifyLog::create([
                'business_id'     => $businessId,
                'event_key'       => $eventKey,
                'channel'         => $channel,
                'recipient_type'  => $recipient['type'],
                'recipient_id'    => $recipient['id'],
                'rendered_subject' => $subject,
                'status'          => $status,
                'error'           => $error,
                'sent_at'         => $sentAt,
            ]);
        } catch (\Throwable $e) {
            \Log::warning('MedNotify could not write log row: ' . $e->getMessage());
        }
    }

    /** Lazily instantiate + memoise the adapter for a channel. */
    protected function adapter(string $channel): Channels\ChannelContract
    {
        if (! isset($this->adapters[$channel])) {
            $this->adapters[$channel] = match ($channel) {
                'in_app'   => new InAppChannel(),
                'email'    => new EmailChannel(),
                'sms'      => new SmsChannel(),
                'whatsapp' => new WhatsAppChannel(),
                default    => new InAppChannel(),
            };
        }

        return $this->adapters[$channel];
    }
}
