<?php
/**
 * Plugin Name: Website LLMs.txt
 * Description: Generates and manages an llms.txt file, a structured, AI-ready index that helps large language models like ChatGPT, Claude, and Perplexity understand your site's most important content.
 * Version: 8.6.1
 * Author: Ryan Howard
 * Author URI: https://completeseo.com/author/ryan-howard/
 * Text Domain: website-llms-txt
 * Domain Path: /languages
 * Requires at least: 5.8
 * Requires PHP: 7.2
 * License: GPL v2 or later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 */

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

// Renamed from LLMS_VERSION, which collides with LifterLMS; see docs/internals-bootstrap-and-admin.md.
define('WEBSITE_LLMS_TXT_VERSION', '8.6.1');
define('LLMS_DB_VERSION', '3');

define('WEBSITE_LLMS_TXT_FILE', __FILE__);
define('WEBSITE_LLMS_TXT_DIR', plugin_dir_path(__FILE__));
define('WEBSITE_LLMS_TXT_URL', plugin_dir_url(__FILE__));

require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-db.php';
require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-lock.php';
require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-access.php';
require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-md.php';
require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-core.php';
require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-cache-manager.php';
require_once WEBSITE_LLMS_TXT_DIR . 'includes/class-llms-visibilitykit.php';

function llms_init() {
    new LLMS_MD();
    new LLMS_Core();
    new LLMS_Cache_Manager();
    new LLMS_VisibilityKit();
}

add_action('plugins_loaded', 'llms_init');

/**
 * Where the generated document can be served from.
 *
 * The scratch temp copy is deliberately absent; see docs/internals-bootstrap-and-admin.md.
 *
 * @return string[] Absolute paths, whether or not they exist.
 */
function llms_served_file_paths() {
    $paths = array();

    if (defined('FLYWHEEL_PLUGIN_DIR')) {
        $paths[] = trailingslashit(dirname(ABSPATH)) . 'www/llms.txt';
    } else {
        $paths[] = ABSPATH . 'llms.txt';
    }

    $domain = llms_generated_file_domain();
    if ('' !== $domain) {
        $upload_dir = wp_get_upload_dir();
        $paths[] = $upload_dir['basedir'] . '/' . $domain . '.llms.txt';
    }

    return $paths;
}

/**
 * Every file a generation owns on disk: the two served copies and the scratch
 * copy.
 *
 * @return string[] Absolute paths, whether or not they exist.
 */
function llms_generated_file_paths() {
    $paths = llms_served_file_paths();

    $domain = llms_generated_file_domain();
    if ('' !== $domain) {
        $upload_dir = wp_get_upload_dir();
        $paths[] = $upload_dir['basedir'] . '/' . $domain . '.temp.llms.txt';
    }

    return $paths;
}

/**
 * The host name the uploads copies are named after.
 *
 * @return string Empty when siteurl cannot be parsed, in which case there are no
 *                uploads copies to name.
 */
function llms_generated_file_domain() {
    $siteurl = get_option('siteurl');

    if (!$siteurl) {
        return '';
    }

    $domain = wp_parse_url($siteurl, PHP_URL_HOST);

    return $domain ? $domain : '';
}

/**
 * What is on disk right now, in a form that can be compared later.
 *
 * The clearstatcache() below is load bearing; see docs/internals-bootstrap-and-admin.md.
 *
 * @return string Empty string when no artifact exists at all.
 */
function llms_generated_artifact_fingerprint() {
    $parts = array();

    foreach (llms_served_file_paths() as $path) {
        clearstatcache(true, $path);

        if (!file_exists($path)) {
            continue;
        }

        $parts[] = $path . ':' . (int) filesize($path) . ':' . (int) filemtime($path);
    }

    if (empty($parts)) {
        return '';
    }

    return md5(implode('|', $parts));
}

/**
 * Delete the three generated artifacts.
 *
 * @return int Number of artifacts that existed and are now gone.
 */
function llms_delete_generated_files(&$survivors = null) {
    $survivors = array();
    $deleted   = 0;
    $served    = llms_served_file_paths();

    foreach (llms_generated_file_paths() as $path) {
        if (!file_exists($path)) {
            continue;
        }

        wp_delete_file($path);

        // The count must come from the filesystem, not from what existed; see docs/internals-bootstrap-and-admin.md.
        clearstatcache(true, $path);

        if (file_exists($path)) {
            if (in_array($path, $served, true)) {
                $survivors[] = $path;
            }

            continue;
        }

        $deleted++;
    }

    return $deleted;
}

// At file scope, not in LLMS_Core::__construct(); see docs/internals-bootstrap-and-admin.md.
register_activation_hook(__FILE__, array('LLMS_Core', 'activate'));

register_deactivation_hook(__FILE__, function() {
    wp_clear_scheduled_hook('llms_update_llms_file_cron');
    wp_clear_scheduled_hook('llms_scheduled_update');

    llms_delete_generated_files();

    // Delete the stored rules rather than flushing them; see docs/internals-bootstrap-and-admin.md.
    delete_option('rewrite_rules');
});

/**
 * Rebuild the generated file once, on the first admin request after the
 * migration, when nothing else has rebuilt it yet.
 *
 * Nothing here claims the rebuild flag; see docs/internals-bootstrap-and-admin.md.
 *
 * @return void
 */
function llms_maybe_rebuild_generated_file() {
    if (wp_doing_ajax() || !current_user_can('manage_options')) {
        return;
    }

    if (!class_exists('LLMS_DB') || !LLMS_DB::rebuild_pending()) {
        return;
    }

    if (LLMS_DB::LIFECYCLE_OWED !== LLMS_DB::lifecycle_state()) {
        return;
    }

    if (!LLMS_DB::claim_rebuild_attempt()) {
        return;
    }

    if (function_exists('set_time_limit')) {
        @set_time_limit(0);
    }

    try {
        do_action('llms_update_llms_file_cron');
    } catch (\Throwable $e) {
        // Nothing is put back here, because nothing was taken; see docs/internals-bootstrap-and-admin.md.
        if (defined('WP_DEBUG') && WP_DEBUG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Diagnostic for a failed rebuild, WP_DEBUG only.
            error_log('website-llms-txt: the post-update rebuild failed: ' . $e->getMessage());
        }

        return;
    }

    // Only a rebuild that produced a document makes the scheduled event redundant.
    if (!LLMS_DB::rebuild_pending()) {
        wp_clear_scheduled_hook('llms_update_llms_file_cron');
    }
}
add_action('admin_init', 'llms_maybe_rebuild_generated_file', 20);

// LLMS_PLUGIN_FILE, LLMS_PLUGIN_DIR, LLMS_PLUGIN_URL and LLMS_VERSION are deliberately not defined here, not even as aliases; see docs/internals-bootstrap-and-admin.md.
