<?php

namespace App\Support;

use App\Models\Appointment;
use App\Models\AppSetting;
use App\Models\User;
use Carbon\Carbon;

/**
 * Textos del bot de WhatsApp: defaults + overrides en app_settings (wa_copy_*).
 * Los textos por defecto usan voseo costarricense (coherente con imperativos como «escribí», «elegí»).
 * Placeholders comunes: {nombre}, {negocio}, {url_registro}, {url_app}, {app_extra}, {staff_label}
 * Reserva: {servicio}, {profesional}, {fecha}, {hora}
 * Cancelar confirmación: {fecha}, {hora}, {servicio}, {profesional}
 * Línea de cita: {fecha}, {hora}, {servicio}, {profesional}, {estado}
 */
class WhatsAppMessageCopy
{
    public static function publicAppUrl(): string
    {
        return rtrim((string) config('app.frontend_url'), '/');
    }

    public static function registrationUrl(string $phoneDigits): string
    {
        return self::publicAppUrl().'/register?'.http_build_query(['phone' => $phoneDigits]);
    }

    public static function staffLabelSingular(): string
    {
        return (string) AppSetting::getValue('label_staff_singular', 'Profesional');
    }

    /** Nombre del negocio (Ajustes App). */
    public static function businessName(): string
    {
        $n = trim((string) AppSetting::getValue('business_name', 'Barbería DIAICR'));

        return $n !== '' ? $n : 'Barbería DIAICR';
    }

    public static function includeAppUrl(): bool
    {
        return filter_var(AppSetting::getValue('wa_include_app_url', '1'), FILTER_VALIDATE_BOOLEAN);
    }

    /** text | template | both */
    public static function afterBookingMode(): string
    {
        $m = (string) AppSetting::getValue('wa_after_booking', 'text');

        return in_array($m, ['text', 'template', 'both'], true) ? $m : 'text';
    }

    /**
     * Claves guardadas en app_settings (validación, payload, formulario admin).
     *
     * @return list<string>
     */
    public static function persistedSettingKeys(): array
    {
        return array_keys(self::defaults());
    }

    /**
     * Textos predeterminados (admin: Mensajes del bot).
     *
     * @return array<string, string>
     */
    public static function defaults(): array
    {
        $base = [
            'wa_copy_unregistered' => self::defaultUnregistered(),
            'wa_copy_main_menu' => self::defaultMainMenu(),
            'wa_copy_farewell_after_followup' => self::defaultFarewellAfterFollowup(),
            'wa_copy_human_message_ack' => self::defaultHumanMessageAck(),
            'wa_copy_contact_barber' => self::defaultHumanMessageAck(),
            'wa_copy_conversational_ack' => self::defaultHumanMessageAck(),
            'wa_copy_soft_after_followup' => self::defaultSoftAfterFollowup(),
            'wa_copy_quiet_reminder' => self::defaultQuietReminder(),
            'wa_copy_pending_booking' => self::defaultPendingBooking(),
            'wa_copy_appointments_empty' => self::defaultAppointmentsEmpty(),
            'wa_copy_appointments_footer' => self::defaultAppointmentsFooter(),
            'wa_copy_date_list_body' => self::defaultDateListBody(),
            'wa_copy_date_list_footer' => self::defaultDateListFooter(),
            'wa_copy_cancel_list_intro' => self::defaultCancelListIntro(),
        ];

        return array_merge($base, self::defaultFlowStrings());
    }

    /**
     * @return array<string, string>
     */
    protected static function defaultFlowStrings(): array
    {
        return [
            'wa_copy_btn_reservar' => '1. Reservar cita',
            'wa_copy_btn_cancelar' => '2. Cancelar cita',
            'wa_copy_btn_mis_citas' => '3. Mis citas',
            'wa_copy_services_list_body' => 'Elegí el servicio:',
            'wa_copy_services_list_button' => 'Ver servicios',
            'wa_copy_services_section_title' => 'Servicios',
            'wa_copy_no_services' => 'No hay servicios disponibles en este momento.',
            'wa_copy_invalid_service' => 'Servicio no válido.',
            'wa_copy_barbers_list_body' => 'Elegí al {staff_label}:',
            'wa_copy_barbers_list_button' => 'Ver equipo',
            'wa_copy_barbers_section_title' => 'Equipo',
            'wa_copy_no_barbers' => 'No hay {staff_label} disponible para este servicio.',
            'wa_copy_invalid_barber' => '{staff_label} no válido.',
            'wa_copy_no_dates' => 'No hay fechas disponibles en los próximos días.',
            'wa_copy_invalid_date' => 'Fecha no válida.',
            'wa_copy_no_slots_date' => 'No hay horarios disponibles para esa fecha. Elegí otra.',
            'wa_copy_time_list_body' => 'Elegí el horario:',
            'wa_copy_time_list_button' => 'Ver horarios',
            'wa_copy_time_section_title' => 'Horarios',
            'wa_copy_flow_lost' => 'Se perdió el paso del flujo. Escribí «menu» o empezá de nuevo con 1.',
            'wa_copy_slot_mismatch' => 'Horario no disponible. Elegí uno de la lista o escribí la hora (ej. 09:00).',
            'wa_copy_account_not_found' => 'No encontramos tu cuenta con este número. Verificá que el teléfono coincida con el de tu registro.',
            'wa_copy_slot_taken' => 'Ese horario ya no está disponible. Elegí otro desde el menú.',
            'wa_copy_booking_error' => 'Hubo un error al crear la cita. Intentá de nuevo o reservá desde la app.',
            'wa_copy_cancel_none' => 'No tenés citas próximas para cancelar.',
            'wa_copy_cancel_list_body' => 'Tocá la cita a cancelar:',
            'wa_copy_cancel_list_button' => 'Ver citas',
            'wa_copy_cancel_list_section' => 'Citas',
            'wa_copy_cancel_invalid' => 'No encontramos esa cita o ya no se puede cancelar por acá.',
            'wa_copy_cancel_confirm_body' => "¿Confirmás que querés cancelar esta cita?\n\n{fecha} · {hora}\n{servicio} con {profesional}.",
            'wa_copy_cancel_confirm_yes' => 'Sí, cancelar',
            'wa_copy_cancel_confirm_no' => 'No, volver',
            'wa_copy_cancel_done' => 'Listo, cancelamos la cita. Si necesitás otra reserva, escribí 1 o «hola».',
            'wa_copy_cancel_kept' => 'Perfecto, dejamos la cita como estaba. Si querés otra cosa, escribí «hola» o elegí una opción del menú.',
            'wa_copy_appointments_header' => 'Tus próximas citas:',
            'wa_copy_appointment_line' => '• {fecha} · {hora} — {servicio} con {profesional} ({estado})',
            'wa_copy_status_pending_label' => 'pendiente de aprobación',
            'wa_copy_status_confirmed_label' => 'confirmada',
        ];
    }

    protected static function get(string $key, string $default): string
    {
        $v = AppSetting::getValue($key);
        if ($v === null || trim((string) $v) === '') {
            return $default;
        }

        return (string) $v;
    }

    protected static function replaceStaffLabel(string $text): string
    {
        return str_replace('{staff_label}', self::staffLabelSingular(), $text);
    }

    public static function firstNameForGreeting(?User $client, ?string $waProfileName): string
    {
        if ($client) {
            $first = explode(' ', trim((string) $client->name), 2)[0];
            if ($first !== '') {
                return $first;
            }
        }
        if ($waProfileName !== null && trim($waProfileName) !== '') {
            $first = explode(' ', trim($waProfileName), 2)[0];

            return $first !== '' ? $first : 'Cliente';
        }

        return 'Cliente';
    }

    protected static function defaultUnregistered(): string
    {
        return "¡Bienvenido a {negocio}, {nombre}! Gracias por escribirnos por WhatsApp.\n\n"
            ."Para reservar o cancelar citas desde este chat necesitás una cuenta en nuestra app con el mismo número de teléfono que usás acá (así podemos reconocerte).\n\n"
            ."Pasos:\n"
            ."1) Abrí el enlace y completá el registro (correo y contraseña):\n{url_registro}\n"
            ."2) En el teléfono del registro, dejá el mismo número que figura en este chat (con código de país, por ejemplo 506…).\n"
            ."3) Cuando terminés, escribí de nuevo «hola» o «reservar» en este chat y seguimos con tu cita.{app_extra}\n\n"
            ."¿Necesitás algo más o tenés alguna duda? Escribinos cuando querás, con gusto te ayudamos.";
    }

    protected static function defaultMainMenu(): string
    {
        return "Hola, {nombre}. Gracias por escribirnos a {negocio}.\n\n"
            ."Elegí lo que necesitás con los botones de abajo o escribí el número:\n\n"
            ."1 — Reservar cita\n"
            ."2 — Cancelar cita\n"
            ."3 — Ver mis citas{app_extra}\n\n"
            ."Para hablar con alguien del equipo, escribí *contacto*.\n\n"
            ."Si querés otra cosa o tenés una consulta, decinos y te ayudamos. ¡Que tengás un buen día!";
    }

    protected static function defaultFarewellAfterFollowup(): string
    {
        return "¡Gracias, {nombre}! Nos alegra haberte ayudado.\n\n"
            .'Cuando querás volver a reservar o gestionar algo, escribí *menú* o *hola*. '
            ."Si preferís hablar con el equipo, escribí *contacto*.\n\n"
            .'¡Que tengás un excelente día!';
    }

    /** Mensaje al cliente cuando escribe fuera del menú o pide contacto (un solo texto sensato). */
    protected static function defaultHumanMessageAck(): string
    {
        return "Gracias por tu mensaje, {nombre}. Te respondemos por este mismo chat cuando podamos.\n\n"
            .'Para ver el menú automático (reservar, cancelar o ver tus citas), escribí *menú* o *1*.';
    }

    protected static function defaultSoftAfterFollowup(): string
    {
        return '¿Necesitás algo más del menú? Respondé *sí* para ver opciones o *no* para cerrar. '
            .'Para hablar con el equipo: *contacto*.';
    }

    protected static function defaultQuietReminder(): string
    {
        return 'Si querés reservar o ver opciones, escribí *menú* o *hola*. '
            .'Para hablar con alguien del equipo: *contacto*.';
    }

    protected static function defaultPendingBooking(): string
    {
        return "¡Gracias! En {negocio} ya registramos tu solicitud.\n\n"
            ."Servicio: {servicio}\n"
            ."Profesional: {profesional}\n"
            ."Fecha: {fecha}\n"
            ."Hora: {hora}\n\n"
            .'Estado: pendiente de aprobación. Cuando el profesional confirme, podés ver el detalle en la app. '
            ."Si necesitás cambiar algo, escribinos por este chat.{app_extra}\n\n"
            .'¿Necesitás algo más? Estamos para ayudarte.';
    }

    protected static function defaultAppointmentsEmpty(): string
    {
        return "Por ahora no tenés citas próximas registradas.\n\n"
            ."Escribí 1 para reservar o elegí una opción del menú.";
    }

    protected static function defaultAppointmentsFooter(): string
    {
        return 'Para reservar otra cita escribí 1, para cancelar 2.{app_extra}';
    }

    protected static function defaultDateListBody(): string
    {
        return 'Elegí una fecha (mostramos los próximos días con turno disponible).';
    }

    protected static function defaultDateListFooter(): string
    {
        return 'Para ver más fechas y horarios, abrí la app: {url_app}{app_extra}';
    }

    protected static function defaultCancelListIntro(): string
    {
        return 'Elegí la cita que querés cancelar:';
    }

    public static function unregistered(string $phoneDigits, ?string $waProfileName = null): string
    {
        $nombre = self::firstNameForGreeting(null, $waProfileName);
        $appExtra = self::includeAppUrl()
            ? "\n\nTambién podés entrar a la app desde el navegador: {url_app}"
            : '';
        $t = self::get('wa_copy_unregistered', self::defaultUnregistered());
        $t = str_replace('{app_extra}', $appExtra, $t);
        $t = str_replace('{nombre}', $nombre, $t);
        $t = str_replace('{negocio}', self::businessName(), $t);
        $t = str_replace('{url_registro}', self::registrationUrl($phoneDigits), $t);
        $t = str_replace('{url_app}', self::includeAppUrl() ? self::publicAppUrl() : '', $t);

        return self::normalizeBlankLines($t);
    }

    public static function mainMenuBody(User $client, ?string $waProfileName = null): string
    {
        $nombre = self::firstNameForGreeting($client, $waProfileName);
        $appExtra = self::includeAppUrl()
            ? "\n\n📱 App: {url_app}"
            : '';
        $t = self::get('wa_copy_main_menu', self::defaultMainMenu());
        $t = str_replace('{app_extra}', $appExtra, $t);
        $t = str_replace('{nombre}', $nombre, $t);
        $t = str_replace('{negocio}', self::businessName(), $t);
        $t = str_replace('{url_app}', self::includeAppUrl() ? self::publicAppUrl() : '', $t);

        return self::normalizeBlankLines($t);
    }

    public static function farewellAfterFollowup(User $client, ?string $waProfileName = null): string
    {
        $nombre = self::firstNameForGreeting($client, $waProfileName);
        $t = self::get('wa_copy_farewell_after_followup', self::defaultFarewellAfterFollowup());
        $t = str_replace('{nombre}', $nombre, $t);
        $t = str_replace('{negocio}', self::businessName(), $t);

        return self::normalizeBlankLines($t);
    }

    public static function contactBarberShop(User $client, ?string $waProfileName = null): string
    {
        return self::humanMessageAck($client, $waProfileName);
    }

    public static function conversationalAck(User $client, ?string $waProfileName = null): string
    {
        return self::humanMessageAck($client, $waProfileName);
    }

    public static function humanMessageAck(User $client, ?string $waProfileName = null): string
    {
        $nombre = self::firstNameForGreeting($client, $waProfileName);
        $appExtra = self::includeAppUrl()
            ? "\n\n📱 App: {url_app}"
            : '';
        $primary = AppSetting::getValue('wa_copy_human_message_ack');
        $t = ($primary !== null && trim((string) $primary) !== '') ? (string) $primary : null;
        if ($t === null) {
            $fallback = AppSetting::getValue('wa_copy_conversational_ack');
            $t = ($fallback !== null && trim((string) $fallback) !== '') ? (string) $fallback : self::defaultHumanMessageAck();
        }
        $t = str_replace('{app_extra}', $appExtra, $t);
        $t = str_replace('{nombre}', $nombre, $t);
        $t = str_replace('{negocio}', self::businessName(), $t);
        $t = str_replace('{url_app}', self::includeAppUrl() ? self::publicAppUrl() : '', $t);

        return self::normalizeBlankLines($t);
    }

    public static function softAfterFollowup(): string
    {
        return self::normalizeBlankLines(self::get('wa_copy_soft_after_followup', self::defaultSoftAfterFollowup()));
    }

    public static function quietReminder(): string
    {
        return self::normalizeBlankLines(self::get('wa_copy_quiet_reminder', self::defaultQuietReminder()));
    }

    /** Etiquetas de botones del menú (máx. ~20 caracteres cada una en WhatsApp). */
    public static function mainMenuButtons(): array
    {
        $d = self::defaultFlowStrings();

        return [
            'reservar' => self::get('wa_copy_btn_reservar', $d['wa_copy_btn_reservar']),
            'cancelar' => self::get('wa_copy_btn_cancelar', $d['wa_copy_btn_cancelar']),
            'mis_citas' => self::get('wa_copy_btn_mis_citas', $d['wa_copy_btn_mis_citas']),
        ];
    }

    public static function servicesListBody(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_services_list_body', $d['wa_copy_services_list_body']));
    }

    public static function servicesListButton(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_services_list_button', $d['wa_copy_services_list_button']);
    }

    public static function servicesSectionTitle(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_services_section_title', $d['wa_copy_services_section_title']);
    }

    public static function noServices(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_no_services', $d['wa_copy_no_services']));
    }

    public static function invalidService(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_invalid_service', $d['wa_copy_invalid_service']));
    }

    public static function barbersListBody(): string
    {
        $d = self::defaultFlowStrings();
        $t = self::get('wa_copy_barbers_list_body', $d['wa_copy_barbers_list_body']);

        return self::normalizeBlankLines(self::replaceStaffLabel($t));
    }

    public static function barbersListButton(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_barbers_list_button', $d['wa_copy_barbers_list_button']);
    }

    public static function barbersSectionTitle(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_barbers_section_title', $d['wa_copy_barbers_section_title']);
    }

    public static function noBarbersForService(): string
    {
        $d = self::defaultFlowStrings();
        $t = self::get('wa_copy_no_barbers', $d['wa_copy_no_barbers']);

        return self::normalizeBlankLines(self::replaceStaffLabel($t));
    }

    public static function invalidBarber(): string
    {
        $d = self::defaultFlowStrings();
        $t = self::get('wa_copy_invalid_barber', $d['wa_copy_invalid_barber']);

        return self::normalizeBlankLines(self::replaceStaffLabel($t));
    }

    public static function noDatesAvailable(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_no_dates', $d['wa_copy_no_dates']));
    }

    public static function invalidDate(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_invalid_date', $d['wa_copy_invalid_date']));
    }

    public static function noSlotsForDate(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_no_slots_date', $d['wa_copy_no_slots_date']));
    }

    public static function timeListBody(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_time_list_body', $d['wa_copy_time_list_body']));
    }

    public static function timeListButton(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_time_list_button', $d['wa_copy_time_list_button']);
    }

    public static function timeSectionTitle(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_time_section_title', $d['wa_copy_time_section_title']);
    }

    public static function flowLost(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_flow_lost', $d['wa_copy_flow_lost']));
    }

    public static function slotMismatch(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_slot_mismatch', $d['wa_copy_slot_mismatch']));
    }

    public static function accountNotFound(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_account_not_found', $d['wa_copy_account_not_found']));
    }

    public static function slotTakenConflict(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_slot_taken', $d['wa_copy_slot_taken']));
    }

    public static function bookingError(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_booking_error', $d['wa_copy_booking_error']));
    }

    public static function cancelNone(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_cancel_none', $d['wa_copy_cancel_none']));
    }

    public static function cancelListBody(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_cancel_list_body', $d['wa_copy_cancel_list_body']));
    }

    public static function cancelListButton(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_cancel_list_button', $d['wa_copy_cancel_list_button']);
    }

    public static function cancelListSectionTitle(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_cancel_list_section', $d['wa_copy_cancel_list_section']);
    }

    public static function cancelInvalidAppointment(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_cancel_invalid', $d['wa_copy_cancel_invalid']));
    }

    public static function cancelConfirmBody(Appointment $appointment): string
    {
        $appointment->load(['service', 'barber.user']);
        $d = self::defaultFlowStrings();
        $t = self::get('wa_copy_cancel_confirm_body', $d['wa_copy_cancel_confirm_body']);
        $t = str_replace('{fecha}', self::formatDateSpanish($appointment->appointment_date), $t);
        $t = str_replace('{hora}', self::formatTimeDisplay($appointment->start_time), $t);
        $t = str_replace('{servicio}', $appointment->service?->name ?? '—', $t);
        $t = str_replace('{profesional}', $appointment->barber?->user?->name ?? '—', $t);
        $t = self::replaceStaffLabel($t);

        return self::normalizeBlankLines($t);
    }

    public static function cancelConfirmYesLabel(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_cancel_confirm_yes', $d['wa_copy_cancel_confirm_yes']);
    }

    public static function cancelConfirmNoLabel(): string
    {
        $d = self::defaultFlowStrings();

        return self::get('wa_copy_cancel_confirm_no', $d['wa_copy_cancel_confirm_no']);
    }

    public static function cancelDone(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_cancel_done', $d['wa_copy_cancel_done']));
    }

    public static function cancelKept(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_cancel_kept', $d['wa_copy_cancel_kept']));
    }

    public static function appointmentsHeader(): string
    {
        $d = self::defaultFlowStrings();

        return self::normalizeBlankLines(self::get('wa_copy_appointments_header', $d['wa_copy_appointments_header']));
    }

    public static function appointmentLine(Appointment $a): string
    {
        $a->load(['service', 'barber.user']);
        $d = self::defaultFlowStrings();
        $pending = self::get('wa_copy_status_pending_label', $d['wa_copy_status_pending_label']);
        $confirmed = self::get('wa_copy_status_confirmed_label', $d['wa_copy_status_confirmed_label']);
        $estado = $a->status === 'pending' ? $pending : $confirmed;
        $t = self::get('wa_copy_appointment_line', $d['wa_copy_appointment_line']);
        $t = str_replace('{fecha}', self::formatDateSpanish($a->appointment_date), $t);
        $t = str_replace('{hora}', self::formatTimeDisplay($a->start_time), $t);
        $t = str_replace('{servicio}', $a->service?->name ?? '—', $t);
        $t = str_replace('{profesional}', $a->barber?->user?->name ?? '—', $t);
        $t = str_replace('{estado}', $estado, $t);

        return self::normalizeBlankLines($t);
    }

    /** Cuerpo del mensaje de lista de fechas (antes del botón «Ver fechas»). */
    public static function dateListBody(): string
    {
        $t = self::get('wa_copy_date_list_body', self::defaultDateListBody());

        return self::normalizeBlankLines($t);
    }

    /** Mensaje corto después de enviar la lista de fechas (sugerencia de app). Vacío si no hay URL de app. */
    public static function dateListFooter(): string
    {
        if (! self::includeAppUrl()) {
            return '';
        }
        $t = self::get('wa_copy_date_list_footer', self::defaultDateListFooter());
        $t = str_replace('{app_extra}', '', $t);
        $t = str_replace('{url_app}', self::publicAppUrl(), $t);

        return self::normalizeBlankLines($t);
    }

    public static function cancelListIntro(): string
    {
        return self::normalizeBlankLines(self::get('wa_copy_cancel_list_intro', self::defaultCancelListIntro()));
    }

    public static function pendingBooking(Appointment $appointment): string
    {
        $appointment->load(['service', 'barber.user']);
        $appExtra = self::includeAppUrl()
            ? "\n\n📱 App: {url_app}"
            : '';
        $t = self::get('wa_copy_pending_booking', self::defaultPendingBooking());
        $t = str_replace('{app_extra}', $appExtra, $t);
        $t = str_replace('{negocio}', self::businessName(), $t);
        $t = str_replace('{servicio}', $appointment->service?->name ?? 'Servicio', $t);
        $t = str_replace('{profesional}', $appointment->barber?->user?->name ?? 'Profesional', $t);
        $t = str_replace('{fecha}', self::formatDateSpanish($appointment->appointment_date), $t);
        $t = str_replace('{hora}', self::formatTimeDisplay($appointment->start_time), $t);
        $t = str_replace('{url_app}', self::includeAppUrl() ? self::publicAppUrl() : '', $t);

        return self::normalizeBlankLines($t);
    }

    public static function appointmentsEmpty(): string
    {
        return self::get('wa_copy_appointments_empty', self::defaultAppointmentsEmpty());
    }

    public static function appointmentsFooter(): string
    {
        $appExtra = self::includeAppUrl()
            ? ' Podés gestionar todo en la app: {url_app}'
            : '';
        $t = self::get('wa_copy_appointments_footer', self::defaultAppointmentsFooter());
        $t = str_replace('{app_extra}', $appExtra, $t);
        $t = str_replace('{url_app}', self::includeAppUrl() ? self::publicAppUrl() : '', $t);

        return self::normalizeBlankLines($t);
    }

    /** Fecha en español (ej. «miércoles 16 de abril de 2026»). */
    public static function formatDateSpanish(mixed $date): string
    {
        if ($date instanceof Carbon) {
            $c = $date->copy()->locale('es');
        } else {
            $c = Carbon::parse($date)->locale('es');
        }

        return $c->isoFormat('dddd D [de] MMMM [de] YYYY');
    }

    /** Día corto en español para filas de lista (ej. «mié 16/04»). */
    public static function formatWeekdayShortSpanish(mixed $date): string
    {
        if ($date instanceof Carbon) {
            $c = $date->copy()->locale('es');
        } else {
            $c = Carbon::parse($date)->locale('es');
        }

        return $c->translatedFormat('D d/m');
    }

    public static function formatTimeDisplay(mixed $time): string
    {
        if ($time === null) {
            return '';
        }
        $s = is_string($time) ? $time : (string) $time;
        try {
            return Carbon::parse($s)->format('H:i');
        } catch (\Throwable) {
            return $s;
        }
    }

    protected static function normalizeBlankLines(string $text): string
    {
        $out = preg_replace("/\n{3,}/", "\n\n", trim($text)) ?? $text;

        return $out;
    }
}
