<?php
defined( 'ABSPATH' ) || exit;

final class MADC_Cleanup {
    const OPTION = 'madc_cleanup_plan';

    public static function active() {
        $plan = get_option( self::OPTION );
        return $plan && ! empty( $plan['started'] ) && $plan['expires'] > time();
    }

    private static function owned_posts() {
        $allowed = array( 'post' => array_column( array_merge( MADC_Importer::data( 'articles' ), MADC_Importer::data( 'articles', 'en' ) ), 'id' ), 'product' => array(), 'product_variation' => array(), 'attachment' => array() );
        foreach ( array_merge( MADC_Importer::data( 'products' ), MADC_Importer::data( 'products', 'en' ) ) as $p ) {
            $allowed['product'][] = $p['id'];
            foreach ( $p['variations'] ?? array() as $v ) { $allowed['product_variation'][] = $v['id']; }
        }
        foreach ( glob( MADC_PATH . 'data/images/*.webp' ) as $image ) { $allowed['attachment'][] = 'media:' . basename( $image ); $allowed['attachment'][] = 'en:media:' . basename( $image ); }
        $items = array();
        // A fixed bundled catalog limits which ownership markers are accepted, including older releases.
        foreach ( array( 'product_variation', 'product', 'post', 'attachment' ) as $type ) {
            $ids = get_posts( array( 'post_type' => $type, 'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future', 'trash', 'inherit' ), 'meta_key' => '_madc_key', 'fields' => 'ids', 'posts_per_page' => -1 ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Ownership lookup runs only during an explicit admin import/cleanup; retaining metadata lookup also finds legacy imports and avoids duplicate content.
            foreach ( $ids as $id ) {
                $key = get_post_meta( $id, '_madc_key', true );
                if ( in_array( $key, $allowed[ $type ], true ) ) { $items[] = array( 'kind' => $type, 'id' => (int) $id, 'key' => $key ); }
            }
        }
        return $items;
    }

    public static function preview() {
        if ( self::active() ) {
            $existing = get_option( self::OPTION );
            if ( (int) $existing['user'] === get_current_user_id() ) {
                $remaining = array_slice( $existing['items'], $existing['cursor'] );
                return array( 'token' => $existing['token'], 'total' => count( $remaining ), 'counts' => array_count_values( array_column( $remaining, 'kind' ) ) );
            }
            return new WP_Error( 'madc_cleanup_active', __( 'Cleanup is already running. Finish it or wait one hour before starting a new preview.', 'majed-arabic-demo-content' ), array( 'status' => 409 ) ); }
        $items = self::owned_posts();
        foreach ( $items as $item ) {
            if ( in_array( $item['kind'], array( 'product', 'product_variation' ), true ) && ! class_exists( 'WooCommerce' ) ) {
                return new WP_Error( 'madc_cleanup_wc', __( 'Activate WooCommerce before deleting imported products.', 'majed-arabic-demo-content' ), array( 'status' => 400 ) );
            }
        }
        foreach ( array( 'category', 'post_tag', 'product_cat', 'product_tag' ) as $taxonomy ) {
            if ( ! taxonomy_exists( $taxonomy ) ) { continue; }
            $terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, 'orderby' => 'term_id', 'meta_key' => '_madc_created', 'meta_value' => '1', 'fields' => 'ids' ) ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Only explicitly owned terms qualify for deletion; names and slugs cannot establish ownership.
            if ( is_wp_error( $terms ) ) { return $terms; }
            foreach ( $terms as $id ) { $items[] = array( 'kind' => 'term', 'id' => (int) $id, 'taxonomy' => $taxonomy ); }
        }
        foreach ( array_merge( MADC_Importer::data( 'download-texts' ), MADC_Importer::data( 'download-texts', 'en' ) ) as $download ) {
            if ( get_option( 'madc_download_' . $download['id'] ) ) { $items[] = array( 'kind' => 'download', 'key' => $download['id'] ); }
        }
        $plan = array( 'token' => wp_generate_password( 40, false ), 'user' => get_current_user_id(), 'expires' => time() + HOUR_IN_SECONDS, 'started' => false, 'items' => $items, 'cursor' => 0, 'deleted' => 0, 'kept' => 0 );
        update_option( self::OPTION, $plan, false );
        return array( 'token' => $plan['token'], 'total' => count( $items ), 'counts' => array_count_values( array_column( $items, 'kind' ) ) );
    }

    private static function contains_id( $value, $id ) {
        if ( is_string( $value ) && is_serialized( $value ) ) {
            $value = unserialize( $value, array( 'allowed_classes' => false ) );
        }
        if ( is_object( $value ) ) { return true; }
        if ( is_array( $value ) ) {
            foreach ( $value as $part ) { if ( self::contains_id( $part, $id ) ) { return true; } }
            return false;
        }
        if ( is_int( $value ) || is_float( $value ) ) { return (string) $value === (string) $id; }
        if ( ! is_string( $value ) ) { return false; }
        $json = json_decode( $value, true );
        if ( is_array( $json ) ) { return self::contains_id( $json, $id ); }
        if ( preg_match( '/^\s*\d+(?:\s*,\s*\d+)*\s*$/', $value ) ) {
            return in_array( $id, array_map( 'intval', explode( ',', $value ) ), true );
        }
        return false;
    }

    private static function referenced( $id, $url, $file ) {
        global $wpdb;
        $filename = basename( $file );
        if ( '' === $filename ) { return true; }
        $like = '%' . $wpdb->esc_like( $filename ) . '%';
        // Include drafts and trash: content that may be restored must not lose its media.
        if ( $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID <> %d AND post_content LIKE %s LIMIT 1", $id, $like ) ) ) { return true; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared read-only cross-content reference check immediately before deletion; no equivalent core API, and cached results could delete newly shared media.
        if ( $wpdb->get_var( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id <> %d AND meta_key NOT LIKE %s AND meta_value LIKE %s LIMIT 1", $id, $wpdb->esc_like( '_madc_' ) . '%', $like ) ) ) { return true; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared read-only cross-content reference check immediately before deletion; no equivalent core API, and cached results could delete newly shared media.
        if ( $id ) {
            $pattern = '(^|[^0-9])' . absint( $id ) . '([^0-9]|$)';
            $values = $wpdb->get_col( $wpdb->prepare( "SELECT pm.meta_value FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.post_id <> %d AND pm.meta_key NOT IN ('_edit_last','_edit_lock','_price','_regular_price','_sale_price','_stock','_wp_attachment_metadata','_wp_attached_file','_wp_attachment_image_alt') AND pm.meta_key NOT LIKE %s AND pm.meta_value REGEXP %s", $id, $wpdb->esc_like( '_madc_' ) . '%', $pattern ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared read-only cross-content reference check immediately before deletion; no equivalent core API, and cached results could delete newly shared media.
            foreach ( $values as $value ) { if ( self::contains_id( $value, $id ) ) { return true; } }
            $options = $wpdb->get_col( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name NOT LIKE %s AND option_name NOT IN ('comments_per_page','close_comments_days_old','woocommerce_schema_version') AND option_name NOT REGEXP '^_site_transient_wp_theme_files_patterns-' AND option_name NOT REGEXP '(size_[wh]$|image_width$|image_height$)' AND option_value REGEXP %s", 'madc' . '%', $pattern ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared read-only cross-content reference check immediately before deletion; no equivalent core API, and cached results could delete newly shared media.
            foreach ( $options as $value ) { if ( self::contains_id( $value, $id ) ) { return true; } }
            $block = '%"id":' . absint( $id ) . '%';
            $class = '%wp-image-' . absint( $id ) . '%';
            $content_pattern = '(wp-image-|"id"[[:space:]]*:[[:space:]]*)' . absint( $id ) . '([^0-9]|$)|(\\[gallery[^]]*|"ids"[[:space:]]*:[[:space:]]*\\[[^]]*)[^0-9]' . absint( $id ) . '([^0-9]|$)';
            if ( $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID <> %d AND (post_content LIKE %s OR post_content LIKE %s OR post_content REGEXP %s) LIMIT 1", $id, $block, $class, $content_pattern ) ) ) { return true; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared read-only cross-content reference check immediately before deletion; no equivalent core API, and cached results could delete newly shared media.
        }
        // Widgets and theme settings can also reference an imported file by name/URL.
        if ( $wpdb->get_var( $wpdb->prepare( "SELECT option_id FROM {$wpdb->options} WHERE option_name NOT LIKE %s AND option_value LIKE %s LIMIT 1", 'madc' . '%', $like ) ) ) { return true; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Prepared read-only cross-content reference check immediately before deletion; no equivalent core API, and cached results could delete newly shared media.
        return false;
    }

    private static function remove_item( $item ) {
        if ( 'term' === $item['kind'] ) {
            $id = $item['id']; $taxonomy = $item['taxonomy'];
            if ( ! term_exists( $id, $taxonomy ) ) { return 'deleted'; }
            $tax = get_taxonomy( $taxonomy );
            if ( ! $tax || ! current_user_can( $tax->cap->delete_terms ) || '1' !== (string) get_term_meta( $id, '_madc_created', true ) ) { return 'kept'; }
            $objects = get_objects_in_term( $id, $taxonomy );
            $children = is_taxonomy_hierarchical( $taxonomy ) ? get_term_children( $id, $taxonomy ) : array();
            if ( is_wp_error( $objects ) || is_wp_error( $children ) || $objects || $children || (int) get_option( 'default_category' ) === $id || (int) get_option( 'default_product_cat' ) === $id ) { return 'kept'; }
            $result = wp_delete_term( $id, $taxonomy );
            if ( is_wp_error( $result ) || ! $result ) { throw new RuntimeException( 'Term cleanup failed.' ); }
            return 'deleted';
        }
        if ( 'download' === $item['kind'] ) {
            $option = 'madc_download_' . $item['key']; $saved = get_option( $option );
            if ( ! $saved ) { return 'deleted'; }
            $uploads = wp_get_upload_dir();
            $file = realpath( $saved['file'] ); $base = realpath( $uploads['basedir'] );
            if ( ! $file ) { delete_option( $option ); return 'deleted'; }
            if ( ! $base || 0 !== strpos( $file, $base . DIRECTORY_SEPARATOR ) || self::referenced( 0, $saved['url'], $file ) ) { return 'kept'; }
            wp_delete_file( $file );
            if ( is_file( $file ) ) { throw new RuntimeException( 'Download cleanup failed.' ); }
            delete_option( $option ); return 'deleted';
        }
        $id = $item['id']; $post = get_post( $id );
        if ( ! $post ) { return 'deleted'; }
        if ( $post->post_type !== $item['kind'] || get_post_meta( $id, '_madc_key', true ) !== $item['key'] || ! current_user_can( 'delete_post', $id ) ) { return 'kept'; }
        if ( 'attachment' === $item['kind'] ) {
            if ( ( $post->post_parent && get_post( $post->post_parent ) ) || self::referenced( $id, wp_get_attachment_url( $id ), (string) get_attached_file( $id ) ) ) { return 'kept'; }
            if ( ! wp_delete_attachment( $id, true ) ) { throw new RuntimeException( 'Media cleanup failed.' ); }
        } elseif ( in_array( $item['kind'], array( 'product', 'product_variation' ), true ) ) {
            if ( ! function_exists( 'wc_get_product' ) ) { throw new RuntimeException( 'WooCommerce is required.' ); }
            $children = get_posts( array( 'post_type' => 'product_variation', 'post_status' => array( 'publish', 'draft', 'private', 'pending', 'trash' ), 'post_parent' => $id, 'fields' => 'ids', 'posts_per_page' => 1 ) );
            // Owned variations were processed first. Never cascade-delete a remaining, unowned child.
            if ( $children ) { return 'kept'; }
            $product = wc_get_product( $id );
            if ( ! $product || ! $product->delete( true ) ) { throw new RuntimeException( 'Product cleanup failed.' ); }
        } else {
            if ( ! wp_delete_post( $id, true ) ) { throw new RuntimeException( 'Post cleanup failed.' ); }
        }
        return 'deleted';
    }

    public static function step( $token ) {
        $plan = get_option( self::OPTION );
        if ( ! $plan || ! hash_equals( $plan['token'], $token ) || (int) $plan['user'] !== get_current_user_id() || $plan['expires'] <= time() ) {
            return new WP_Error( 'madc_cleanup_token', __( 'The cleanup preview expired. Preview the cleanup again.', 'majed-arabic-demo-content' ), array( 'status' => 400 ) );
        }
        $plan['started'] = true; $plan['expires'] = time() + HOUR_IN_SECONDS;
        update_option( self::OPTION, $plan, false );
        $end = min( count( $plan['items'] ), $plan['cursor'] + 5 );
        for ( ; $plan['cursor'] < $end; ) {
            $result = self::remove_item( $plan['items'][ $plan['cursor'] ] );
            ++$plan[ $result ]; ++$plan['cursor'];
            update_option( self::OPTION, $plan, false );
        }
        $done = $plan['cursor'] >= count( $plan['items'] );
        if ( $done ) {
            // Keep the completed result briefly: retrying the final request must be idempotent.
            $plan['started'] = false; update_option( self::OPTION, $plan, false );
        }
        return array( 'done' => $done, 'processed' => $plan['cursor'], 'total' => count( $plan['items'] ), 'deleted' => $plan['deleted'], 'kept' => $plan['kept'] );
    }
}
