<?php
/**
 * Plugin Name: PressConduit
 * Description: The conduit between AI assistants and your WordPress site. Pairs with the PressConduit relay so Claude / ChatGPT / Cursor get a full Divi or Bricks + WordPress tool surface.
 * Version: 0.25.0
 * Author: PC Designs
 * Author URI: https://pcdesignstx.com
 * License: GPL-2.0-or-later
 * Text Domain: pressconduit
 * Requires at least: 6.0
 * Requires PHP: 7.4
 */

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

define( 'PCD_PC_VERSION', '0.25.0' );
define( 'PCD_PC_FILE', __FILE__ );
define( 'PCD_PC_DIR', plugin_dir_path( __FILE__ ) );
define( 'PCD_PC_URL', plugin_dir_url( __FILE__ ) );

// Default relay URL — overridable via the admin "Backend URL" setting or wp-config.
if ( ! defined( 'PCD_PC_DEFAULT_BACKEND_URL' ) ) {
	define( 'PCD_PC_DEFAULT_BACKEND_URL', 'https://mcp.pcdesignstx.com' );
}

/*
 * Distribution mode. The relay (direct) build keeps in-plugin licensing and
 * self-updates. The Divi Marketplace build ships a build-distribution.php that
 * sets this to 'marketplace', which makes pcd_pc_is_direct_build() false so the
 * licensing + update code is never loaded or hooked (and those files are
 * physically excluded from that package). Defaults to 'direct' when no
 * build-distribution.php is present, so the normal relay build is unchanged.
 */
$pcd_pc_dist_file = PCD_PC_DIR . 'build-distribution.php';
if ( file_exists( $pcd_pc_dist_file ) ) {
	require_once $pcd_pc_dist_file;
}
if ( ! defined( 'PCD_PC_DISTRIBUTION_MODE' ) ) {
	define( 'PCD_PC_DISTRIBUTION_MODE', 'direct' );
}

if ( ! function_exists( 'pcd_pc_distribution_mode' ) ) {
	/**
	 * @return string 'direct' or 'marketplace'
	 */
	function pcd_pc_distribution_mode(): string {
		$mode = strtolower( (string) PCD_PC_DISTRIBUTION_MODE );
		return in_array( $mode, [ 'direct', 'marketplace', 'wporg' ], true ) ? $mode : 'direct';
	}
}
if ( ! function_exists( 'pcd_pc_is_marketplace_build' ) ) {
	function pcd_pc_is_marketplace_build(): bool {
		return 'marketplace' === pcd_pc_distribution_mode();
	}
}
if ( ! function_exists( 'pcd_pc_is_wporg_build' ) ) {
	function pcd_pc_is_wporg_build(): bool {
		return 'wporg' === pcd_pc_distribution_mode();
	}
}
if ( ! function_exists( 'pcd_pc_is_direct_build' ) ) {
	function pcd_pc_is_direct_build(): bool {
		return 'direct' === pcd_pc_distribution_mode();
	}
}

require_once PCD_PC_DIR . 'includes/class-pcd-pc-tools.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-snapshot.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-audit.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-change-tracker.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-live-reload.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-auth.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-builder.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-pair.php';
// Licensing + self-updates are direct-build only — the Divi Marketplace forbids
// in-plugin licensing and custom update systems, and excludes these files.
if ( pcd_pc_is_direct_build() ) {
	require_once PCD_PC_DIR . 'includes/class-pcd-pc-license.php';
	require_once PCD_PC_DIR . 'includes/class-pcd-pc-updater.php';
}
require_once PCD_PC_DIR . 'includes/class-pcd-pc-mcp.php';
require_once PCD_PC_DIR . 'includes/class-pcd-pc-admin.php';
require_once PCD_PC_DIR . 'includes/tools/class-pcd-pc-tools-wp.php';
require_once PCD_PC_DIR . 'includes/tools/class-pcd-pc-tools-divi.php';
require_once PCD_PC_DIR . 'includes/tools/class-pcd-pc-tools-media.php';
require_once PCD_PC_DIR . 'includes/tools/class-pcd-pc-tools-woo.php';
// File editing, plugin/theme management, Bricks doc-fetch, and protected file
// writes are EXCLUDED from the WordPress.org (Lite) build — that store forbids
// editing/installing code and remote file fetches. The full relay/marketplace
// build includes them.
if ( ! pcd_pc_is_wporg_build() ) {
	require_once PCD_PC_DIR . 'includes/class-pcd-pc-file-ops.php';
	require_once PCD_PC_DIR . 'includes/tools/class-pcd-pc-tools-bricks.php';
	require_once PCD_PC_DIR . 'includes/tools/class-pcd-pc-tools-files.php';
	require_once PCD_PC_DIR . 'includes/tools/class-pcd-pc-tools-admin.php';
}

register_activation_hook( PCD_PC_FILE, [ 'PCD_PC_Auth', 'on_activate' ] );

// On deactivation, tell the relay this site is going dark so any browser
// sessions referencing it stop seeing it on the next MCP call. Fire and
// forget — deactivation shouldn't block on the network.
register_deactivation_hook( PCD_PC_FILE, function () {
	if ( class_exists( 'PCD_PC_Pair' ) && PCD_PC_Pair::is_paired() ) {
		PCD_PC_Pair::notify_disconnect( false );
	}
} );

// On activation, reset PHP's bytecode cache so freshly-replaced plugin files
// take effect immediately — hosts with opcache.validate_timestamps=0 otherwise
// keep serving the previously-cached version of the plugin until a PHP-FPM
// restart, hiding newly-registered tools from the MCP tools/list response.
register_activation_hook( PCD_PC_FILE, function () {
	if ( function_exists( 'opcache_reset' ) ) {
		@opcache_reset();
	}
} );

// After WordPress finishes a plugin update, invalidate every PressConduit file
// in opcache so the next request loads the new code. This fires inside the
// upgrade request from the OLD code's perspective, but opcache_invalidate
// operates on filenames so it marks the new on-disk files for re-read.
add_action( 'upgrader_process_complete', function ( $upgrader, $hook_extra ) {
	if ( ( $hook_extra['action'] ?? '' ) !== 'update' ) return;
	if ( ( $hook_extra['type'] ?? '' ) !== 'plugin' ) return;
	$plugins = $hook_extra['plugins'] ?? [];
	$ours_updated = false;
	foreach ( $plugins as $plugin_file ) {
		if ( strpos( (string) $plugin_file, 'pressconduit/' ) === 0 ) {
			$ours_updated = true;
			break;
		}
	}
	if ( ! $ours_updated ) return;

	if ( function_exists( 'opcache_reset' ) ) {
		@opcache_reset();
	} elseif ( function_exists( 'opcache_invalidate' ) ) {
		$base = PCD_PC_DIR;
		$it   = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base, RecursiveDirectoryIterator::SKIP_DOTS ) );
		foreach ( $it as $file ) {
			if ( substr( $file->getFilename(), -4 ) === '.php' ) {
				@opcache_invalidate( $file->getPathname(), true );
			}
		}
	}
}, 10, 2 );

add_action( 'plugins_loaded', function () {
	PCD_PC_Audit::init();
	PCD_PC_Snapshot::init();
	if ( pcd_pc_is_direct_build() ) {
		PCD_PC_Updater::init();
	}
	PCD_PC_Live_Reload::init();
	PCD_PC_MCP::init();

	if ( PCD_PC_Builder::is_supported() ) {
		PCD_PC_Tools_WP::register();
		if ( ! pcd_pc_is_wporg_build() && 'bricks' === PCD_PC_Builder::primary() ) {
			PCD_PC_Tools_Bricks::register();
		} elseif ( 'divi' === PCD_PC_Builder::primary() ) {
			PCD_PC_Tools_Divi::register();
		}
		PCD_PC_Tools_Media::register();
		PCD_PC_Tools_Woo::register();
		if ( ! pcd_pc_is_wporg_build() ) {
			PCD_PC_Tools_Files::register();
			PCD_PC_Tools_Admin::register();
		}
	}

	if ( is_admin() ) {
		PCD_PC_Admin::init();
	}
} );

// One-time identity backfill: sites paired before 0.24.1 sent only their URL,
// so the relay has no owner email for them. After an update, re-pair once to
// refresh the relay record with admin_email + site_title. Gated by a stored
// version stamp so it runs only when the version actually changes (once per
// update), and only for already-paired sites (fresh installs pair normally on
// activation with the new payload).
//
// Hooked on BOTH admin_init and rest_api_init: connected sites are driven over
// the REST API (MCP), not the wp-admin screen, so admin_init alone could take a
// long time to fire (or never, on a site nobody logs into). rest_api_init makes
// it fire on the next MCP call. The version stamp guarantees it runs only once
// regardless of which hook wins.
function pcd_pc_maybe_backfill_identity() {
	if ( ! class_exists( 'PCD_PC_Pair' ) ) {
		return;
	}
	if ( get_option( 'pcd_pc_identity_stamp', '' ) === PCD_PC_VERSION ) {
		return;
	}
	// Set the stamp first so a slow/failed re-pair can't loop on every request.
	update_option( 'pcd_pc_identity_stamp', PCD_PC_VERSION );
	if ( PCD_PC_Pair::is_paired() ) {
		// Pass the existing connector URL so the relay refreshes the SAME record
		// (attach-to-existing path) instead of forking a new bundle.
		PCD_PC_Pair::pair( PCD_PC_Pair::connector_url() );
	}
}
add_action( 'admin_init', 'pcd_pc_maybe_backfill_identity' );
add_action( 'rest_api_init', 'pcd_pc_maybe_backfill_identity' );
