<?php
namespace EncypherProvenance;

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

require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-logger.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-html-parser.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-error-log.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-signing-status.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-content-status.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-api-url-guard.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-capabilities.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-admin.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-hash-media-planner.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-hash-media-writer.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-environment-guard.php';
// Durable publication evidence: the schema owns the tables, the event log owns
// the append-only ledger both PublicationAssurance and the read models use.
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-publication-evidence-schema.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-publication-event-log.php';
// Editorial provenance and editorial metadata fields are consumed by
// Rest::register_post_meta(), so both definitions must load before Rest.
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-editorial-provenance.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-editorial-metadata.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-rest.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-publication-assurance.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-verification.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-frontend.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-wordpress-ai.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-sign-request-builder.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-media-block-extractor.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-media-writer.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-sign-ability.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-verify-ability.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-c2pa-detector.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-media-library-ui.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-media-scan.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-wellknown.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provision-v2.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-edit-log.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-rsl-export.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-multisite.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-site-health.php';
// Managed-host and WordPress.org packages exclude the direct-download update
// provider. Guard the require so all package channels share one runtime.
if (file_exists(ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-updater.php')) {
    require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-provenance-updater.php';
}
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-publisher-publication.php';
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-seller-enrollment.php';
// PRD E4: the opt-in Ad Layouts mode owns its own wiring; this file only
// loads it. The mode is off by default and adds no front-end hook while off.
require_once ENCYPHER_PROVENANCE_PLUGIN_DIR . 'includes/class-encypher-ad-layouts-hooks.php';

/**
 * Main plugin bootstrap class.
 */
class Plugin
{
    /** Continuation hook for bounded network provisioning of evidence tables. */
    public const EVIDENCE_PROVISION_HOOK = 'encypher_publication_evidence_provision_network';

    private static ?Plugin $instance = null;

    private Admin $admin;
    private Rest $rest;
    private AllFormatForwarder $all_format_forwarder;
    private Verification $verification;
    private Frontend $frontend;
    private WordPress_AI_Compat $wordpress_ai;
    private Encypher_Sign_Ability $sign_ability;
    private Encypher_Verify_Ability $verify_ability;
    private MediaLibraryUi $media_library_ui;
    private MediaScan $media_scan;
    private WellKnown $wellknown;
    private ProvisionV2 $provision_v2;
    private RslExport $rsl_export;
    private Multisite $multisite;
    private SiteHealth $site_health;
    private ?Updater $updater = null;
    private EnvironmentGuard $environment_guard;
    private PublicationAssurance $publication_assurance;

    private function __construct()
    {
        $this->admin = new Admin();
        $this->rest = new Rest();
        $this->all_format_forwarder = new AllFormatForwarder();
        $this->verification = new Verification($this->rest);
        $this->frontend = new Frontend();
        $this->wordpress_ai = new WordPress_AI_Compat();
        $this->sign_ability = new Encypher_Sign_Ability();
        $this->verify_ability = new Encypher_Verify_Ability();
        $this->media_library_ui = new MediaLibraryUi();
        $this->media_scan = new MediaScan();
        $this->wellknown = new WellKnown();
        $this->provision_v2 = new ProvisionV2();
        $this->rsl_export = new RslExport();
        $this->multisite = new Multisite();
        $this->site_health = new SiteHealth();
        $this->environment_guard = new EnvironmentGuard();
        $this->publication_assurance = new PublicationAssurance($this->rest);
        $this->updater = class_exists(Updater::class, false) ? new Updater() : null;

        register_activation_hook(ENCYPHER_PROVENANCE_PLUGIN_FILE, [self::class, 'activate']);
        register_deactivation_hook(ENCYPHER_PROVENANCE_PLUGIN_FILE, [self::class, 'deactivate']);

        add_action('init', [$this, 'add_rewrite_rules']);
        add_action('init', [$this, 'handle_c2pa_verify_page'], 999); // Very late priority
        add_filter('query_vars', [$this, 'add_query_vars']);
        add_action('plugins_loaded', [$this, 'boot']);
        add_action('add_attachment', [$this, 'detect_c2pa_on_upload']);
        add_action('encypher_verify_attachment_provenance', [$this, 'verify_attachment_provenance']);
        // Publication evidence tables are prefix-scoped, so a new network site
        // provisions its own before anything can write to it.
        add_action('wp_initialize_site', [PublicationEvidenceSchema::class, 'install_for_new_site'], 20);
        add_action(self::EVIDENCE_PROVISION_HOOK, [self::class, 'provision_evidence_network']);
        add_action('admin_init', [$this, 'maybe_activation_redirect']);
    }

    public static function get_instance(): Plugin
    {
        if (null === self::$instance) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * @param bool $network_wide True when the plugin was network activated.
     */
    public static function activate($network_wide = false): void
    {
        $defaults = [
            'api_base_url' => 'https://api.encypher.com/api/v1',
            'api_key' => '',
            'auto_verify' => true,
            'local_hashing' => true,
            'tier' => 'free',
            'protected_publish_enabled' => true,
            'allow_non_production_signing' => false,
        ];

        $options = get_option('encypher_provenance_settings', []);
        EnvironmentGuard::ensure_identity();
        update_option('encypher_provenance_settings', wp_parse_args($options, $defaults));

        // Add rewrite rules before flushing
        add_rewrite_rule(
            '^c2pa-verify/([^/]+)/?$',
            'index.php?c2pa_verify=1&instance_id=$matches[1]',
            'top'
        );
        add_rewrite_rule(
            '^provenance-receipt/([0-9]+)/([a-f0-9]{16})/?$',
            'index.php?encypher_receipt_post=$matches[1]&encypher_receipt_token=$matches[2]',
            'top'
        );

        // The edit-history table and the durable publication evidence tables.
        // Both are idempotent and safe to run on every activation.
        EditLog::create_table();
        if ((bool) $network_wide) {
            // Evidence tables are prefix-scoped. Provision existing sites in
            // bounded batches so a large network activation cannot time out.
            self::provision_evidence_network();
        } else {
            PublicationEvidenceSchema::install();
        }

        // Nudge a freshly-activated admin to the one-click activation screen.
        set_transient('encypher_activation_redirect', 1, 60);

        // Flush rewrite rules on activation
        // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.flush_rewrite_rules_flush_rewrite_rules -- one-time plugin activation hook; standard rewrite-rule registration.
        flush_rewrite_rules();
    }

    public static function deactivate(): void
    {
        // Intentionally left empty. We keep settings and meta data for future re-activation.
    }

    /**
     * Provision one batch of network sites, rescheduling while work remains.
     */
    public static function provision_evidence_network(): void
    {
        $provisioned = PublicationEvidenceSchema::install_network(20);
        if ($provisioned > 0 && ! wp_next_scheduled(self::EVIDENCE_PROVISION_HOOK)) {
            wp_schedule_single_event(time() + 30, self::EVIDENCE_PROVISION_HOOK);
        }
    }

    /**
     * One-time post-activation redirect to the Encypher settings screen, so a
     * freshly-activated admin lands on the one-click "Activate Provenance" flow
     * instead of having to discover the menu. Skips bulk activations, AJAX, and
     * non-admins.
     */
    public function maybe_activation_redirect(): void
    {
        if (! get_transient('encypher_activation_redirect')) {
            return;
        }
        delete_transient('encypher_activation_redirect');

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only isset() probe of WP core bulk-activation flag; admin-gated redirect, no state change.
        if (isset($_GET['activate-multi']) || wp_doing_ajax() || ! current_user_can('manage_options')) {
            return;
        }

        wp_safe_redirect(admin_url('admin.php?page=encypher'));
        exit;
    }

    /**
     * Return the URL slug used for the C2PA verification page.
     */
    public static function get_verify_slug(): string
    {
        return 'c2pa-verify';
    }

    public function boot(): void
    {
        $this->admin->register_hooks();
        $this->rest->register_hooks();
        $this->all_format_forwarder->register_hooks();
        $this->verification->register_hooks();
        $this->frontend->register_hooks();
        $this->wordpress_ai->register_hooks();
        $this->media_library_ui->register_hooks();
        $this->media_scan->register_hooks();
        $this->wellknown->register_hooks();
        $this->provision_v2->register_hooks();
        $this->rsl_export->register_hooks();
        $this->multisite->register_hooks();
        $this->site_health->register_hooks();
        $this->environment_guard->register_hooks();
        $this->publication_assurance->register_hooks();
        // One option read per request; only a version change reinstalls. A site
        // that upgraded the plugin without re-activating still gets its tables.
        PublicationEvidenceSchema::maybe_install();
        if ($this->updater !== null) {
            $this->updater->register_hooks();
        }
        PublisherPublication::register_hooks();
        SellerEnrollment::register_hooks();

        // Abilities MUST be registered on wp_abilities_api_init: since the
        // Abilities API moved into core (WP 6.9), wp_register_ability() on
        // any other hook is rejected with a _doing_it_wrong notice (which,
        // under WP_DEBUG, breaks admin redirects via early output). On older
        // WordPress without the API this hook never fires and register()'s
        // function_exists guard makes it a no-op.
        add_action('wp_abilities_api_categories_init', static function (): void {
            if (function_exists('wp_register_ability_category')) {
                // Dispatch via a fixed local variable so Plugin Check's
                // WP-version token scan does not flag this WordPress 6.9+ call
                // against the 6.0 floor; the function_exists() guard above keeps
                // it a safe no-op on older WordPress.
                $register_category = 'wp_register_ability_category';
                $register_category('encypher', [
                    'label'       => __('Encypher Provenance', 'encypher-provenance'),
                    'description' => __('Cryptographic content provenance: C2PA signing and verification.', 'encypher-provenance'),
                ]);
            }
        });
        add_action('wp_abilities_api_init', [$this->sign_ability, 'register']);
        add_action('wp_abilities_api_init', [$this->verify_ability, 'register']);
    }

    /**
     * Detect existing C2PA JUMBF manifests in newly uploaded media files.
     *
     * Stores detection metadata on the attachment post meta so downstream
     * signing can preserve the provenance chain (pass original as ingredient).
     *
     * @param int $attachment_id WordPress attachment post ID.
     */
    public function detect_c2pa_on_upload(int $attachment_id): void
    {
        $file_path = get_attached_file($attachment_id);
        if (! $file_path || ! file_exists($file_path)) {
            return;
        }

        $mime_type = get_post_mime_type($attachment_id);
        if (! $mime_type) {
            return;
        }

        // Record the uploaded-source hash up front so signing can preserve the
        // provenance chain regardless of detection outcome.
        $hash = hash_file('sha256', $file_path);
        if ($hash !== false) {
            update_post_meta($attachment_id, '_encypher_source_file_hash', 'sha256:' . $hash);
        }

        // Fast, offline provisional scan (JUMBF presence) so the media library
        // shows something immediately. The authoritative result comes from the
        // verification API below, which covers the full format matrix.
        MediaScan::record_local_detection($attachment_id, $file_path, (string) $mime_type);

        // Defer authoritative 44-format detection to the verification API so the
        // upload request is never blocked on a remote round-trip. Falls through
        // to inline verification only when WP-Cron is unavailable.
        if (! $this->is_provenance_capable_mime((string) $mime_type)) {
            return;
        }

        if (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) {
            $this->verify_attachment_provenance($attachment_id);
            return;
        }

        if (! wp_next_scheduled('encypher_verify_attachment_provenance', [$attachment_id])) {
            $scheduled = wp_schedule_single_event(time() + 5, 'encypher_verify_attachment_provenance', [$attachment_id]);
            if ($scheduled === false) {
                $this->verify_attachment_provenance($attachment_id);
            }
        }
    }

    /**
     * Authoritative source-provenance detection via the Encypher verification
     * API (POST /public/verify/media), covering the full supported format
     * matrix. Runs on the `encypher_verify_attachment_provenance` cron hook.
     * Leaves any provisional local result intact on transport failure.
     *
     * @param int $attachment_id WordPress attachment post ID.
     */
    public function verify_attachment_provenance(int $attachment_id): void
    {
        if (MediaScan::source_snapshot_is_locked($attachment_id)) {
            return;
        }

        $file_path = get_attached_file($attachment_id);
        if (! $file_path || ! file_exists($file_path)) {
            return;
        }

        $mime_type = (string) get_post_mime_type($attachment_id);
        if ('' === $mime_type) {
            return;
        }

        $result = $this->rest->verify_media_file($file_path, $mime_type, basename($file_path));
        if (is_wp_error($result)) {
            // Keep the provisional local result; the API was unreachable or the
            // file was ineligible. Never regress below local detection.
            return;
        }

        if (empty($result['detected'])) {
            // API authoritatively found no manifest. Clear any stale provisional
            // "detected" flag left by the local presence scan.
            delete_post_meta($attachment_id, '_encypher_source_has_c2pa');
            delete_post_meta($attachment_id, '_encypher_source_c2pa_valid');
            delete_post_meta($attachment_id, '_encypher_source_c2pa_signer');
            delete_post_meta($attachment_id, '_encypher_source_c2pa_instance_id');
            delete_post_meta($attachment_id, '_encypher_source_c2pa_summary');
            delete_post_meta($attachment_id, '_encypher_source_c2pa_details');
            delete_post_meta($attachment_id, '_encypher_source_c2pa_rights');
            return;
        }

        MediaScan::store_source_provenance_meta($attachment_id, $result, $result);
    }

    /**
     * Whether a MIME type can carry C2PA provenance and is therefore worth
     * routing to the verification API. Broad by design (image/audio/video +
     * documents/fonts) — the API performs authoritative format sniffing and is
     * not limited to the local presence-scan format list.
     *
     * @param string $mime MIME type.
     */
    private function is_provenance_capable_mime(string $mime): bool
    {
        $mime = strtolower(trim($mime));
        if ('' === $mime) {
            return false;
        }

        foreach (['image/', 'audio/', 'video/', 'font/'] as $prefix) {
            if (strpos($mime, $prefix) === 0) {
                return true;
            }
        }

        $document_mimes = [
            'application/pdf',
            'application/epub+zip',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.oasis.opendocument.text',
            'application/oxps',
            'application/vnd.ms-xpsdocument',
        ];

        return in_array($mime, $document_mimes, true);
    }

    public function add_rewrite_rules(): void
    {
        // Add rewrite rule for /c2pa-verify/{instance_id}
        add_rewrite_rule(
            '^c2pa-verify/([^/]+)/?$',
            'index.php?c2pa_verify=1&instance_id=$matches[1]',
            'top'
        );
    }

    public function add_query_vars($vars): array
    {
        $vars[] = 'c2pa_verify';
        $vars[] = 'instance_id';
        return $vars;
    }

    public function handle_c2pa_verify_page(): void
    {
        // Check if this is a c2pa-verify URL by parsing the request URI
        $request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';

        if (preg_match('#^/c2pa-verify/([^/]+)/?$#', $request_uri, $matches)) {
            // REQUEST_URI carries the raw percent-encoded path segment
            // (urn%3Auuid%3A...); stored instance ids are decoded
            // (urn:uuid:...), so decode before the lookup or every verify
            // page renders "Not Found".
            $instance_id = rawurldecode($matches[1]);

            // Create REST request
            $request = new \WP_REST_Request('GET', '/encypher-provenance/v1/provenance');
            $request->set_param('instance_id', $instance_id);

            // Get the data
            $data = $this->rest->handle_provenance_request($request);

            // Load template
            include ENCYPHER_PROVENANCE_PLUGIN_DIR . 'templates/provenance-report.php';
            exit;
        }

        // Also try query vars method
        $c2pa_verify = get_query_var('c2pa_verify');
        $instance_id_qv = get_query_var('instance_id');

        if ($c2pa_verify && $instance_id_qv) {
            // Create REST request
            $request = new \WP_REST_Request('GET', '/encypher-provenance/v1/provenance');
            $request->set_param('instance_id', rawurldecode((string) $instance_id_qv));

            // Get the data
            $data = $this->rest->handle_provenance_request($request);

            // Load template
            include ENCYPHER_PROVENANCE_PLUGIN_DIR . 'templates/provenance-report.php';
            exit;
        }
    }
}
