<?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\Domain\Upload;

use APD\Admin\AdminSettings;
use APD\Support\Capabilities;
use WP_REST_Request;

defined('ABSPATH') || exit;

class UploadService
{
    public function handle(WP_REST_Request $request) : array
    {
        $files = $request->get_file_params();
        if ($files === []) {
            return $this->failure('upload_missing', __('No file uploaded.', 'asuka-product-designer'));
        }

        $fileField = array_key_first($files);
        $file = is_string($fileField) && isset($files[$fileField]) && is_array($files[$fileField]) ? $files[$fileField] : null;
        if (! $file) {
            return $this->failure('upload_missing', __('No file uploaded.', 'asuka-product-designer'));
        }

        $errorCode = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
        if ($errorCode !== UPLOAD_ERR_OK) {
            $status = in_array($errorCode, [UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE], true) ? 413 : 400;
            return $this->failure('upload_failed', $this->uploadErrorMessage($errorCode), $status);
        }

        $tmpName = isset($file['tmp_name']) ? (string) $file['tmp_name'] : '';
        $originalName = isset($file['name']) ? (string) $file['name'] : '';
        if ($tmpName === '' || $originalName === '') {
            return $this->failure('upload_invalid', __('The uploaded file is invalid.', 'asuka-product-designer'));
        }

        $mode = $this->resolveMode($request);
        $source = $this->resolveSource($request);
        $isPrivilegedAdminUpload = $source === 'admin'
            && (current_user_can(Capabilities::MANAGE) || current_user_can('manage_options'));
        if (in_array($mode, ['model', 'svg'], true) && ! $isPrivilegedAdminUpload) {
            return $this->failure(
                'upload_type_forbidden',
                __('This file type can only be uploaded from an Asuka Designer administration editor.', 'asuka-product-designer'),
                415
            );
        }

        $rateLimit = (new RuntimeUploadRateLimiter())->consume($request);
        if (empty($rateLimit['allowed'])) {
            return $this->failure(
                'upload_rate_limited',
                __('Too many files were uploaded. Please wait before trying again.', 'asuka-product-designer'),
                429,
                max(1, (int) ($rateLimit['retryAfter'] ?? 1))
            );
        }

        $defaultMaxBytes = (new AdminSettings())->uploadMaxBytes(10);
        // 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_runtime_upload_max_bytes', $defaultMaxBytes, $request, $file));
        $size = isset($file['size']) ? (int) $file['size'] : 0;
        if ($size <= 0 || $size > $maxBytes) {
            return $this->failure(
                'upload_too_large',
                sprintf(
                    /* translators: %s = maximum upload size */
                    __('The uploaded file exceeds the maximum allowed size of %s.', 'asuka-product-designer'),
                    size_format($maxBytes)
                ),
                413
            );
        }

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
        $rasterMimeTypes = (array) apply_filters('apd_runtime_upload_mime_types', [
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
            'webp' => 'image/webp',
            'avif' => 'image/avif',
        ], $request, $file);

        foreach ($rasterMimeTypes as $extensions => $allowedMime) {
            $extensionList = preg_split('/\|/', strtolower((string) $extensions)) ?: [];
            if (strtolower((string) $allowedMime) === 'image/svg+xml' || array_intersect($extensionList, ['svg', 'svgz'])) {
                unset($rasterMimeTypes[$extensions]);
            }
        }

        $allowedMimeTypes = match ($mode) {
            'model' => [
                'glb' => 'model/gltf-binary',
                'gltf' => 'model/gltf+json',
            ],
            'svg' => ['svg' => 'image/svg+xml'],
            default => $rasterMimeTypes,
        };

        $checkedType = wp_check_filetype_and_ext($tmpName, $originalName, $allowedMimeTypes);
        $mime = $this->normalizeMime((string) ($checkedType['type'] ?? ''));
        if ($mime === '' || ! in_array($mime, array_values($allowedMimeTypes), true)) {
            $message = match ($mode) {
                'model' => __('Only validated GLB or glTF 2.0 model files are allowed.', 'asuka-product-designer'),
                'svg' => __('Only sanitized SVG files are allowed for this field.', 'asuka-product-designer'),
                default => __('Only supported raster image uploads are allowed.', 'asuka-product-designer'),
            };
            return $this->failure('upload_type_forbidden', $message, 415);
        }

        $width = 0;
        $height = 0;
        if ($mode === 'image') {
            $actualMime = function_exists('wp_get_image_mime') ? wp_get_image_mime($tmpName) : false;
            if (! is_string($actualMime) || $this->normalizeMime($actualMime) !== $mime) {
                return $this->failure('upload_content_mismatch', __('The file contents do not match the selected image format.', 'asuka-product-designer'), 415);
            }

            $imageSize = @getimagesize($tmpName);
            $width = is_array($imageSize) ? (int) ($imageSize[0] ?? 0) : 0;
            $height = is_array($imageSize) ? (int) ($imageSize[1] ?? 0) : 0;
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The apd_ hook is a documented integration API retained for backward compatibility.
            $maxPixels = max(1, (int) apply_filters('apd_runtime_upload_max_pixels', 40_000_000, $request, $file));
            if ($width <= 0 || $height <= 0) {
                return $this->failure('upload_dimensions_invalid', __('The uploaded image dimensions could not be read.', 'asuka-product-designer'), 415);
            }
            if (($width * $height) > $maxPixels) {
                return $this->failure('upload_dimensions_too_large', __('The uploaded image dimensions are too large.', 'asuka-product-designer'), 413);
            }
        }

        if (! function_exists('media_handle_upload')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            require_once ABSPATH . 'wp-admin/includes/image.php';
            require_once ABSPATH . 'wp-admin/includes/media.php';
        }

        $attachmentId = media_handle_upload($fileField, 0, [], [
            'mimes' => $allowedMimeTypes,
            'test_form' => false,
        ]);

        if (is_wp_error($attachmentId)) {
            return $this->failure('upload_storage_failed', $attachmentId->get_error_message());
        }

        $attachmentId = (int) $attachmentId;
        $url = (string) wp_get_attachment_url($attachmentId);
        $filename = basename((string) get_attached_file($attachmentId));
        $storedMime = $this->normalizeMime((string) get_post_mime_type($attachmentId));
        if ($storedMime !== '') {
            $mime = $storedMime;
        }

        update_post_meta($attachmentId, '_apd_runtime_upload', '1');
        update_post_meta($attachmentId, '_apd_upload_source', $source);
        update_post_meta($attachmentId, '_apd_upload_mode', $mode);

        return [
            'success' => true,
            'url' => $url,
            'src' => $url,
            'value' => $url,
            'fileUrl' => $url,
            'downloadUrl' => $url,
            'attachmentId' => $attachmentId,
            'id' => $attachmentId,
            'filename' => $filename,
            'mime' => $mime,
            'width' => $width,
            'height' => $height,
            'source' => $source,
            'mode' => $mode,
            'data' => [
                'url' => $url,
                'attachmentId' => $attachmentId,
                'filename' => $filename,
                'mime' => $mime,
                'width' => $width,
                'height' => $height,
                'mode' => $mode,
            ],
            'result' => [
                'url' => $url,
                'attachmentId' => $attachmentId,
                'filename' => $filename,
                'mime' => $mime,
                'width' => $width,
                'height' => $height,
                'mode' => $mode,
            ],
        ];
    }

    private function resolveMode(WP_REST_Request $request) : string
    {
        $mode = sanitize_key((string) $request->get_param('asuka_mode'));
        if ($mode === '') {
            $context = $request->get_param('apd_context');
            $context = is_string($context) ? json_decode($context, true) : $context;
            if (is_array($context)) {
                $mode = sanitize_key((string) ($context['mode'] ?? ''));
            }
        }

        return match ($mode) {
            'model' => 'model',
            'svg' => 'svg',
            default => 'image',
        };
    }

    private function resolveSource(WP_REST_Request $request) : string
    {
        $source = sanitize_key((string) ($request->get_param('apd_source') ?: ''));
        if ($source === '') {
            $source = sanitize_key((string) ($request->get_param('asuka_source') ?: 'runtime'));
        }
        return $source !== '' ? $source : 'runtime';
    }

    private function failure(string $code, string $message, int $status = 400, int $retryAfter = 0) : array
    {
        return [
            'success' => false,
            'code' => $code,
            'message' => $message,
            '_status' => $status,
            '_retryAfter' => $retryAfter,
        ];
    }

    private function normalizeMime(string $mime) : string
    {
        $mime = strtolower(trim($mime));
        return $mime === 'image/jpg' ? 'image/jpeg' : $mime;
    }

    private function uploadErrorMessage(int $errorCode) : string
    {
        return match ($errorCode) {
            UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => __('The uploaded file is too large.', 'asuka-product-designer'),
            UPLOAD_ERR_PARTIAL => __('The uploaded file was only partially received.', 'asuka-product-designer'),
            UPLOAD_ERR_NO_FILE => __('No file uploaded.', 'asuka-product-designer'),
            default => __('The uploaded file could not be processed.', 'asuka-product-designer'),
        };
    }
}
