<?php

namespace Storeplugin\PDFInvoice\Framework\Rest;

defined('ABSPATH') || exit;

use Storeplugin\PDFInvoice\Framework\Forms\OrderMeta;
use Storeplugin\PDFInvoice\Framework\Fields\FieldValidator;

/**
 * Register WooCommerce order meta REST schemas for framework order metaboxes.
 *
 * WooCommerce order endpoints do not consume WordPress' registered post-meta
 * schema directly, so order metaboxes expose a dedicated typed `meta` object
 * on the order REST resource instead.
 */
final class OrderMetaRegistrar
{
    /**
     * Registered order metabox instances keyed by object hash.
     *
     * @var array<string, OrderMeta>
     */
    private static $registeredMetaBoxes = [];

    /**
     * Whether the WooCommerce pre-insert filter has been added.
     *
     * @var bool
     */
    private static $restInsertFilterRegistered = false;

    /**
     * Whether the WooCommerce response filter has been added.
     *
     * @var bool
     */
    private static $restResponseFilterRegistered = false;

    /**
     * Register the order metabox for WooCommerce REST exposure.
     *
     * @param OrderMeta $metaBox
     * @return void
     */
    public static function register(OrderMeta $metaBox): void
    {
        $objectHash = \spl_object_hash($metaBox);
        if (isset(self::$registeredMetaBoxes[$objectHash])) {
            return;
        }

        self::assertUniqueFieldIds($metaBox);
        self::$registeredMetaBoxes[$objectHash] = $metaBox;

        if (!self::$restInsertFilterRegistered) {
            self::$restInsertFilterRegistered = true;
            \add_filter(
                'woocommerce_rest_pre_insert_shop_order_object',
                [self::class, 'prepareOrderForRestInsert'],
                10,
                3
            );
        }

        if (!self::$restResponseFilterRegistered) {
            self::$restResponseFilterRegistered = true;
            \add_filter(
                'woocommerce_rest_prepare_shop_order_object',
                [self::class, 'filterOrderResponse'],
                10,
                3
            );
        }

        \register_rest_field('shop_order', 'meta', [
            'get_callback' => [self::class, 'getMetaForResponse'],
            'schema' => self::buildRestFieldSchema(),
        ]);
    }

    /**
     * Return registered order meta values for the REST response.
     *
     * @param array|object $preparedOrder
     * @param string       $fieldName
     * @param mixed        $request
     * @param string       $objectType
     * @return array<string, mixed>
     */
    public static function getMetaForResponse($preparedOrder, string $fieldName = '', $request = null, string $objectType = ''): array
    {
        unset($fieldName, $request, $objectType);

        $order = self::resolveOrderFromResponseInput($preparedOrder);
        if ($order === null) {
            return [];
        }

        $values = [];
        foreach (self::getRegisteredFields() as $metaKey => $registeredField) {
            $exists = method_exists($order, 'meta_exists')
                ? $order->meta_exists($metaKey)
                : \metadata_exists('post', $order->get_id(), $metaKey);

            if (!$exists) {
                continue;
            }

            $values[$metaKey] = $order->get_meta($metaKey, true);
        }

        return $values;
    }

    /**
     * Apply submitted framework order meta before WooCommerce saves the order.
     *
     * @param object            $order
     * @param \WP_REST_Request  $request
     * @param bool              $creating
     * @return object|\WP_Error
     */
    public static function prepareOrderForRestInsert($order, \WP_REST_Request $request, bool $creating)
    {
        if (!is_object($order) || !method_exists($order, 'get_id') || !method_exists($order, 'update_meta_data')) {
            return $order;
        }

        $registeredFields = self::getRegisteredFields();
        $submittedMeta = self::normalizeSubmittedMeta($request, $registeredFields);

        if ($submittedMeta instanceof \WP_Error) {
            return $submittedMeta;
        }

        if ($submittedMeta === []) {
            return $order;
        }

        $groupedMetaInput = self::groupSubmittedMetaByOwner($submittedMeta, $registeredFields);
        if ($groupedMetaInput === []) {
            return $order;
        }

        $orderId = (int) $order->get_id();
        $payloads = [];
        $validationErrors = [];

        foreach ($groupedMetaInput as $entry) {
            /** @var OrderMeta $metaBox */
            $metaBox = $entry['meta_box'];

            FieldValidator::clearErrors();
            $payload = $metaBox->buildPreparedMetaSavePayload(
                $orderId,
                $entry['input'],
                $creating
            );

            if (FieldValidator::hasErrors()) {
                $validationErrors = array_merge($validationErrors, FieldValidator::getErrors());
                continue;
            }

            $payloads[] = $payload;
        }

        FieldValidator::clearErrors();

        if ($validationErrors !== []) {
            return new \WP_Error(
                'starter_rest_order_meta_validation',
                'Order meta validation failed.',
                [
                    'status' => 400,
                    'fields' => $validationErrors,
                ]
            );
        }

        foreach ($payloads as $payload) {
            foreach ($payload as $metaKey => $preparedValue) {
                $value = is_array($preparedValue) && array_key_exists('value', $preparedValue)
                    ? $preparedValue['value']
                    : null;
                $order->update_meta_data($metaKey, $value);
            }
        }

        return $order;
    }

    /**
     * Remove framework-managed keys from WooCommerce's generic `meta_data`.
     *
     * De-duplicate only when the typed top-level `meta` field is already part
     * of the prepared response. If a client explicitly requests only native
     * WooCommerce `meta_data`, keep those entries visible for compatibility.
     *
     * @param \WP_REST_Response $response
     * @param object            $order
     * @param \WP_REST_Request  $request
     * @return \WP_REST_Response
     */
    public static function filterOrderResponse(\WP_REST_Response $response, $order, \WP_REST_Request $request): \WP_REST_Response
    {
        unset($order, $request);

        $data = $response->get_data();
        if (!is_array($data) || !isset($data['meta_data']) || !is_array($data['meta_data'])) {
            return $response;
        }

        if (!array_key_exists('meta', $data)) {
            return $response;
        }

        $registeredKeys = array_fill_keys(array_keys(self::getRegisteredFields()), true);
        if ($registeredKeys === []) {
            return $response;
        }

        $data['meta_data'] = array_values(array_filter($data['meta_data'], static function ($entry) use ($registeredKeys): bool {
            $metaKey = self::resolveMetaDataEntryKey($entry);

            return $metaKey === '' || !isset($registeredKeys[$metaKey]);
        }));

        $response->set_data($data);

        return $response;
    }

    /**
     * Build the aggregate schema for the order REST `meta` object.
     *
     * @return array<string, mixed>
     */
    private static function buildRestFieldSchema(): array
    {
        $properties = [];

        foreach (self::getRegisteredFields() as $metaKey => $registeredField) {
            $properties[$metaKey] = FieldSchemaFactory::buildForField($registeredField['field']);
        }

        return [
            'description' => 'Registered framework order meta values.',
            'type' => 'object',
            'context' => ['view', 'edit'],
            'properties' => $properties,
            'additionalProperties' => false,
        ];
    }

    /**
     * Flatten all registered order metabox fields into a single keyed map.
     *
     * @return array<string, array{meta_box:OrderMeta, field:array}>
     */
    private static function getRegisteredFields(): array
    {
        $fields = [];

        foreach (self::$registeredMetaBoxes as $metaBox) {
            foreach ($metaBox->restFields() as $field) {
                if (!is_array($field)) {
                    continue;
                }

                $metaKey = \sanitize_key((string) ($field['id'] ?? ''));
                if ($metaKey === '') {
                    continue;
                }

                $fields[$metaKey] = [
                    'meta_box' => $metaBox,
                    'field' => $field,
                ];
            }
        }

        return $fields;
    }

    /**
     * Fail fast when multiple order metaboxes reuse the same REST field id.
     *
     * @param OrderMeta $metaBox
     * @return void
     */
    private static function assertUniqueFieldIds(OrderMeta $metaBox): void
    {
        $knownIds = [];

        foreach (self::getRegisteredFields() as $metaKey => $registeredField) {
            $knownIds[$metaKey] = $registeredField['meta_box'];
        }

        foreach ($metaBox->restFields() as $field) {
            if (!is_array($field)) {
                continue;
            }

            $metaKey = \sanitize_key((string) ($field['id'] ?? ''));
            if ($metaKey === '') {
                continue;
            }

            if (isset($knownIds[$metaKey])) {
                if (\function_exists('_doing_it_wrong')) {
                    \_doing_it_wrong(
                        __METHOD__,
                        \sprintf(
                            'Duplicate order meta field id "%s" detected. Use a unique field id across OrderMeta builders.',
                            esc_html($metaKey)
                        ),
                        esc_html('2.0.1')
                    );
                }

                throw new \InvalidArgumentException(
                    \sprintf(
                        'Order meta field id "%s" is already registered. Use a unique field id across OrderMeta builders.',
                        esc_html($metaKey)
                    )
                );
            }

            $knownIds[$metaKey] = $metaBox;
        }
    }

    /**
     * Normalize framework order meta from either `meta` or Woo `meta_data`.
     *
     * `meta` is the typed framework contract. `meta_data` remains supported for
     * compatibility with native WooCommerce order writes.
     *
     * @param \WP_REST_Request                                   $request
     * @param array<string, array{meta_box:OrderMeta, field:array}> $registeredFields
     * @return array<string, mixed>|\WP_Error
     */
    private static function normalizeSubmittedMeta(\WP_REST_Request $request, array $registeredFields)
    {
        $metaInput = self::normalizeMetaObjectInput($request->get_param('meta'), $registeredFields);
        if ($metaInput instanceof \WP_Error) {
            return $metaInput;
        }

        $metaDataInput = self::normalizeMetaDataInput($request->get_param('meta_data'), $registeredFields);
        if ($metaDataInput instanceof \WP_Error) {
            return $metaDataInput;
        }

        $conflictingKeys = array_intersect(array_keys($metaInput), array_keys($metaDataInput));
        if ($conflictingKeys !== []) {
            $metaKey = (string) reset($conflictingKeys);

            return new \WP_Error(
                'starter_rest_order_meta_conflict',
                \sprintf(
                    'Order meta field "%s" was submitted in both meta and meta_data. Use one payload shape per field.',
                    $metaKey
                ),
                ['status' => 400]
            );
        }

        return $metaInput + $metaDataInput;
    }

    /**
     * Normalize the typed `meta` object payload.
     *
     * @param mixed                                              $metaInput
     * @param array<string, array{meta_box:OrderMeta, field:array}> $registeredFields
     * @return array<string, mixed>|\WP_Error
     */
    private static function normalizeMetaObjectInput($metaInput, array $registeredFields)
    {
        if ($metaInput === null) {
            return [];
        }

        if (!is_array($metaInput)) {
            return new \WP_Error(
                'starter_rest_order_meta_invalid',
                'Order meta must be an object.',
                ['status' => 400]
            );
        }

        $normalized = [];

        foreach ($metaInput as $metaKey => $value) {
            $normalizedKey = \sanitize_key((string) $metaKey);

            if ($normalizedKey === '' || !isset($registeredFields[$normalizedKey])) {
                return new \WP_Error(
                    'starter_rest_order_meta_unknown',
                    \sprintf('Order meta field "%s" is not registered.', $normalizedKey),
                    ['status' => 400]
                );
            }

            if (array_key_exists($normalizedKey, $normalized)) {
                return new \WP_Error(
                    'starter_rest_order_meta_conflict',
                    \sprintf(
                        'Order meta field "%s" was submitted more than once in meta.',
                        $normalizedKey
                    ),
                    ['status' => 400]
                );
            }

            $normalized[$normalizedKey] = $value;
        }

        return $normalized;
    }

    /**
     * Normalize framework-managed entries from WooCommerce `meta_data`.
     *
     * Unknown meta keys remain WooCommerce's responsibility and are ignored
     * here so the native controller can keep handling generic order meta.
     *
     * @param mixed                                              $metaDataInput
     * @param array<string, array{meta_box:OrderMeta, field:array}> $registeredFields
     * @return array<string, mixed>|\WP_Error
     */
    private static function normalizeMetaDataInput($metaDataInput, array $registeredFields)
    {
        if ($metaDataInput === null) {
            return [];
        }

        if (!is_array($metaDataInput)) {
            return new \WP_Error(
                'starter_rest_order_meta_data_invalid',
                'Order meta_data must be an array.',
                ['status' => 400]
            );
        }

        $normalized = [];

        foreach ($metaDataInput as $entry) {
            if (!is_array($entry)) {
                continue;
            }

            $metaKey = \sanitize_key((string) ($entry['key'] ?? ''));
            if ($metaKey === '' || !isset($registeredFields[$metaKey])) {
                continue;
            }

            if (array_key_exists($metaKey, $normalized)) {
                return new \WP_Error(
                    'starter_rest_order_meta_conflict',
                    \sprintf(
                        'Order meta field "%s" was submitted more than once in meta_data.',
                        $metaKey
                    ),
                    ['status' => 400]
                );
            }

            $normalized[$metaKey] = $entry['value'] ?? null;
        }

        return $normalized;
    }

    /**
     * Resolve the meta key from a WooCommerce `meta_data` response entry.
     *
     * @param mixed $entry
     * @return string
     */
    private static function resolveMetaDataEntryKey($entry): string
    {
        if (is_array($entry)) {
            return \sanitize_key((string) ($entry['key'] ?? ''));
        }

        if (is_object($entry)) {
            if (method_exists($entry, 'get_data')) {
                $entryData = $entry->get_data();

                return is_array($entryData)
                    ? \sanitize_key((string) ($entryData['key'] ?? ''))
                    : '';
            }

            if (isset($entry->key) && is_scalar($entry->key)) {
                return \sanitize_key((string) $entry->key);
            }
        }

        return '';
    }

    /**
     * Group submitted meta keys by the metabox instance that owns them.
     *
     * @param array<string, mixed>                                      $submittedMeta
     * @param array<string, array{meta_box:OrderMeta, field:array}>     $registeredFields
     * @return array<string, array{meta_box:OrderMeta, input:array<string, mixed>}>
     */
    private static function groupSubmittedMetaByOwner(array $submittedMeta, array $registeredFields): array
    {
        $groupedInput = [];

        foreach ($submittedMeta as $metaKey => $value) {
            $metaKey = \sanitize_key((string) $metaKey);
            if ($metaKey === '' || !isset($registeredFields[$metaKey])) {
                continue;
            }

            $metaBox = $registeredFields[$metaKey]['meta_box'];
            $ownerHash = \spl_object_hash($metaBox);

            if (!isset($groupedInput[$ownerHash])) {
                $groupedInput[$ownerHash] = [
                    'meta_box' => $metaBox,
                    'input' => [],
                ];
            }

            $groupedInput[$ownerHash]['input'][$metaKey] = $value;
        }

        return $groupedInput;
    }

    /**
     * Resolve a REST callback payload into a WooCommerce order object.
     *
     * @param mixed $preparedOrder
     * @return object|null
     */
    private static function resolveOrderFromResponseInput($preparedOrder)
    {
        if (is_object($preparedOrder)
            && method_exists($preparedOrder, 'get_id')
            && method_exists($preparedOrder, 'get_meta')
        ) {
            return $preparedOrder;
        }

        $orderId = is_array($preparedOrder) && isset($preparedOrder['id'])
            ? (int) $preparedOrder['id']
            : 0;

        return self::resolveOrderById($orderId);
    }

    /**
     * Resolve an order ID into a WooCommerce order object.
     *
     * @param int $orderId
     * @return object|null
     */
    private static function resolveOrderById(int $orderId)
    {
        if ($orderId <= 0 || !function_exists('wc_get_order')) {
            return null;
        }

        $order = \wc_get_order($orderId);

        return is_object($order) && method_exists($order, 'get_id') ? $order : null;
    }
}
