<?php
/**
 * Tutorial Data Uninstaller
 *
 * Removes all data and media created by MBS_Tutorial_Importer.
 * Identifies tutorial posts by the '_mbs_tutorial_book' postmeta flag,
 * orphan attachments by '_mbs_tutorial_asset', and taxonomy terms by
 * '_mbs_tutorial_term' term_meta. Child-table cascade is handled by
 * MBS_Plugin::on_delete_book() via the before_delete_post hook.
 *
 * @package    MyBookShowroom
 * @subpackage Import
 * @since      1.0.0
 */

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

/**
 * Removes all tutorial data installed by MBS_Tutorial_Importer.
 *
 * @since 1.0.0
 */
class MBS_Tutorial_Uninstaller { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

    /**
     * Run the full uninstall sequence.
     *
     * Deletes tutorial books (cascading to all child tables via
     * before_delete_post), orphaned media attachments, and taxonomy
     * terms, then clears the installed option.
     *
     * @since  1.0.0
     * @return array {
     *     Summary of deleted items.
     *
     *     @type int   $books_deleted       Number of book posts deleted.
     *     @type int   $attachments_deleted Number of media attachments deleted.
     *     @type int   $terms_deleted       Number of taxonomy terms deleted.
     *     @type array $errors              Array of WP_Error messages.
     * }
     */
    public static function run(): array {
        $stats = array(
            'books_deleted'       => 0,
            'attachments_deleted' => 0,
            'terms_deleted'       => 0,
            'errors'              => array(),
        );

        // Orphan attachments MUST be deleted before books so that video poster
        // frames (children of video attachments) are still reachable via their
        // post_parent before WordPress's one-level cascade removes the video.
        self::delete_orphan_attachments( $stats );
        self::delete_books( $stats );
        self::delete_terms( $stats );

        delete_option( 'mbs_tutorial_imported' );

        return $stats;
    }

    /**
     * Delete all tutorial book posts.
     *
     * Queries for posts with _mbs_tutorial_book='1' (any post_status).
     * wp_delete_post( $id, true ) fires before_delete_post which
     * triggers MBS_Plugin::on_delete_book() -- cascading all child-table
     * rows (format rows, buy links, endorsements, contributors, awards,
     * classification codes, book meta, ARC rows, territorial rights, etc.).
     *
     * @since  1.0.0
     * @param  array $stats Stats array passed by reference.
     * @return void
     */
    private static function delete_books( array &$stats ): void {
        $post_ids = get_posts( array(
            'post_type'      => 'mbs_book',
            'post_status'    => 'any',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'meta_query'     => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- standard WP query pattern; VIP rule does not apply to this plugin
                array(
                    'key'   => '_mbs_tutorial_book',
                    'value' => '1',
                ),
            ),
            'no_found_rows'  => true,
        ) );

        foreach ( $post_ids as $post_id ) {
            $result = wp_delete_post( (int) $post_id, true );
            if ( false === $result || null === $result ) {
                $stats['errors'][] = sprintf(
                    /* translators: %d: post ID */
                    __( 'Failed to delete book post ID %d.', 'my-book-showroom' ),
                    $post_id
                );
            } else {
                $stats['books_deleted']++;
            }
        }
    }

    /**
     * Delete tutorial media attachments not removed by post cascade.
     *
     * Attachments parented to tutorial books are deleted by wp_delete_post().
     * This method cleans up any attachments (author headshots, term banners)
     * that were uploaded without a parent post, identified by the
     * '_mbs_tutorial_asset' postmeta flag.
     *
     * @since  1.0.0
     * @param  array $stats Stats array passed by reference.
     * @return void
     */
    private static function delete_orphan_attachments( array &$stats ): void {
        global $wpdb;

        // Phase A: attachments explicitly flagged as tutorial assets.
        $flagged_ids = get_posts( array(
            'post_type'      => 'attachment',
            'post_status'    => 'any',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            'meta_query'     => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- standard WP query pattern; VIP rule does not apply to this plugin
                array(
                    'key'   => '_mbs_tutorial_asset',
                    'value' => '1',
                ),
            ),
            'no_found_rows'  => true,
        ) );

        // Phase B: child attachments of flagged ones (e.g., video poster frames
        // created by wp_generate_attachment_metadata). These are NOT in the
        // importer's attachment_ids and never received the _mbs_tutorial_asset flag.
        // NOTE: run() calls this method before delete_books() specifically so that
        // video parents are still in the DB when this query executes.
        $child_ids = array();
        foreach ( $flagged_ids as $parent_id ) {
            $children = get_posts( array(
                'post_type'      => 'attachment',
                'post_status'    => 'any',
                'posts_per_page' => -1,
                'fields'         => 'ids',
                'post_parent'    => (int) $parent_id,
                'no_found_rows'  => true,
            ) );
            foreach ( $children as $child_id ) {
                $child_ids[] = (int) $child_id;
            }
        }

        $all_ids = array_unique( array_merge(
            array_map( 'intval', $flagged_ids ),
            $child_ids
        ) );

        // Phase C: video poster frames that Phases A and B cannot reach.
        // WordPress's video processing (ffmpeg) creates poster frames with the
        // '-mp4-image' filename infix. These are created as attachment posts with
        // post_parent = 0 (not the video attachment ID), so the post_parent
        // query in Phase B misses them. Step 6b in the importer now flags
        // post_parent=0 frames at install time, so Phase A catches them on
        // future installs. Phase C remains as a safety net for:
        //   (a) pre-fix installs where frames were never flagged, and
        //   (b) frames whose video parent was deleted by a prior partial uninstall.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $poster_orphan_ids = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT p.ID
                   FROM {$wpdb->posts} p
                   INNER JOIN {$wpdb->postmeta} pm
                           ON pm.post_id = p.ID
                          AND pm.meta_key = %s
                   LEFT  JOIN {$wpdb->posts} parent
                           ON parent.ID = p.post_parent
                  WHERE p.post_type  = %s
                    AND pm.meta_value LIKE %s
                    AND (
                        p.post_parent = 0
                        OR ( p.post_parent != 0 AND parent.ID IS NULL )
                    )",
                '_wp_attached_file',
                'attachment',
                '%%-mp4-image%%'
            )
        );

        foreach ( $poster_orphan_ids as $orphan_id ) {
            $orphan_id = (int) $orphan_id;
            if ( ! in_array( $orphan_id, $all_ids, true ) ) {
                $all_ids[] = $orphan_id;
            }
        }

        // Delete all collected IDs (Phases A + B + C).
        foreach ( $all_ids as $attach_id ) {
            $result = wp_delete_attachment( $attach_id, true );
            if ( false === $result ) {
                $stats['errors'][] = sprintf(
                    /* translators: %d: attachment ID */
                    __( 'Failed to delete attachment ID %d.', 'my-book-showroom' ),
                    $attach_id
                );
            } else {
                $stats['attachments_deleted']++;
            }
        }
    }

    /**
     * Delete all tutorial taxonomy terms.
     *
     * Queries term_meta for _mbs_tutorial_term='1' across all MBS taxonomies.
     * wp_delete_term() removes the term and its term_meta. Post-term
     * relationships are already gone because the posts were deleted first.
     *
     * @since  1.0.0
     * @param  array $stats Stats array passed by reference.
     * @return void
     */
    private static function delete_terms( array &$stats ): void {
        global $wpdb;

        // Find all term_ids flagged as tutorial terms.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $term_ids = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT term_id FROM {$wpdb->termmeta}
                 WHERE meta_key = %s AND meta_value = %s",
                '_mbs_tutorial_term',
                '1'
            )
        );

        if ( empty( $term_ids ) ) {
            return;
        }

        $taxonomies = array(
            MBS_TAX_AUTHOR,
            MBS_TAX_PUBLISHER,
            MBS_TAX_IMPRINT,
            MBS_TAX_SERIES,
            MBS_TAX_COLLECTION,
            MBS_TAX_UNIVERSE,
            MBS_TAX_GENRE,
            MBS_TAX_TAG,
            MBS_TAX_CONTRIBUTOR,
            MBS_TAX_THEME,
            MBS_TAX_CONTENT_WARNING,
        );

        foreach ( $term_ids as $term_id ) {
            $term_id = (int) $term_id;
            $deleted = false;

            foreach ( $taxonomies as $taxonomy ) {
                $term = get_term( $term_id, $taxonomy );
                if ( $term instanceof WP_Term ) {
                    $result = wp_delete_term( $term_id, $taxonomy );
                    if ( is_wp_error( $result ) ) {
                        $stats['errors'][] = sprintf(
                            /* translators: 1: term ID, 2: taxonomy name, 3: error message */
                            __( 'Failed to delete term ID %1$d in taxonomy %2$s: %3$s', 'my-book-showroom' ),
                            $term_id,
                            $taxonomy,
                            $result->get_error_message()
                        );
                    } else {
                        $stats['terms_deleted']++;
                        $deleted = true;
                    }
                    break;
                }
            }

            if ( ! $deleted ) {
                // Term meta exists but term not found in known taxonomies -- clean up stale meta.
                delete_term_meta( $term_id, '_mbs_tutorial_term' );
            }
        }
    }
}
