<?php

namespace App\Services;

use App\Models\Claim\Area;
use App\Models\Claim\CareSetting;
use App\Models\Client\Client;
use App\Models\Contract;
use App\Models\Solicitor\Solicitor;
use App\Models\User\User;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

/**
 * Outbound integration with the Fintrixor eSign API (one account per solicitor),
 * using a draft-first flow.
 *
 * Preferred (envelope) flow — multiple contracts behind ONE signing link:
 *
 *   1. createDraftRequest() — POST /requests {auto_send:false, templates:[…]}  → request + per-contract draft_url
 *   2. (preview each draft.pdf via its signed draft_url)
 *   3. issueSendRequest()   — POST /requests/send {signing_request_id}         → issues the ONE signing link
 *
 * Legacy (per-document) flow — one signing link per contract:
 *
 *   1. createDraft()  — POST /documents/create {auto_send:false}              → draft + draft_url
 *   2. issueSend()    — POST /documents/send                                 → issues that document's link
 *
 * Only contracts the solicitor has mapped to a Fintrixor template are sent;
 * unmapped contracts (Part-1/3, manual) are handled by the caller and skipped.
 */
class EsignService
{
    protected int $timeout;

    public function __construct()
    {
        $this->timeout = (int) config('services.esign.timeout', 15);
    }

    /**
     * Create a DRAFT document (auto_send=false) for a mapped contract. No signing
     * link is issued yet; the response carries a signed `draft_url` for preview.
     *
     * @return array{document_id: ?string, draft_url: ?string, response: array}|null
     *         null when eSign isn't configured, the contract has no template, or the call fails.
     */
    public function createDraft(Contract $contract, Client $client): ?array
    {
        $solicitor = Solicitor::find($client->solicitor_id) ?? $client->solicitor;
        if (!$solicitor || !$solicitor->esignReady()) {
            return null;
        }

        $templateId = $contract->fintrixor_template_id;
        if (!$templateId) {
            return null;
        }

        try {
            $response = Http::withToken($solicitor->esign_api_key)
                ->acceptJson()
                ->asJson()
                ->timeout($this->timeout)
                ->retry(2, 500)
                ->post($this->baseUrl($solicitor) . '/documents/create', [
                    'template_id'     => $templateId,
                    'recipient_name'  => $client->fullName,
                    'recipient_email' => (string) $client->email_address,
                    // Canonical merge variables overlaid with the full prefill set.
                    // Every variable is sent every time — Fintrixor only consumes the
                    // ones the chosen template actually defines.
                    'variables'       => array_merge(
                        $this->variablesFor($client, $solicitor, auth()->user()),
                        $this->prefillFor($client, $solicitor),
                    ),
                    'auto_send'       => false,
                ]);

            if ($response->successful()) {
                $json = $response->json();
                return [
                    'document_id' => $json['id'] ?? null,
                    'draft_url'   => $json['draft_url'] ?? null,
                    'response'    => is_array($json) ? $json : [],
                ];
            }

            Log::error('eSign createDraft failed', [
                'contract_id' => $contract->id,
                'status'      => $response->status(),
                'response'    => $response->body(),
            ]);
        } catch (\Throwable $e) {
            Log::critical('eSign createDraft exception', [
                'contract_id' => $contract->id,
                'message'     => $e->getMessage(),
            ]);
        }

        return null;
    }

    /**
     * Fetch the signed, no-login draft.pdf and return it base64-encoded for preview.
     */
    public function fetchDraftBase64(?string $draftUrl): ?string
    {
        if (!$draftUrl) {
            return null;
        }

        try {
            $response = Http::timeout($this->timeout)->get($draftUrl);
            if ($response->successful() && str_starts_with((string) $response->body(), '%PDF')) {
                return base64_encode($response->body());
            }
            Log::warning('eSign draft fetch returned a non-PDF response', ['status' => $response->status()]);
        } catch (\Throwable $e) {
            Log::warning('eSign draft fetch failed', ['message' => $e->getMessage()]);
        }

        return null;
    }

    /**
     * Issue the signing link for a previously-created draft (marks it sent).
     *
     * @return array|null the updated document json (with signing_url), or null on failure.
     */
    public function issueSend(Solicitor $solicitor, string $providerDocumentId): ?array
    {
        try {
            $response = Http::withToken($solicitor->esign_api_key)
                ->acceptJson()
                ->asJson()
                ->timeout($this->timeout)
                ->retry(2, 500)
                ->post($this->baseUrl($solicitor) . '/documents/send', [
                    'document_id' => $providerDocumentId,
                ]);

            if ($response->successful()) {
                return $response->json();
            }

            Log::error('eSign issueSend failed', [
                'document_id' => $providerDocumentId,
                'status'      => $response->status(),
                'response'    => $response->body(),
            ]);
        } catch (\Throwable $e) {
            Log::critical('eSign issueSend exception', [
                'document_id' => $providerDocumentId,
                'message'     => $e->getMessage(),
            ]);
        }

        return null;
    }

    /**
     * Create a DRAFT signing request (envelope, auto_send=false) bundling every
     * mapped contract behind ONE link. No link is issued yet; each returned draft
     * carries a signed `draft_url` for preview.
     *
     * A contract tagged with a Redress `redress_role` fans out into one document
     * per care setting (Part 1 annex: settings 5+, Part 2 annex: every setting),
     * so a single contract can yield several drafts. Drafts are returned in send
     * order, which the API preserves for the documents it echoes back.
     *
     * @param  array<int, Contract>  $contracts  the selected contracts, in display order
     * @return array{request_id: ?string, drafts: array<int, array{contract_id: int, name: string, care_setting_id: ?int, document_id: ?string, draft_url: ?string}>, response: array}|null
     *         null when eSign isn't configured, no contract is mapped/expandable, or the call fails.
     */
    public function createDraftRequest(array $contracts, Client $client): ?array
    {
        $solicitor = Solicitor::find($client->solicitor_id) ?? $client->solicitor;
        if (!$solicitor || !$solicitor->esignReady()) {
            return null;
        }

        // Only contracts mapped to a Fintrixor template can be bundled into the request.
        $mapped = array_values(array_filter(
            $contracts,
            static fn (Contract $c) => filled($c->fintrixor_template_id),
        ));
        if (empty($mapped)) {
            return null;
        }

        // The canonical + prefill variable bag sent for every template; each
        // template merges only the variable_names its own fields define.
        $baseVariables = array_merge(
            $this->variablesFor($client, $solicitor, auth()->user()),
            $this->prefillFor($client, $solicitor),
        );

        // Care settings in order (uncapped) so annex contracts can fan out.
        $client->claim?->loadMissing('careSettings.institution.area.council', 'careSettings.area.council');
        $careSettings = $client->claim?->careSettings ?? collect();

        // Expand each selected contract into one-or-more request entries. Annex
        // contracts fan out into one document per care setting; $descriptors keeps
        // parallel metadata so the API's ordered documents[] map back afterwards.
        $entries     = [];
        $descriptors = [];
        foreach ($mapped as $contract) {
            $this->appendContractEntries($contract, $careSettings, $baseVariables, $entries, $descriptors);
        }

        // Nothing to send — e.g. only an annex contract was selected but the client
        // has no (or too few) care settings to produce any annex.
        if (empty($entries)) {
            return null;
        }

        try {
            $response = Http::withToken($solicitor->esign_api_key)
                ->acceptJson()
                ->asJson()
                ->timeout($this->timeout)
                ->retry(2, 500)
                ->post($this->baseUrl($solicitor) . '/requests', [
                    'recipient_name'  => $client->fullName,
                    'recipient_email' => (string) $client->email_address,
                    'templates'       => $entries,
                    'auto_send'       => false,
                ]);

            if ($response->successful()) {
                $json      = $response->json();
                $documents = array_values((array) ($json['documents'] ?? []));

                // The API returns documents in the same order as the templates sent,
                // so position aligns with $descriptors.
                $drafts = [];
                foreach ($descriptors as $i => $descriptor) {
                    $doc = $documents[$i] ?? null;
                    $drafts[] = [
                        'contract_id'     => $descriptor['contract_id'],
                        'name'            => $descriptor['name'],
                        'care_setting_id' => $descriptor['care_setting_id'],
                        'document_id'     => $doc['id'] ?? null,
                        'draft_url'       => $doc['draft_url'] ?? null,
                    ];
                }

                return [
                    'request_id' => $json['id'] ?? null,
                    'drafts'     => $drafts,
                    'response'   => is_array($json) ? $json : [],
                ];
            }

            Log::error('eSign createDraftRequest failed', [
                'client_id' => $client->id,
                'status'    => $response->status(),
                'response'  => $response->body(),
            ]);
        } catch (\Throwable $e) {
            Log::critical('eSign createDraftRequest exception', [
                'client_id' => $client->id,
                'message'   => $e->getMessage(),
            ]);
        }

        return null;
    }

    /**
     * Issue the single signing link for a previously-created draft request (marks
     * every contract in it sent).
     *
     * @return array|null the updated request json (request-level `signing_url` plus
     *                    a `documents[]` array), or null on failure.
     */
    public function issueSendRequest(Solicitor $solicitor, string $providerRequestId): ?array
    {
        try {
            $response = Http::withToken($solicitor->esign_api_key)
                ->acceptJson()
                ->asJson()
                ->timeout($this->timeout)
                ->retry(2, 500)
                ->post($this->baseUrl($solicitor) . '/requests/send', [
                    'signing_request_id' => $providerRequestId,
                ]);

            if ($response->successful()) {
                return $response->json();
            }

            Log::error('eSign issueSendRequest failed', [
                'request_id' => $providerRequestId,
                'status'     => $response->status(),
                'response'   => $response->body(),
            ]);
        } catch (\Throwable $e) {
            Log::critical('eSign issueSendRequest exception', [
                'request_id' => $providerRequestId,
                'message'    => $e->getMessage(),
            ]);
        }

        return null;
    }

    private function baseUrl(Solicitor $solicitor): string
    {
        return rtrim($solicitor->esign_base_url ?: config('services.esign.base_url'), '/');
    }

    /**
     * Canonical merge variables sent to every document. The solicitor's Fintrixor
     * template merges whichever of these `variable_name`s its fields define; any
     * not present in the template are ignored. Signature/field POSITIONS live in
     * the template, not here.
     *
     * Documented canonical names: client_full_name, client_first_name,
     * client_last_name, date, reference, date_of_birth, current_address,
     * previous_name, witness_name, witness_occupation, firm_name, firm_email, firm_tel.
     *
     * @return array<string, string>
     */
    protected function variablesFor(Client $client, Solicitor $solicitor, ?User $user): array
    {
        $clientName = collect([$client->name_first, $client->name_middle, $client->name_last])
            ->filter()->implode(' ');

        $address = $client->currentAddress;

        return array_filter([
            'client_full_name'   => $clientName,
            'client_first_name'  => $client->name_first,
            'client_last_name'   => $client->name_last,
            'reference'          => str_pad((string) $client->id, 6, '0', STR_PAD_LEFT),
            'date_of_birth'      => $client->date_of_birth?->format('d/m/Y'),
            'current_address'    => $address
                ? collect([$address->line_1, $address->line_2, $address->line_3, $address->postcode])->filter()->implode(', ')
                : null,
            'previous_name'      => $client->previousNames()->first()?->fullName,
            'witness_name'       => $user ? trim($user->name_first . ' ' . $user->name_last) : null,
            'witness_occupation' => $user ? 'Case Handler' : null,
            'firm_name'          => $solicitor->name,
            // Sourced from the solicitor's primary email sender / phone number rather
            // than the (deprecated) email_address / phone_number columns.
            'firm_email'         => $solicitor->emailSenderFor('general')?->email_address,
            'firm_tel'           => $solicitor->phoneNumbers()->active()->primary()->first()?->phone_number,
        ], static fn ($v) => $v !== null && $v !== '');
    }

    /**
     * The full prefill variable set, sent on every draft regardless of contract.
     * It's just a flat bag of values — Fintrixor merges whichever ones the chosen
     * template defines and ignores the rest. Firm details are sourced from the
     * solicitor and its current address (set via the solicitor's Addresses tab);
     * the Part-1/Part-3 application fields are computed directly here (see
     * {@see applicationFields()}).
     *
     * @return array<string, mixed>
     */
    protected function prefillFor(Client $client, Solicitor $solicitor): array
    {
        $user = auth()->user();
        $address = $client->currentAddress;
        $previousAddress = $client->addresses()->where('is_current', false)->first();
        $firmAddress = $solicitor->currentAddress;

        $firmAddressLine = $firmAddress
            ? collect([$firmAddress->line_1, $firmAddress->line_2, $firmAddress->line_3])->filter()->implode(', ')
            : null;
        $firmAddressFull = $firmAddress
            ? collect([$firmAddress->line_1, $firmAddress->line_2, $firmAddress->line_3, $firmAddress->postcode])->filter()->implode(', ')
            : null;

        $fields = [
            'reference'         => $client->displayId,
            'current_address'   => $address
                ? collect([$address->line_1, $address->line_2, $address->line_3, $address->postcode])->filter()->implode("\n")
                : null,
            'address_line_1'    => $address?->line_1,
            'address_line_2'    => $address?->line_2,
            'address_line_3'    => $address?->line_3,
            'address_postcode'  => $address?->postcode,
            'prev_name'         => $client->previousNames()->first()?->fullName,
            'prev_address'      => $previousAddress
                ? collect([$previousAddress->line_1, $previousAddress->line_2, $previousAddress->line_3, $previousAddress->postcode])->filter()->implode("\n")
                : null,
            'witness_signature' => $user ? trim($user->name_first . ' ' . $user->name_last) : null,
            'witness_address'   => $firmAddressFull,
            // firm_name/firm_email/firm_tel are already provided by variablesFor().
            'firm_address'      => $firmAddressLine,
            'firm_postcode'     => $firmAddress?->postcode,
        ];

        // Part-1/Part-3 application fields, computed directly from the claim data.
        // Decoupled from ApplicationService (being retired) and null-safe, so a
        // client missing prerequisites still yields whatever data exists.
        $fields = array_merge($fields, $this->applicationFields($client, $solicitor));

        return array_filter($fields, static fn ($v) => $v !== null && $v !== '');
    }

    /**
     * The Part-1 / Part-3 application fields for a client, computed directly from
     * the claim data. Ported from the (soon-to-be-retired) ApplicationService and
     * made null-safe so a missing address / impact detail / institution doesn't
     * drop the whole set.
     *
     * @return array<string, mixed>
     */
    private function applicationFields(Client $client, Solicitor $solicitor): array
    {
        $claim = $client->claim;
        if (! $claim) {
            return [];
        }

        $fields = $this->part1Fields($client, $solicitor);

        if ($claim->careSettings()->count() > 0) {
            $fields = array_merge($fields, $this->part3Fields($client, $claim->careSettings->take(4)));
        }

        return $fields;
    }

    /**
     * Part-1 application form fields.
     *
     * @return array<string, mixed>
     */
    private function part1Fields(Client $client, Solicitor $solicitor): array
    {
        $claim       = $client->claim;
        $ben         = $claim->beneficiary ?: null;
        $ti          = $claim->terminalIllness ?: null;
        $address     = $client->currentAddress;
        $firmAddress = $solicitor->currentAddress;
        $prefs       = (array) ($client->preferences ?: []);

        $profTypes = [
            'gp'               => 'GP',
            'consultant'       => 'Consultant',
            'specialist_nurse' => 'Specialist',
            'district_nurse'   => 'District',
        ];

        $hasAltFormat = ($prefs['braille'] ?? false) || ($prefs['large_print'] ?? false)
            || ($prefs['audio'] ?? false) || ($prefs['other_lang'] ?? false);

        return [
            'client_last_name' => $client->name_last,
            'client_dob_day' => $client->date_of_birth?->format('d'),
            'client_dob_month' => $client->date_of_birth?->format('m'),
            'client_dob_year' => $client->date_of_birth?->format('Y'),
            'client_address' => collect([$address?->line_1, $address?->line_2, $address?->line_3])->filter()->implode("\n"),
            'client_address_country' => 'United Kingdom',
            'client_postcode' => $address?->postcode,
            'client_first_name' => $client->name_first,
            'contact_email_check' => 'Yes',
            'contact_email' => $solicitor->emailSenderFor('general')?->email_address,
            'contact_letter_correspondence' => 'Yes',
            'contact_necessary_radio' => 'correspondence_addres',
            // Client elects to receive correspondence at the firm's office, sourced
            // from the solicitor's current address rather than a hardcoded one.
            'contact_correspondence_addres' => $firmAddress
                ? collect([$firmAddress->line_1, $firmAddress->line_2, $firmAddress->line_3])->filter()->implode("\n")
                : null,
            'contact_correspondence_country' => 'United Kingdom',
            'contact_correspondence_postcode' => $firmAddress?->postcode,
            'contact_format_braille' => ($prefs['braille'] ?? false) ? 'Yes' : 'No',
            'contact_format_large' => ($prefs['large_print'] ?? false) ? 'Yes' : 'No',
            'contact_format_audio' => ($prefs['audio'] ?? false) ? 'Yes' : 'No',
            'contact_format_other' => ($prefs['other_lang'] ?? false) ? 'Yes' : 'No',
            'contact_format_other_text' => $prefs['language_required'] ?? '',
            'contact_format_na' => $hasAltFormat ? 'No' : 'Yes',
            'help_complete' => 'No',
            'identity_radio' => 'Later',
            // The firm's named help contact, sourced from the solicitor record.
            // Phone/email come from the same phone-numbers / email-senders
            // subsystems used for firm_tel/firm_email above.
            'help_first_name' => $solicitor->help_contact_first_name,
            'help_last_name' => $solicitor->help_contact_last_name,
            'help_telephone' => $solicitor->phoneNumbers()->active()->primary()->first()?->phone_number,
            'help_email' => $solicitor->emailSenderFor('general')?->email_address,
            'help_contact_type' => 'Solicitor',
            'help_contact_firm' => $solicitor->name,
            'ti_check' => $ti ? 'Yes' : 'No',
            'ti_aps' => $ti?->applied_advance_payment ? 'Yes' : 'No',
            'ti_aps_confirm' => $ti?->permission_confirm_health ? 'Yes' : 'No',
            'ti_ds1500' => $ti?->provide_ds1500 ? 'Yes' : 'No',
            'ti_professional_check' => $ti?->agree_contact_healthcare ? 'Y' : 'No',
            'ti_nhs_number_check' => $ti?->has_medical_ref ? 'Yes' : 'No',
            'ti_nhs_number' => $ti?->medical_reference_number,
            'ti_prof_type' => $ti ? ($profTypes[$ti->healthcare_type] ?? '') : '',
            'ti_prof_name' => $ti?->healthcare_name,
            'ti_prof_place' => $ti?->healthcare_place_of_work,
            'ti_prof_address' => $ti
                ? collect([$ti->healthcare_address_1, $ti->healthcare_address_2, $ti->healthcare_address_3])->filter()->implode("\n")
                : null,
            'ti_prof_email' => (string) $ti?->healthcare_email,
            'ti_prof_tel' => $ti?->healthcare_phone,
            'ben_check' => $ben ? 'Yes' : 'No',
            'ben_name' => $ben?->full_name,
            'ben_dob_day' => $ben?->date_of_birth?->format('d'),
            'ben_dob_month' => $ben?->date_of_birth?->format('m'),
            'ben_dob_year' => $ben?->date_of_birth?->format('Y'),
            'ben_address' => collect([$ben?->address_line_1, $ben?->address_line_2, $ben?->address_line_3, $ben?->address_line_4])->filter()->implode("\n"),
            'ben_tel' => (string)$ben?->telephone,
            'ben_email' => (string) $ben?->email,
            'redress_type' => $claim->redress_type ? ucwords(strtolower($claim->redress_type)) : null,
            'cs_1_name' => $claim->careSettings->get(0)?->institution?->display_name,
            'cs_1_typ' => '',
            'cs_1_prev' => collect([$claim->furtherInformation?->address_prior_line_1, $claim->furtherInformation?->address_prior_line_2, $claim->furtherInformation?->address_prior_line_3, $claim->furtherInformation?->address_prior_postcode])->filter()->implode("\n"),
            'cs_1_date' => $claim->careSettings->get(0)?->dates,
            'cs_1_area' => $this->settingArea($claim->careSettings->get(0))?->name,
            'cs_1_local_authority' => $this->settingArea($claim->careSettings->get(0))?->council?->name,
            'cs_2_name' => $claim->careSettings->get(1)?->institution?->display_name,
            'cs_2_type' => '',
            'cs_2_prev' => collect([$claim->furtherInformation?->address_prior_line_1, $claim->furtherInformation?->address_prior_line_2, $claim->furtherInformation?->address_prior_line_3, $claim->furtherInformation?->address_prior_postcode])->filter()->implode("\n"),
            'cs_2_date' => $claim->careSettings->get(1)?->dates,
            'cs_2_area' => $this->settingArea($claim->careSettings->get(1))?->name,
            'cs_2_local_authority' => $this->settingArea($claim->careSettings->get(1))?->council?->name,
            'cs_3_name' => $claim->careSettings->get(2)?->institution?->display_name,
            'cs_3_type' => '',
            'cs_3_prev' => collect([$claim->furtherInformation?->address_prior_line_1, $claim->furtherInformation?->address_prior_line_2, $claim->furtherInformation?->address_prior_line_3, $claim->furtherInformation?->address_prior_postcode])->filter()->implode("\n"),
            'cs_3_date' => $claim->careSettings->get(2)?->dates,
            'cs_3_area' => $this->settingArea($claim->careSettings->get(2))?->name,
            'cs_3_local_authority' => $this->settingArea($claim->careSettings->get(2))?->council?->name,
            'cs_4_name' => $claim->careSettings->get(3)?->institution?->display_name,
            'cs_4_type' => '',
            'cs_4_prev' => collect([$claim->furtherInformation?->address_prior_line_1, $claim->furtherInformation?->address_prior_line_2, $claim->furtherInformation?->address_prior_line_3, $claim->furtherInformation?->address_prior_postcode])->filter()->implode("\n"),
            'cs_4_date' => $claim->careSettings->get(3)?->dates,
            'cs_4_area' => $this->settingArea($claim->careSettings->get(3))?->name,
            'cs_4_local_authority' => $this->settingArea($claim->careSettings->get(3))?->council?->name,
            'aps' => $claim->furtherInformation?->apply_aps ? 'Yes' : 'No',
            'aps_confirm' => $claim->furtherInformation?->confirm_care_setting ? 'Yes' : 'No',
            'supporting_documents' => 'Later',
            'name_change_care_check' => is_null($claim->furtherInformation?->name_in_care) ? 'No' : 'Yes',
            'name_change_care' => $claim->furtherInformation?->name_in_care,
            'legal_name_change_check' => (is_null($claim->furtherInformation?->legal_name_change) && $client->previousNames()->count() == 0) ? 'No' : 'Yes',
            'legal_name_change' => $claim->furtherInformation?->legal_name ?: $client->previousNames()->first()?->fullName ?: '',
            'name_change_doc' => (is_null($claim->furtherInformation?->legal_name_change) && $client->previousNames()->count() == 0) ? 'No' : 'Yes',
            'mothers_name' => $claim->furtherInformation?->mothers_name,
            'fathers_name' => $claim->furtherInformation?->fathers_name,
            'payment' => $claim->impactDetail?->has_received_relevant_payment ? 'Yes' : 'No',
            'payment_date_day' => $claim->impactDetail?->payment_date?->format('d'),
            'payment_date_month' => $claim->impactDetail?->payment_date?->format('m'),
            'payment_date_year' => $claim->impactDetail?->payment_date?->format('Y'),
            'payment_organisation' => $claim->impactDetail?->paying_organisation,
            'payment_amount' => $claim->impactDetail?->payment_amount,
            'cc' => $claim->impactDetail?->has_criminal_conviction ? 'Yes' : 'No',
            'bank_name' => $claim->bankDetail?->name_on_account,
            'bank_sort_code' => $claim->bankDetail?->sort_code,
            'bank_sort_cod' => $claim->bankDetail?->sort_code,
            'bank_account_number' => $claim->bankDetail?->account_number,
            'bank_account_numb' => $claim->bankDetail?->account_number,
            'bank_reference' => $claim->bankDetail?->reference_number,
            'bank_bic' => $claim->bankDetail?->bic_swift,
            'bank_clearing_code' => $claim->bankDetail?->clearing_code,
            'bank_iban' => $claim->bankDetail?->iban,
            'contact_redress_check' => 'Yes'
        ];
    }

    /**
     * Part-3 application form fields for up to the first four care settings.
     *
     * @param  \Illuminate\Support\Collection  $careSettings
     * @return array<string, mixed>
     */
    private function part3Fields(Client $client, $careSettings): array
    {
        $data = [
            'client_full_name' => $client->name_first . ' ' . $client->name_last,
            'client_dob_day' => $client->date_of_birth?->format('d'),
            'client_dob_month' => $client->date_of_birth?->format('m'),
            'client_dob_year' => $client->date_of_birth?->format('Y'),
            'supporting_documents' => 'collecting',
        ];

        foreach ($careSettings->values() as $i => $setting) {
            $suffix = $i + 1;
            $data["setting_{$suffix}_name"] = $setting->institution?->display_name;
            $data["setting_{$suffix}_date"] = $setting->dates;
            $data["setting_{$suffix}_persons_involved"] = $setting->people_involved;
            $settingArea = $this->settingArea($setting);
            $data["setting_{$suffix}_area"] = $settingArea?->name;
            $data["setting_{$suffix}_local_authority"] = $settingArea?->council?->name;
            // The full statement now always lives in a Part 2 annex document, so
            // the base form shows a standardised "See Attached" for every setting.
            $data["setting_{$suffix}_statement"] = 'See Attached';
        }

        return $data;
    }

    /**
     * Expand one selected contract into one-or-more signing-request entries and
     * append them (plus parallel descriptor metadata) to the given arrays:
     *
     *  - ordinary / base contract → a single entry with the shared variable bag;
     *  - Part 1 annex             → one entry per care setting BEYOND the base 4;
     *  - Part 2 annex             → one entry per care setting (every setting).
     *
     * Each entry keeps the contract's own `fintrixor_template_id`, so annexes send
     * on the firm's own account alongside the base documents in one envelope.
     *
     * @param  \Illuminate\Support\Collection<int, CareSetting>  $careSettings  in order
     * @param  array<string, mixed>  $baseVariables
     * @param  array<int, array{template_id: ?string, variables: array}>  $entries
     * @param  array<int, array{contract_id: int, name: string, care_setting_id: ?int}>  $descriptors
     */
    private function appendContractEntries(Contract $contract, $careSettings, array $baseVariables, array &$entries, array &$descriptors): void
    {
        $templateId = $contract->fintrixor_template_id;

        $push = function (array $variables, string $name, ?int $careSettingId = null) use ($contract, $templateId, &$entries, &$descriptors): void {
            $entries[]     = ['template_id' => $templateId, 'variables' => $variables];
            $descriptors[] = [
                'contract_id'     => $contract->id,
                'name'            => $name,
                'care_setting_id' => $careSettingId,
            ];
        };

        $settings = $careSettings->values();

        // Part 1 annex — one "Additional Care Settings" document per setting 5+.
        if ($contract->isPart1Annex()) {
            foreach ($settings as $i => $setting) {
                if ($i < 4) {
                    continue;
                }
                $index = $i + 1;
                $push(
                    array_merge($baseVariables, $this->part1AnnexVariables($setting, $index)),
                    $this->annexName($contract, $setting, $index),
                    $setting->id,
                );
            }

            return;
        }

        // Part 2 annex — one "Additional Statement Pages" document per setting.
        if ($contract->isPart2Annex()) {
            foreach ($settings as $i => $setting) {
                $index = $i + 1;
                $push(
                    array_merge($baseVariables, $this->part2AnnexVariables($setting, $index)),
                    $this->annexName($contract, $setting, $index),
                    $setting->id,
                );
            }

            return;
        }

        // Ordinary / base contract — a single document with the shared bag.
        $push($baseVariables, $contract->name);
    }

    /**
     * The area used for a care setting on the eSign forms. Area/local authority
     * are a property of the INSTITUTION (the canonical source), so we read the
     * institution's area, falling back to any area recorded on the setting itself
     * only when the institution has none.
     */
    private function settingArea(?CareSetting $setting): ?Area
    {
        return $setting?->institution?->area ?? $setting?->area;
    }

    /** Human-readable draft name for an annex instance, e.g. "Statement — Care Setting 5: Foo House". */
    private function annexName(Contract $contract, CareSetting $setting, int $index): string
    {
        $name = $contract->name . ' — Care Setting ' . $index;
        $institution = $setting->institution?->display_name;

        return $institution ? $name . ': ' . $institution : $name;
    }

    /**
     * Per-care-setting merge values for a Part 1 "Additional Care Settings" annex
     * (one document per care setting beyond the base four). These bind to the
     * annex template's `label` fields.
     *
     * @return array<string, mixed>
     */
    private function part1AnnexVariables(CareSetting $setting, int $index): array
    {
        $furtherInfo = $setting->claim?->furtherInformation;
        $area = $this->settingArea($setting);

        return array_filter([
            'annex_cs_index'           => (string) $index,
            'annex_cs_name'            => $setting->institution?->display_name,
            'annex_cs_area'            => $area?->name,
            'annex_cs_local_authority' => $area?->council?->name,
            'annex_cs_prev'            => $furtherInfo
                ? collect([$furtherInfo->address_prior_line_1, $furtherInfo->address_prior_line_2, $furtherInfo->address_prior_line_3, $furtherInfo->address_prior_postcode])->filter()->implode("\n")
                : null,
            'annex_cs_date'            => $setting->dates,
        ], static fn ($v) => $v !== null && $v !== '');
    }

    /**
     * Per-care-setting merge values for a Part 2 "Additional Statement Pages"
     * annex (one document per care setting). Carries the setting's FULL statement
     * — the base Part 2 shows a standardised "See Attached".
     *
     * @return array<string, mixed>
     */
    private function part2AnnexVariables(CareSetting $setting, int $index): array
    {
        $area = $this->settingArea($setting);

        return array_filter([
            'annex_setting_index'            => (string) $index,
            'annex_setting_name'             => $setting->institution?->display_name,
            'annex_setting_area'             => $area?->name,
            'annex_setting_local_authority'  => $area?->council?->name,
            'annex_setting_date'             => $setting->dates,
            'annex_setting_persons_involved' => $setting->people_involved,
            'annex_setting_statement'        => $setting->statement,
        ], static fn ($v) => $v !== null && $v !== '');
    }
}
