<?php
/**
 * Merges validated incoming editor settings into the current option payload.
 *
 * ## Adding or extending merge behavior
 *
 * 1. **Core option keys** — Add a default in `EditorSettingsService::defaultEditorSettingsSchema()` so
 *    the key exists in stored options, then add a private `apply*` method here and register it in
 *    `getCoreMergeSteps()`. Expose the field via the editor settings REST API if the UI should use it.
 *
 * 2. **Without forking this class** — Use filter `fleekdash_editor_settings_merge_steps`. It receives
 *    the ordered list of `callable(array $current, array $incoming): array` merge steps (same shape as
 *    core handlers) and this merger instance. You may append steps, wrap the list, or replace it; invalid
 *    entries are skipped. Returning a non-array or empty array falls back to the core steps only.
 *
 * @package FleekDash\Services
 */

namespace FleekDash\Services;

if (!defined('ABSPATH')) {
    exit;
}

final class EditorSettingsIncomingMerger
{
    /**
     * @var callable(string): bool
     */
    private $isSafeRelativeUrl;

    /**
     * @param callable(string): bool $isSafeRelativeUrl
     */
    public function __construct(callable $isSafeRelativeUrl)
    {
        $this->isSafeRelativeUrl = $isSafeRelativeUrl;
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    public function mergeInto(array $current, array $incoming): array
    {
        foreach ($this->resolveMergeSteps() as $step) {
            $current = $step($current, $incoming);
        }

        return $current;
    }

    /**
     * Ordered merge steps (core). Filter `fleekdash_editor_settings_merge_steps` may replace or extend.
     *
     * @return list<callable(array<string, mixed>, array<string, mixed>): array<string, mixed>>
     */
    private function resolveMergeSteps(): array
    {
        $core = $this->getCoreMergeSteps();
        $filtered = \apply_filters('fleekdash_editor_settings_merge_steps', $core, $this);
        if (!\is_array($filtered) || $filtered === []) {
            return $core;
        }
        $out = [];
        foreach ($filtered as $step) {
            if (\is_callable($step)) {
                $out[] = $step;
            }
        }

        return $out !== [] ? $out : $core;
    }

    /**
     * Built-in field merge handlers (order matters).
     *
     * @return list<callable(array<string, mixed>, array<string, mixed>): array<string, mixed>>
     */
    private function getCoreMergeSteps(): array
    {
        return [
            [$this, 'applyOpenBehavior'],
            [$this, 'applyDefaultEditor'],
            [$this, 'applyPerPostType'],
            [$this, 'applyDisableGutenberg'],
            [$this, 'applyDisableGutenbergPostTypes'],
            [$this, 'applyEditorOrder'],
            [$this, 'applyCustomEditors'],
        ];
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    private function applyOpenBehavior(array $current, array $incoming): array
    {
        if (!isset($incoming['open_behavior'])) {
            return $current;
        }
        $v = $incoming['open_behavior'];
        $current['open_behavior'] = \in_array($v, ['new_tab', 'fleek_window'], true) ? $v : 'fleek_window';

        return $current;
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    private function applyDefaultEditor(array $current, array $incoming): array
    {
        if (!isset($incoming['default_editor'])) {
            return $current;
        }
        $current['default_editor'] = \is_string($incoming['default_editor'])
            ? \sanitize_text_field($incoming['default_editor'])
            : $current['default_editor'];

        return $current;
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    private function applyPerPostType(array $current, array $incoming): array
    {
        if (!isset($incoming['per_post_type']) || !\is_array($incoming['per_post_type'])) {
            return $current;
        }
        $prev = \is_array($current['per_post_type'] ?? null) ? $current['per_post_type'] : [];
        $sanitized = $this->sanitizePerPostType($incoming['per_post_type'], $prev);
        $current['per_post_type'] = \array_replace($prev, $sanitized);

        return $current;
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    private function applyDisableGutenberg(array $current, array $incoming): array
    {
        if (!isset($incoming['disable_gutenberg'])) {
            return $current;
        }
        $current['disable_gutenberg'] = (bool) $incoming['disable_gutenberg'];

        return $current;
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    private function applyDisableGutenbergPostTypes(array $current, array $incoming): array
    {
        if (!isset($incoming['disable_gutenberg_post_types'])) {
            return $current;
        }
        $current['disable_gutenberg_post_types'] = $this->sanitizeDisableGutenbergPostTypeSlugs(
            $incoming['disable_gutenberg_post_types'],
        );

        return $current;
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    private function applyEditorOrder(array $current, array $incoming): array
    {
        if (!isset($incoming['editor_order']) || !\is_array($incoming['editor_order'])) {
            return $current;
        }
        $current['editor_order'] = $this->sanitizeStringArray($incoming['editor_order']);

        return $current;
    }

    /**
     * @param array<string, mixed> $current
     * @param array<string, mixed> $incoming
     * @return array<string, mixed>
     */
    private function applyCustomEditors(array $current, array $incoming): array
    {
        if (!isset($incoming['custom_editors']) || !\is_array($incoming['custom_editors'])) {
            return $current;
        }
        $current['custom_editors'] = $this->sanitizeCustomEditors($incoming['custom_editors']);

        return $current;
    }

    /**
     * @param mixed $arr
     * @return string[]
     */
    private function sanitizeStringArray($arr): array
    {
        if (!\is_array($arr)) {
            return [];
        }

        return \array_values(\array_filter(\array_map(function ($t) {
            return \is_string($t) ? \sanitize_text_field($t) : null;
        }, $arr)));
    }

    /**
     * Post type slugs that exist in this site (optionally allow '*' for disable-Gutenberg list).
     *
     * @param mixed $arr
     * @return string[]
     */
    private function sanitizeRegisteredPostTypeSlugs($arr, bool $allowWildcard): array
    {
        $strings = $this->sanitizeStringArray($arr);
        $out = [];
        foreach ($strings as $slug) {
            if ($allowWildcard && $slug === '*') {
                $out[] = $slug;
                continue;
            }
            if (\function_exists('post_type_exists') && \post_type_exists($slug)) {
                $out[] = $slug;
            }
        }

        return \array_values(\array_unique($out));
    }

    /**
     * @param mixed $arr
     * @return string[]
     */
    private function sanitizeDisableGutenbergPostTypeSlugs($arr): array
    {
        return $this->sanitizeRegisteredPostTypeSlugs($arr, true);
    }

    /**
     * @param array<int, array<string, mixed>> $customEditors
     * @return array<int, array{id: string, name: string, edit_url_template: string, post_types: string[]}>
     */
    private function sanitizeCustomEditors(array $customEditors): array
    {
        $sanitized = [];
        $check = $this->isSafeRelativeUrl;
        foreach ($customEditors as $ce) {
            if (!\is_array($ce) || empty($ce['id']) || empty($ce['edit_url_template'])) {
                continue;
            }
            $template = $ce['edit_url_template'];
            if (!$check($template)) {
                continue;
            }
            $sanitized[] = [
                'id' => \sanitize_text_field($ce['id']),
                'name' => \sanitize_text_field($ce['name'] ?? ''),
                'edit_url_template' => $template,
                'post_types' => $this->sanitizeRegisteredPostTypeSlugs($ce['post_types'] ?? [], false),
            ];
        }

        return $sanitized;
    }

    /**
     * @param mixed $input
     * @param array<string, array<string, mixed>> $previousByPt
     * @return array<string, array<string, mixed>>
     */
    private function sanitizePerPostType($input, array $previousByPt): array
    {
        $result = [];
        if (!\is_array($input)) {
            return $result;
        }
        foreach ($input as $ptKey => $cfg) {
            $postType = \is_string($ptKey) ? \sanitize_key($ptKey) : '';
            $prevRow = ($postType !== '' && isset($previousByPt[$postType]) && \is_array($previousByPt[$postType]))
                ? $previousByPt[$postType]
                : [];
            $row = $this->sanitizePerPostTypeRow($ptKey, $cfg, $prevRow);
            if ($row !== null) {
                $result[$row['key']] = $row['value'];
            }
        }

        return $result;
    }

    /**
     * @param mixed $ptKey
     * @param mixed $cfg
     * @param array<string, mixed> $previous
     * @return array{key: string, value: array<string, mixed>}|null
     */
    private function sanitizePerPostTypeRow($ptKey, $cfg, array $previous): ?array
    {
        if (!\is_string($ptKey) || !\is_array($cfg)) {
            return null;
        }
        $postType = \sanitize_key($ptKey);
        if ($postType === '' || !\function_exists('post_type_exists') || !\post_type_exists($postType)) {
            return null;
        }
        $prevDefault = isset($previous['default_editor']) && \is_string($previous['default_editor'])
            ? $previous['default_editor']
            : '';
        $default_editor = $prevDefault;
        if (isset($cfg['default_editor']) && \is_string($cfg['default_editor'])) {
            $default_editor = \sanitize_text_field($cfg['default_editor']);
        }
        $enabled_editors = \array_key_exists('enabled_editors', $cfg)
            ? $this->sanitizePerPostTypeEnabledEditors($cfg['enabled_editors'])
            : $this->sanitizePerPostTypeEnabledEditors($previous['enabled_editors'] ?? null);
        $quick_edit_enabled = \array_key_exists('quick_edit_enabled', $cfg)
            ? (bool) $cfg['quick_edit_enabled']
            : (bool) ($previous['quick_edit_enabled'] ?? false);

        return [
            'key' => $postType,
            'value' => [
                'default_editor' => $default_editor,
                'enabled_editors' => $enabled_editors,
                'quick_edit_enabled' => $quick_edit_enabled,
                'open_behavior' => $this->resolvePerPostTypeOpenBehavior($cfg, $previous),
                'listing_default_mode' => $this->resolveListingDefaultMode($cfg, $previous),
            ],
        ];
    }

    /**
     * Fleek posts list default: WP admin list vs table vs calendar.
     *
     * @param array<string, mixed> $cfg
     * @param array<string, mixed> $previous
     */
    private function resolveListingDefaultMode(array $cfg, array $previous): string
    {
        $allowed = ['wp-mode', 'grid', 'calendar'];
        $prev = '';
        if (isset($previous['listing_default_mode']) && \is_string($previous['listing_default_mode'])) {
            $p = $previous['listing_default_mode'];
            $prev = \in_array($p, $allowed, true) ? $p : '';
        }
        if (!isset($cfg['listing_default_mode'])) {
            return $prev;
        }
        $v = $cfg['listing_default_mode'];
        if (!\is_string($v)) {
            return $prev;
        }
        $v = \sanitize_text_field($v);

        return \in_array($v, $allowed, true) ? $v : $prev;
    }

    /**
     * @param array<string, mixed> $cfg
     * @param array<string, mixed> $previous
     */
    private function resolvePerPostTypeOpenBehavior(array $cfg, array $previous): string
    {
        $open_behavior = '';
        if (isset($previous['open_behavior']) && \is_string($previous['open_behavior'])) {
            $open_behavior = $previous['open_behavior'];
        }
        if (isset($cfg['open_behavior']) && \is_string($cfg['open_behavior'])) {
            $ob = $cfg['open_behavior'];
            $open_behavior = \in_array($ob, ['new_tab', 'fleek_window', ''], true) ? $ob : '';
        }

        return $open_behavior;
    }

    /**
     * @param mixed $raw
     * @return string[]
     */
    private function sanitizePerPostTypeEnabledEditors($raw): array
    {
        $enabled_editors = [];
        if (!\is_array($raw)) {
            return $enabled_editors;
        }
        foreach ($raw as $eid) {
            if (!\is_string($eid)) {
                continue;
            }
            $clean = \sanitize_text_field(\trim($eid));
            if ($clean !== '') {
                $enabled_editors[] = $clean;
            }
        }

        return \array_values(\array_unique($enabled_editors));
    }
}
