<?php

namespace App\Services;

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
{
    /** Statement box dimensions for the Part-3 "See Attached" overflow check. */
    private const STATEMENT_MAX_LINES = 60;
    private const STATEMENT_MAX_CHARS_PER_LINE = 75;

    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. Drafts are aligned to the input
     * contracts by order (the API preserves the order of the templates sent).
     *
     * @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, document_id: ?string, draft_url: ?string}>, response: array}|null
     *         null when eSign isn't configured, no contract is mapped, 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 same canonical + prefill variable bag is sent for every template;
        // each template merges only the variable_names its own fields define.
        $variables = array_merge(
            $this->variablesFor($client, $solicitor, auth()->user()),
            $this->prefillFor($client, $solicitor),
        );

        $templates = array_map(static fn (Contract $c) => [
            'template_id' => $c->fintrixor_template_id,
            'variables'   => $variables,
        ], $mapped);

        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'       => $templates,
                    'auto_send'       => false,
                ]);

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

                // The API returns documents in the same order as the templates sent.
                $drafts = [];
                foreach ($mapped as $i => $contract) {
                    $doc = $documents[$i] ?? null;
                    $drafts[] = [
                        'contract_id' => $contract->id,
                        'name'        => $contract->name,
                        '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_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_date' => $claim->careSettings->get(0)?->dates,
            'cs_2_name' => $claim->careSettings->get(1)?->institution?->display_name,
            'cs_2_type' => '',
            'cs_2_date' => $claim->careSettings->get(1)?->dates,
            'cs_3_name' => $claim->careSettings->get(2)?->institution?->display_name,
            'cs_3_type' => '',
            'cs_3_date' => $claim->careSettings->get(2)?->dates,
            'cs_4_name' => $claim->careSettings->get(3)?->institution?->display_name,
            'cs_4_type' => '',
            'cs_4_date' => $claim->careSettings->get(3)?->dates,
            '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,
        ];
    }

    /**
     * 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;
            $data["setting_{$suffix}_statement"] = $this->statementExceedsBox((string) $setting->statement)
                ? 'See Attached'
                : $setting->statement;
        }

        return $data;
    }

    /**
     * Whether a Part-3 statement exceeds the printable box, in which case it's
     * replaced with "See Attached". Mirrors ApplicationService's calculation.
     */
    private function statementExceedsBox(?string $statement): bool
    {
        if (empty($statement)) {
            return false;
        }

        $statement = str_replace("\r\n", "\n", $statement);
        $totalLines = 0;

        foreach (explode("\n", $statement) as $paragraph) {
            if (trim($paragraph) === '') {
                $totalLines++;
                continue;
            }

            $wrapped = wordwrap($paragraph, self::STATEMENT_MAX_CHARS_PER_LINE, "\n", true);
            $totalLines += substr_count($wrapped, "\n") + 1;
        }

        return $totalLines > self::STATEMENT_MAX_LINES;
    }
}
