<?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 ProductPackageArchive
{
    public const SCHEMA = 'asuka-product-package';
    public const SCHEMA_VERSION = 1;
    public const MANIFEST_FILE = 'manifest.json';
    public const CHECKSUM_FILE = 'checksums.sha256';

    private const MAX_FILES = 1000;
    private const MAX_FILE_BYTES = 134217728;
    private const MAX_TOTAL_BYTES = 536870912;

    public static function temporaryDirectory(string $prefix = 'apd-package') : string|\WP_Error
    {
        $base = trailingslashit(get_temp_dir()) . sanitize_file_name($prefix);
        if (! wp_mkdir_p($base)) {
            return new \WP_Error('package_temp_failed', __('The package workspace could not be created.', 'asuka-product-designer'));
        }

        $directory = trailingslashit($base) . wp_generate_uuid4();
        if (! wp_mkdir_p($directory)) {
            return new \WP_Error('package_temp_failed', __('The package workspace could not be created.', 'asuka-product-designer'));
        }

        return wp_normalize_path($directory);
    }

    public static function create(string $sourceDirectory, string $archivePath) : true|\WP_Error
    {
        self::loadPclZip();
        $sourceDirectory = untrailingslashit(wp_normalize_path($sourceDirectory));
        $files = self::filesInDirectory($sourceDirectory);
        if ($files === []) {
            return new \WP_Error('package_empty', __('The product package contains no files.', 'asuka-product-designer'));
        }

        $archive = new \PclZip($archivePath);
        $result = $archive->create(
            $files,
            PCLZIP_OPT_REMOVE_PATH,
            $sourceDirectory
        );
        if ($result === 0) {
            return new \WP_Error('package_archive_failed', sanitize_text_field((string) $archive->errorInfo(true)));
        }

        return true;
    }

    public static function extract(string $archivePath) : array|\WP_Error
    {
        if (! is_file($archivePath) || ! is_readable($archivePath)) {
            return new \WP_Error('package_missing', __('The selected package could not be read.', 'asuka-product-designer'));
        }

        $archiveBytes = (int) filesize($archivePath);
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        $maxArchiveBytes = max(1048576, (int) apply_filters('apd_product_package_max_archive_bytes', self::MAX_TOTAL_BYTES));
        if ($archiveBytes <= 0 || $archiveBytes > $maxArchiveBytes) {
            return new \WP_Error('package_too_large', __('The selected package is too large.', 'asuka-product-designer'));
        }

        self::loadPclZip();
        $archive = new \PclZip($archivePath);
        $entries = $archive->listContent();
        if (! is_array($entries) || $entries === []) {
            return new \WP_Error('package_invalid_archive', __('The selected file is not a readable ZIP package.', 'asuka-product-designer'));
        }

        $validation = self::validateEntries($entries, $archiveBytes);
        if (is_wp_error($validation)) {
            return $validation;
        }

        $directory = self::temporaryDirectory('apd-package-extract');
        if (is_wp_error($directory)) {
            return $directory;
        }

        $result = $archive->extract(PCLZIP_OPT_PATH, $directory, PCLZIP_OPT_REPLACE_NEWER);
        if ($result === 0) {
            self::removeDirectory($directory);
            return new \WP_Error('package_extract_failed', sanitize_text_field((string) $archive->errorInfo(true)));
        }

        $root = realpath($directory);
        if (! is_string($root)) {
            self::removeDirectory($directory);
            return new \WP_Error('package_extract_failed', __('The package workspace could not be resolved.', 'asuka-product-designer'));
        }
        $root = untrailingslashit(wp_normalize_path($root));
        foreach ($validation['files'] as $relativePath) {
            $resolved = realpath($root . '/' . $relativePath);
            if (! is_string($resolved) || ! self::pathInside($resolved, $root) || is_link($resolved)) {
                self::removeDirectory($directory);
                return new \WP_Error('package_path_invalid', __('The package contains an unsafe file path.', 'asuka-product-designer'));
            }
        }

        $integrity = self::verifyChecksums($root, $validation['files']);
        if (is_wp_error($integrity)) {
            self::removeDirectory($directory);
            return $integrity;
        }

        $manifest = self::readJson($root . '/' . self::MANIFEST_FILE);
        if (is_wp_error($manifest)) {
            self::removeDirectory($directory);
            return $manifest;
        }
        if (
            ($manifest['schema'] ?? '') !== self::SCHEMA
            || (int) ($manifest['schemaVersion'] ?? 0) !== self::SCHEMA_VERSION
        ) {
            self::removeDirectory($directory);
            return new \WP_Error('package_schema_invalid', __('This file is not a supported Asuka product package.', 'asuka-product-designer'));
        }

        return [
            'directory' => $root,
            'manifest' => $manifest,
            'files' => $validation['files'],
            'totalBytes' => $validation['totalBytes'],
        ];
    }

    public static function writeJson(string $path, array $payload) : true|\WP_Error
    {
        $directory = dirname($path);
        if (! is_dir($directory) && ! wp_mkdir_p($directory)) {
            return new \WP_Error('package_write_failed', __('A package directory could not be created.', 'asuka-product-designer'));
        }

        $json = wp_json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if (! is_string($json) || file_put_contents($path, $json . "\n", LOCK_EX) === false) {
            return new \WP_Error('package_write_failed', __('A package file could not be written.', 'asuka-product-designer'));
        }

        return true;
    }

    public static function readJson(string $path) : array|\WP_Error
    {
        $raw = is_file($path) ? file_get_contents($path) : false;
        $payload = is_string($raw) ? json_decode($raw, true) : null;
        if (! is_array($payload) || json_last_error() !== JSON_ERROR_NONE) {
            return new \WP_Error('package_json_invalid', __('A required package document is invalid.', 'asuka-product-designer'));
        }

        return $payload;
    }

    public static function writeChecksums(string $root) : true|\WP_Error
    {
        $root = untrailingslashit(wp_normalize_path($root));
        $lines = [];
        foreach (self::filesInDirectory($root) as $absolutePath) {
            $relativePath = ltrim(substr(wp_normalize_path($absolutePath), strlen($root)), '/');
            if ($relativePath === self::CHECKSUM_FILE) {
                continue;
            }
            $hash = hash_file('sha256', $absolutePath);
            if (! is_string($hash)) {
                return new \WP_Error('package_checksum_failed', __('A package checksum could not be generated.', 'asuka-product-designer'));
            }
            $lines[$relativePath] = $hash . '  ' . $relativePath;
        }
        ksort($lines, SORT_NATURAL | SORT_FLAG_CASE);

        $contents = implode("\n", $lines) . "\n";
        if (file_put_contents($root . '/' . self::CHECKSUM_FILE, $contents, LOCK_EX) === false) {
            return new \WP_Error('package_checksum_failed', __('The package checksums could not be written.', 'asuka-product-designer'));
        }

        return true;
    }

    public static function removeDirectory(string $directory) : void
    {
        $directory = wp_normalize_path($directory);
        $temp = untrailingslashit(wp_normalize_path(get_temp_dir()));
        if (! is_dir($directory) || ! self::pathInside($directory, $temp)) {
            return;
        }

        if (! function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (! WP_Filesystem()) {
            return;
        }

        global $wp_filesystem;
        $wp_filesystem->delete($directory, true, 'd');
    }

    private static function validateEntries(array $entries, int $archiveBytes) : array|\WP_Error
    {
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        $maxFiles = max(1, (int) apply_filters('apd_product_package_max_files', self::MAX_FILES));
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        $maxFileBytes = max(1, (int) apply_filters('apd_product_package_max_file_bytes', self::MAX_FILE_BYTES));
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        $maxTotalBytes = max(1, (int) apply_filters('apd_product_package_max_total_bytes', self::MAX_TOTAL_BYTES));
        $files = [];
        $seen = [];
        $totalBytes = 0;

        foreach ($entries as $entry) {
            $path = self::normalizeArchivePath((string) ($entry['filename'] ?? ''));
            if ($path === '' || ! self::safeRelativePath($path)) {
                return new \WP_Error('package_path_invalid', __('The package contains an unsafe file path.', 'asuka-product-designer'));
            }
            $lookup = strtolower($path);
            if (isset($seen[$lookup])) {
                return new \WP_Error('package_duplicate_path', __('The package contains duplicate file paths.', 'asuka-product-designer'));
            }
            $seen[$lookup] = true;

            if (! empty($entry['folder'])) {
                continue;
            }
            $size = max(0, (int) ($entry['size'] ?? 0));
            if ($size > $maxFileBytes) {
                return new \WP_Error('package_file_too_large', __('A file inside the package is too large.', 'asuka-product-designer'));
            }
            $totalBytes += $size;
            if ($totalBytes > $maxTotalBytes) {
                return new \WP_Error('package_expanded_too_large', __('The expanded package is too large.', 'asuka-product-designer'));
            }
            $files[] = $path;
        }

        if (count($files) > $maxFiles) {
            return new \WP_Error('package_too_many_files', __('The package contains too many files.', 'asuka-product-designer'));
        }
        if (! in_array(self::MANIFEST_FILE, $files, true) || ! in_array(self::CHECKSUM_FILE, $files, true)) {
            return new \WP_Error('package_documents_missing', __('The package manifest or checksums are missing.', 'asuka-product-designer'));
        }
        if ($totalBytes > 10485760 && $archiveBytes > 0 && ($totalBytes / $archiveBytes) > 250) {
            return new \WP_Error('package_compression_unsafe', __('The package compression ratio is unsafe.', 'asuka-product-designer'));
        }

        return ['files' => $files, 'totalBytes' => $totalBytes];
    }

    private static function verifyChecksums(string $root, array $files) : true|\WP_Error
    {
        $checksumPath = $root . '/' . self::CHECKSUM_FILE;
        $raw = is_file($checksumPath) ? file($checksumPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) : false;
        if (! is_array($raw) || $raw === []) {
            return new \WP_Error('package_checksums_invalid', __('The package checksums are invalid.', 'asuka-product-designer'));
        }

        $expected = [];
        foreach ($raw as $line) {
            if (preg_match('/^([a-f0-9]{64})  (.+)$/', trim((string) $line), $matches) !== 1) {
                return new \WP_Error('package_checksums_invalid', __('The package checksums are invalid.', 'asuka-product-designer'));
            }
            $path = self::normalizeArchivePath($matches[2]);
            if (! self::safeRelativePath($path) || isset($expected[$path])) {
                return new \WP_Error('package_checksums_invalid', __('The package checksums are invalid.', 'asuka-product-designer'));
            }
            $expected[$path] = $matches[1];
        }

        $actualFiles = array_values(array_filter($files, static fn(string $path) : bool => $path !== self::CHECKSUM_FILE));
        sort($actualFiles, SORT_STRING);
        $expectedFiles = array_keys($expected);
        sort($expectedFiles, SORT_STRING);
        if ($actualFiles !== $expectedFiles) {
            return new \WP_Error('package_checksums_incomplete', __('The package file list does not match its checksums.', 'asuka-product-designer'));
        }

        foreach ($expected as $path => $hash) {
            $actual = hash_file('sha256', $root . '/' . $path);
            if (! is_string($actual) || ! hash_equals($hash, $actual)) {
                return new \WP_Error('package_checksum_mismatch', __('The package is damaged or has been modified.', 'asuka-product-designer'));
            }
        }

        return true;
    }

    private static function filesInDirectory(string $root) : array
    {
        if (! is_dir($root)) {
            return [];
        }
        $files = [];
        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS)
        );
        foreach ($iterator as $item) {
            if ($item->isFile() && ! $item->isLink()) {
                $files[] = wp_normalize_path($item->getPathname());
            }
        }
        sort($files, SORT_STRING);
        return $files;
    }

    private static function loadPclZip() : void
    {
        if (! class_exists('PclZip')) {
            require_once ABSPATH . 'wp-admin/includes/class-pclzip.php';
        }
    }

    private static function normalizeArchivePath(string $path) : string
    {
        return trim(str_replace('\\', '/', $path), '/');
    }

    private static function safeRelativePath(string $path) : bool
    {
        if ($path === '' || strlen($path) > 240 || str_contains($path, "\0") || preg_match('/^[a-zA-Z]:/', $path)) {
            return false;
        }
        foreach (explode('/', $path) as $segment) {
            if ($segment === '' || $segment === '.' || $segment === '..') {
                return false;
            }
        }
        return true;
    }

    private static 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 . '/');
    }
}
