<?php

namespace App\Services;

/**
 * PHP-based CSS/JS minifier.
 *
 * Removes comments, excessive whitespace, and optional safe transformations.
 * Caches minified output in storage/cache/assets/ with automatic invalidation
 * when the source file changes.
 */
class AssetMinifier
{
    private string $cacheDir;
    private string $publicDir;

    public function __construct(string $basePath)
    {
        $this->cacheDir  = $basePath . '/storage/cache/assets';
        $this->publicDir = $basePath . '/public';

        if (!is_dir($this->cacheDir)) {
            @mkdir($this->cacheDir, 0755, true);
        }
    }

    /**
     * Get the URL path for a minified asset.
     *
     * If the minified version is cached and fresh, returns the cached path.
     * Otherwise minifies the source, caches it, and returns the cached path.
     * Falls back to the original path on error.
     *
     * @param string $publicPath Relative path from public/ (e.g. 'themes/default/assets/css/style.css')
     * @return string URL path to serve (either cached minified or original)
     */
    public function getMinifiedUrl(string $publicPath): string
    {
        $sourceFile = $this->publicDir . '/' . ltrim($publicPath, '/');
        if (!is_file($sourceFile)) {
            return '/' . ltrim($publicPath, '/');
        }

        $ext = strtolower(pathinfo($sourceFile, PATHINFO_EXTENSION));
        if (!in_array($ext, ['css', 'js'], true)) {
            return '/' . ltrim($publicPath, '/');
        }

        // Already minified?
        if (preg_match('/\.min\.(css|js)$/i', $sourceFile)) {
            return '/' . ltrim($publicPath, '/');
        }

        $cacheKey  = md5($publicPath) . '.' . $ext;
        $cacheFile = $this->cacheDir . '/' . $cacheKey;
        $metaFile  = $cacheFile . '.meta';

        // Check if cache is fresh
        if (is_file($cacheFile) && is_file($metaFile)) {
            $meta = @file_get_contents($metaFile);
            if ($meta !== false) {
                $stored = json_decode($meta, true);
                if (
                    isset($stored['mtime'], $stored['size']) &&
                    (int) $stored['mtime'] === filemtime($sourceFile) &&
                    (int) $stored['size'] === filesize($sourceFile)
                ) {
                    // Cache is fresh – return URL to cached file
                    return '/storage/cache/assets/' . $cacheKey . '?v=' . filemtime($cacheFile);
                }
            }
        }

        // Minify and cache
        $source = file_get_contents($sourceFile);
        if ($source === false) {
            return '/' . ltrim($publicPath, '/');
        }

        $minified = ($ext === 'css') ? self::minifyCss($source) : self::minifyJs($source);

        // Write cache (fallback to original on failure)
        if (file_put_contents($cacheFile, $minified, LOCK_EX) === false) {
            return '/' . ltrim($publicPath, '/');
        }
        file_put_contents($metaFile, json_encode([
            'source' => $publicPath,
            'mtime'  => filemtime($sourceFile),
            'size'   => filesize($sourceFile),
        ]), LOCK_EX);

        return '/storage/cache/assets/' . $cacheKey . '?v=' . filemtime($cacheFile);
    }

    /**
     * Minify CSS: remove comments, collapse whitespace, remove unnecessary chars.
     */
    public static function minifyCss(string $css): string
    {
        // Remove multi-line comments (preserve IE hacks starting with /*!)
        $css = preg_replace('/\/\*(?!!)[^*]*\*+(?:[^\/][^*]*\*+)*\//', '', $css);

        // Remove single-line comments that aren't inside strings
        // (only standalone // comments on their own line)
        $css = preg_replace('/^\s*\/\/[^\n]*$/m', '', $css);

        // Collapse whitespace
        $css = preg_replace('/\s+/', ' ', $css);

        // Remove spaces around special chars
        $css = preg_replace('/\s*([{}:;,>+~])\s*/', '$1', $css);

        // Remove trailing semicolons before closing braces
        $css = str_replace(';}', '}', $css);

        // Remove leading/trailing whitespace
        return trim($css);
    }

    /**
     * Minify JS: remove comments, collapse whitespace (safe mode).
     *
     * This is a conservative minifier that only removes comments and
     * collapses whitespace. It does NOT rename variables or perform
     * advanced optimizations.
     */
    public static function minifyJs(string $js): string
    {
        $result = '';
        $len    = strlen($js);
        $i      = 0;

        while ($i < $len) {
            $char = $js[$i];

            // String literals – pass through unchanged
            if ($char === '"' || $char === '\'' || $char === '`') {
                $quote   = $char;
                $result .= $char;
                $i++;
                while ($i < $len) {
                    $c = $js[$i];
                    $result .= $c;
                    if ($c === '\\' && $i + 1 < $len) {
                        $result .= $js[++$i];
                    } elseif ($c === $quote && $quote !== '`') {
                        break;
                    } elseif ($c === $quote) {
                        break;
                    }
                    $i++;
                }
                $i++;
                continue;
            }

            // Single-line comment
            if ($char === '/' && $i + 1 < $len && $js[$i + 1] === '/') {
                $i += 2;
                while ($i < $len && $js[$i] !== "\n") {
                    $i++;
                }
                // Preserve the newline as space (may be significant for ASI)
                $result .= "\n";
                $i++;
                continue;
            }

            // Multi-line comment
            if ($char === '/' && $i + 1 < $len && $js[$i + 1] === '*') {
                $i += 2;
                while ($i + 1 < $len && !($js[$i] === '*' && $js[$i + 1] === '/')) {
                    $i++;
                }
                $i += 2;
                $result .= ' ';
                continue;
            }

            // Regex literal – detect and pass through
            if ($char === '/' && $i > 0) {
                // Simple heuristic: regex follows (, =, [, !, &, |, ;, {, }, :, ,, ?
                $prev = rtrim(substr($result, -10));
                $lastChar = $prev !== '' ? $prev[strlen($prev) - 1] : '';
                if (in_array($lastChar, ['(', '=', '[', '!', '&', '|', ';', '{', '}', ':', ',', '?', "\n", ''], true)) {
                    $result .= $char;
                    $i++;
                    while ($i < $len) {
                        $c = $js[$i];
                        $result .= $c;
                        if ($c === '\\' && $i + 1 < $len) {
                            $result .= $js[++$i];
                        } elseif ($c === '/') {
                            break;
                        }
                        $i++;
                    }
                    $i++;
                    continue;
                }
            }

            $result .= $char;
            $i++;
        }

        // Collapse multiple whitespace/newlines into single space
        $result = preg_replace('/[ \t]+/', ' ', $result);
        $result = preg_replace('/\n\s*\n/', "\n", $result);

        // Remove spaces around operators (conservative set)
        $result = preg_replace('/\s*([{};,:])\s*/', '$1', $result);

        // Preserve required space after JS keywords followed by identifier/number/bracket
        $result = preg_replace(
            '/\b(return|function|var|let|const|new|typeof|instanceof|delete|throw|case|in|of|class|extends|export|import|default|async|await|yield|void)\b(?=\S)/',
            '$1 ',
            $result
        );

        // Preserve required space between closing brace and keyword
        $result = preg_replace('/\}(else|catch|finally)\b/', '} $1', $result);

        return trim($result);
    }

    /**
     * Clear the entire asset cache.
     */
    public function clear(): int
    {
        if (!is_dir($this->cacheDir)) {
            return 0;
        }

        $count = 0;
        foreach (glob($this->cacheDir . '/*') as $file) {
            if (is_file($file)) {
                @unlink($file);
                $count++;
            }
        }
        return $count;
    }
}
