<?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\Helper;

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use NRFramework\Conditions\ConditionBuilder;

/**
 * Helper class for managing code snippets data
 * 
 * This helper is responsible only for fetching snippets from the database.
 * Injection logic is handled by Location classes.
 */
class SnippetsHelper
{
    /**
     * Cache for loaded snippets
     *
     * @var    array|null
     */
    private static $snippets = null;

    /**
     * Cached snippet types
     *
     * @var array|null
     */
    private static $types = null;

    /**
     * Get all published snippets from the database
     * 
     * Note: This only fetches from DB with basic filters (state, language, access).
     * Publishing rules (conditions) are evaluated by LocationManager.
     *
     * @return  array
     */
    public static function getSnippets(): array
    {
        if (self::$snippets !== null)
        {
            return self::$snippets;
        }

        self::$snippets = [];

        $app  = Factory::getApplication();
        $user = $app->getIdentity();

        // Get the component's MVC factory
        /** @var MVCFactoryInterface $factory */
        $factory = $app->bootComponent('com_tassoscodesnippets')->getMVCFactory();

        // Get the snippets model
        $model = $factory->createModel('Snippets', 'Administrator', ['ignore_request' => true]);

        // Set filters for published snippets
        $model->setState('filter.state', 1);
        $model->setState('filter.language', [$app->getLanguage()->getTag(), '*']);
        $model->setState('filter.access', $user->getAuthorisedViewLevels());
        $model->setState('list.ordering', 'a.ordering');
        $model->setState('list.direction', 'ASC');

        // Get items using built-in Joomla method
        $items = $model->getItems();

        if (!empty($items))
        {
            self::$snippets = $items;
        }

        return self::$snippets;
    }

    /**
     * Unpublish a snippet by its ID and record any error that caused it to be disabled.
     *
     * @param   int              $id  The snippet ID
     * @param   \Throwable|null  $th  The throwable that caused the auto-disable (optional)
     *
     * @return  bool  True on success, false on failure
     */
    public static function unpublishSnippet(int $id, ?\Throwable $th = null): bool
    {
        if ($id <= 0)
        {
            return false;
        }

        // Get the snippets table instance
        $table = Factory::getApplication()
            ->bootComponent('com_tassoscodesnippets')
            ->getMVCFactory()
            ->createTable('Snippet', 'Administrator');

        // Load the snippet
        if (!$table->load($id))
        {
            return false;
        }

        // Set state to auto-disabled (-3)
        $table->state = -3;

        // Record the error details if a throwable was provided
        if ($th !== null)
        {
            $table->last_error = $th->getMessage();
            $table->last_error_date = Factory::getDate()->toSql();
        }

        // Save the table
        if (!$table->store())
        {
            return false;
        }

        // Clear the cached snippets
        self::$snippets = null;

        return true;
    }

    public static function getSnippetClass($snippetName, $data)
    {
        $snippetName = explode('_', $snippetName);  
        $snippetName = array_map('ucfirst', $snippetName);
        $snippetName = implode('', $snippetName);

        $snippetClass = 'Tassos\\CodeSnippets\\Administrator\\Snippet\\' . $snippetName . 'Snippet';

        if (!class_exists($snippetClass))
        {
            return;
        }
        
        return new $snippetClass($data);
    }

    /**
     * Get available snippet types by scanning the Snippet folder
     *
     * @return array Array of snippet types ['html', 'css', 'css_file', 'js', 'js_file', 'php']
     */
    public static function getSnippetTypes(): array
    {
        if (self::$types !== null)
        {
            return self::$types;
        }

        self::$types = [];

        $snippetPath = JPATH_ADMINISTRATOR . '/components/com_tassoscodesnippets/src/Snippet';

        if (!is_dir($snippetPath))
        {
            return self::$types;
        }

        $files = glob($snippetPath . '/*Snippet.php');

        foreach ($files as $file)
        {
            $filename = basename($file, '.php');

            // Skip the base Snippet class
            if ($filename === 'Snippet')
            {
                continue;
            }

            // Remove "Snippet" suffix: CssFileSnippet -> CssFile
            $name = str_replace('Snippet', '', $filename);

            // Convert CamelCase to snake_case: CssFile -> css_file
            $type = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name));

            self::$types[] = $type;
        }

        // Sort alphabetically
        sort(self::$types);

        return self::$types;
    }

    /**
     * Get snippet type label key
     *
     * @param string $type The snippet type (e.g., 'css_file')
     * 
     * @return string The language key
     */
    public static function getTypeLabelKey(string $type): string
    {
        return 'COM_TASSOSCODESNIPPETS_SNIPPET_TYPE_' . strtoupper($type);
    }

    /**
     * Replace {tcs snippet.alias} shortcodes in the given content string.
     *
     * This is the single place that initialises SmartTags and performs the
     * replacement. It is used both by the system plugin (onContentPrepare)
     * and by the Snippet SmartTag itself when resolving nested shortcodes.
     *
     * @param   string  $content  The content to process (passed by reference)
     *
     * @return  void
     */
    public static function replaceShortcodes(string &$content): void
    {
        $content = SmartTagsHelper::replace($content);
    }

    /**
     * Check whether a snippet's display conditions pass in the current context.
     *
     * This is the single authoritative implementation used by both the Location
     * classes (auto-insert) and the Snippet SmartTag (shortcode), so both paths
     * always evaluate conditions identically.
     *
     * @param   object  $snippet     The snippet object (params already expanded as direct properties)
     * @param   array   $visitedIds  Snippet IDs already visited — prevents infinite loops when mirroring
     *
     * @return  bool
     */
    public static function passesDisplayConditions(object $snippet, array $visitedIds = []): bool
    {
        $type     = (string) ($snippet->display_conditions_type ?? '');
        $mirrorId = (int) ($snippet->mirror_snippet ?? 0);

        // All pages — always display
        if ($type === '')
        {
            return true;
        }

        // Mirror another snippet's conditions
        if ($type === 'mirror')
        {
            if ($mirrorId <= 0)
            {
                return true;
            }

            // Guard against circular references
            if (in_array($mirrorId, $visitedIds, true))
            {
                return true;
            }

            $factory  = Factory::getApplication()->bootComponent('com_tassoscodesnippets')->getMVCFactory();
            $model    = $factory->createModel('Snippet', 'Administrator', ['ignore_request' => true]);
            $mirrored = $model->getItem($mirrorId);

            if (!$mirrored || empty($mirrored->id) || (int) $mirrored->state !== 1)
            {
                return true;
            }

            return self::passesDisplayConditions($mirrored, array_merge($visitedIds, [$snippet->id]));
        }

        // Selected pages — evaluate ConditionBuilder rules
        $rules = $snippet->publishing_rules ?? [];

        if (is_string($rules))
        {
            $rules = json_decode($rules, true) ?: [];
        }

        if (empty($rules))
        {
            return true;
        }

        return ConditionBuilder::pass($rules);
    }
}