<?php

namespace App\Services\Communications\Placeholders;

use App\Enum\Communication\PhoneNumberTypeSlug;
use App\Enum\Dsar\DsarStatus;
use App\Models\Claim\Claim;
use App\Models\Client\Client;
use App\Models\Communication\SolicitorPhoneNumber;
use App\Models\Solicitor\Solicitor;
use App\ValueObjects\Communications\PlaceholderContext;
use Illuminate\Support\Collection;

/**
 * Registers the default set of placeholders available to every template.
 * Additional tenant/custom placeholders can be added via
 * PlaceholderRegistry::register() from any service provider's boot() method.
 */
class DefaultPlaceholders
{
    /**
     * Known UK honorifics — used by smartName() to detect and repair legacy
     * data that was corrupted by the HasName::fullName setter (which split
     * "Mr John Smith" into name_first="Mr", name_last="John Smith").
     */
    private const KNOWN_TITLES = [
        'mr', 'mrs', 'ms', 'miss', 'mx', 'dr', 'prof', 'professor',
        'sir', 'dame', 'lord', 'lady', 'rev', 'reverend', 'hon',
    ];

    public static function register(PlaceholderRegistry $registry): void
    {
        // ---- E-Sign (values injected via the dispatch extraContext) ----
        $registry->register('esign.signing_url', null,
            fn (PlaceholderContext $c) => (string) ($c->extra['esign.signing_url'] ?? ''),
            description: 'E-signature signing link for the recipient to access');

        $registry->register('esign.contract_name', null,
            fn (PlaceholderContext $c) => (string) ($c->extra['esign.contract_name'] ?? ''),
            description: 'Name of the contract being e-signed');

        // ---- Client ----
        $registry->register('client.title', Client::class,
            fn (PlaceholderContext $c) => self::smartName(self::client($c))['title'],
            fallback: '',
            description: 'Client title / honorific (e.g. Mr, Mrs, Dr)');

        $registry->register('client.first_name', Client::class,
            fn (PlaceholderContext $c) => self::smartName(self::client($c))['first'],
            fallback: 'there',
            description: 'Client first name');

        $registry->register('client.last_name', Client::class,
            fn (PlaceholderContext $c) => self::smartName(self::client($c))['last'],
            fallback: '',
            description: 'Client last name');

        $registry->register('client.preferred_name', Client::class,
            fn (PlaceholderContext $c) => (($client = self::client($c)) ? self::rawOrLive($client, 'name_preferred') : null)
                ?: self::smartName(self::client($c))['first'],
            fallback: 'there',
            description: 'Preferred name if set, otherwise first name');

        $registry->register('client.full_name', Client::class,
            function (PlaceholderContext $c) {
                $client = self::client($c);
                $parts = self::smartName($client);
                $middle = $client ? self::rawOrLive($client, 'name_middle') : null;
                return trim(implode(' ', array_filter([
                    $parts['title'],
                    $parts['first'],
                    $middle,
                    $parts['last'],
                ])));
            },
            fallback: 'there',
            description: 'Client full name (title + first + middle + last)');

        $registry->register('client.email', Client::class,
            fn (PlaceholderContext $c) => (string) (self::client($c)?->email_address ?? ''),
            description: 'Client email address');

        $registry->register('client.phone', Client::class,
            fn (PlaceholderContext $c) => (string) (self::client($c)?->phone_number ?? ''),
            description: 'Client phone number');

        $registry->register('client.reference', Client::class,
            fn (PlaceholderContext $c) => self::client($c)?->displayId ?? '',
            description: 'Client reference / display id');

        // ---- Lead Source ----
        $registry->register('client.lead_source', Client::class,
            fn (PlaceholderContext $c) => self::client($c)?->leadSource->display_name ?? '',
            description: 'Clients lead source name');

        $registry->register('client.lead_source_endpoint', Client::class,
            fn (PlaceholderContext $c) => self::client($c)?->endpoint->display_name ?? '',
            description: 'The endpoint that client came from');

        // ---- Claim (also works when messageable is the Claim or when a Client with a Claim relation is passed) ----
        $registry->register('claim.reference', Claim::class,
            fn (PlaceholderContext $c) => self::claim($c)?->claim_id ?? '',
            description: 'Claim internal reference');

        $registry->register('claim.status_name', Claim::class,
            fn (PlaceholderContext $c) => self::claim($c)?->status?->display_name ?? '',
            description: 'Current claim status name');

        $registry->register('claim.redress_type', Claim::class,
            fn (PlaceholderContext $c) => self::claim($c)?->redress_type ?? '',
            description: 'Claim redress type');

        $registry->register('claim.handler_name', Claim::class,
            fn (PlaceholderContext $c) => optional(self::claim($c)?->handlers()->first())->name ?? '',
            description: 'Assigned handler name');

        // ---- DSAR targets (claim-scoped; counts only DSARs that have been sent) ----
        $registry->register('dsar.targets_count', Client::class,
            fn (PlaceholderContext $c) => (string) self::sentDsars($c)->count(),
            fallback: '0',
            description: 'Number of DSAR targets the claim has sent requests to');

        $registry->register('dsar.targets_list', Client::class,
            fn (PlaceholderContext $c) => self::sentDsars($c)
                ->map(fn ($d) => $d->institution?->display_name)
                ->filter()
                ->unique()
                ->values()
                ->implode(', '),
            description: 'Names of DSAR targets (recipients) as a comma-separated list');

        $registry->register('dsar.targets_received', Client::class,
            fn (PlaceholderContext $c) => (string) self::sentDsars($c)
                ->whereIn('status', DsarStatus::terminal())
                ->count(),
            fallback: '0',
            description: 'Number of DSAR targets that have responded (completed)');

        $registry->register('dsar.targets_not_received', Client::class,
            function (PlaceholderContext $c) {
                $sent = self::sentDsars($c);
                $received = $sent->whereIn('status', DsarStatus::terminal())->count();
                return (string) ($sent->count() - $received);
            },
            fallback: '0',
            description: 'Number of DSAR targets still outstanding (not completed)');

        // ---- Solicitor ----
        $registry->register('solicitor.name', null,
            fn (PlaceholderContext $c) => $c->solicitor?->name ?? '',
            description: 'Solicitor / firm name');

        $registry->register('solicitor.email', null,
            fn (PlaceholderContext $c) => (string) ($c->solicitor?->email_address ?? ''),
            description: 'Solicitor primary email address');

        $registry->register('solicitor.phone', null,
            fn (PlaceholderContext $c) => (string) ($c->solicitor?->phone_number ?? ''),
            description: 'Solicitor primary phone number');

        // ---- Dialler contact numbers (one per phone-number-type, solicitor-specific) ----
        // Dynamically registers dialler.main, dialler.client, dialler.evidence, etc.
        // so each configured line type is exposed as its own placeholder.
        foreach (PhoneNumberTypeSlug::cases() as $slug) {
            $registry->register("dialler.{$slug->value}", null,
                fn (PlaceholderContext $c) => self::solicitorPhoneOfType($c, $slug->value),
                description: $slug->label() . ' contact number for the current solicitor');
        }

        // ---- WhatsApp contact number (first WhatsApp-capable line for the solicitor) ----
        $registry->register('whatsapp.number', null,
            fn (PlaceholderContext $c) => self::solicitorWhatsappNumber($c),
            description: 'WhatsApp contact number (first WhatsApp-enabled line for the solicitor)');

        // ---- Sender identity ----
        $registry->register('sender.display_name', null,
            fn (PlaceholderContext $c) => $c->sender?->label() ?? '',
            description: 'Configured sender display name');

        $registry->register('sender.email', null,
            fn (PlaceholderContext $c) => $c->sender?->value() ?? '',
            description: 'Configured sender email or phone');

        // ---- Authenticated user ----
        $registry->register('user.name', null,
            fn (PlaceholderContext $c) => trim(($c->user?->name_first ?? '') . ' ' . ($c->user?->name_last ?? '')),
            description: 'Current user name');

        $registry->register('user.email', null,
            fn (PlaceholderContext $c) => (string) ($c->user?->email_address ?? ''),
            description: 'Current user email');

        // ---- System ----
        $registry->register('system.today', null,
            fn () => now()->format('d/m/Y'),
            description: "Today's date (d/m/Y)");

        $registry->register('system.today_long', null,
            fn () => now()->format('l jS F Y'),
            description: "Today's date (long form)");

        $registry->register('system.system_name', null,
            fn () => config('app.name'),
            description: 'System name');
    }

    private static function client(PlaceholderContext $c): ?Client
    {
        if ($c->messageable instanceof Client) return $c->messageable;
        if ($c->related instanceof Client) return $c->related;
        if ($c->messageable instanceof Claim) return $c->messageable->client;
        if ($c->related instanceof Claim) return $c->related->client;
        return null;
    }

    /**
     * Defensive name parser.  Returns ['title', 'first', 'last'] from a Client,
     * repairing legacy data corruption where the HasName::fullName setter has
     * cracked a value like "Mr John Smith" into name_first="Mr",
     * name_last="John Smith".
     *
     * Heuristic: when name_first matches a known UK honorific (Mr, Mrs, Dr, ...)
     * and name_last contains a space, treat name_first as the title and re-split
     * name_last into first + remaining parts.
     */
    private static function smartName(?Client $client): array
    {
        if (!$client) {
            return ['title' => null, 'first' => null, 'last' => null];
        }

        // Read the original DB values via getRawOriginal so we are immune to
        // any in-memory mutations triggered by Eloquent's cached cast merge
        // (HasName::fullName has a SET mutator + shouldCache that, when read
        // earlier in the request, can corrupt name_first / name_last on the
        // model instance later in the same request).
        $title = self::rawOrLive($client, 'title');
        $first = self::rawOrLive($client, 'name_first');
        $last = self::rawOrLive($client, 'name_last');

        if ($first && $last && str_contains($last, ' ')) {
            $stripped = strtolower(rtrim(trim($first), '.'));
            if (in_array($stripped, self::KNOWN_TITLES, true)) {
                // Promote name_first to title if title is blank, then re-split last.
                $title = $title ?: $first;
                $parts = preg_split('/\s+/', trim($last));
                $first = array_shift($parts);
                $last = implode(' ', $parts);
            }
        }

        return ['title' => $title, 'first' => $first, 'last' => $last];
    }

    /**
     * Read a name attribute, preferring the synced DB value but tolerating a
     * freshly-created model whose original hasn't been synced yet.
     *
     * getRawOriginal() reads Eloquent's `original` array, which is only populated
     * by syncOriginal() in finishSave() — AFTER the `created` event fires. When an
     * automatic Contact trigger renders inside a model's `created` event (e.g. the
     * API lead-intake path), original is still empty and every name would render
     * blank ("Dear there"). Prefer the raw original (immune to the HasName::fullName
     * in-memory corruption noted above); fall back to the live attribute — read via
     * getAttributes() so we don't trip the fullName accessor cache — only when the
     * original isn't available yet.
     */
    private static function rawOrLive(Client $client, string $key): ?string
    {
        $original = $client->getRawOriginal($key);
        if ($original !== null && $original !== '') {
            return $original;
        }

        return ($client->getAttributes()[$key] ?? null) ?: null;
    }

    private static function claim(PlaceholderContext $c): ?Claim
    {
        if ($c->messageable instanceof Claim) return $c->messageable;
        if ($c->related instanceof Claim) return $c->related;
        if ($c->messageable instanceof Client) return $c->messageable->claim;
        if ($c->related instanceof Client) return $c->related->claim;
        return null;
    }

    /**
     * DSARs for the context's claim that have actually been sent, i.e. excluding
     * draft / approved (pre-send) records.  This mirrors the "sent" definition
     * used by the DSAR reports (see DsarStatus::preSend()).  The institution is
     * eager-loaded so the targets_list placeholder can read display names without
     * an extra query per row.
     */
    private static function sentDsars(PlaceholderContext $c): Collection
    {
        $claim = self::claim($c);

        if (!$claim) {
            return collect();
        }

        return $claim->dsars()
            ->whereNotIn('status', DsarStatus::preSend())
            ->with('institution')
            ->orderBy('id')
            ->get();
    }

    /**
     * The current solicitor's contact number for a given phone-number-type slug
     * (e.g. 'client', 'evidence').  Prefers the primary number, then the oldest
     * active number of that type.  Returns '' when none is configured.
     */
    private static function solicitorPhoneOfType(PlaceholderContext $c, string $typeSlug): string
    {
        $solicitor = $c->solicitor;

        if (!$solicitor) {
            return '';
        }

        $number = $solicitor->phoneNumbers()
            ->ofType($typeSlug)
            ->where('is_active', true)
            ->orderByDesc('is_primary')
            ->orderBy('id')
            ->first();

        return (string) ($number?->phone_number ?? '');
    }

    /**
     * The current solicitor's WhatsApp contact number — the first active line
     * (primary preferred) whose capabilities include 'whatsapp'.  Capability is
     * filtered in PHP via the model helper so we stay portable across the array
     * cast rather than depending on JSON column queries.  Returns '' when none.
     */
    private static function solicitorWhatsappNumber(PlaceholderContext $c): string
    {
        $solicitor = $c->solicitor;

        if (!$solicitor) {
            return '';
        }

        $number = $solicitor->phoneNumbers()
            ->where('is_active', true)
            ->orderByDesc('is_primary')
            ->orderBy('id')
            ->get()
            ->first(fn (SolicitorPhoneNumber $n) => $n->isWhatsappEnabled());

        return (string) ($number?->phone_number ?? '');
    }
}
