<?php

declare(strict_types=1);

namespace Autogedal\Labels\Model;

use DOMDocument;
use DOMElement;
use DOMXPath;

class SvgSanitizer
{
    /**
     * Sanitize SVG XML and return a safe XML string, or null when the payload
     * cannot be trusted.
     */
    public function sanitize(string $svg): ?string
    {
        $svg = trim($svg);
        if ($svg === '' || stripos($svg, '<svg') === false) {
            return null;
        }

        $previous = libxml_use_internal_errors(true);

        try {
            $document = new DOMDocument();
            $document->preserveWhiteSpace = false;
            $document->formatOutput = false;

            if (!$document->loadXML($svg, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING)) {
                return null;
            }

            if ($document->doctype !== null) {
                return null;
            }

            $xpath = new DOMXPath($document);
            $unsafeNodes = $xpath->query(
                '//*[local-name()="script" or local-name()="foreignObject" or local-name()="iframe" or local-name()="object" or local-name()="embed" or local-name()="audio" or local-name()="video" or local-name()="image" or local-name()="animate" or local-name()="animateTransform" or local-name()="set"]'
            );
            if ($unsafeNodes !== false) {
                /** @var DOMElement $node */
                foreach ($unsafeNodes as $node) {
                    if ($node->parentNode) {
                        $node->parentNode->removeChild($node);
                    }
                }
            }

            /** @var DOMElement $node */
            foreach ($xpath->query('//*') ?: [] as $node) {
                $attributesToRemove = [];

                foreach ($node->attributes ?? [] as $attribute) {
                    $name = strtolower($attribute->nodeName);
                    $value = trim((string)$attribute->nodeValue);

                    if (str_starts_with($name, 'on')) {
                        $attributesToRemove[] = $attribute->nodeName;
                        continue;
                    }

                    if (in_array($name, ['href', 'xlink:href', 'src'], true) && !$this->isSafeReference($value)) {
                        $attributesToRemove[] = $attribute->nodeName;
                        continue;
                    }

                    if ($name === 'style' && $this->containsUnsafeCss($value)) {
                        $attributesToRemove[] = $attribute->nodeName;
                    }
                }

                foreach ($attributesToRemove as $attributeName) {
                    $node->removeAttribute($attributeName);
                }
            }

            $result = $document->saveXML();
            if (!is_string($result) || trim($result) === '') {
                return null;
            }

            return $result;
        } finally {
            libxml_clear_errors();
            libxml_use_internal_errors($previous);
        }
    }

    /**
     * Sanitize an SVG file in place.
     */
    public function sanitizeFile(string $filePath): bool
    {
        $contents = @file_get_contents($filePath);
        if ($contents === false) {
            return false;
        }

        $sanitized = $this->sanitize($contents);
        if ($sanitized === null) {
            return false;
        }

        return file_put_contents($filePath, $sanitized) !== false;
    }

    private function isSafeReference(string $value): bool
    {
        if ($value === '') {
            return false;
        }

        if ($value[0] === '#') {
            return true;
        }

        return false;
    }

    private function containsUnsafeCss(string $value): bool
    {
        $value = strtolower($value);

        return str_contains($value, 'url(')
            || str_contains($value, 'expression(')
            || str_contains($value, '@import')
            || str_contains($value, 'javascript:')
            || str_contains($value, 'vbscript:')
            || str_contains($value, 'data:');
    }
}
