<?php

// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedNamespaceFound -- APD is the established namespace shared with the Pro add-on and retained for backward compatibility.
namespace APD\Portability;

defined('ABSPATH') || exit;

final class PortableAssetRegistry
{
    private array $assets = [];
    private array $pathToId = [];
    private array $externalHosts = [];
    private array $uploads;

    public function __construct()
    {
        $this->uploads = wp_upload_dir();
    }

    public function rewrite(mixed $value, string $key = '') : mixed
    {
        if (is_array($value)) {
            $rewritten = [];
            foreach ($value as $childKey => $childValue) {
                $rewritten[$childKey] = $this->rewrite($childValue, (string) $childKey);
            }
            return $rewritten;
        }
        if (is_object($value)) {
            return $this->rewrite((array) $value, $key);
        }

        if ((is_int($value) || (is_string($value) && ctype_digit($value))) && $this->isAttachmentKey($key)) {
            $attachmentId = (int) $value;
            if ($attachmentId > 0 && get_post_type($attachmentId) === 'attachment') {
                $reference = $this->registerAttachment($attachmentId, true);
                if ($reference !== '') {
                    return $reference;
                }
            }
        }

        if (! is_string($value) || trim($value) === '') {
            return $value;
        }

        $exact = $this->registerUrl($value);
        if ($exact !== '') {
            return $exact;
        }

        return preg_replace_callback(
            '~https?://[^\s"\'<>\\)]+~i',
            function (array $matches) : string {
                $url = html_entity_decode((string) $matches[0], ENT_QUOTES | ENT_HTML5, 'UTF-8');
                $reference = $this->registerUrl($url);
                if ($reference !== '') {
                    return $reference;
                }
                $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
                if ($host !== '') {
                    $this->externalHosts[$host] = true;
                }
                return (string) $matches[0];
            },
            $value
        ) ?? $value;
    }

    public function write(string $packageRoot) : true|\WP_Error
    {
        foreach ($this->assets as $asset) {
            $target = untrailingslashit($packageRoot) . '/' . $asset['path'];
            if (! wp_mkdir_p(dirname($target))) {
                return new \WP_Error('package_asset_write_failed', __('A media directory could not be created.', 'asuka-product-designer'));
            }
            $written = isset($asset['content'])
                ? file_put_contents($target, (string) $asset['content'], LOCK_EX)
                : copy((string) $asset['sourcePath'], $target);
            if ($written === false) {
                return new \WP_Error('package_asset_write_failed', __('A media file could not be added to the package.', 'asuka-product-designer'));
            }
        }

        return true;
    }

    public function manifest() : array
    {
        $manifest = [];
        foreach ($this->assets as $asset) {
            $content = isset($asset['content']) ? (string) $asset['content'] : null;
            $hash = $content !== null ? hash('sha256', $content) : hash_file('sha256', (string) $asset['sourcePath']);
            $size = $content !== null ? strlen($content) : (int) filesize((string) $asset['sourcePath']);
            $manifest[] = [
                'id' => (string) $asset['id'],
                'path' => (string) $asset['path'],
                'filename' => (string) $asset['filename'],
                'extension' => (string) $asset['extension'],
                'mime' => (string) $asset['mime'],
                'bytes' => $size,
                'sha256' => is_string($hash) ? $hash : '',
                'dependencies' => array_values(array_unique((array) ($asset['dependencies'] ?? []))),
            ];
        }
        usort($manifest, static fn(array $left, array $right) : int => strcmp($left['path'], $right['path']));
        return $manifest;
    }

    public function externalHosts() : array
    {
        $hosts = array_keys($this->externalHosts);
        sort($hosts, SORT_STRING);
        return $hosts;
    }

    private function registerAttachment(int $attachmentId, bool $idReference = false) : string
    {
        $path = get_attached_file($attachmentId);
        $url = wp_get_attachment_url($attachmentId);
        if (! is_string($path) || ! is_string($url)) {
            return '';
        }
        $id = $this->registerFile($path, $url, $attachmentId);
        if ($id === '') {
            return '';
        }
        return ($idReference ? 'asuka-attachment://' : 'asuka-asset://') . $id;
    }

    private function registerUrl(string $url) : string
    {
        $url = trim(html_entity_decode($url, ENT_QUOTES | ENT_HTML5, 'UTF-8'));
        if ($url === '' || str_starts_with($url, 'data:') || str_starts_with($url, 'blob:')) {
            return '';
        }

        $path = $this->localPathForUrl($url);
        if ($path === '') {
            return '';
        }

        $attachmentId = attachment_url_to_postid($url);
        $id = $this->registerFile($path, $url, (int) $attachmentId);
        return $id !== '' ? 'asuka-asset://' . $id : '';
    }

    private function registerFile(string $path, string $sourceUrl = '', int $attachmentId = 0) : string
    {
        $realPath = realpath($path);
        $baseDirectory = realpath((string) ($this->uploads['basedir'] ?? ''));
        if (
            ! is_string($realPath)
            || ! is_string($baseDirectory)
            || ! is_file($realPath)
            || ! $this->pathInside($realPath, $baseDirectory)
        ) {
            return '';
        }

        $realPath = wp_normalize_path($realPath);
        if (isset($this->pathToId[$realPath])) {
            return (string) $this->pathToId[$realPath];
        }

        $extension = strtolower((string) pathinfo($realPath, PATHINFO_EXTENSION));
        if (! in_array($extension, $this->allowedExtensions(), true)) {
            return '';
        }
        $size = (int) filesize($realPath);
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        $maxBytes = max(1, (int) apply_filters('apd_product_package_max_asset_bytes', 134217728));
        if ($size <= 0 || $size > $maxBytes) {
            return '';
        }

        $sourceHash = hash_file('sha256', $realPath);
        if (! is_string($sourceHash)) {
            return '';
        }
        $id = substr($sourceHash, 0, 24);
        if (isset($this->assets[$id])) {
            $this->pathToId[$realPath] = $id;
            return $id;
        }

        $filename = sanitize_file_name(basename($realPath));
        if ($filename === '') {
            $filename = 'asset.' . $extension;
        }
        $asset = [
            'id' => $id,
            'path' => 'assets/' . $id . '-' . $filename,
            'filename' => $filename,
            'extension' => $extension,
            'mime' => $this->mimeForExtension($extension),
            'sourcePath' => $realPath,
            'sourceUrl' => $sourceUrl,
            'attachmentId' => $attachmentId,
            'dependencies' => [],
        ];
        $this->assets[$id] = $asset;
        $this->pathToId[$realPath] = $id;

        if ($extension === 'gltf') {
            $this->prepareGltf($id, $realPath);
        }

        return $id;
    }

    private function prepareGltf(string $assetId, string $path) : void
    {
        $raw = file_get_contents($path);
        $document = is_string($raw) ? json_decode($raw, true) : null;
        if (! is_array($document) || ! is_array($document['asset'] ?? null)) {
            return;
        }

        $dependencies = [];
        foreach (['buffers', 'images'] as $collection) {
            if (! is_array($document[$collection] ?? null)) {
                continue;
            }
            foreach ($document[$collection] as $index => $entry) {
                if (! is_array($entry) || ! is_string($entry['uri'] ?? null)) {
                    continue;
                }
                $uri = trim((string) $entry['uri']);
                if ($uri === '' || str_starts_with($uri, 'data:') || preg_match('~^[a-z]+://~i', $uri)) {
                    continue;
                }
                $candidate = realpath(dirname($path) . DIRECTORY_SEPARATOR . rawurldecode($uri));
                if (! is_string($candidate)) {
                    continue;
                }
                $dependencyId = $this->registerFile($candidate);
                if ($dependencyId === '') {
                    continue;
                }
                $document[$collection][$index]['uri'] = 'asuka-asset://' . $dependencyId;
                $dependencies[] = $dependencyId;
            }
        }

        if ($dependencies !== []) {
            $content = wp_json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
            if (is_string($content)) {
                $this->assets[$assetId]['content'] = $content . "\n";
                $this->assets[$assetId]['dependencies'] = $dependencies;
            }
        }
    }

    private function localPathForUrl(string $url) : string
    {
        $baseUrl = (string) ($this->uploads['baseurl'] ?? '');
        $baseDirectory = (string) ($this->uploads['basedir'] ?? '');
        if ($baseUrl === '' || $baseDirectory === '') {
            return '';
        }

        $cleanUrl = strtok($url, '?#');
        if (! is_string($cleanUrl)) {
            return '';
        }
        $basePath = untrailingslashit((string) wp_parse_url($baseUrl, PHP_URL_PATH));
        $urlPath = (string) wp_parse_url($cleanUrl, PHP_URL_PATH);
        if ($basePath === '' || $urlPath === '' || ! str_starts_with($urlPath, $basePath . '/')) {
            return '';
        }

        $relative = ltrim(rawurldecode(substr($urlPath, strlen($basePath))), '/');
        if ($relative === '' || str_contains($relative, '..')) {
            return '';
        }
        $candidate = wp_normalize_path(trailingslashit($baseDirectory) . $relative);
        return is_file($candidate) ? $candidate : '';
    }

    private function isAttachmentKey(string $key) : bool
    {
        return preg_match('/^(attachment|media|image|preview|thumbnail|file)(Id|_id)$/i', $key) === 1;
    }

    private function allowedExtensions() : array
    {
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        return (array) apply_filters('apd_product_package_asset_extensions', [
            'jpg', 'jpeg', 'png', 'gif', 'webp', 'avif', 'svg',
            'glb', 'gltf', 'bin',
            'woff2', 'woff', 'ttf', 'otf',
        ]);
    }

    private function mimeForExtension(string $extension) : string
    {
        return match ($extension) {
            'jpg', 'jpeg' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'avif' => 'image/avif',
            'svg' => 'image/svg+xml',
            'glb' => 'model/gltf-binary',
            'gltf' => 'model/gltf+json',
            'woff2' => 'font/woff2',
            'woff' => 'font/woff',
            'ttf' => 'font/ttf',
            'otf' => 'font/otf',
            default => 'application/octet-stream',
        };
    }

    private function pathInside(string $path, string $root) : bool
    {
        $path = wp_normalize_path($path);
        $root = untrailingslashit(wp_normalize_path($root));
        return $path === $root || str_starts_with($path, $root . '/');
    }
}
