<?php

declare(strict_types=1);

namespace SmsNotifier\Facade;

use SmsNotifier\Data\NotificationData;
use SmsNotifier\Factory\MessageTextFactory;
use SmsNotifier\Service\Logger;
use SmsNotifier\Service\SmsNumberProvider;
use GuzzleHttp\Client;
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use Stripe\StripeClient;

abstract class AbstractStripeOperationsFacade
{
    protected $logger;
    protected $messageTextFactory;
    protected $clientPhoneNumber;
    protected $ucrmApi;
    private $systemAttributesCache = null;

    public function __construct(Logger $logger, MessageTextFactory $messageTextFactory, SmsNumberProvider $clientPhoneNumber)
    {
        $this->logger = $logger;
        $this->messageTextFactory = $messageTextFactory;
        $this->clientPhoneNumber = $clientPhoneNumber;

        $config = PluginConfigManager::create()->loadConfig();
        $ipServer = $config['ipserver'] ?? 'localhost';
        $apiUrl = "https://$ipServer/crm/api/v1.0/";

        $client = new Client([
            'base_uri' => $apiUrl,
            'verify' => false,
        ]);

        $this->ucrmApi = new UcrmApi($client, $config['apitoken'] ?? '');
    }

    public function createPaymentIntent(array $eventJson): void
    {
        $config = PluginConfigManager::create()->loadConfig();
        $stripe = new StripeClient($config['tokenstripe']);

        $customer = $eventJson['data']['object']['customer'] ?? null;
        $amount = $eventJson['data']['object']['net_amount'] ?? 0;

        if (!$customer || $amount <= 0) {
            $this->logger->warning("Datos inválidos para PaymentIntent: Customer=$customer, Amount=$amount");
            return;
        }

        try {
            $stripeCustomer = $stripe->customers->retrieve($customer);
            $ucrmClientId = $stripeCustomer->metadata->ucrm_client_id ?? null;
            // [FIX] Validar el Saldo Disponible en la Billetera (Cash Balance)
            // Si la cuenta tiene reconciliación automática, Stripe absorbe fondos instantáneamente
            // para pagar intenciones preexistentes. Si ya se gastaron los fondos, evitamos duplicar.
            $cashBalance = $stripe->customers->retrieveCashBalance($customer, []);
            $availableMxn = $cashBalance->available->mxn ?? 0;
            $amountCents = (int)$amount;

            // Si el saldo disponible es menor al monto fondeado, significa que una PI
            // preexistente ya "se tragó" este saldo automáticamente.
            if ($availableMxn < $amountCents) {
                $this->logger->info("Fondos ya reconciliados por Stripe. Saldo disponible ({$availableMxn}) es menor al evento ({$amountCents}). Se descarta la creación de un nuevo PaymentIntent duplicado.");
                return;
            }


            $pi = $stripe->paymentIntents->create([
                'amount' => (int)$amount,
                'currency' => 'mxn',
                'customer' => $customer,
                'payment_method_types' => ['customer_balance'],
                'payment_method_data' => ['type' => 'customer_balance'],
                'confirm' => true,
                'payment_method_options' => [
                    'customer_balance' => [
                        'funding_type' => 'bank_transfer',
                        'bank_transfer' => ['type' => 'mx_bank_transfer']
                    ],
                ],
                'metadata' => [
                    'clientId' => $ucrmClientId,
                    'createdBy' => 'UCRM',
                    'signedInAdminId' => $config['idPaymentAdminCRM'],
                    'tipoPago' => 'Transferencia Bancaria'
                ],
            ], ['idempotency_key' => $eventJson['id'] ?? null]);
            $this->logger->info("PaymentIntent creado: " . $pi->id);
        } catch (\Exception $e) {
            $this->logger->error("Error creando PaymentIntent: " . $e->getMessage());
        }
    }

    public function registerPaymentFromWebhook(array $eventJson): void
    {
        $config = PluginConfigManager::create()->loadConfig();
        $stripe = new StripeClient($config['tokenstripe']);
        $data = $eventJson['data']['object'];

        $piId = $data['applied_to_payment']['payment_intent'] ?? null;
        if (!$piId) return;

        try {
            $pi = $stripe->paymentIntents->retrieve($piId);
            $clientId = $pi->metadata->clientId ?? null;
            if (!$clientId) return;

            $methodId = $this->findPaymentMethodId('Transferencia bancaria');

            if ($methodId) {
                $this->ucrmApi->post('payments', [
                    'clientId' => (int)$clientId,
                    'amount' => abs($data['net_amount']) / 100,
                    'currencyCode' => strtoupper($pi->currency),
                    'methodId' => $methodId,
                    'note' => "Transferencia Bancaria (Stripe Saldo) - PI: $piId",
                    'createdDate' => date('c'),
                ]);
            }
        } catch (\Exception $e) {
            $this->logger->error("Error al registrar pago en UCRM: " . $e->getMessage());
        }
    }

    public function registerPaymentFromIntent(array $data): void
    {
        $piId = $data['id'] ?? null;
        if (!$piId) return;

        if (($data['status'] ?? '') !== 'succeeded') {
            $this->logger->warning("PaymentIntent $piId no está succeeded (status: " . ($data['status'] ?? 'unknown') . ")");
            return;
        }

        // Buscar clientId en metadata (prioridad: clientId > ucrm_client_id)
        $clientId = $data['metadata']['clientId'] ?? $data['metadata']['ucrm_client_id'] ?? null;

        // Fallback: Buscar por Stripe Customer ID si no hay metadata
        if (!$clientId && !empty($data['customer'])) {
            $clientId = $this->findClientIdByStripeCustomer($data['customer']);
            if ($clientId) {
                $this->logger->info("Cliente encontrado por Stripe ID ({$data['customer']}): $clientId");
            }
        }

        if (!$clientId) {
            $this->logger->warning("PaymentIntent $piId no tiene clientId en metadata y no se encontró por Customer ID ({$data['customer']}).");
            return;
        }

        // [NEW] Check for duplicate payment (same PI ID)
        try {
            $existingPayments = $this->ucrmApi->get('payments', [
                'clientId' => $clientId,
                'limit' => 20,
                'order' => 'createdDate',
                'direction' => 'DESC'
            ]);

            foreach ($existingPayments as $p) {
                // Check note for PI ID
                if (isset($p['note']) && strpos($p['note'], $piId) !== false) {
                    $this->logger->info("Pago duplicado detectado para PI $piId (ID existente: {$p['id']}). Omitiendo creación.");
                    return;
                }
                // Also check duplicate by transaction ID if applicable
                if (isset($p['transactionId']) && $p['transactionId'] === $piId) {
                    $this->logger->info("Pago duplicado detectado (Transaction ID) para PI $piId (ID existente: {$p['id']}). Omitiendo creación.");
                    return;
                }
            }
        } catch (\Exception $e) {
            $this->logger->warning("Falló la verificación de duplicados para PI $piId: " . $e->getMessage());
        }

        try {
            // Intentar detectar Payment Method name basado en tipo
            $type = $data['payment_method_types'][0] ?? 'card';
            $methodSearchName = ($type === 'oxxo') ? 'OXXO' : 'Stripe';

            // Buscar ID de metodo en UCRM
            $methodId = $this->findPaymentMethodId($methodSearchName);
            if (!$methodId && $methodSearchName === 'OXXO') {
                $methodId = $this->findPaymentMethodId('Stripe'); // Fallback
            }

            if ($methodId) {
                $checkAmount = ($data['amount_received'] ?? $data['amount']) / 100;
                $this->ucrmApi->post('payments', [
                    'clientId' => (int)$clientId,
                    'amount' => $checkAmount,
                    'currencyCode' => strtoupper($data['currency'] ?? 'MXN'),
                    'methodId' => $methodId,
                    'note' => "Stripe ($methodSearchName) - PI: $piId",
                    'createdDate' => date('c'),
                ]);

                $this->logger->info("Pago registrado en UCRM vía PI Succeeded: $piId, Cliente: $clientId, Monto: $checkAmount");
            } else {
                $this->logger->warning("No se encontró método de pago para '$methodSearchName' en UCRM.");
            }
        } catch (\Exception $e) {
            $this->logger->error("Error al registrar pago desde PaymentIntent: " . $e->getMessage());
        }
    }

    private function findClientIdByStripeCustomer(string $stripeCustomerId): ?int
    {
        try {
            // Nota: Esto puede ser lento si hay muchos clientes, pero es un fallback.
            // Idealmente usaríamos $this->ucrmApi->get('clients', ['customAttributeKey' => 'stripeCustomerId', ...]) si existiera ese filtro.
            // Como fallback, buscamos en los clientes que tenemos cacheados o hacemos un search.
            // UCRM API permite filtrar por userIdent? No directamente por atributo custom en GET /clients sin plugin extentions.
            // PERO podemos usar la API de 'clients' y filtrar en memoria si no son muchos, o confiar en que el usuario ya usó metadata en el futuro.
            // MEJOR OPCION: Usar el endpoint de attributes si es posible, o iterar. 
            // Dado que no queremos matar el server, limitaremos la búsqueda o asumiremos que el FIX de metadata es el principal.
            // Sin embargo, para este caso específico, vamos a intentar buscar en los clientes recientes o usar una búsqueda básica.
            // UCRM API v1.0 GET /clients soporta ?customAttributeId&customAttributeValue

            $attrId = $this->resolveAttributeId('stripeCustomerId');
            if (!$attrId) return null;

            $clients = $this->ucrmApi->get('clients', [
                'customAttributeId' => $attrId,
                'customAttributeValue' => $stripeCustomerId
            ]);

            if (!empty($clients) && isset($clients[0]['id'])) {
                return (int)$clients[0]['id'];
            }
        } catch (\Exception $e) {
            $this->logger->error("Error buscando cliente por Stripe ID: " . $e->getMessage());
        }
        return null;
    }

    private function findPaymentMethodId(string $name): ?int
    {
        try {
            $methods = $this->ucrmApi->get('payment-methods');
            foreach ($methods as $m) {
                if (stripos($m['name'], $name) !== false) {
                    return $m['id'];
                }
            }
        } catch (\Exception $e) {
        }
        return null;
    }

    public function createStripeClient(NotificationData $notificationData, string $tagName, bool $generateSpei = true): void
    {
        $clientId = $notificationData->clientId;
        if (!$clientId) return;

        $config = PluginConfigManager::create()->loadConfig();
        $stripe = new StripeClient($config['tokenstripe']);

        try {
            $clientCRM = $this->ucrmApi->get("clients/$clientId", []);

            // Automaticamente creará el el Stripe Customer si no existe

            $customer = $this->createCustomerStripe($stripe, $clientCRM, $generateSpei);

            if ($customer) {
                $this->logger->info("Cliente Stripe procesado para ID: $clientId (Tag: $tagName, SPEI: " . ($generateSpei ? 'SI' : 'NO') . ")");
            }
        } catch (\Exception $e) {
            $this->logger->error("Error en createStripeClient para cliente $clientId: " . $e->getMessage());
        } finally {
            // Garantizamos quitar la etiqueta siempre para evitar re-ejecuciones accidentales
            $this->removeTagFromClient($clientId, $tagName);
        }
    }

    protected function createCustomerStripe(StripeClient $stripe, array $clientCRM, bool $generateSpei): ?\Stripe\Customer
    {
        $clientId = $clientCRM['id'];

        // Extraer email de contactos (prioridad) o username
        $email = $clientCRM['username'] ?? null;
        foreach ($clientCRM['contacts'] ?? [] as $contact) {
            if ($contact['isBilling'] || $contact['isContact']) {
                $email = $contact['email'] ?? $email;
                if ($email) break;
            }
        }

        $name = trim(($clientCRM['firstName'] ?? '') . ' ' . ($clientCRM['lastName'] ?? ''));
        if (empty($name) && !empty($clientCRM['companyName'])) {
            $name = $clientCRM['companyName'];
        }

        // Obtener IDs de atributos dinámicamente desde el sistema
        $cidAttrId = $this->resolveAttributeId('stripeCustomerId');
        $clabeAttrId = $this->resolveAttributeId('clabeInterbancaria');

        $this->logger->debug("IDs de atributos resueltos: Stripe=$cidAttrId, CLABE=$clabeAttrId");

        // Buscar cliente existente por metadata
        $customers = $stripe->customers->search([
            'query' => "metadata['ucrm_client_id']:'$clientId'",
        ]);

        if ($customers->count() > 0) {
            $customer = $customers->data[0];
            // Sincronizar datos básicos
            $stripe->customers->update($customer->id, [
                'email' => $email,
                'name' => $name,
            ]);
            $this->logger->info("Datos básicos de Cliente Stripe sincronizados para ID: $clientId");
        } else {
            $params = [
                'email' => $email,
                'name' => $name,
                'metadata' => ['ucrm_client_id' => $clientId]
            ];

            $customer = $stripe->customers->create($params);
            $this->logger->info("Nuevo Cliente Stripe creado para ID: $clientId. CID: {$customer->id}");
        }

        // Guardar CID en UCRM siempre, por si no estaba sincronizado
        $this->patchClientCustomAttribute($clientId, (int)$cidAttrId, $customer->id);

        // Si se requiere SPEI, generamos las instrucciones de fondeo para obtener la CLABE
        if ($generateSpei) {
            try {
                $this->logger->info("Solicitando instrucciones de fondeo (CLABE) para cliente: {$customer->id}");
                $fundingInstructions = $stripe->customers->createFundingInstructions(
                    $customer->id,
                    [
                        'currency' => 'mxn',
                        'funding_type' => 'bank_transfer',
                        'bank_transfer' => ['type' => 'mx_bank_transfer'],
                    ]
                );

                $clabe = $fundingInstructions['bank_transfer']['financial_addresses'][0]['spei']['clabe'] ?? null;

                if ($clabe) {
                    $this->logger->info("CLABE obtenida via Funding Instructions para cliente $clientId: $clabe");
                    $this->patchClientCustomAttribute($clientId, (int)$clabeAttrId, $clabe);
                } else {
                    $this->logger->warning("Stripe no devolvió una CLABE en las instrucciones de fondeo para el cliente $clientId.");
                }
            } catch (\Exception $e) {
                $this->logger->error("Error al crear instrucciones de fondeo para cliente $clientId: " . $e->getMessage());
            }
        }

        return $customer;
    }

    protected function getVaultCredentialsByClientId($clientId): string
    {
        $config = PluginConfigManager::create()->loadConfig();
        $ipServer = $config['ipserver'] ?? '';

        try {
            // OPT: Lazy Check - Si ya tiene pass válido en CRM, no hace falta procesar nada
            $clientData = $this->ucrmApi->get("clients/$clientId");
            $passCRM = '';
            if (isset($clientData['attributes'])) {
                foreach ($clientData['attributes'] as $attr) {
                    if ($attr['key'] === 'passwordAntenaCliente') {
                        $passCRM = $attr['value'] ?? '';
                        break;
                    }
                }
            }

            // Si el campo no está vacío y no tiene advertencias, usamos el actual para ahorrar recursos
            if (!empty($passCRM) && strpos($passCRM, '⚠️') === false) {
                return $passCRM;
            }

            // 1. Obtener los servicios del cliente
            $svcs = $this->ucrmApi->get('clients/services', ['clientId' => $clientId]);

            if (empty($svcs)) {
                $msg = '⚠️ Cliente sin servicios/antenas';
                $this->syncPasswordWithCrm((int)$clientId, $msg);
                return $msg;
            }

            $unms = new Client(['base_uri' => "https://{$ipServer}/nms/api/v2.1/", 'verify' => false, 'headers' => ['X-Auth-Token' => $config['apitoken']]]);
            $allServicePasswords = [];
            $isTestEnv = ($ipServer === '172.16.5.134' || $ipServer === 'pruebas.internet.mx' || $ipServer === 'venus.siip.mx');

            $numServices = count($svcs);
            foreach ($svcs as $index => $svc) {
                $label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : "";
                $siteId = $svc['unmsClientSiteId'] ?? null;
                $passwordValue = "";

                if (!$siteId) {
                    $passwordValue = "⚠️ Sin sitio";
                } else {
                    if ($isTestEnv) {
                        // Lógica de bypass: intentar recuperar de la cadena existente
                        $foundInCRM = false;
                        if (!empty($passCRM)) {
                            if ($numServices > 1) {
                                if (preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $passCRM, $matches)) {
                                    $passwordValue = $matches[1];
                                    $foundInCRM = true;
                                }
                            } else {
                                // Caso de un solo servicio: si no tiene advertencias ni etiquetas, asumimos que es el pass
                                if (strpos($passCRM, '⚠️') === false && strpos($passCRM, 'Servicio') === false) {
                                    $passwordValue = trim($passCRM);
                                    $foundInCRM = true;
                                }
                            }
                        }

                        if (!$foundInCRM) {
                            $passwordValue = $this->generateStrongPassword(16);
                            // [NEW] Static Test Data for Site/Antenna
                            $this->syncNetworkDataWithCrm((int)$clientId, 'VENUS', 'Sectorial de pruebas 172.16.5.134');
                        } else {
                            // Even if found, ensure static data in test env
                            $this->syncNetworkDataWithCrm((int)$clientId, 'VENUS', 'Sectorial de pruebas 172.16.5.134');
                        }
                    } else {
                        // Lógica de producción
                        try {
                            $respDev = $unms->get("devices?siteId=$siteId", [
                                'headers' => ['X-Auth-Token' => $config['unmsApiToken']]
                            ]);
                            $devs = json_decode($respDev->getBody()->getContents(), true);

                            if (empty($devs)) {
                                $passwordValue = "⚠️ Sin antena";
                            } else {
                                $passVault = null;
                                $firstDeviceId = null;
                                foreach ($devs as $dev) {
                                    $deviceId = $dev['identification']['id'] ?? null;
                                    if (!$deviceId) continue;
                                    if (!$firstDeviceId) $firstDeviceId = $deviceId;
                                    try {
                                        $respVault = $unms->get("vault/$deviceId/credentials", [
                                            'headers' => ['X-Auth-Token' => $config['unmsApiToken']]
                                        ]);
                                        $vault = json_decode($respVault->getBody()->getContents(), true);
                                        if (isset($vault['credentials'][0]['password'])) {
                                            $passVault = $vault['credentials'][0]['password'];
                                            break;
                                        }
                                    } catch (\Exception $e) {
                                        continue;
                                    }
                                }

                                if ($passVault) {
                                    $passwordValue = $passVault;
                                } else if ($firstDeviceId) {
                                    // Regenerar
                                    $newPass = $this->generateStrongPassword(16);
                                    try {
                                        $unms->post("vault/$firstDeviceId/credentials/regenerate", [
                                            'headers' => ['X-Auth-Token' => $config['unmsApiToken']],
                                            'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
                                        ]);
                                        $passwordValue = $newPass;
                                    } catch (\Exception $e) {
                                        $passwordValue = $newPass;
                                    }
                                } else {
                                    $passwordValue = "⚠️ Sin antena";
                                }
                            }
                        } catch (\Exception $e) {
                            $passwordValue = "⚠️ Error API";
                        }
                    }
                }
                $allServicePasswords[] = trim("$label $passwordValue");
            }

            $finalValue = implode(' ', $allServicePasswords);

            // Evitar sincronización redundante si el valor es idéntico al actual (anti-bucle)
            if ($finalValue === $passCRM) {
                return $finalValue;
            }

            $this->syncPasswordWithCrm((int)$clientId, $finalValue);
            return $finalValue;
        } catch (\Exception $e) {
            $this->logger->error("Excepción en getVaultCredentialsByClientId (Cliente: $clientId): " . $e->getMessage());
            return 'Error: ' . $e->getMessage();
        }
    }

    private function syncNetworkDataWithCrm(int $clientId, string $siteName, string $deviceInfo): void
    {
        try {
            $siteAttrId = $this->resolveAttributeId('site');
            $antennaAttrId = $this->resolveAttributeId('antenaSectorial');

            if ($siteAttrId) {
                $this->patchClientCustomAttribute($clientId, $siteAttrId, $siteName);
            }
            if ($antennaAttrId) {
                $this->patchClientCustomAttribute($clientId, $antennaAttrId, $deviceInfo);
            }
        } catch (\Exception $e) {
            $this->logger->warning("Fallo al sincronizar datos de red estáticos en test env para cliente $clientId: " . $e->getMessage());
        }
    }

    private function syncPasswordWithCrm(int $clientId, string $passVault): void
    {
        try {
            $clientData = $this->ucrmApi->get("clients/$clientId");

            $passCRM = '';
            $attributeId = $this->resolveAttributeId('passwordAntenaCliente');

            if (isset($clientData['attributes'])) {
                foreach ($clientData['attributes'] as $attr) {
                    if ($attr['key'] === 'passwordAntenaCliente') {
                        $passCRM = $attr['value'] ?? '';
                        $attributeId = $attr['customAttributeId'] ?? $attributeId;
                        break;
                    }
                }
            }

            if ($attributeId <= 0) {
                $this->logger->error("No se pudo resolver el ID de atributo para 'passwordAntenaCliente'. Omitiendo sincronización para cliente $clientId.");
                return;
            }

            if (empty($passCRM) || $passCRM !== $passVault) {
                $this->logger->info("Sincronizando contraseña en CRM para cliente $clientId. [" . ($passCRM ?: 'VACIO') . "] -> [$passVault] (Atributo ID: $attributeId)");
                $this->patchClientCustomAttribute($clientId, (int)$attributeId, $passVault);
            }
        } catch (\Exception $e) {
            $this->logger->warning("Fallo al sincronizar contraseña con CRM para cliente $clientId: " . $e->getMessage());
        }
    }

    protected function generateStrongPassword(int $length = 16): string
    {
        $lower = 'abcdefghijkmnopqrstuvwxyz'; // Eliminamos 'l'
        $upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // Eliminamos 'I', 'O'
        $digits = '23456789'; // Eliminamos '1', '0'
        $symbols = '@#'; // Solo símbolos amigables para impresoras térmicas

        $all = $lower . $upper . $digits . $symbols;
        $pwChars = [];

        // Asegurar que tenga al menos uno de cada tipo si es posible
        $pwChars[] = $lower[random_int(0, strlen($lower) - 1)];
        $pwChars[] = $upper[random_int(0, strlen($upper) - 1)];
        $pwChars[] = $digits[random_int(0, strlen($digits) - 1)];
        $pwChars[] = $symbols[random_int(0, strlen($symbols) - 1)];

        for ($i = count($pwChars); $i < $length; $i++) {
            $pwChars[] = $all[random_int(0, strlen($all) - 1)];
        }

        // Mezclar Fisher-Yates
        $n = count($pwChars);
        for ($i = $n - 1; $i > 0; $i--) {
            $j = random_int(0, $i);
            $tmp = $pwChars[$i];
            $pwChars[$i] = $pwChars[$j];
            $pwChars[$j] = $tmp;
        }

        return implode('', $pwChars);
    }

    protected function patchClientCustomAttribute(int $clientId, int $attributeId, string $value): bool
    {
        if ($attributeId <= 0) {
            $this->logger->error("Intento de patchAttribute con ID inválido ($attributeId) para cliente $clientId");
            return false;
        }
        try {
            $this->logger->debug("Intentando PATCH en clients/$clientId para atributo ID: $attributeId con valor: $value");
            $this->ucrmApi->patch("clients/$clientId", [
                'attributes' => [
                    [
                        'customAttributeId' => $attributeId,
                        'value' => $value,
                    ],
                ],
            ]);
            return true;
        } catch (\Exception $e) {
            $this->logger->error("Error patching custom attribute for client $clientId: " . $e->getMessage());
            return false;
        }
    }

    private function resolveAttributeId(string $key): int
    {
        if ($this->systemAttributesCache === null) {
            try {
                $this->systemAttributesCache = $this->ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
            } catch (\Exception $e) {
                $this->logger->error("No se pudieron cargar los atributos del sistema: " . $e->getMessage());
                return 0;
            }
        }

        foreach ($this->systemAttributesCache as $attr) {
            if ($attr['key'] === $key) {
                return (int)$attr['id'];
            }
        }

        return 0;
    }

    private function resolvePaymentAttributeId(string $key): int
    {
        try {
            $attrs = $this->ucrmApi->get('custom-attributes', ['attributeType' => 'payment']);
            foreach ($attrs as $attr) {
                if ($attr['key'] === $key) {
                    return (int)$attr['id'];
                }
            }
        } catch (\Exception $e) {
            $this->logger->error("Error resolving payment custom attribute '$key': " . $e->getMessage());
        }
        return 0;
    }

    protected function comparePasswords(?string $crm, ?string $vault): string
    {
        if ($crm && strpos($crm, 'Error') !== 0) return $crm;
        if ($vault && strpos($vault, 'Error') !== 0) return $vault;
        return '⚠️ Probar pass conocida.';
    }

    public function syncStripeCustomerData(int $clientId, string $name, ?string $email): void
    {
        $config = PluginConfigManager::create()->loadConfig();
        $stripe = new StripeClient($config['tokenstripe']);
        try {
            $customers = $stripe->customers->search([
                'query' => "metadata['ucrm_client_id']:'$clientId'",
            ]);

            if ($customers->count() > 0) {
                $customer = $customers->data[0];
                $stripe->customers->update($customer->id, [
                    'name' => $name,
                    'email' => $email,
                ]);
                $this->logger->info("Sincronización automática a Stripe exitosa para cliente $clientId.");
            }
        } catch (\Exception $e) {
            $this->logger->error("Error sincronizando cliente $clientId a Stripe: " . $e->getMessage());
        }
    }

    protected function removeTagFromClient(int $clientId, string $tagName): void
    {
        try {
            $client = $this->ucrmApi->get("clients/$clientId");
            $targetTagId = null;
            $remainingTags = [];
            foreach ($client['tags'] as $tag) {
                if ($tag['name'] === $tagName) {
                    $targetTagId = $tag['id'];
                    break;
                }
            }

            if ($targetTagId) {
                // The proper UCRM endpoint to remove a tag from a client is PATCH /clients/{id}/remove-tag/{tagId}
                $this->ucrmApi->patch("clients/$clientId/remove-tag/$targetTagId");
                $this->logger->info("Etiqueta '$tagName' (ID: $targetTagId) removida del cliente $clientId.");
            } else {
                $this->logger->debug("Etiqueta '$tagName' no encontrada en el cliente $clientId, nada que remover.");
            }
        } catch (\Exception $e) {
            $this->logger->error("Error al remover etiqueta '$tagName' del cliente $clientId: " . $e->getMessage());
        }
    }

    protected function validarNumeroTelefono($n): string
    {
        if (!$n) return '';
        $n = preg_replace('/\D/', '', (string)$n);
        if (strlen($n) === 10) {
            return '521' . $n;
        }
        if (strlen($n) === 12 && strpos($n, '52') === 0) {
            return '521' . substr($n, 2);
        }
        return $n;
    }

    public function ensureStripePaymentAttribute($notificationObject): void
    {
        // 1. Get Payment ID
        $paymentId = is_object($notificationObject) ? ($notificationObject->entityId ?? null) : ($notificationObject['entityId'] ?? null);

        if (!$paymentId) {
            $this->logger->warning("ensureStripePaymentAttribute: No entityId found in notification.");
            return;
        }

        $this->logger->info("Verificando existencia de atributo 'tipoPagoStripe' para Payment ID: $paymentId");

        try {
            // Load Config
            $config = PluginConfigManager::create()->loadConfig();
            $ipPuppeteer = $config['ipPuppeteer'] ?? 'localhost';
            $portPuppeteer = $config['portPuppeteer'] ?? '3000';
            $stripeUserId = $config['idPaymentAdminCRM'] ?? null;

            $microserviceBaseUrl = "http://$ipPuppeteer:$portPuppeteer";
            $httpClient = new Client();

            // 2. Fetch Metadata from Microservice (DB Access)
            $metadataTipoPago = null;
            try {
                $response = $httpClient->get("$microserviceBaseUrl/stripe-metadata/$paymentId", ['timeout' => 5]);
                $data = json_decode($response->getBody()->getContents(), true);
                if (isset($data['metadata']['tipoPago'])) {
                    $metadataTipoPago = $data['metadata']['tipoPago'];
                    $this->logger->info("Microservice found metadata: tipoPago = '$metadataTipoPago'");
                }
                // Capture Stripe ID for prefix-based heuristic detection below
                $metadataStripeId = $data['stripeId'] ?? null;
            } catch (\Throwable $e) {
                $this->logger->warning("Microservice metadata fetch failed: " . $e->getMessage());
            }

            // --- LOCAL FALLBACK LOGIC ---
            if (!$metadataTipoPago && isset($config['tokenstripe'])) {
                try {
                    $this->logger->info("Attempting direct Stripe API fetch for Payment $paymentId");
                    $paymentInfo = $this->ucrmApi->get('payments/' . $paymentId);
                    $clientId = $paymentInfo['clientId'] ?? null;
                    $amount = isset($paymentInfo['amount']) ? round($paymentInfo['amount'] * 100) : null;

                    if ($clientId && $amount) {
                        $clientInfo = $this->ucrmApi->get('clients/' . $clientId);
                        $stripeCustomerId = null;
                        if (isset($clientInfo['attributes'])) {
                            foreach ($clientInfo['attributes'] as $attr) {
                                if (stripos($attr['name'] ?? '', 'Stripe') !== false && stripos($attr['name'] ?? '', 'Customer') !== false) {
                                    $stripeCustomerId = $attr['value'];
                                    break;
                                }
                                if (($attr['key'] ?? '') === 'stripeCustomerId' || ($attr['key'] ?? '') === 'customerIdStripe') {
                                    $stripeCustomerId = $attr['value'];
                                    break;
                                }
                            }
                        }

                        if ($stripeCustomerId) {
                            $stripeClient = new Client([
                                'base_uri' => 'https://api.stripe.com/v1/',
                                'auth' => [$config['tokenstripe'], ''],
                                'timeout' => 5
                            ]);
                            $res = $stripeClient->get("payment_intents?customer=$stripeCustomerId&limit=5");
                            $intents = json_decode($res->getBody()->getContents(), true);

                            if (isset($intents['data']) && is_array($intents['data'])) {
                                foreach ($intents['data'] as $intent) {
                                    if (isset($intent['amount']) && $intent['amount'] == $amount) {
                                        if (isset($intent['metadata']['tipoPago'])) {
                                            $metadataTipoPago = $intent['metadata']['tipoPago'];
                                            $this->logger->info("Direct Stripe API found metadata: tipoPago = '$metadataTipoPago'");
                                            break;
                                        }
                                        
                                        if (isset($intent['payment_method_types'])) {
                                            if (in_array('oxxo', $intent['payment_method_types'])) {
                                                $metadataTipoPago = 'OXXO';
                                                $this->logger->info("Direct Stripe API guessed 'OXXO' from payment_method_types");
                                                break;
                                            } elseif (in_array('customer_balance', $intent['payment_method_types']) || in_array('spei', $intent['payment_method_types'])) {
                                                $metadataTipoPago = 'Transferencia Bancaria';
                                                $this->logger->info("Direct Stripe API guessed 'Transferencia Bancaria' from payment_method_types");
                                                break;
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                } catch (\Throwable $e) {
                    $this->logger->warning("Direct Stripe API fallback failed: " . $e->getMessage());
                }
            }

            // 2.5. Last-resort heuristic: use Stripe ID prefix when metadata and API both fail
            // In Mexico ISP context: py_ prefix = OXXO (cash) payment, ch_ prefix = card charge
            if (!$metadataTipoPago && !empty($metadataStripeId)) {
                if (str_starts_with($metadataStripeId, 'py_')) {
                    $metadataTipoPago = 'OXXO';
                    $this->logger->info("Heuristic detection: stripe_id '$metadataStripeId' starts with 'py_' → assumed OXXO Pay");
                } elseif (str_starts_with($metadataStripeId, 'ch_')) {
                    $this->logger->debug("Heuristic detection: stripe_id '$metadataStripeId' starts with 'ch_' → confirmed card");
                    // No change: will default to Tarjeta de crédito/débito
                }
            }

            // 3. Update User ID if missing (Direct DB Patch via Microservice)
            // UCRM API doesn't support PATCH userId, so we use microservice
            if ($stripeUserId) {
                try {
                    $payment = $this->ucrmApi->get('payments/' . $paymentId);
                    if (empty($payment['userId'])) {
                        $this->logger->info("Payment $paymentId has no User ID. Assigning Stripe User ID: $stripeUserId");
                        $httpClient->patch("$microserviceBaseUrl/payments/$paymentId/user", [
                            'json' => ['userId' => $stripeUserId],
                            'timeout' => 5
                        ]);
                    } else {
                        $this->logger->debug("Payment $paymentId already has User ID: " . $payment['userId']);
                    }
                } catch (\Throwable $e) {
                    $this->logger->error("Failed to patch User ID via microservice: " . $e->getMessage());
                }
            }


            // [NEW] 3.5. Patch Payment Method ID via Microservice
            $targetMethodId = null;

            if ($metadataTipoPago === 'OXXO') {
                $targetMethodId = 'b01c0b35-b42c-48d9-9ad9-ea6591adfbbb'; // OXXO Pay
            } elseif ($metadataTipoPago === 'Transferencia Bancaria') {
                $targetMethodId = '4145b5f5-3bbc-45e3-8fc5-9cda970c62fb'; // Transferencia Bancaria
            } else {
                // [NEW] Default to "Credit/Debit Card" if no specific metadata type found
                $targetMethodId = '93814765-66a1-4c7d-a777-05c18fd6aab3'; // Tarjeta de crédito/débito
            }

            if ($targetMethodId) {
                // [NEW] Update Notification Object in Memory so the calling code knows the change
                if (is_object($notificationObject) && isset($notificationObject->paymentData)) {
                    // Fix for "Indirect modification of overloaded property" error
                    // We must read the array, modify it, and write it back.
                    $pData = $notificationObject->paymentData;
                    if (is_array($pData)) {
                        $pData['methodId'] = $targetMethodId;
                        $notificationObject->paymentData = $pData;
                    }
                }

                try {
                    // Check current methodId (reuse 'payment' if available, otherwise fetch)
                    // Note: We fetched 'payment' in Step 3 ONLY if stripeUserId was valid. 
                    // Safe to fetch again or reuse specific check.
                    $paymentCheck = $this->ucrmApi->get('payments/' . $paymentId);

                    if ($paymentCheck['methodId'] !== $targetMethodId) {
                        $this->logger->info("Payment $paymentId has wrong Method ID ({$paymentCheck['methodId']}). Patching to $targetMethodId via Microservice.");

                        $httpClient->patch("$microserviceBaseUrl/payments/$paymentId/method", [
                            'json' => ['methodId' => $targetMethodId],
                            'timeout' => 5
                        ]);
                        $this->logger->info("Payment Method ID patched successfully.");
                    }
                } catch (\Throwable $e) {
                    $this->logger->error("Failed to patch Payment Method ID via microservice: " . $e->getMessage());
                }
            }

            // 4. Determine Target Attribute Value
            // Truth Source Priority: 1. Metadata (DB), 2. Existing Attribute, 3. Method Name (Guess)

            // A. Check Existing Attribute (Don't overwrite valid values unless Metadata says otherwise?)
            // Actually, Metadata keys are stronger than manual edits if the flow is automatic. 
            // But let's respect existing valid attributes if metadata is missing.

            $paymentAttributeId = $this->resolvePaymentAttributeId('tipoPagoStripe');
            $payment = $this->ucrmApi->get('payments/' . $paymentId); // Re-fetch in case changed? Or Use previous result.
            $currentValue = null;
            $hasAttribute = false;
            foreach ($payment['attributes'] as $attr) {
                if ($attr['key'] === 'tipoPagoStripe' || ($paymentAttributeId > 0 && $attr['customAttributeId'] == $paymentAttributeId)) {
                    $hasAttribute = true;
                    $currentValue = $attr['value'];
                    if ($paymentAttributeId <= 0) {
                        $paymentAttributeId = $attr['customAttributeId'];
                    }
                    break;
                }
            }

            $targetValue = null;

            if ($metadataTipoPago) {
                // Normalize Metadata Values to Attribute Choice Values
                if ($metadataTipoPago === 'OXXO') {
                    $targetValue = 'OXXO Pay';
                } else {
                    $targetValue = $metadataTipoPago;
                }
            } else {
                // Fallback to Method Name Guessing if Metadata missing
                if ($hasAttribute && in_array($currentValue, ['OXXO Pay', 'Transferencia Bancaria', 'Tarjeta de Crédito'])) {
                    $this->logger->debug("Payment $paymentId ya tiene atributo '$currentValue' y no hay metadata. Respetando.");
                    return;
                }

                $methodId = $payment['methodId'];
                $method = $this->ucrmApi->get('payment-methods/' . $methodId);
                $methodName = $method['name'] ?? '';

                if (stripos($methodName, 'OXXO') !== false) {
                    $targetValue = 'OXXO Pay';
                } elseif (
                    stripos($methodName, 'Transferencia') !== false ||
                    stripos($methodName, 'Bank') !== false ||
                    stripos($methodName, 'transfer') !== false ||
                    stripos($methodName, 'ACH') !== false
                ) {
                    $targetValue = 'Transferencia Bancaria';
                } elseif (
                    stripos($methodName, 'Tarjeta') !== false ||
                    stripos($methodName, 'card') !== false ||
                    stripos($methodName, 'Crédito') !== false ||
                    stripos($methodName, 'débito') !== false
                ) {
                    $targetValue = 'Tarjeta de Crédito';
                }
                $this->logger->debug("Fallback Method Guessing '$methodName' -> '$targetValue'");
            }

            // 5. Apply Update
            if ($targetValue && $paymentAttributeId > 0) {
                // Check redundancy
                if ($hasAttribute && $currentValue === $targetValue) {
                    $this->logger->debug("Attribute already matches target '$targetValue'. Skipping patch.");
                    return;
                }

                $this->logger->info("PATCHING Payment $paymentId: Setting tipoPagoStripe = '$targetValue' (Atributo ID: $paymentAttributeId)");
                $this->ucrmApi->patch('payments/' . $paymentId, [
                    'attributes' => [
                        [
                            'customAttributeId' => $paymentAttributeId,
                            'value' => $targetValue
                        ]
                    ]
                ]);
            } else {
                $this->logger->debug("No se pudo determinar el tipoPagoStripe o no se resolvió el ID del atributo.");
            }
        } catch (\Throwable $e) {
            $this->logger->error("Error in ensureStripePaymentAttribute: " . $e->getMessage());
        }
    }

    abstract protected function sendWhatsApp(NotificationData $notificationData, string $clientPhoneNumber): void;
}
