<?php

namespace GravityKit\GravityExport\Exporting;

use GFExcel\Routing\Router;
use GravityKit\GravityExport\Feature;
use GravityKit\GravityExport\Routing\GravityExportRouter;

/**
 * Sets a custom export endpoint.
 *
 * @since 1.5.0
 */
final class CustomExportEndpoint extends Feature
{
    /**
     * The name of the setting.
     *
     * @since 1.5.0
     */
    private const SETTING_CUSTOM_ENDPOINT = 'custom-endpoint';

    /**
     * @inheritDoc
     * @since 1.5.0
     */
    protected function init(): void
    {
        if ( ! interface_exists( Router::class ) ) {
            return;
        }

        add_filter(
            'gk/gravityexport/router/default_endpoint',
            \Closure::fromCallable( [ $this, 'set_default_endpoint' ] ),
            0, // Execute before other filters.
        );

        add_filter( 'gk/gravityexport/settings/sections', \Closure::fromCallable( [ $this, 'add_settings' ] ), 10, 2 );
    }

    /**
     * Adds the settings for the appropriate section.
     *
     * @since 1.5.0
     *
     * @param array $sections The original sections.
     *
     * @return array The updated sections.
     */
    private function add_settings( array $sections ): array
    {
        foreach ( $sections as $i => $section ) {
            if ( 'general-section' !== ( $section[ 'id' ] ?? null ) ) {
                continue;
            }

            $endpoint = $this->addon->get_plugin_setting(
                self::SETTING_CUSTOM_ENDPOINT
            ) ?: GravityExportRouter::DEFAULT_ACTION;

            $base_url = str_replace( 'https://', '', get_site_url( null, '', 'https' ) );
            // Translators: [url] is replaced by the preview URL.
            $example_label = esc_html__( 'Example: [url]', 'gk-gravityexport' );
            // Translators: [slug] is replaced by the slug.
            $default_label = esc_html__( 'Default: [slug]', 'gk-gravityexport' );

            $sections[ $i ][ 'settings' ][] = [
                'title'       => esc_html__( 'Download slug', 'gk-gravityexport' ),
                'name'        => self::SETTING_CUSTOM_ENDPOINT,
                'id'          => self::SETTING_CUSTOM_ENDPOINT,
                'value'       => $this->addon->get_plugin_setting( self::SETTING_CUSTOM_ENDPOINT ),
                'description' => strtr(
                    implode(
                        '<br/><br/>',
                        [
                            $example_label,
                            esc_html__(
                                'The slug must be at least 3 characters, and cannot contain certain words. See documentation for more information.',
                                'gk-gravityexport'
                            ),
                        ],
                    ),
                    [
                        '[url]' => sprintf(
                            '%s/%s/' . md5( 'hash' ),
                            $base_url,
                            '<strong>' . $endpoint . '</strong>',
                        ),
                    ],
                ),
                'type'        => 'text',
                'placeholder' => strtr( $default_label, [ '[slug]' => GravityExportRouter::DEFAULT_ACTION ] ),
                'validation'  => $this->slug_validation(),
                'link'        => [
                    'title' => esc_html__( 'Read documentation', 'gk-gravityexport' ),
                    'url'   => 'https://docs.gravitykit.com/article/1064-customize-download-urls-in-gravityexport',
                ],
            ];
        }

        return $sections;
    }

    /**
     * Updates the endpoint.
     *
     * @since 1.5.0
     *
     * @param string $endpoint The endpoint.
     *
     * @return string The updated endpoint.
     */
    private function set_default_endpoint( string $endpoint ): string
    {
        return $this->addon->get_plugin_setting( self::SETTING_CUSTOM_ENDPOINT ) ?: $endpoint;
    }

    /**
     * Returns the validation for generic slugs, based on the current environment.
     *
     * @since 1.5.0
     *
     * @return array The validation rules.
     */
    private function slug_validation(): array
    {
        if ( ! $this->is_backend_validation() ) {
            return [
                [
                    'rule'    => 'matches:(^[a-zA-Z0-9_{}\-]*$)',
                    'message' => esc_html__(
                        'Only letters, numbers, underscores and dashes are allowed.',
                        'gk-gravityexport',
                    ),
                ],
                [
                    'rule'    => 'matches:(^$|(?:.*?[A-Za-z]){3})',
                    'message' => strtr(
                    // Translators: [count] is replaced by the amount of letters.
                        esc_html__( 'At least [count] letters are required.', 'gk-gravityexport' ),
                        [ '[count]' => 3 ],
                    ),
                ],
                [
                    'rule'    => 'matches:^(?!' . self::get_reserved_terms_regex_group() . '(\/|$)).*',
                    'message' => esc_html__( 'You have used a reserved word.', 'gk-gravityexport' ),
                ],
            ];
        }

        return [
            'rule' => function ( array $settings, ?string $value = null ) {
                if ( empty( $value ) && ! is_numeric( $value ) ) {
                    return true;
                }

                return self::validate_slug( $value, self::SETTING_CUSTOM_ENDPOINT === (string) ( $settings[ 'id' ] ?? '' ) );
            },
        ];
    }

    /**
     * Returns whether the current request is a backend validation.
     *
     * @since 1.5.0
     *
     * @return bool whether the current request is a backend validation.
     */
    private function is_backend_validation(): bool
    {
        return 'save_settings' === ( $_REQUEST[ 'ajaxRoute' ] ?? '' );
    }

    /**
     * Validates a slug.
     *
     * @since 1.5.0
     *
     * @param string $slug                   The slug to validate.
     * @param bool   $exclude_reserved_terms Whether to exclude reserved terms from the slug.
     *
     * @return bool Whether the provided slug is valid.
     */
    private static function validate_slug( string $slug, bool $exclude_reserved_terms = false ): bool
    {
        // Slug needs to be at least 3 characters.
        if ( strlen( $slug ) < 3 ) {
            return false;
        }

        if (
            $exclude_reserved_terms
            && preg_match( '/^' . self::get_reserved_terms_regex_group() . '(\/|$)/i', $slug )
        ) {
            return false;
        }

        return true;
    }

    /**
     * Returns a list of reserved WordPress terms {@see https://codex.wordpress.org/Reserved_Terms}.
     *
     * @since 1.5.0
     *
     * @return string[] The reserved terms.
     */
    private static function get_reserved_terms(): array
    {
        $reserved_terms = [
            'action',
            'attachment',
            'attachment_id',
            'author',
            'author_name',
            'calendar',
            'cat',
            'category',
            'category__and',
            'category__in',
            'category__not_in',
            'category_name',
            'comments_per_page',
            'comments_popup',
            'custom',
            'customize_messenger_channel',
            'customized',
            'cpage',
            'day',
            'debug',
            'embed',
            'error',
            'exact',
            'feed',
            'fields',
            'hour',
            'link_category',
            'm',
            'minute',
            'monthnum',
            'more',
            'name',
            'nav_menu',
            'nonce',
            'nopaging',
            'offset',
            'order',
            'orderby',
            'p',
            'page',
            'page_id',
            'paged',
            'pagename',
            'pb',
            'perm',
            'post',
            'post__in',
            'post__not_in',
            'post_format',
            'post_mime_type',
            'post_status',
            'post_tag',
            'post_type',
            'posts',
            'posts_per_archive_page',
            'posts_per_page',
            'preview',
            'robots',
            's',
            'search',
            'second',
            'sentence',
            'showposts',
            'static',
            'status',
            'subpost',
            'subpost_id',
            'tag',
            'tag__and',
            'tag__in',
            'tag__not_in',
            'tag_id',
            'tag_slug__and',
            'tag_slug__in',
            'taxonomy',
            'tb',
            'term',
            'terms',
            'theme',
            'title',
            'type',
            'types',
            'w',
            'withcomments',
            'withoutcomments',
            'year',
        ];

        /**
         * Modifies the list of reserved terms that are excluded from permalinks.
         *
         * @filter `gk/gravityexport/permalinks/reserved-terms`
         *
         * @since 1.5.0
         *
         * @param string[] $extra_reserved_terms List of extra reserved terms.
         * @param string[] $reserved_terms       The list of reserved WordPress terms.
         */
        $extra_reserved_terms = apply_filters( 'gk/gravityexport/permalinks/reserved-terms', [], $reserved_terms );
        $extra_reserved_terms = array_filter( $extra_reserved_terms, 'is_string' );

        // Using array_merge to avoid the ability to change the reserved terms of WordPress.
        return array_merge( $reserved_terms, $extra_reserved_terms );
    }

    /**
     * Returns imploded regex group that matched reserved terms.
     *
     * @since 1.5.0
     *
     * @return string The regex.
     */
    private static function get_reserved_terms_regex_group(): string
    {
        $reserved_terms = array_map(
            static function ( string $term ): string {
                return preg_quote( $term, '/' );
            },
            self::get_reserved_terms()
        );

        return '(' . implode( '|', $reserved_terms ) . ')';
    }
}
