<?php
/**
 * @package         Tassos Code Snippets
 * @version         1.0.1-RC2 Free
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2026 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
 */

namespace Tassos\CodeSnippets\Administrator\Snippet;

use Joomla\CMS\Factory;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Mail\MailerFactoryInterface;
use Joomla\CMS\Uri\Uri;
use Tassos\CodeSnippets\Administrator\Helper\SnippetsHelper;
use Tassos\CodeSnippets\Administrator\Helper\SmartTagsHelper;

abstract class Snippet
{
    /**
     * Snippet data passed to this class.
     *
     * Can be an array, object or any value depending on the snippet type.
     *
     * @var mixed
     */
    protected $snippet;

    /**
     * Shortcode attributes passed to the snippet.
     * e.g. {tcs snippet.my-php --name=Tassos} → ['name' => 'Tassos']
     *
     * @var array
     */
    protected $attributes = [];

    /**
     * Display color used for the snippet (hex).
     *
     * @var string
     */
    protected $color = '#333';

    public function __construct($snippet)
    {
        $this->snippet = $snippet;
    }

    public function getColor()
    {
        return $this->color;
    }

    /**
     * Set shortcode attributes to be available inside the snippet.
     *
     * @param  array $attributes
     * @return self
     */
    public function setAttributes(array $attributes): self
    {
        $this->attributes = $attributes;
        return $this;
    }

    /**
     * Execute or render the snippet
     *
     * @return string|null Output or null for side-effect snippets
     */
    abstract public function run();

    /**
     * Safely execute the snippet, catching any errors.
     *
     * On error: auto-disables the snippet (state = -3), records the error in the DB,
     * optionally writes a log file and sends an admin notification email.
     *
     * @return string|null
     */
    final public function execute(): ?string
    {
        try
        {
            $output = $this->run();

            if ($output !== null)
            {
                $output = $this->replaceAttrVars($output);
            }

            if ($output !== null && !empty($this->snippet->content_prepare))
            {
                $output = $this->prepareContent($output);
            }

            return $output;
        }
        catch (\Throwable $th)
        {
            // Auto-disable the snippet and record the error
            SnippetsHelper::unpublishSnippet($this->snippet->id, $th);

            $params = ComponentHelper::getParams('com_tassoscodesnippets');
            $alias  = $this->snippet->alias ?? 'snippet-' . $this->snippet->id;

            if ($params->get('log_errors', 1))
            {
                $this->writeLog($th, $alias);
            }

            if ($params->get('notify_admin', 1))
            {
                $this->notifyAdmin($th, $alias);
            }

            return null;
        }
    }

    /**
     * Replace {tcs attr.KEY} placeholders in the snippet output using the
     * SmartTags engine, so all built-in modifiers (--filter=, --htmlSpecialChars=,
     * etc.) are available. Unknown keys are left untouched.
     *
     * A fresh SmartTags instance is created per invocation so nested snippets
     * with different --params never share each other's variable scope.
     *
     * @param   string  $output
     *
     * @return  string
     */
    private function replaceAttrVars(string $output): string
    {
        if (empty($this->attributes))
        {
            return $output;
        }

        return SmartTagsHelper::replace($output, '{tcs attr.', $this->attributes, true);
    }

    /**
     * Pass output through Joomla content plugins (onContentPrepare).
     *
     * @param   string  $output  The raw snippet output.
     *
     * @return  string  The processed output.
     */
    protected function prepareContent(string $output): string
    {
        return HTMLHelper::_('content.prepare', $output, 'com_tassoscodesnippets.snippet');
    }

    /**
     * Write error details to a per-snippet log file.
     *
     * @param   \Throwable  $th     The caught throwable
     * @param   string      $alias  The snippet alias (used in the filename)
     *
     * @return  void
     */
    private function writeLog(\Throwable $th, string $alias): void
    {
        $logFile = 'snippet_' . $alias . '_' . (int) ($this->snippet->id ?? 0) . '.log';
        $logPath = Factory::getApplication()->get('log_path') . '/tassos_code_snippets';

        if (!is_dir($logPath))
        {
            mkdir($logPath, 0755, true);
        }

        Log::addLogger(
            [
                'text_file'      => $logFile,
                'text_file_path' => $logPath,
            ],
            Log::ALL,
            ['tassos_code_snippets']
        );

        $url           = Uri::getInstance()->toString();
        $headersString = print_r($_SERVER, true);

        $message = sprintf(
            "Snippet Auto-Disabled | ID: %d | Title: %s | Alias: %s | Type: %s\nError: %s\nFile: %s | Line: %d\n\nRequest URL: %s\n\nRequest Headers:\n%s\n\nStack Trace:\n%s",
            (int) ($this->snippet->id ?? 0),
            $this->snippet->title ?? 'Unknown',
            $alias,
            $this->snippet->code_type ?? 'Unknown',
            $th->getMessage(),
            $th->getFile(),
            $th->getLine(),
            $url,
            $headersString,
            $th->getTraceAsString()
        );

        Log::add($message, Log::ERROR, 'tassos_code_snippets');
    }

    /**
     * Send an email notification to the site administrator about the error.
     *
     * @param   \Throwable  $th     The caught throwable
     * @param   string      $alias  The snippet alias
     *
     * @return  void
     */
    private function notifyAdmin(\Throwable $th, string $alias): void
    {
        try
        {
            $app    = Factory::getApplication();
            $config = $app->getConfig();

            $siteName   = $config->get('sitename', 'Joomla');
            $adminEmail = $config->get('mailfrom', '');
            $adminName  = $config->get('fromname', $siteName);

            if (empty($adminEmail))
            {
                return;
            }

            $subject = sprintf('[%s] Snippet Auto-Disabled: %s', $siteName, $this->snippet->title ?? 'Unknown');

            $body  = '<h2 style="color:#721c24;">A snippet has been automatically disabled due to an error.</h2>';
            $body .= '<table style="border-collapse:collapse;width:100%;max-width:600px;">';
            $body .= '<tr><td style="padding:6px;font-weight:bold;">Snippet ID</td><td style="padding:6px;">' . (int) ($this->snippet->id ?? 0) . '</td></tr>';
            $body .= '<tr><td style="padding:6px;font-weight:bold;">Title</td><td style="padding:6px;">' . htmlspecialchars($this->snippet->title ?? 'Unknown') . '</td></tr>';
            $body .= '<tr><td style="padding:6px;font-weight:bold;">Alias</td><td style="padding:6px;">' . htmlspecialchars($alias) . '</td></tr>';
            $body .= '<tr><td style="padding:6px;font-weight:bold;">Type</td><td style="padding:6px;">' . htmlspecialchars($this->snippet->code_type ?? 'Unknown') . '</td></tr>';
            $body .= '<tr><td style="padding:6px;font-weight:bold;">Error</td><td style="padding:6px;">' . htmlspecialchars($th->getMessage()) . '</td></tr>';
            $body .= '<tr><td style="padding:6px;font-weight:bold;">File</td><td style="padding:6px;">' . htmlspecialchars($th->getFile()) . '</td></tr>';
            $body .= '<tr><td style="padding:6px;font-weight:bold;">Line</td><td style="padding:6px;">' . (int) $th->getLine() . '</td></tr>';
            $body .= '</table>';
            $body .= '<h3>Stack Trace</h3>';
            $body .= '<pre style="background:#f5f5f5;padding:10px;font-size:12px;overflow:auto;">' . htmlspecialchars($th->getTraceAsString()) . '</pre>';

            $mailer = Factory::getContainer()
                ->get(MailerFactoryInterface::class)
                ->createMailer();

            $mailer->sendMail($adminEmail, $adminName, $adminEmail, $subject, $body, true);
        }
        catch (\Throwable $e)
        {
            // Silently fail — do not let email errors propagate
        }
    }
}
