<?php
if (!defined('ABSPATH')) {
    exit;
}

class LLMS_Core {
    /** @var LLMS_Generator */
    private $generator;

    /**
     * Option holding the last generation's exclusion summary.
     */
    const EXCLUDED_OPTION = 'llms_last_excluded';

    /**
     * How many distinct reason lines the summary keeps.
     */
    const EXCLUDED_REASON_LIMIT = 60;

    /**
     * Post meta the site owner sets to assert that one post is publicly readable.
     *
     * Same string as LLMS_Access::INCLUDE_META, and the two have to agree.
     */
    const INCLUDE_META = '_llmstxt_force_include';

    /**
     * True when this request is a whole site generation run.
     *
     * @var bool
     */
    private $generation_run = false;

    /**
     * llms_last_generated as it was when this request was marked a generation run.
     *
     * @var int
     */
    private $generation_started_at = 0;

    public function __construct()
    {
        // NO register_activation_hook() here. See LLMS_Core::activate().

        add_action('wp_initialize_site', array($this, 'on_new_site'), 10, 1);

        add_action('init', array($this, 'init'), 0);

        add_action('admin_menu', array($this, 'add_admin_menu'));
        add_filter('plugin_action_links_' . plugin_basename(WEBSITE_LLMS_TXT_FILE), array($this, 'add_settings_link'));

        add_action('admin_post_clear_caches', array($this, 'handle_cache_clearing'));

        add_action('init', array($this, 'init_seo_integrations'), -1);

        add_action('admin_init', array($this, 'register_settings'));

        add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));

        add_action('wp_head', array($this, 'wp_head'));

        add_action('admin_notices', array($this, 'render_vk_banner'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_notice_script'));
        add_action('admin_enqueue_scripts', array($this, 'enqueue_banner_styles'));
        add_action('wp_ajax_dismiss_llms_vk_banner', array($this, 'dismiss_llms_vk_banner'));
        add_filter('redirect_canonical', array($this, 'redirect_canonical'), 10, 2);

        // Two arguments. add_meta_boxes passes the post type and the post.
        add_action('add_meta_boxes', array($this, 'add_include_meta_box'), 10, 2);
        add_action('save_post', array($this, 'save_include_meta'));

        add_filter('auth_post_meta_' . self::INCLUDE_META, array(__CLASS__, 'deny_meta_write'), PHP_INT_MAX, 6);

        // Both hooks are needed, not just the generic one. See deny_meta_write().
        foreach (get_post_types() as $llms_registered_type) {
            self::deny_meta_write_for_post_type($llms_registered_type);
        }
        add_action('registered_post_type', array(__CLASS__, 'deny_meta_write_for_post_type'), PHP_INT_MAX);

        // Priority 1 so the marker is set before the generator's own callback runs.
        foreach (self::generation_hooks() as $hook) {
            add_action($hook, array($this, 'mark_generation_run'), 1);
        }
        add_action('shutdown', array($this, 'record_excluded_summary'), 1);
    }

    /**
     * The actions that mean "a whole site generation pass is running in this request".
     *
     * @return string[]
     */
    private static function generation_hooks()
    {
        return array(
            'updates_all_posts',
            'llms_update_llms_file_cron',
            'llms_scheduled_update',
            'admin_post_run_manual_update_llms_file',
            'wp_ajax_run_llms_txt_reset_file',
            'wp_ajax_llms_update_file',
        );
    }

    /**
     * Remember that this request is a generation run.
     *
     * @return void
     */
    public function mark_generation_run()
    {
        $this->generation_run = true;
        $this->generation_started_at = (int) get_option('llms_last_generated');
    }

    /**
     * Write down what the run that just happened left out of the file.
     *
     * reasons() is read BEFORE the two state calls below it.
     *
     * @return void
     */
    public function record_excluded_summary()
    {
        if (!$this->generation_run) {
            return;
        }

        $this->generation_run = false;

        if (!class_exists('LLMS_Access')) {
            return;
        }

        $reasons = LLMS_Access::reasons();
        if (!is_array($reasons)) {
            $reasons = array();
        }

        // A run that neither finished nor recorded anything replaces nothing.
        $finished = ((int) get_option('llms_last_generated')) > $this->generation_started_at;
        if (!$finished && !$reasons && get_option(self::EXCLUDED_OPTION)) {
            return;
        }

        $summary = array(
            'time'       => time(),
            'precise'    => self::precise_access_enabled(),
            'site_gated' => (bool) LLMS_Access::site_is_gated(),
            'reasons'    => array_slice($reasons, 0, self::EXCLUDED_REASON_LIMIT),
            'truncated'  => (count($reasons) > self::EXCLUDED_REASON_LIMIT),
        );

        update_option(self::EXCLUDED_OPTION, $summary, false);
    }

    /**
     * Whether per post evaluation is turned on.
     *
     * Reads the same option key with the same default as LLMS_Access::precise_access().
     *
     * @return bool
     */
    public static function precise_access_enabled()
    {
        $settings = get_option('llms_generator_settings');

        return (is_array($settings) && !empty($settings['precise_access']));
    }

    /**
     * Turn a stored summary into rows a screen can print.
     *
     * @param array $summary Stored llms_last_excluded record.
     * @return array {
     *     @type array $types    [ post_type => [ label, count, detail ] ] excluded
     *     @type array $included [ post_type => count ] kept by the per post include
     *     @type array $notes    [ [ detail, count ] ] everything else
     * }
     */
    public static function excluded_summary_rows($summary)
    {
        $out = array('types' => array(), 'included' => array(), 'notes' => array());

        if (!is_array($summary) || empty($summary['reasons']) || !is_array($summary['reasons'])) {
            return $out;
        }

        foreach ($summary['reasons'] as $reason) {
            if (!is_array($reason)) {
                continue;
            }
            $code   = isset($reason['code']) ? (string) $reason['code'] : '';
            $detail = isset($reason['detail']) ? (string) $reason['detail'] : '';
            $count  = isset($reason['count']) ? (int) $reason['count'] : 0;

            $type = '';
            $rest = $detail;
            $split = strpos($detail, ': ');
            if (false !== $split) {
                $candidate = substr($detail, 0, $split);
                if (post_type_exists($candidate)) {
                    $type = $candidate;
                    $rest = substr($detail, $split + 2);
                }
            }

            if ('force_included' === $code && $type) {
                if (!isset($out['included'][$type])) {
                    $out['included'][$type] = 0;
                }
                $out['included'][$type] += $count;
                continue;
            }

            if ('post_type_gated' === $code && $type) {
                // Keyed by post type AND cause, not by post type alone.
                $key = $type . '|' . $rest;
                if (!isset($out['types'][$key])) {
                    $obj = get_post_type_object($type);
                    $out['types'][$key] = array(
                        'label'  => ($obj && isset($obj->labels->name)) ? $obj->labels->name : $type,
                        'count'  => 0,
                        'detail' => $rest,
                    );
                }
                $out['types'][$key]['count'] += $count;
                continue;
            }

            $out['notes'][] = array('detail' => $detail, 'count' => $count);
        }

        return $out;
    }

    /**
     * The capability that may assert a post is publicly readable.
     *
     * NOT edit_post, and the difference is the whole point.
     *
     * @param int $post_id
     * @return string
     */
    public static function include_capability($post_id = 0)
    {
        $cap = apply_filters('llms_include_capability', 'manage_options', (int) $post_id);

        return is_string($cap) && '' !== $cap ? $cap : 'manage_options';
    }

    /**
     * May the current user make the include assertion on this post?
     *
     * @param int $post_id
     * @return bool
     */
    public static function can_include($post_id)
    {
        $post_id = (int) $post_id;
        if (!$post_id) {
            return false;
        }

        if (!current_user_can('edit_post', $post_id)) {
            return false;
        }

        return current_user_can(self::include_capability($post_id), $post_id);
    }

    /**
     * Refuse every write to the include meta that goes through the meta capability API.
     *
     * Hooked at PHP_INT_MAX to BOTH hooks in map_meta_cap()'s auth chain for this key.
     *
     * @param bool   $allowed
     * @param string $meta_key
     * @param int    $object_id
     * @param int    $user_id
     * @param string $cap
     * @param array  $caps
     * @return bool
     */
    public static function deny_meta_write($allowed = false, $meta_key = '', $object_id = 0, $user_id = 0, $cap = '', $caps = array())
    {
        return false;
    }

    /**
     * Put the include-meta refusal on one post type's subtype auth hook.
     *
     * Hooked at PHP_INT_MAX for the same reason as deny_meta_write().
     *
     * @param string $post_type Post type name, as passed by the registered_post_type action.
     * @return void
     */
    public static function deny_meta_write_for_post_type($post_type)
    {
        if (!is_string($post_type) || '' === $post_type) {
            return;
        }

        add_filter(
            'auth_post_meta_' . self::INCLUDE_META . '_for_' . $post_type,
            array(__CLASS__, 'deny_meta_write'),
            PHP_INT_MAX,
            6
        );
    }

    /**
     * The per post include control, in the editor.
     *
     * PER POST, not per post type.
     *
     * @param string  $post_type
     * @param WP_Post $post
     * @return void
     */
    public function add_include_meta_box($post_type, $post = null)
    {
        if (!class_exists('LLMS_Access') || !is_object($post) || empty($post->ID)) {
            return;
        }

        if (!self::can_include($post->ID)) {
            return;
        }

        $settings = apply_filters('get_llms_generator_settings', array());
        $types    = isset($settings['post_types']) && is_array($settings['post_types']) ? $settings['post_types'] : array();
        $already  = ('1' === (string) get_post_meta($post->ID, self::INCLUDE_META, true));

        $reason = '';

        if (!$already) {
            if (!in_array($post_type, $types, true)) {
                return;
            }

            if ('publish' !== $post->post_status || '' !== $post->post_password) {
                return;
            }

            // The cheap exit, and it is the one that has to stay cheap.
            $gated = LLMS_Access::gated_post_types();
            $reason = isset($gated[$post_type]) ? $gated[$post_type] : '';
            if (!$gated && !LLMS_Access::site_is_gated() && !self::precise_access_enabled()) {
                return;
            }

            // Ask the class about this one post rather than about its post type.
            $verdict = LLMS_Access::filter_publicly_readable(array((int) $post->ID));
            if (!empty($verdict[(int) $post->ID])) {
                return;
            }
        }

        add_meta_box(
            'llms_include',
            __('Llms.txt access', 'website-llms-txt'),
            array($this, 'render_include_meta_box'),
            $post_type,
            'side',
            'default',
            array('reason' => $reason)
        );
    }

    /**
     * Print the per post include control.
     *
     * @param WP_Post $post
     * @param array   $box add_meta_box callback args
     * @return void
     */
    public function render_include_meta_box($post, $box = array())
    {
        if (!is_object($post) || empty($post->ID) || !self::can_include($post->ID)) {
            return;
        }

        $included = ('1' === (string) get_post_meta($post->ID, self::INCLUDE_META, true));
        $reason   = isset($box['args']['reason']) ? (string) $box['args']['reason'] : '';

        $deterministic = '';
        if ('publish' !== $post->post_status) {
            $deterministic = __('This post is not published, so it is left out of llms.txt whatever this setting says.', 'website-llms-txt');
        } elseif ('' !== $post->post_password) {
            $deterministic = __('This post is password protected, so it is left out of llms.txt whatever this setting says.', 'website-llms-txt');
        }

        wp_nonce_field('llms_include_' . $post->ID, 'llms_include_nonce');

        if ($deterministic) {
            echo '<p><strong>' . esc_html($deterministic) . '</strong></p>';
        } elseif ($included) {
            echo '<p><strong>' . esc_html__('This post is in llms.txt because you said it is public.', 'website-llms-txt') . '</strong></p>';
        } elseif ($reason) {
            echo '<p>' . esc_html($reason) . '</p>';
        } else {
            echo '<p>' . esc_html__('This post is being left out of llms.txt.', 'website-llms-txt') . '</p>';
        }
        ?>
        <p>
            <label>
                <input type="checkbox" name="llms_force_include" value="1" <?php checked($included); ?> />
                <?php esc_html_e('Anyone can read this post without logging in. Include it in llms.txt.', 'website-llms-txt'); ?>
            </label>
        </p>
        <p class="description">
            <?php esc_html_e('llms.txt is a public file. Ticking this is your statement that this post is public too, and the plugin takes your word for it instead of leaving the post out.', 'website-llms-txt'); ?>
        </p>
        <?php
    }

    /**
     * Save the per post include assertion.
     *
     * The capability is can_include(), not edit_post.
     *
     * @param int $post_id
     * @return void
     */
    public function save_include_meta($post_id)
    {
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return;
        }

        if (!isset($_POST['llms_include_nonce'])) {
            return;
        }

        if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['llms_include_nonce'])), 'llms_include_' . $post_id)) {
            return;
        }

        if (!self::can_include($post_id)) {
            return;
        }

        if (!empty($_POST['llms_force_include'])) {
            update_post_meta($post_id, self::INCLUDE_META, '1');
        } else {
            delete_post_meta($post_id, self::INCLUDE_META);
        }
    }

    public function enqueue_banner_styles() {
        wp_enqueue_style('llms-banner-styles', WEBSITE_LLMS_TXT_URL . 'admin/admin-banner.css', array(), WEBSITE_LLMS_TXT_VERSION);
    }

    public function render_vk_banner() {
        if (!current_user_can('manage_options')) {
            return;
        }
        if (get_user_meta(get_current_user_id(), 'llms_vk_banner_dismissed', true)) {
            return;
        }
        if (get_option('vk_embed_token')) {
            return;
        }

        $banner_href = admin_url('tools.php?page=llms-file-manager');
        ?>
        <div class="notice is-dismissible llms-vk-banner">
            <p class="vk-banner-text">
                <?php esc_html_e('Are AI platforms visiting your site?', 'website-llms-txt'); ?>
                <a href="<?php echo esc_url($banner_href); ?>" class="vk-banner-link"><?php esc_html_e('See your tracking data', 'website-llms-txt'); ?> &rarr;</a>
            </p>
        </div>
        <?php
    }

    /**
     * Stop core's canonical redirect from rewriting a request for the llms.txt path.
     *
     * strpos(), NOT str_contains().
     *
     * @param string|false $redirect_url  The URL core wants to redirect to, or false if
     *                                    a callback before us already cancelled it.
     * @param string       $requested_url The URL that was requested. Unused, kept because
     *                                    the filter is registered with two arguments.
     * @return string|false false cancels the redirect.
     */
    public function redirect_canonical($redirect_url, $requested_url)
    {
        if (!is_string($redirect_url) || '' === $redirect_url) {
            return $redirect_url;
        }

        $redirect = wp_parse_url($redirect_url);
        if (!is_array($redirect) || !isset($redirect['path'])) {
            return $redirect_url;
        }

        $ll_redirect_path = strtolower($redirect['path']);
        if (false !== strpos($ll_redirect_path, 'llms')) {
            return false;
        }

        return $redirect_url;
    }

    public function dismiss_llms_vk_banner() {
        check_ajax_referer('llms_dismiss_notice', 'nonce');
        update_user_meta(get_current_user_id(), 'llms_vk_banner_dismissed', 1);
        wp_send_json_success();
    }

    public function enqueue_notice_script() {
        wp_enqueue_script('llms-notice-script', WEBSITE_LLMS_TXT_URL . 'admin/notice-dismiss.js', array('jquery'), WEBSITE_LLMS_TXT_VERSION, true);
        wp_localize_script('llms-notice-script', 'llmsNoticeAjax', array(
            'ajax_url' => admin_url('admin-ajax.php'),
            'nonce'    => wp_create_nonce('llms_dismiss_notice')
        ));
    }

    public function wp_head() {
        echo '<link rel="llms-sitemap" href="' . esc_url( home_url( '/llms.txt' ) ) . '" />' . "\n";
    }

    public function get_llms_post() {
        $posts = get_posts(array(
            'post_type' => 'llms_txt',
            'posts_per_page' => 1,
            'post_status' => 'publish'
        ));

        return !empty($posts) ? $posts[0] : null;
    }

    public function init() {
        $this->create_post_type();
        require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-generator.php';
        $this->generator = new LLMS_Generator();

        $this->add_rewrite_rule();
        add_filter('query_vars', array($this, 'add_query_vars'));
        add_action('template_redirect', array($this, 'handle_llms_request'));
    }

    public function create_post_type() {
        register_post_type('llms_txt', array(
            'public' => false,
            'publicly_queryable' => false,
            'show_ui' => false,
            'show_in_menu' => false,
            'show_in_admin_bar' => false,
            'show_in_nav_menus' => false,
            'show_in_rest' => false,
            'rewrite' => false,
            'capability_type' => 'post',
            'has_archive' => false,
            'hierarchical' => false,
            'supports' => array('title', 'editor'),
            'exclude_from_sitemap' => true
        ));
    }

    public function init_seo_integrations() {
        if (class_exists('RankMath')) {
            require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-provider.php';
            require_once WEBSITE_LLMS_TXT_DIR . 'includes/rank-math.php';
        }

        if (defined('WPSEO_VERSION') && class_exists('WPSEO_Sitemaps')) {
            require_once WEBSITE_LLMS_TXT_DIR . 'includes/yoast.php';
        }

        if (defined('SLIM_SEO_VER')) {
            require_once WEBSITE_LLMS_TXT_DIR . 'includes/slim-seo.php';
        }

        if (function_exists('aioseo')) {
            require_once WEBSITE_LLMS_TXT_DIR . 'includes/aioseo.php';
        }
    }

    public function register_settings() {
        register_setting(
            'llms_generator_settings',
            'llms_generator_settings',
            array(
                'type' => 'array',
                'sanitize_callback' => array($this, 'sanitize_settings'),
                'default' => array(
                    'post_types' => array('page', 'documentation', 'post'),
                    'post_name' => array(),
                    'max_posts' => 100,
                    'max_words' => 250,
                    'include_meta' => false,
                    'include_excerpts' => false,
                    'include_taxonomies' => false,
                    'gform_include' => false,
                    'update_frequency' => 'immediate',
                    'need_check_option' => true,
                    'llms_allow_indexing' => false,
                    'noindex_header' => false,
                    'include_md_file' => false,
                    'detailed_content' => false,
                    'precise_access' => false,
                    'llms_txt_title' => '',
                    'llms_txt_description' => '',
                    'llms_after_txt_description' => '',
                    'llms_end_file_description' => ''
                )
            )
        );
    }

    public function sanitize_settings($value) {
        global $wpdb;
        if (!is_array($value)) {
            return array();
        }
        $clean = array();

        $settings = $this->generator->get_llms_generator_settings();
        
        $clean['post_types'] = array();
        if (isset($value['post_types']) && is_array($value['post_types'])) {
            $valid_types = get_post_types(array('public' => true));
            foreach ($value['post_types'] as $type) {
                if (in_array($type, $valid_types) && $type !== 'attachment' && $type !== 'llms_txt') {
                    $clean['post_types'][] = sanitize_key($type);
                }
            }
        }
        
        $clean['post_name'] = array();
        if (isset($value['post_name']) && is_array($value['post_name'])) {
            foreach ($value['post_name'] as $name => $custom_name) {
                $clean['post_name'][$name] = sanitize_text_field($custom_name);
            }
        }

        $clean['max_posts'] = isset($value['max_posts']) ? 
            min(max(absint($value['max_posts']), 1), 100000) : 100;

        $clean['max_words'] = isset($value['max_words']) ?
            min(max(absint($value['max_words']), 1), 100000) : 250;
        
        $clean['llms_allow_indexing'] = !empty($value['llms_allow_indexing']);
        $clean['include_meta'] = !empty($value['include_meta']);
        $clean['noindex_header'] = !empty($value['noindex_header']);
        $clean['include_excerpts'] = !empty($value['include_excerpts']);
        $clean['include_taxonomies'] = !empty($value['include_taxonomies']);
        $clean['gform_include'] = !empty($value['gform_include']);
        $clean['llms_txt_title'] = !isset($value['llms_txt_title']) ? '' : $value['llms_txt_title'];
        $clean['llms_txt_description'] = !isset($value['llms_txt_description']) ? '' : $value['llms_txt_description'];
        $clean['llms_after_txt_description'] = !isset($value['llms_after_txt_description']) ? '' : $value['llms_after_txt_description'];
        $clean['llms_end_file_description'] = !isset($value['llms_end_file_description']) ? '' : $value['llms_end_file_description'];
        $clean['include_md_file'] = !empty($value['include_md_file']);
        $clean['detailed_content'] = !empty($value['detailed_content']);

        // Deliberately NOT in the truncate comparison below.
        $clean['precise_access'] = !empty($value['precise_access']);

        if(
            ($clean['include_excerpts'] != $settings['include_excerpts']) ||
            ($clean['include_md_file'] != $settings['include_md_file']) ||
            ($clean['include_taxonomies'] != $settings['include_taxonomies']) ||
            ($clean['detailed_content'] != $settings['detailed_content']) ||
            ($clean['include_meta'] != $settings['include_meta'])
        ) {
            $table_cache = $wpdb->prefix . 'llms_txt_cache';
            $wpdb->query("TRUNCATE " . $table_cache);
        }

        $clean['update_frequency'] = isset($value['update_frequency']) && 
            in_array($value['update_frequency'], array('immediate', 'daily', 'weekly')) ? 
            sanitize_key($value['update_frequency']) : 'immediate';

        // Everything this method did not name. Preserved, not trusted.
        foreach ($value as $key => $unknown) {
            if (array_key_exists($key, $clean)) {
                continue;
            }
            $key  = is_string($key) ? $key : (string) $key;
            $kept = self::sanitize_unknown_setting($unknown);
            if (null !== $kept) {
                $clean[$key] = $kept;
            }
        }

        return $clean;
    }

    /**
     * Sanitise a settings value this class has no schema for.
     *
     * @param mixed $value
     * @param int   $depth
     * @return mixed|null Null means do not store this.
     */
    private static function sanitize_unknown_setting($value, $depth = 0)
    {
        if ($depth > 5) {
            return null;
        }

        if (is_array($value)) {
            $out = array();
            foreach ($value as $k => $v) {
                $kept = self::sanitize_unknown_setting($v, $depth + 1);
                if (null !== $kept) {
                    $out[is_string($k) ? $k : (string) $k] = $kept;
                }
            }
            return $out;
        }

        if (is_bool($value) || is_int($value) || is_float($value)) {
            return $value;
        }

        if (is_string($value)) {
            return sanitize_textarea_field($value);
        }

        return null;
    }

    public function enqueue_admin_scripts($hook) {
        if (!in_array($hook, ['tools_page_llms-file-manager', 'toplevel_page_llms-file-manager'])) {
            return;
        }

        wp_enqueue_script('jquery-ui-sortable');

        wp_enqueue_style('llms-admin-styles', WEBSITE_LLMS_TXT_URL . 'admin/admin-styles.css', array('dashicons'), WEBSITE_LLMS_TXT_VERSION);
        wp_enqueue_script('llms-admin-script', WEBSITE_LLMS_TXT_URL . 'admin/admin-script.js', array('jquery', 'jquery-ui-sortable'), WEBSITE_LLMS_TXT_VERSION, true);
        wp_localize_script('llms-admin-script', 'LLMS_GEN', [
            'nonce' => wp_create_nonce('llms_gen_nonce'),
        ]);
        wp_localize_script('llms-admin-script', 'LLMS_VK', [
            'nonce' => wp_create_nonce('vk_connect_nonce'),
        ]);
    }

    /**
     * Activation. Register the llms.txt rewrite rule, then flush.
     *
     * STATIC, and registered from file scope in website-llms-txt.php, not from this
     * constructor.
     *
     * @param bool $network_wide True when the plugin was activated for the whole network.
     * @return void
     */
    public static function activate($network_wide = false) {
        if (is_multisite() && $network_wide) {
            // 'number' => 0 is required, not tidy-up.
            foreach (get_sites(array('fields' => 'ids', 'number' => 0)) as $blog_id) {
                switch_to_blog($blog_id);
                self::invalidate_rewrite_rules();
                restore_current_blog();
            }

            return;
        }

        self::add_rewrite_rule();
        flush_rewrite_rules();
    }

    /**
     * Make this blog rebuild its rewrite rules on its next request.
     *
     * DELETE THE OPTION. DO NOT FLUSH FROM INSIDE A switch_to_blog().
     * See docs/internals-core-and-lock.md.
     *
     * @return void
     */
    private static function invalidate_rewrite_rules()
    {
        delete_option('rewrite_rules');
    }

    public function on_new_site($new_site) {
        if (!function_exists('is_plugin_active_for_network')) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        if (!is_plugin_active_for_network(plugin_basename(WEBSITE_LLMS_TXT_FILE))) {
            return;
        }
        // Delete rather than flush. See invalidate_rewrite_rules().
        switch_to_blog($new_site->blog_id);
        self::invalidate_rewrite_rules();
        restore_current_blog();
    }

    public function add_admin_menu() {
        add_submenu_page(
            'tools.php',
            'Llms.txt',
            'Llms.txt',
            'manage_options',
            'llms-file-manager',
            array($this, 'render_admin_page')
        );
    }

    public function add_settings_link($links) {
        // The page is registered under tools.php (see add_admin_menu).
        $settings_link = '<a href="' . esc_url(admin_url('tools.php?page=llms-file-manager')) . '">' . __('Settings', 'website-llms-txt') . '</a>';
        array_unshift($links, $settings_link);
        return $links;
    }

    public function render_admin_page() {
        include WEBSITE_LLMS_TXT_DIR . 'admin/admin-page.php';
    }

    public function handle_cache_clearing() {
        if (!current_user_can('manage_options')) {
            wp_die('Unauthorized');
        }

        check_admin_referer('clear_caches', 'clear_caches_nonce');
        set_time_limit(0);
        do_action('llms_clear_seo_caches');
        $this->add_rewrite_rule();
        flush_rewrite_rules();

        if ($this->generator) {
            $this->mark_generation_run();
            $this->generator->update_llms_file();
        }


        // tools.php, not admin.php.
        wp_safe_redirect(add_query_arg(array(
            'page' => 'llms-file-manager',
            'cache_cleared' => 'true',
            '_wpnonce' => wp_create_nonce('llms_cache_cleared')
        ), admin_url('tools.php')));
        exit;
    }

    /**
     * Register the llms.txt rewrite rule on the current blog.
     *
     * @return void
     */
    public static function add_rewrite_rule() {
        global $wp_rewrite;

        if($wp_rewrite) {
            $wp_rewrite->add_rule('llms.txt', 'index.php?llms_txt=1', 'top');
        }
    }

    public function add_query_vars($vars) {
        $vars[] = 'llms_txt';
        return $vars;
    }

    public function handle_llms_request() {
        $settings = apply_filters('get_llms_generator_settings', []);
        $raw_uri = isset($_SERVER['REQUEST_URI']) ? esc_url_raw(wp_unslash($_SERVER['REQUEST_URI'])) : '';
        $request_uri = $raw_uri ? trim(wp_parse_url($raw_uri, PHP_URL_PATH), '/') : '';

        if ($request_uri === 'llms.txt') {
            $disable_noindex = $settings['noindex_header'] ?? '';
            if ( !$disable_noindex ) {
                header('X-Robots-Tag: noindex');
            }
        }

        if (get_query_var('llms_txt')) {
            $latest_post = apply_filters('get_llms_content', '');
            if ($latest_post) {
                header('Content-Type: text/plain; charset=utf-8');
                // nosniff, not escaping, is the control on this body: it has to go out
                // byte for byte to match the file the web server serves when one exists.
                header('X-Content-Type-Options: nosniff');
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- text/plain body, see docs/internals-core-and-lock.md.
                echo $latest_post;
                exit;
            }
        }
    }
}