<?php
/**
 * This file is part of FacturaScripts
 * Copyright (C) 2019-2026 Carlos Garcia Gomez <carlos@facturascripts.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

namespace FacturaScripts\Core\Lib\Email;

use FacturaScripts\Core\DataSrc\Empresas;
use FacturaScripts\Core\Html;
use FacturaScripts\Core\Model\User;
use FacturaScripts\Core\Tools;
use FacturaScripts\Core\Validator;
use FacturaScripts\Dinamic\Model\AttachedFile;
use FacturaScripts\Dinamic\Model\EmailNotification;
use FacturaScripts\Dinamic\Model\EmailSent;
use FacturaScripts\Dinamic\Model\Empresa;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\PHPMailer;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;

/**
 * Description of NewMail
 *
 * @author Carlos Garcia Gomez      <carlos@facturascripts.com>
 * @author Daniel Fernández Giménez <contacto@danielfg.es>
 */
class NewMail
{
    const ATTACHMENTS_TMP_PATH = 'MyFiles/Tmp/Email/';

    /** @var Empresa */
    public $empresa;

    /** @var string */
    public $fromEmail;

    /** @var string */
    public $fromName;

    /** @var string */
    public $fromNick;

    /** @var string */
    public $signature;

    /** @var string */
    public $text;

    /** @var string */
    public $title;

    /** @var string */
    public $verificode;

    /** @var array<string, callable> */
    private static $blockHandlers = [];

    /** @var BaseBlock[] */
    protected $footerBlocks = [];

    /** @var string */
    protected $html;

    /** @var bool */
    protected $lowsecure;

    /** @var PHPMailer */
    protected $mail;

    /** @var BaseBlock[] */
    protected $mainBlocks = [];

    /** @var array */
    private static $mailer = ['mail' => 'Mail', 'sendmail' => 'SendMail', 'smtp' => 'SMTP'];

    /** @var string */
    protected static $template = 'NewTemplate.html.twig';

    public function __construct()
    {
        static::addBlockHandler('BoxBlock');
        static::addBlockHandler('ButtonBlock');
        static::addBlockHandler('HtmlBlock');
        static::addBlockHandler('SpaceBlock');
        static::addBlockHandler('TableBlock');
        static::addBlockHandler('TextBlock');
        static::addBlockHandler('TitleBlock');

        $this->empresa = Empresas::default();

        $this->fromEmail = Tools::settings('email', 'email');
        $this->fromName = $this->empresa->nombrecorto;

        $this->mail = new PHPMailer();

        $this->mail->Debugoutput = function ($str) {
            Tools::log()->warning($str);
        };

        $this->mail->CharSet = PHPMailer::CHARSET_UTF8;
        $this->mail->Mailer = Tools::settings('email', 'mailer');

        $this->mail->SMTPSecure = Tools::settings('email', 'enc', '');
        if ($this->mail->SMTPSecure) {
            $this->mail->SMTPAuth = true;
            $this->mail->AuthType = Tools::settings('email', 'authtype', '');
        }

        $this->mail->Host = Tools::settings('email', 'host');
        $this->mail->Port = Tools::settings('email', 'port');
        $this->mail->Username = Tools::settings('email', 'user') ?
            Tools::settings('email', 'user') :
            Tools::settings('email', 'email');
        $this->mail->Password = Tools::settings('email', 'password');
        $this->lowsecure = (bool)Tools::settings('email', 'lowsecure');

        foreach (static::splitEmails(Tools::settings('email', 'emailcc', '')) as $email) {
            $this->cc($email);
        }

        foreach (static::splitEmails(Tools::settings('email', 'emailbcc', '')) as $email) {
            $this->bcc($email);
        }

        $this->signature = Tools::settings('email', 'signature', '');
        $this->verificode = Tools::randomString(20);
    }

    /**
     * @deprecated since version 2023.09
     */
    public function addAddress(string $email, string $name = ''): NewMail
    {
        return $this->to($email, $name);
    }

    /**
     * Añade un adjunto al correo.
     *
     * @throws Exception
     */
    public function addAttachment(string $path, string $name): NewMail
    {
        $this->mail->addAttachment($path, $name);

        return $this;
    }

    /**
     * @deprecated since version 2023.09
     */
    public function addBCC(string $email, string $name = ''): NewMail
    {
        return $this->bcc($email, $name);
    }

    /**
     * Registra un handler personalizado para un tipo de shortcode de bloque.
     * El callable recibe (array $attrs, string $content) y debe devolver un BaseBlock o null.
     * Ejemplo: NewMail::addBlockHandler('myblock', fn($attrs, $content) => new MyBlock($content));
     */
    public static function addBlockHandler(string $tag): void
    {
        $className = '\\FacturaScripts\\Dinamic\\Lib\\Email\\' . $tag;
        if (false === isset(self::$blockHandlers[$tag]) && class_exists($className)) {
            self::$blockHandlers[$tag] = $className;
        }
    }

    /**
     * @deprecated since version 2023.09
     */
    public function addCC(string $email, string $name = ''): NewMail
    {
        return $this->cc($email, $name);
    }

    /**
     * Añade un bloque al pie del correo.
     */
    public function addFooterBlock(BaseBlock $block): NewMail
    {
        $block->setVerificode($this->verificode);
        $this->footerBlocks[] = $block;

        return $this;
    }

    /**
     * Añade un bloque al cuerpo del correo.
     */
    public function addMainBlock(BaseBlock $block): NewMail
    {
        $block->setVerificode($this->verificode);
        $this->mainBlocks[] = $block;

        return $this;
    }

    public static function addMailer(string $key, string $name): void
    {
        if (array_key_exists($key, self::$mailer)) {
            return;
        }

        self::$mailer[$key] = $name;
    }

    /**
     * @deprecated since version 2023.09
     */
    public function addReplyTo(string $address, string $name = ''): NewMail
    {
        return $this->replyTo($address, $name);
    }

    public function bcc(string $email, string $name = ''): NewMail
    {
        $this->mail->addBCC($email, $name);

        return $this;
    }

    public function body(string $body): NewMail
    {
        $this->text = $body;

        return $this;
    }

    /**
     * Verifica si se puede enviar el correo.
     */
    public function canSendMail(): bool
    {
        return !empty($this->fromEmail) && !empty($this->mail->Password) && !empty($this->mail->Host);
    }

    public function cc(string $email, string $name = ''): NewMail
    {
        $this->mail->addCC($email, $name);

        return $this;
    }

    public static function create(): NewMail
    {
        return new static();
    }

    /**
     * Devuelve los nombres de los archivos adjuntos.
     */
    public function getAttachmentNames(): array
    {
        $names = [];
        foreach ($this->mail->getAttachments() as $attach) {
            $names[] = $attach[1];
        }

        return $names;
    }

    public static function getAttachmentPath(?string $email, string $folder): string
    {
        $path = 'MyFiles/Email/{{email}}/' . $folder . '/';
        return str_replace('{{email}}', $email, $path);
    }

    /**
     * Devuelve un array con los emails disponibles para el usuario.
     */
    public function getAvailableMailboxes(): array
    {
        return empty($this->fromEmail) ? [] : [$this->fromEmail];
    }

    /**
     * Devuelve un array con los emails con copia oculta.
     */
    public function getBCCAddresses(): array
    {
        $addresses = [];
        foreach ($this->mail->getBccAddresses() as $addr) {
            $addresses[] = $addr[0];
        }

        return $addresses;
    }

    /**
     * Devuelve un array con los emails con copia.
     */
    public function getCCAddresses(): array
    {
        $addresses = [];
        foreach ($this->mail->getCcAddresses() as $addr) {
            $addresses[] = $addr[0];
        }

        return $addresses;
    }

    public static function getMailer(): array
    {
        if (false === function_exists('mail')) {
            unset(self::$mailer['mail']);
        }

        if (false === ini_get('sendmail_path')) {
            unset(self::$mailer['sendmail']);
        }

        return self::$mailer;
    }

    /**
     * Devuelve los bloques del cuerpo del correo.
     */
    public function getMainBlocks(): array
    {
        // si no hay texto, devolvemos los bloques principales tal cual
        if (empty($this->text)) {
            return $this->mainBlocks;
        }

        // procesamos el texto: shortcodes, html y texto plano
        $this->replaceTextToBlock();

        return $this->mainBlocks;
    }

    public static function getTemplate(): string
    {
        return static::$template;
    }

    /**
     * Devuelve un array con los emails hacia donde va el mensaje.
     */
    public function getToAddresses(): array
    {
        $addresses = [];
        foreach ($this->mail->getToAddresses() as $addr) {
            $addresses[] = $addr[0];
        }

        return $addresses;
    }

    public function replyTo(string $address, string $name = ''): NewMail
    {
        $this->mail->addReplyTo($address, $name);

        return $this;
    }

    /**
     * Envía el correo.
     *
     * @throws Exception
     * @throws LoaderError
     * @throws RuntimeError
     * @throws SyntaxError
     */
    public function send(): bool
    {
        if (false === $this->canSendMail()) {
            Tools::log()->warning('email-not-configured');
            return false;
        }

        $this->mail->setFrom($this->fromEmail, $this->fromName);
        $this->mail->Subject = $this->title;

        // comprobamos que tenemos todos los emails en copia configurados añadidos
        $emailAddedCC = $this->getCCAddresses();
        foreach (static::splitEmails(Tools::settings('email', 'emailcc', '')) as $email) {
            if (!in_array($email, $emailAddedCC) && Validator::email($email)) {
                $this->cc($email);
            }
        }

        // comprobamos que tenemos todos los emails en copia oculta configurados añadidos
        $emailAddedBCC = $this->getBCCAddresses();
        foreach (static::splitEmails(Tools::settings('email', 'emailbcc', '')) as $email) {
            if (!in_array($email, $emailAddedBCC) && Validator::email($email)) {
                $this->bcc($email);
            }
        }

        // verificamos que haya al menos un destinatario en TO
        // si no hay, intentamos mover uno de CC o BCC a TO
        if (empty($this->getToAddresses())) {
            $ccAddresses = $this->getCCAddresses();
            $bccAddresses = $this->getBCCAddresses();

            if (!empty($ccAddresses)) {
                // limpiamos primero para evitar conflicto por dirección duplicada al pasar de CC a TO
                $this->mail->clearCCs();

                // movemos el primer CC a TO
                $firstCC = array_shift($ccAddresses);
                $this->to($firstCC);

                // volvemos a agregar los CC restantes
                foreach ($ccAddresses as $email) {
                    $this->cc($email);
                }
            } elseif (!empty($bccAddresses)) {
                // limpiamos primero para evitar conflicto por dirección duplicada al pasar de BCC a TO
                $this->mail->clearBCCs();

                // movemos el primer BCC a TO
                $firstBCC = array_shift($bccAddresses);
                $this->to($firstBCC);

                // volvemos a agregar los BCC restantes
                foreach ($bccAddresses as $email) {
                    $this->bcc($email);
                }
            } else {
                // no hay ningún destinatario
                Tools::log()->warning('email-no-recipients');
                return false;
            }

            // PHPMailer evita destinatarios duplicados entre TO/CC/BCC.
            // Si no hemos podido promover ninguno, cancelamos el envío.
            if (empty($this->getToAddresses())) {
                Tools::log()->warning('email-no-recipients');
                return false;
            }
        }

        $this->renderHTML();
        $this->mail->msgHTML($this->html);

        if ('smtp' === strtolower($this->mail->Mailer) && false === $this->mail->smtpConnect($this->smtpOptions())) {
            Tools::log()->warning('mail-server-error');
            return false;
        }

        if ($this->mail->send()) {
            $this->saveMailSent();
            return true;
        }

        Tools::log()->error('error', ['%error%' => $this->mail->ErrorInfo]);
        return false;
    }

    /**
     * @throws Exception
     * @throws SyntaxError
     * @throws RuntimeError
     * @throws LoaderError
     * @deprecated since version 2023.09
     */
    public function sendNotification(string $notificationName, array $params): bool
    {
        // ¿La notificación existe?
        $notification = new EmailNotification();
        if (false === $notification->load($notificationName)) {
            Tools::log()->warning('email-notification-not-exists', ['%name%' => $notificationName]);
            return false;
        }

        // ¿Está desactivada?
        if (false === $notification->enabled) {
            Tools::log()->warning('email-notification-disabled', ['%name%' => $notificationName]);
            return false;
        }

        if (!isset($params['verificode'])) {
            $params['verificode'] = $this->verificode;
        }

        $this->title = MailNotifier::getText($notification->subject, $params);
        $this->text = MailNotifier::getText($notification->body, $params);

        return $this->send();
    }

    public function setMailbox(string $emailFrom): NewMail
    {
        $this->fromEmail = $emailFrom;

        return $this;
    }

    public static function setTemplate(string $template): void
    {
        static::$template = $template;
    }

    /**
     * Establece el usuario que manda el email.
     */
    public function setUser(User $user): NewMail
    {
        $this->fromNick = $user->nick;

        return $this;
    }

    /**
     * Separa los emails de una cadena en array.
     */
    public static function splitEmails(string $emails): array
    {
        $return = [];
        foreach (explode(',', $emails) as $part) {
            $email = trim($part);
            if (!empty($email)) {
                $return[] = $email;
            }
        }

        return $return;
    }

    public function subject(string $subject): NewMail
    {
        $this->title = $subject;

        return $this;
    }

    /**
     * Pruebe la conexión PHPMailer.
     *
     * @throws Exception
     */
    public function test(): bool
    {
        switch ($this->mail->Mailer) {
            case 'smtp':
            case 'SMTP':
                $this->mail->SMTPDebug = 3;
                return $this->mail->smtpConnect($this->smtpOptions());

            default:
                Tools::log()->warning('test-' . $this->mail->Mailer . '-not-implemented');
                return false;
        }
    }

    public function to(string $email, string $name = ''): NewMail
    {
        $this->mail->addAddress($email, $name);

        return $this;
    }

    /**
     * Añade un HtmlBlock si el texto contiene etiquetas HTML, o un TextBlock si es texto plano.
     * Siempre usa la clase Dinamic para que los plugins puedan sobreescribirla.
     */
    protected function addTextOrHtmlBlock(string $text): void
    {
        if (strip_tags($text) !== $text) {
            $className = self::$blockHandlers['HtmlBlock'] ?? null;
            if ($className !== null) {
                $this->addMainBlock(new $className($text));
            }
            return;
        }

        $className = self::$blockHandlers['TextBlock'] ?? null;
        if ($className !== null) {
            $this->addMainBlock(new $className($text));
        }
    }

    /**
     * Instancia el bloque correspondiente al tipo de shortcode delegando en su método fromShortcode().
     */
    protected function createBlockFromShortcode(string $type, array $attrs, string $content): ?BaseBlock
    {
        $tag = $type . 'Block';
        $className = null;
        foreach (self::$blockHandlers as $handlerTag => $handlerClass) {
            if (strcasecmp($handlerTag, $tag) === 0) {
                $className = $handlerClass;
                break;
            }
        }

        if ($className === null) {
            return null;
        }

        return $className::fromShortcode($attrs, $content);
    }

    /**
     * Devuelve los bloques del pie del correo.
     */
    protected function getFooterBlocks(): array
    {
        $signature = Tools::fixHtml($this->signature);
        if (empty($signature)) {
            return $this->footerBlocks;
        }

        // usamos la clase Dinamic para que los plugins puedan sobreescribirla
        $className = self::$blockHandlers['TextBlock'] ?? null;
        if ($className === null) {
            return $this->footerBlocks;
        }

        return array_merge($this->footerBlocks, [new $className($signature, 'text-footer')]);
    }

    /**
     * Parsea los atributos de un shortcode en un array clave => valor.
     * Soporta comillas simples y dobles: attr="valor" o attr='valor'
     */
    protected static function parseShortcodeAttributes(string $attrsStr): array
    {
        preg_match_all('/(\w+)=["\']([^"\']*)["\']/', $attrsStr, $matches);
        $attrs = [];
        foreach ($matches[1] as $i => $key) {
            $attrs[$key] = $matches[2][$i];
        }
        return $attrs;
    }

    /**
     * Renderiza el HTML del correo.
     *
     * @throws SyntaxError
     * @throws RuntimeError
     * @throws LoaderError
     */
    protected function renderHTML(): void
    {
        $logoUrl = '';

        $logoId = Tools::settings('email', 'idlogo');
        if (!empty($logoId)) {
            $attFile = AttachedFile::find($logoId);
            if ($attFile) {
                $logoUrl = Tools::siteUrl() . '/' . $attFile->url('download-permanent');
            }
        }

        $this->html = Html::render('Email/' . static::$template, [
            'company' => $this->empresa,
            'logoUrl' => $logoUrl,
            'footerBlocks' => $this->getFooterBlocks(),
            'mainBlocks' => $this->getMainBlocks(),
            'title' => $this->title
        ]);
    }

    /**
     * Busca shortcodes de bloque en el texto del email y los convierte en objetos BaseBlock.
     * También detecta HTML entre bloques y lo envuelve en HtmlBlock; el texto plano va a TextBlock.
     * Sintaxis: [blockTipo atributos]contenido[/blockTipo] o [blockTipo atributos] (auto-cierre)
     * Ejemplos:
     *   [blockTitle type="h2"]Bienvenido[/blockTitle]
     *   [blockText]Párrafo de texto[/blockText]
     *   [blockHtml]<ul><li>item</li></ul>[/blockHtml]
     *   [blockButton label="Reservar" href="https://..."]
     *   [blockSpace height="20"]
     * Los plugins pueden registrar tipos adicionales con NewMail::addBlockHandler().
     */
    protected function replaceTextToBlock(): void
    {
        if (empty($this->text)) {
            return;
        }

        // buscamos shortcodes con o sin contenido interior: [blockXxx attrs]...[/blockXxx] o [blockXxx attrs]
        preg_match_all(
            '/\[block(\w+)([^\]]*)\](?:(.*?)\[\/block\1\])?/s',
            $this->text,
            $matches,
            PREG_OFFSET_CAPTURE
        );

        // guardamos los bloques existentes para añadirlos al final
        $existingBlocks = $this->mainBlocks;
        $this->mainBlocks = [];
        $text = $this->text;
        $this->text = '';

        // si no hay shortcodes, procesamos el texto completo como html o texto plano
        if (empty($matches[0])) {
            $this->addTextOrHtmlBlock($text);
            foreach ($existingBlocks as $block) {
                $this->mainBlocks[] = $block;
            }
            return;
        }

        $lastPos = 0;

        foreach ($matches[0] as $idx => $match) {
            $fullMatch = $match[0];
            $offset = $match[1];
            $blockType = $matches[1][$idx][0];
            $attrsStr = $matches[2][$idx][0];
            $content = $matches[3][$idx][0] ?? '';

            // texto antes del shortcode → HtmlBlock si contiene html, TextBlock si es texto plano
            $before = trim(substr($text, $lastPos, $offset - $lastPos));
            if ('' !== $before) {
                $this->addTextOrHtmlBlock($before);
            }
            $lastPos = $offset + strlen($fullMatch);

            // creamos el bloque correspondiente al shortcode
            $attrs = static::parseShortcodeAttributes($attrsStr);
            $block = $this->createBlockFromShortcode($blockType, $attrs, $content);
            if ($block !== null) {
                $this->addMainBlock($block);
            }
        }

        // texto restante tras el último shortcode → HtmlBlock si contiene html, TextBlock si es texto plano
        $remaining = trim(substr($text, $lastPos));
        if ('' !== $remaining) {
            $this->addTextOrHtmlBlock($remaining);
        }

        // reañadimos los bloques que existían antes
        foreach ($existingBlocks as $block) {
            $this->mainBlocks[] = $block;
        }
    }

    /**
     * Guarda el correo enviado en la base de datos.
     */
    protected function saveMailSent(): void
    {
        // Obtiene todas las direcciones de correo electrónico
        $addresses = array_merge($this->getToAddresses(), $this->getCCAddresses(), $this->getBCCAddresses());

        // Generamos un identificador único para el correo electrónico
        $uuid = uniqid();

        // obtenemos los adjuntos
        $attachments = $this->mail->getAttachments();

        // guardar correo electrónico enviado
        foreach (array_unique($addresses) as $address) {
            $emailSent = new EmailSent();
            $emailSent->addressee = $address;
            $emailSent->attachment = count($attachments) > 0;
            $emailSent->body = $this->text;
            $emailSent->email_from = $this->fromEmail;
            $emailSent->html = $this->html;
            $emailSent->nick = $this->fromNick;
            $emailSent->subject = $this->title;
            $emailSent->uuid = $uuid;
            $emailSent->verificode = $this->verificode;
            $emailSent->save();
        }

        // si no hay adjuntos, terminamos
        if (empty($attachments)) {
            return;
        }

        // creamos la carpeta de adjuntos para el email
        $path = FS_FOLDER . '/' . static::getAttachmentPath($this->fromEmail, 'Sent') . $uuid . '/';
        Tools::folderCheckOrCreate($path);

        foreach ($attachments as $attach) {
            $newPath = $path . $attach[1];

            // movemos los adjuntos de la carpeta temporal a la carpeta de adjuntos del email
            $tmpPath = FS_FOLDER . '/' . static::ATTACHMENTS_TMP_PATH . $attach[1];
            if (file_exists($tmpPath)) {
                rename($tmpPath, $newPath);
                continue;
            }

            // si el adjunto está fuera de la carpeta temporal, lo copiamos
            $currentPath = FS_FOLDER . '/MyFiles/' . $attach[0];
            if (file_exists($currentPath)) {
                copy($currentPath, $newPath);
                continue;
            }

            $currentPath = FS_FOLDER . '/' . $attach[0];
            if (file_exists($currentPath)) {
                copy($currentPath, $newPath);
                continue;
            }

            if (file_exists($attach[0])) {
                copy($attach[0], $newPath);
                continue;
            }

            Tools::log('NewMail')->warning('attachment-not-found', ['%file%' => $attach[0]]);
        }
    }

    /**
     * Devuelve las opciones SMTP.
     */
    protected function smtpOptions(): array
    {
        if ($this->lowsecure) {
            return [
                'ssl' => [
                    'verify_peer' => false,
                    'verify_peer_name' => false,
                    'allow_self_signed' => true
                ]
            ];
        }

        return [];
    }
}
