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

defined('_JEXEC') or die;

use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Factory;
use Tassos\CodeSnippets\Administrator\Helper\SnippetsHelper;
use Tassos\CodeSnippets\Administrator\Snippet\PhpSnippet;
use Tassos\CodeSnippets\Administrator\Snippet\CssSnippet;
use Tassos\CodeSnippets\Administrator\Snippet\CssFileSnippet;
use Tassos\CodeSnippets\Administrator\Snippet\JsSnippet;
use Tassos\CodeSnippets\Administrator\Snippet\JsFileSnippet;
use Tassos\CodeSnippets\Administrator\Snippet\HtmlSnippet;
use Tassos\CodeSnippets\Administrator\Snippet\TextSnippet;

/**
 * Abstract base class for all snippet locations.
 * 
 * Each location handles its own:
 * - Event registration (or immediate execution)
 * - canRun() logic (client type, document type, etc.)
 * - Injection method (WebAsset, buffer manipulation, etc.)
 */
abstract class AbstractLocation
{
    /**
     * The snippets to inject at this location
     *
     * @var array
     */
    protected $snippets = [];

    /**
     * The application instance
     *
     * @var CMSApplication
     */
    protected $app;

    /**
     * Track which assets have been registered to avoid duplicates
     *
     * @var array
     */
    protected static $registeredAssets = [];

    /**
     * Constructor
     *
     * @param array $snippets The snippets for this location
     */
    public function __construct(array $snippets)
    {
        $this->snippets = $snippets;
        $this->app = Factory::getApplication();
    }

    /**
     * Check if this location can run in the current context
     *
     * @return bool
     */
    abstract public function canRun(): bool;

    /**
     * Inject the snippets at this location
     *
     * @return void
     */
    abstract public function inject(): void;

    /**
     * Get the Joomla event to register for injection.
     * Return null if injection should happen immediately (no event needed).
     *
     * @return string|null
     */
    public function getEvent(): ?string
    {
        return null;
    }

    /**
     * Register this location for injection.
     * If getEvent() returns an event name, register a callback.
     * Otherwise, run immediately.
     *
     * @return void
     */
    public function register(): void
    {
        $event = $this->getEvent();

        if ($event === null)
        {
            if ($this->canRun())
            {
                $this->inject();
            }
            return;
        }

        // We check canRun() inside the callback, not before registerEvent(), because:
        // 
        // LocationManager::initialize() is called during onAfterRoute, which fires
        // BEFORE the document type is determined. At that point, isHtmlDocument()
        // would return false even for HTML pages.
        // 
        // By deferring the canRun() check to when the event actually fires
        // (e.g., onBeforeCompileHead, onAfterRender), the document is fully
        // initialized and we can accurately check conditions like document type,
        // client type (site/admin), etc.
        $this->app->registerEvent($event, function($event = null) {
            if ($this->canRun())
            {
                $this->inject($event);
            }
        });
    }

    /**
     * Check whether a snippet's publishing rules pass in the current context.
     *
     * Conditions are evaluated here (deferred from LocationManager) so they
     * always run after canRun() has confirmed the correct client context
     * (site vs admin). A fatal error or die() inside a condition can therefore
     * never crash the other side of the application.
     *
     * @param object $snippet The snippet object
     *
     * @return bool
     */
    protected function passesConditions(object $snippet): bool
    {
        return SnippetsHelper::passesDisplayConditions($snippet);
    }

    /**
     * Check if document is HTML type
     *
     * @return bool
     */
    protected function isHtmlDocument(): bool
    {
        $document = $this->app->getDocument();
        return $document && $document->getType() === 'html';
    }

    /**
     * Check if running on frontend
     *
     * @return bool
     */
    protected function isSite(): bool
    {
        return $this->app->isClient('site');
    }

    /**
     * Check if running on backend
     *
     * @return bool
     */
    protected function isAdmin(): bool
    {
        return $this->app->isClient('administrator');
    }

    /**
     * Get the WebAssetManager
     *
     * @return \Joomla\CMS\WebAsset\WebAssetManager
     */
    protected function getWebAssetManager()
    {
        return $this->app->getDocument()->getWebAssetManager();
    }

    /**
     * Get the document
     *
     * @return \Joomla\CMS\Document\Document
     */
    protected function getDocument()
    {
        return $this->app->getDocument();
    }

    /**
     * Run a snippet and return its output
     *
     * @param object $snippet The snippet object
     * 
     * @return string|null
     */
    protected function runSnippet(object $snippet): ?string
    {
        if (!$this->passesConditions($snippet))
        {
            return null;
        }

        $class = $this->getSnippetClass($snippet->code_type);

        if (!$class)
        {
            return null;
        }

        $snippetObj = new $class($snippet);
        return $snippetObj->execute();
    }

    /**
     * Get the snippet class for a given type
     *
     * @param string $type The snippet type
     * 
     * @return string|null
     */
    protected function getSnippetClass(string $type): ?string
    {
        switch (strtolower($type))
        {
            case 'php':
                return PhpSnippet::class;
            case 'css':
                return CssSnippet::class;
            case 'css_file':
                return CssFileSnippet::class;
            case 'js':
                return JsSnippet::class;
            case 'js_file':
                return JsFileSnippet::class;
            case 'html':
                return HtmlSnippet::class;
            case 'text':
                return TextSnippet::class;
            default:
                return null;
        }
    }

    /**
     * Inject snippets via WebAssetManager (for head locations)
     *
     * @return void
     */
    protected function injectViaWebAsset(): void
    {
        $wa = $this->getWebAssetManager();
        $document = $this->getDocument();

        foreach ($this->snippets as $snippet)
        {
            $assetKey = 'tcs_snippet_' . $snippet->id;

            if (isset(self::$registeredAssets[$assetKey]))
            {
                continue;
            }

            $output = $this->runSnippet($snippet);

            if (empty($output))
            {
                continue;
            }

            switch ($snippet->code_type)
            {
                case 'css':
                    $wa->addInlineStyle($output);
                    break;

                case 'css_file':
                    // External CSS files - add each URL as a stylesheet
                    $this->registerExternalStyles($snippet, $output);
                    break;

                case 'js':
                    $wa->addInlineScript($output);
                    break;

                case 'js_file':
                    // External JS files - add each URL as a script
                    $this->registerExternalScripts($snippet, $output);
                    break;

                default:
                    // HTML and others - add as custom tag
                    $document->addCustomTag($output);
                    break;
            }

            self::$registeredAssets[$assetKey] = true;
        }
    }

    /**
     * Get content for buffer injection (wraps code in appropriate tags)
     *
     * @return string
     */
    protected function getBufferContent(): string
    {
        $html = [];

        foreach ($this->snippets as $snippet)
        {
            $assetKey = 'tcs_snippet_' . $snippet->id;

            if (isset(self::$registeredAssets[$assetKey]))
            {
                continue;
            }

            $output = $this->runSnippet($snippet);

            if (empty($output))
            {
                continue;
            }

            switch ($snippet->code_type)
            {
                case 'css':
                    $html[] = '<style>' . $output . '</style>';
                    break;

                case 'css_file':
                    // External CSS files - create link tags
                    $html[] = $this->getExternalStyleTags($output);
                    break;

                case 'js':
                    $html[] = '<script>' . $output . '</script>';
                    break;

                case 'js_file':
                    // External JS files - create script tags
                    $html[] = $this->getExternalScriptTags($snippet, $output);
                    break;

                default:
                    // HTML and others - output as-is
                    $html[] = $output;
                    break;
            }

            self::$registeredAssets[$assetKey] = true;
        }

        return implode("\n", $html);
    }

    /**
     * Register external stylesheets via WebAssetManager
     *
     * @param object $snippet The snippet object
     * @param string $urls    The URLs (one per line)
     * 
     * @return void
     */
    protected function registerExternalStyles(object $snippet, string $urls): void
    {
        $wa = $this->getWebAssetManager();
        $lines = array_filter(array_map('trim', explode("\n", $urls)));

        foreach ($lines as $index => $url)
        {
            if (empty($url))
            {
                continue;
            }

            $assetName = 'tassos.codesnippets.' . $snippet->id . '.' . $index;
            $wa->registerAndUseStyle($assetName, $url);
        }
    }

    /**
     * Register external scripts via WebAssetManager
     *
     * @param object $snippet The snippet object
     * @param string $urls    The URLs (one per line)
     * 
     * @return void
     */
    protected function registerExternalScripts(object $snippet, string $urls): void
    {
        $wa = $this->getWebAssetManager();
        $lines = array_filter(array_map('trim', explode("\n", $urls)));

        // Build attributes from script_loading field
        $attributes = $this->getScriptLoadingAttributes($snippet);

        foreach ($lines as $index => $url)
        {
            if (empty($url))
            {
                continue;
            }

            $assetName = 'tcs_js_' . $snippet->id . '_' . $index;
            $wa->registerAndUseScript($assetName, $url, [], $attributes);
        }
    }

    /**
     * Get external stylesheet link tags for buffer injection
     *
     * @param string $urls The URLs (one per line)
     * 
     * @return string
     */
    protected function getExternalStyleTags(string $urls): string
    {
        $tags = [];
        $lines = array_filter(array_map('trim', explode("\n", $urls)));

        foreach ($lines as $url)
        {
            if (empty($url))
            {
                continue;
            }

            $tags[] = '<link rel="stylesheet" href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '">';
        }

        return implode("\n", $tags);
    }

    /**
     * Get external script tags for buffer injection
     *
     * @param object $snippet The snippet object
     * @param string $urls    The URLs (one per line)
     * 
     * @return string
     */
    protected function getExternalScriptTags(object $snippet, string $urls): string
    {
        $tags = [];
        $lines = array_filter(array_map('trim', explode("\n", $urls)));

        // Build loading attributes string
        $loadingAttr = $this->getScriptLoadingAttributeString($snippet);

        foreach ($lines as $url)
        {
            if (empty($url))
            {
                continue;
            }

            $tags[] = '<script src="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '"' . $loadingAttr . '></script>';
        }

        return implode("\n", $tags);
    }

    /**
     * Get script loading attributes for WebAssetManager
     *
     * @param object $snippet The snippet object
     * 
     * @return array Attributes array for WebAssetManager
     */
    protected function getScriptLoadingAttributes(object $snippet): array
    {
        $attributes = [];
        $loading = $this->parseScriptLoading($snippet);

        if (in_array('defer', $loading))
        {
            $attributes['defer'] = true;
        }

        if (in_array('async', $loading))
        {
            $attributes['async'] = true;
        }

        return $attributes;
    }

    /**
     * Get script loading attributes as HTML string for buffer injection
     *
     * @param object $snippet The snippet object
     * 
     * @return string HTML attributes string (e.g., ' defer async')
     */
    protected function getScriptLoadingAttributeString(object $snippet): string
    {
        $loading = $this->parseScriptLoading($snippet);
        $attrs = [];

        if (in_array('defer', $loading))
        {
            $attrs[] = 'defer';
        }

        if (in_array('async', $loading))
        {
            $attrs[] = 'async';
        }

        return empty($attrs) ? '' : ' ' . implode(' ', $attrs);
    }

    /**
     * Parse the script_loading field value
     *
     * @param object $snippet The snippet object
     * 
     * @return array Array of loading options (e.g., ['defer', 'async'])
     */
    protected function parseScriptLoading(object $snippet): array
    {
        $value = $snippet->script_loading ?? '';

        if (empty($value))
        {
            return [];
        }

        // Handle both array (from form) and comma-separated string (from DB)
        if (is_array($value))
        {
            return $value;
        }

        return array_filter(array_map('trim', explode(',', $value)));
    }

    /**
     * Inject content after the opening <body> tag
     *
     * @param string $content The content to inject
     * 
     * @return void
     */
    protected function injectAfterBodyTag(string $content): void
    {
        if (empty($content))
        {
            return;
        }

        $body = $this->app->getBody();

        // Find opening body tag (may have attributes)
        if (preg_match('/<body[^>]*>/i', $body, $matches, \PREG_OFFSET_CAPTURE))
        {
            $position = $matches[0][1] + strlen($matches[0][0]);
            $body = substr($body, 0, $position) . "\n" . $content . substr($body, $position);
            $this->app->setBody($body);
        }
    }

    /**
     * Inject content before the closing </body> tag
     *
     * @param string $content The content to inject
     * 
     * @return void
     */
    protected function injectBeforeBodyClose(string $content): void
    {
        if (empty($content))
        {
            return;
        }

        $body = $this->app->getBody();
        $position = stripos($body, '</body>');

        if ($position !== false)
        {
            $body = substr($body, 0, $position) . $content . "\n" . substr($body, $position);
            $this->app->setBody($body);
        }
    }

    /**
     * Reset registered assets (useful for testing)
     *
     * @return void
     */
    public static function resetRegisteredAssets(): void
    {
        self::$registeredAssets = [];
    }
}
