<?php

namespace App\Services\PromptLandingPageEditor;

class MergeEditedDocument
{
    /**
     * GrapesJS exports body markup and can rewrite CSS. Keep the stored head,
     * CSS, and JavaScript unless the editor payload is a real change.
     *
     * @return array{html: string, css: string, js: string}
     */
    public function merge(
        string $originalHtml,
        string $originalCss,
        string $originalJs,
        string $editedHtml,
        string $editedCss,
        string $editedJs,
    ): array {
        return [
            'html' => $this->mergeHtml($originalHtml, $editedHtml),
            'css' => $this->mergeCss($originalCss, $editedCss),
            'js' => $this->mergeJs($originalJs, $editedJs),
        ];
    }

    public function mergeHtml(string $originalHtml, string $editedHtml): string
    {
        $originalHtml = $this->stripInjectedTools($originalHtml);
        $editedHtml = $this->stripInjectedTools($editedHtml);
        $editedBody = $this->restoreMissingScripts(
            $originalHtml,
            $this->bodyInner($editedHtml),
        );

        if (! $this->isFullHtmlDocument($originalHtml)) {
            return $editedBody;
        }

        return $this->doctype($originalHtml)."\n"
            .$this->htmlOpenTag($originalHtml)."\n"
            .$this->headBlock($originalHtml)."\n"
            .$this->bodyOpenTag($originalHtml)."\n"
            .$editedBody."\n"
            ."</body>\n"
            .'</html>';
    }

    private function mergeCss(string $originalCss, string $editedCss): string
    {
        if (trim($editedCss) === '') {
            return $originalCss;
        }

        if (trim($originalCss) === '') {
            return $editedCss;
        }

        if ($this->normalized($editedCss) === $this->normalized($originalCss)) {
            return $originalCss;
        }

        if ($this->editedCssLostOriginal($editedCss, $originalCss)) {
            return $originalCss;
        }

        return $editedCss;
    }

    private function mergeJs(string $originalJs, string $editedJs): string
    {
        if ($this->containsInjectedTools($editedJs)) {
            return $originalJs;
        }

        if (trim($editedJs) === '') {
            return $originalJs;
        }

        if (trim($editedJs) === trim($originalJs)) {
            return $originalJs;
        }

        return $editedJs;
    }

    private function restoreMissingScripts(string $originalHtml, string $editedBody): string
    {
        foreach ($this->scriptTags($this->bodyInner($originalHtml)) as $script) {
            if ($this->containsInjectedTools($script)) {
                continue;
            }

            if (! str_contains($this->normalized($editedBody), $this->normalized($script))) {
                $editedBody .= "\n".$script;
            }
        }

        return $editedBody;
    }

    /**
     * @return list<string>
     */
    private function scriptTags(string $html): array
    {
        if (preg_match_all('/<script\b[^>]*>.*?<\/script>/is', $html, $matches) !== false) {
            return $matches[0];
        }

        return [];
    }

    private function isFullHtmlDocument(string $html): bool
    {
        return preg_match('/<!doctype html>|<html[\s>]/i', $html) === 1;
    }

    private function doctype(string $html): string
    {
        if (preg_match('/<!doctype html[^>]*>/i', $html, $matches) === 1) {
            return $matches[0];
        }

        return '<!doctype html>';
    }

    private function htmlOpenTag(string $html): string
    {
        if (preg_match('/<html\b[^>]*>/i', $html, $matches) === 1) {
            return $matches[0];
        }

        return '<html>';
    }

    private function bodyOpenTag(string $html): string
    {
        if (preg_match('/<body\b[^>]*>/i', $html, $matches) === 1) {
            return $matches[0];
        }

        return '<body>';
    }

    private function headBlock(string $html): string
    {
        if (preg_match('/<head\b[^>]*>.*?<\/head>/is', $html, $matches) === 1) {
            return $this->stripInjectedTools($matches[0]);
        }

        return '<head></head>';
    }

    private function bodyInner(string $html): string
    {
        if (preg_match('/<body\b[^>]*>(.*)<\/body>/is', $html, $matches) === 1) {
            return trim($matches[1]);
        }

        $html = (string) preg_replace('/<!doctype html[^>]*>/i', '', $html);
        $html = (string) preg_replace('/<\/?html\b[^>]*>/i', '', $html);
        $html = (string) preg_replace('/<head\b[^>]*>.*?<\/head>/is', '', $html);
        $html = (string) preg_replace('/<\/body>/i', '', $html);

        return trim($html);
    }

    private function editedCssLostOriginal(string $editedCss, string $originalCss): bool
    {
        if ($this->containsEditorChrome($editedCss) && ! $this->containsEditorChrome($originalCss)) {
            return true;
        }

        $missing = array_diff(
            $this->cssSelectorNames($originalCss),
            $this->cssSelectorNames($editedCss),
        );

        if ($missing !== [] && (
            $this->containsEditorChrome($editedCss)
            || strlen($editedCss) > strlen($originalCss) * 1.1
        )) {
            return true;
        }

        $originalVars = preg_match_all('/--[A-Za-z_][\w-]*\s*:/', $originalCss);
        $editedVars = preg_match_all('/--[A-Za-z_][\w-]*\s*:/', $editedCss);

        if ($originalVars >= 3 && $editedVars < (int) floor($originalVars * 0.6)) {
            return true;
        }

        return strlen($originalCss) > strlen($editedCss) * 1.4
            && preg_match('/:root|@media/', $originalCss) === 1
            && preg_match('/:root|@media/', $editedCss) !== 1;
    }

    private function containsEditorChrome(string $css): bool
    {
        return preg_match('/\[data-gjs-type|\.gjs-selected|\.gjs-dashed|scroll-invalid|transition-invalid/', $css) === 1;
    }

    /**
     * @return list<string>
     */
    private function cssSelectorNames(string $css): array
    {
        preg_match_all('/\.([A-Za-z_][\w-]*)/', $css, $matches);

        return array_values(array_unique($matches[1]));
    }

    private function stripInjectedTools(string $html): string
    {
        $html = (string) preg_replace(
            '/<script\b[^>]*\bid=(["\'])browser-logger-active\1[^>]*>.*?<\/script>/is',
            '',
            $html,
        );
        $html = (string) preg_replace(
            '/<script\b[^>]*>[^<]*_boost\/browser-logs.*?<\/script>/is',
            '',
            $html,
        );

        return $html;
    }

    private function containsInjectedTools(string $value): bool
    {
        return str_contains($value, '_boost/browser-logs')
            || str_contains($value, 'browser-logger-active');
    }

    private function normalized(string $value): string
    {
        return (string) preg_replace('/\s+/', ' ', trim($value));
    }
}
