<?php
/**
 * Plugin Name:       PitDesk Auto Repair CRM - Work Orders & Service History
 * Description:       Lightweight CRM for auto-repair shops: capture customer visits and turn them into tracked jobs with full customer and vehicle history.
 * Version:           1.5.0
 * Requires PHP:      8.1
 * Requires at least: 6.7
 * Tested up to:      7.1
 * Author:            rwsss
 * License:           GPL-2.0-or-later
 * License URI:       https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain:       pitdesk-repair-crm
 * Domain Path:       /languages
 *
 * @package WrenchPress
 */

defined( 'ABSPATH' ) || exit;

/**
 * Schema version. Bump this integer on every schema change and add a matching
 * entry to the migration map in WrenchPress_Migrations.
 */
define( 'WRENCHPRESS_DB_VERSION', 8 );

/**
 * Option key in wp_options that stores the installed schema version.
 */
define( 'WRENCHPRESS_DB_VERSION_OPTION', 'wrenchpress_db_version' );

/**
 * Absolute path to this plugin's directory, with trailing slash.
 */
define( 'WRENCHPRESS_PATH', plugin_dir_path( __FILE__ ) );

/**
 * URL to this plugin's directory, with trailing slash.
 */
define( 'WRENCHPRESS_URL', plugin_dir_url( __FILE__ ) );

/**
 * Main plugin file path (used by activation hook and feature declarations).
 */
define( 'WRENCHPRESS_FILE', __FILE__ );

require_once WRENCHPRESS_PATH . 'includes/class-capabilities.php';
require_once WRENCHPRESS_PATH . 'includes/class-schema.php';
require_once WRENCHPRESS_PATH . 'includes/class-migrations.php';
require_once WRENCHPRESS_PATH . 'includes/class-activator.php';
require_once WRENCHPRESS_PATH . 'includes/class-deactivator.php';
require_once WRENCHPRESS_PATH . 'includes/class-customers-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-customer-sessions-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-portal-session.php';
require_once WRENCHPRESS_PATH . 'includes/class-leads-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-vehicles-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-work-orders-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-work-order-items-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-events-repository.php';

/*
 * Runtime PDF engine (Strauss-prefixed Dompdf bundled in 07-02). Loaded under a
 * readability guard BEFORE the document classes so a build shipped without the
 * vendor step degrades to the browser-print view (WrenchPress_Pdf_Renderer::
 * is_available() returns false) instead of fataling.
 */
if ( is_readable( WRENCHPRESS_PATH . 'vendor-prefixed/autoload.php' ) ) {
	require_once WRENCHPRESS_PATH . 'vendor-prefixed/autoload.php';
}

require_once WRENCHPRESS_PATH . 'includes/class-money.php';
require_once WRENCHPRESS_PATH . 'includes/class-documents-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-document-counters.php';
require_once WRENCHPRESS_PATH . 'includes/class-document-service.php';
require_once WRENCHPRESS_PATH . 'includes/class-document-template.php';
require_once WRENCHPRESS_PATH . 'includes/class-pdf-renderer.php';
require_once WRENCHPRESS_PATH . 'includes/class-appointments-repository.php';
require_once WRENCHPRESS_PATH . 'includes/class-reminders-repository.php';
require_once WRENCHPRESS_PATH . 'includes/rest/class-appointments-rest-controller.php';
require_once WRENCHPRESS_PATH . 'includes/rest/class-search-rest-controller.php';
require_once WRENCHPRESS_PATH . 'includes/class-promotion.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-anti-abuse.php';
require_once WRENCHPRESS_PATH . 'includes/class-privacy.php';
require_once WRENCHPRESS_PATH . 'includes/class-retention.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-visit-form.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-visit-submission.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-portal-request.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-portal.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-portal-documents.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-shortcode.php';
require_once WRENCHPRESS_PATH . 'includes/frontend/class-form-block.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-customers-list-table.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-customers-admin-page.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-leads-list-table.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-leads-admin-page.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-picker-options.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-vehicles-list-table.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-vehicles-admin-page.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-work-orders-list-table.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-work-orders-admin-page.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-reminders-list-table.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-reminders-admin-page.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-appointments-admin-page.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-settings-page.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-documents-admin.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-admin-menu.php';
require_once WRENCHPRESS_PATH . 'includes/admin/class-dashboard-page.php';
require_once WRENCHPRESS_PATH . 'includes/integrations/class-elementor-widget.php';
require_once WRENCHPRESS_PATH . 'includes/integrations/divi/wrenchpress-divi-module.php';

/**
 * Run the activation routine: create tables, register roles and capabilities,
 * and record the schema version.
 */
register_activation_hook( __FILE__, array( 'WrenchPress_Activator', 'activate' ) );

/**
 * Clear the scheduled retention event on deactivation so WP-Cron does not keep
 * trying to fire wrenchpress_retention_purge after the callback stops loading.
 * This is the plugin's first deactivation hook.
 */
register_deactivation_hook( WRENCHPRESS_FILE, array( 'WrenchPress_Deactivator', 'deactivate' ) );

/**
 * On every load, apply any pending schema migrations. This catches plugin
 * auto-updates, which do NOT fire the activation hook.
 */
add_action( 'plugins_loaded', array( 'WrenchPress_Migrations', 'maybe_upgrade' ) );

/**
 * Register the bundled translations shipped in /languages.
 *
 * The just-in-time loader added in WordPress 6.7 auto-discovers translations
 * that live in WP_LANG_DIR/plugins (the location used by translate.wordpress.org
 * language packs), but it does NOT scan a plugin's own directory. Bundled
 * catalogs like ours are only found once their path is registered on the
 * textdomain registry, which is exactly what load_plugin_textdomain does. It is
 * called on init so the path is registered before any string is translated.
 */
add_action(
	'init',
	static function () {
		load_plugin_textdomain(
			'pitdesk-repair-crm',
			false,
			dirname( plugin_basename( WRENCHPRESS_FILE ) ) . '/languages'
		);
	}
);

/**
 * Schedule the daily data-retention purge once (guarded by wp_next_scheduled so a
 * duplicate event is never registered) and wire the event to the retention
 * runner. run() is idempotent, so a missed or duplicated firing is harmless; it
 * purges stale unpromoted leads, strips old submitter IP/UA, and clears expired
 * portal-session rows, never touching customers or work orders.
 */
add_action(
	'init',
	static function () {
		if ( ! wp_next_scheduled( 'wrenchpress_retention_purge' ) ) {
			wp_schedule_event( time(), 'daily', 'wrenchpress_retention_purge' );
		}
	}
);
add_action( 'wrenchpress_retention_purge', array( 'WrenchPress_Retention', 'run' ) );

/**
 * Register the single top-level WrenchPress menu and all of its submenus
 * (Dashboard, Customers, Leads, Vehicles, Work Orders, Settings). One registrar
 * owns the whole information architecture so the screens consolidate under one
 * parent menu (D-01).
 */
add_action( 'admin_menu', array( 'WrenchPress_Admin_Menu', 'register' ) );

/**
 * Enqueue the shared admin status-badge stylesheet on WrenchPress screens only.
 */
add_action( 'admin_enqueue_scripts', array( 'WrenchPress_Admin_Menu', 'enqueue_admin_assets' ) );

/**
 * Enqueue the built FullCalendar island and localize its bootstrap on the
 * Appointments screen only. The bootstrap mints the wp_rest nonce that secures
 * every REST write and carries the REST base, capability flag, timezone,
 * business hours, statuses, and bounded picker lists the island consumes.
 */
add_action( 'admin_enqueue_scripts', array( 'WrenchPress_Appointments_Admin_Page', 'enqueue' ) );

/**
 * Enqueue the built customer-picker island on the Vehicles screen only. It
 * progressively enhances the server-rendered owning-customer <select> into a
 * searchable combobox. It reads and writes nothing, and the <select> stays the
 * form's value sink, so the POST contract is unchanged.
 */
add_action( 'admin_enqueue_scripts', array( 'WrenchPress_Vehicles_Admin_Page', 'enqueue' ) );

/**
 * Enqueue the built work-order-picker island on the Work Orders screen only. It
 * progressively enhances the Add Work Order form's two server-rendered <select>
 * elements into searchable, coupled comboboxes backed by wrenchpress/v1/search.
 * It replaces assets/js/work-order-form.js, which hid options client-side and is
 * now deleted: two scripts fighting over the same select is a bug waiting to be
 * written. The selects stay the value sinks, so the POST contract is unchanged.
 */
add_action( 'admin_enqueue_scripts', array( 'WrenchPress_Work_Orders_Admin_Page', 'enqueue' ) );

/**
 * Register the settings option and fields independently of the menu so the
 * Settings API save flow keeps working when the parent registrar owns the menu.
 */
add_action( 'admin_init', array( 'WrenchPress_Settings_Page', 'register_settings' ) );

/**
 * Register the wrenchpress/v1 appointment CRUD namespace. Every route is
 * capability-gated in its permission_callback and nonce-protected (wp_rest); the
 * calendar island (04-05) is the only consumer.
 */
add_action( 'rest_api_init', array( 'WrenchPress_Appointments_Rest_Controller', 'register_routes' ) );

/**
 * Register the wrenchpress/v1 picker search namespace: GET /search/customers and
 * GET /search/vehicles. Both are read-only and each declares its own explicit
 * permission_callback calling current_user_can() with a wrenchpress_* capability. They
 * are what retires the 500-row picker cap: the admin pickers search the database instead
 * of filtering a pre-baked list.
 */
add_action( 'rest_api_init', array( 'WrenchPress_Search_Rest_Controller', 'register_routes' ) );

/**
 * Register the anonymous (nopriv) and logged-in visit-form submission handlers
 * on admin-post.php.
 */
WrenchPress_Visit_Submission::register();

/**
 * Register the anonymous (nopriv) and logged-in portal magic-link request
 * handlers on admin-post.php, plus the early template_redirect link-click
 * verification that sets the session cookie before any output.
 */
WrenchPress_Portal_Request::register();

/**
 * Register the staff-only invoice/estimate generate handler on admin-post.php.
 * The handler verifies a per-work-order nonce and the MANAGE_WORKORDERS
 * capability before issuing-or-reusing a frozen document and streaming the PDF or
 * print view.
 */
WrenchPress_Documents_Admin::register();

/**
 * Register the public visit-form embedding surfaces: the [wrenchpress_visit_form]
 * shortcode and the wrenchpress/visit-form block.
 */
WrenchPress_Shortcode::register();
WrenchPress_Form_Block::register();

/**
 * Register the customer-facing [wrenchpress_portal] shortcode and its nonced
 * sign-out handler. The shortcode reads an already-established session and
 * returns the magic-link request form when logged out or the read-only dashboard
 * when authenticated; it never sets the session cookie.
 */
WrenchPress_Portal::register();

/**
 * Register the ownership-scoped portal document download on admin-post.php
 * (both nopriv and priv). Unlike the staff generate handler it holds no
 * capability gate: it verifies a per-work-order nonce, then confirms the
 * requested work order belongs to the session-derived customer, then reuses the
 * frozen Phase 7 document service and PDF renderer to stream the invoice or
 * estimate. A mismatch returns a neutral "Not found." so it is never an IDOR or
 * an existence oracle.
 */
WrenchPress_Portal_Documents::register();

/**
 * Register the optional native builder integrations. Each registration is
 * guarded so neither Elementor nor DIVI is a hard dependency: when a builder is
 * absent the hook simply never fires its widget/module.
 */
add_action( 'wp_enqueue_scripts', 'wrenchpress_register_visit_form_assets' );
add_action( 'elementor/widgets/register', 'wrenchpress_register_elementor_widget' );
add_action( 'et_builder_ready', 'wrenchpress_register_divi_module' );

/**
 * Register the plugin's WrenchPress_Privacy exporter with WordPress core (PRIV-01).
 *
 * @param array<string, array<string, mixed>> $exporters Registered exporters.
 * @return array<string, array<string, mixed>> Exporters with the plugin's added.
 */
function wrenchpress_register_privacy_exporter( $exporters ) {
	$exporters['wrenchpress'] = array(
		'exporter_friendly_name' => __( 'WrenchPress customer records', 'pitdesk-repair-crm' ),
		'callback'               => array( 'WrenchPress_Privacy', 'export' ),
	);

	return $exporters;
}

/**
 * Register the plugin's WrenchPress_Privacy eraser with WordPress core (PRIV-02).
 *
 * @param array<string, array<string, mixed>> $erasers Registered erasers.
 * @return array<string, array<string, mixed>> Erasers with the plugin's added.
 */
function wrenchpress_register_privacy_eraser( $erasers ) {
	$erasers['wrenchpress'] = array(
		'eraser_friendly_name' => __( 'WrenchPress customer records', 'pitdesk-repair-crm' ),
		'callback'             => array( 'WrenchPress_Privacy', 'erase' ),
	);

	return $erasers;
}

/**
 * Wire the plugin's custom-table PII into WordPress core Personal Data: the
 * exporter and eraser keyed by email, plus the suggested privacy-policy content.
 * The policy content registers on admin_init because wp_add_privacy_policy_content
 * must not run earlier (it calls _doing_it_wrong() otherwise).
 */
add_filter( 'wp_privacy_personal_data_exporters', 'wrenchpress_register_privacy_exporter' );
add_filter( 'wp_privacy_personal_data_erasers', 'wrenchpress_register_privacy_eraser' );
add_action( 'admin_init', array( 'WrenchPress_Privacy', 'add_policy_content' ) );
