<?php
/**
 * Abstract Importer class.
 *
 * @package SaveToWishlist\Classes\Importers
 * @since 1.0.0
 */
namespace SaveToWishlist\Classes\Importers;

use SaveToWishlist\Abstracts\Singleton;
use SaveToWishlist\Helpers\Helper_Database;
use SaveToWishlist\Classes\Factories\Collections;

/**
 * Class Abstract_Importer
 *
 * Base class for all wishlist importers.
 *
 * @since 1.0.0
 */
abstract class Abstract_Importer extends Singleton {

    /**
     * Max error strings retained in the per-job progress transient.
     *
     * Older errors are dropped to keep the serialized payload bounded
     * (transients are stored in wp_options as a single LONGTEXT row).
     */
    const MAX_ERRORS_RETAINED = 50;

    /**
     * Import log.
     *
     * @var array
     */
    protected $import_log = array();

    /**
     * Get source name.
     *
     * @since 1.0.0
     * @access public
     * @return string
     */
    abstract public function get_source_name(): string;

    /**
     * Get source slug.
     *
     * @since 1.0.0
     * @access public
     * @return string
     */
    abstract public function get_source_slug(): string;

    /**
     * Get source icon.
     *
     * Default SVG is a generic heart so the importer list always has something
     * to render; subclasses should override with a visually distinct mark so
     * stores running multiple source plugins can tell the entries apart at a
     * glance.
     *
     * @since 1.0.0
     * @access public
     * @return string
     */
    public function get_source_icon(): string {
        return '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z"/></svg>';
    }

    /**
     * Check if source is active.
     *
     * @since 1.0.0
     * @access public
     * @return bool
     */
    abstract public function is_source_active(): bool;

    /**
     * Get source version.
     *
     * @since 1.0.0
     * @access public
     * @return string
     */
    abstract public function get_source_version(): string;

    /**
     * Get source data count.
     *
     * @since 1.0.0
     * @access public
     * @return array
     */
    abstract public function get_source_data_count(): array;

    /**
     * Get preview data.
     *
     * @since 1.0.0
     * @access public
     * @return array
     */
    abstract public function get_preview_data(): array;

    /**
     * Import data.
     *
     * @param array $options Import options.
     * @since 1.0.0
     * @access public
     * @return array
     */
    abstract public function import( array $options ): array;

    /**
     * Get import batch.
     *
     * @param int $offset Offset.
     * @param int $limit Limit.
     * @since 1.0.0
     * @access public
     * @return array
     */
    abstract public function get_import_batch( int $offset, int $limit ): array;

    /**
     * Check whether any of the given source plugin files is currently active.
     *
     * Accepts a single plugin file (e.g. "foo/foo.php") or an array of files
     * — useful for source plugins that ship in multiple flavours (e.g. free
     * and premium). Returns true as soon as one matches.
     *
     * Loads `wp-admin/includes/plugin.php` on demand so this works in REST
     * contexts where it isn't auto-loaded.
     *
     * @param string|string[] $plugin_files Plugin file or array of files relative to plugins dir.
     * @since 1.0.6
     * @access protected
     * @return bool
     */
    protected function is_plugin_installed_and_active( $plugin_files ): bool {
        if ( ! function_exists( 'is_plugin_active' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        foreach ( (array) $plugin_files as $plugin_file ) {
            if ( is_plugin_active( $plugin_file ) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Validate product.
     *
     * @param int $product_id Product ID.
     * @since 1.0.0
     * @access protected
     * @return bool
     */
    protected function validate_product( int $product_id ): bool {
        $product = wc_get_product( $product_id );
        return $product && $product->exists();
    }

    /**
     * Resolve product variation.
     *
     * @param int $prod_id Product ID (may be variation ID).
     * @since 1.0.0
     * @access protected
     * @return array Array with 'product_id' and 'variation_id' keys.
     */
    protected function resolve_product_variation( int $prod_id ): array {
        $product = wc_get_product( $prod_id );

        if ( ! $product ) {
            return array(
                'product_id'   => 0,
                'variation_id' => 0,
            );
        }

        // If it's a variation, get the parent ID.
        if ( $product->is_type( 'variation' ) ) {
            return array(
                'product_id'   => $product->get_parent_id(),
                'variation_id' => $prod_id,
            );
        }

        return array(
            'product_id'   => $prod_id,
            'variation_id' => 0,
        );
    }

    /**
     * Validate user.
     *
     * @param int $user_id User ID.
     * @since 1.0.0
     * @access protected
     * @return bool
     */
    protected function validate_user( int $user_id ): bool {
        $user = get_user_by( 'id', $user_id );
        return $user && $user->exists();
    }

    /**
     * Generate unique URL code.
     *
     * @since 1.0.0
     * @access protected
     * @return string
     */
    protected function generate_unique_url_code(): string {
        $collections = Collections::instance();
        return $collections->generate_url_code();
    }

    /**
     * Map privacy setting.
     *
     * @param int $source_privacy Source privacy value.
     * @since 1.0.0
     * @access protected
     * @return int
     */
    protected function map_privacy( int $source_privacy ): int {
        // Default implementation: 0 = private, 1 = public.
        // Override in child classes for specific mappings.
        return ( 0 === $source_privacy ) ? 0 : 1;
    }

    /**
     * Log message.
     *
     * @param string $message Message.
     * @param string $level Level (info, warning, error).
     * @since 1.0.0
     * @access protected
     * @return void
     */
    protected function log( string $message, string $level = 'info' ): void {
        $this->import_log[] = array(
            'timestamp' => current_time( 'mysql' ),
            'level'     => $level,
            'message'   => $message,
        );
    }

    /**
     * Get import log.
     *
     * @since 1.0.0
     * @access public
     * @return array
     */
    public function get_import_log(): array {
        return $this->import_log;
    }

    /**
     * Clear import log.
     *
     * @since 1.0.0
     * @access public
     * @return void
     */
    public function clear_import_log(): void {
        $this->import_log = array();
    }

    /**
     * Attempt to lift the PHP execution time limit for a full import.
     *
     * Logs a warning when the host disallows it (typical on managed/PaaS).
     *
     * @since 1.0.6
     * @access protected
     * @return void
     */
    protected function set_time_limit_with_fallback(): void {
        $result = @set_time_limit( 0 ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged

        if ( false === $result ) {
            $this->log(
                __( 'Warning: Unable to set execution time limit. Large imports may time out. Consider using batch processing or contact your hosting provider.', 'saveto-wishlist-lite-for-woocommerce' ),
                'warning'
            );
        }
    }

    /**
     * Clear targeted caches for imported data.
     *
     * Instead of flushing the entire WordPress object cache (which impacts all
     * sites/plugins), we clear only the specific cache groups related to
     * imported wishlists.
     *
     * @param array $user_ids Array of user IDs affected by the import.
     * @since 1.0.6
     * @access protected
     * @return void
     */
    protected function clear_import_caches( array $user_ids ): void {
        wp_cache_delete( 'collections', STWLITE_CACHE_GROUP );

        foreach ( $user_ids as $user_id ) {
            wp_cache_delete( 'user_collections_' . $user_id, STWLITE_CACHE_GROUP );
            wp_cache_delete( 'collections_user_' . $user_id, STWLITE_CACHE_GROUP );
        }

        /**
         * Allow other plugins/themes to clear their own caches after import.
         *
         * @param array $user_ids Array of user IDs affected by the import.
         * @since 1.0.3
         */
        do_action( 'stwlite_after_import_clear_cache', $user_ids );
    }

    /**
     * Batch-fetch users by ID and return a map keyed by user ID.
     *
     * @param int[] $user_ids User IDs to resolve in a single get_users() call.
     * @since 1.0.6
     * @access protected
     * @return array<int,\WP_User>
     */
    protected function get_user_map( array $user_ids ): array {
        $user_ids = array_values( array_filter( array_map( 'intval', $user_ids ) ) );
        if ( empty( $user_ids ) ) {
            return array();
        }

        $users = get_users(
            array(
                'include' => $user_ids,
                'fields'  => array( 'ID', 'user_email' ),
            )
        );

        $map = array();
        foreach ( $users as $user ) {
            $map[ (int) $user->ID ] = $user;
        }

        return $map;
    }

    /**
     * Trim a list of error strings down to the most recent MAX_ERRORS_RETAINED entries.
     *
     * Call at the end of each batch so the serialized progress transient stays
     * bounded on large imports.
     *
     * @param array $errors Accumulated error strings.
     * @since 1.0.6
     * @access protected
     * @return array
     */
    protected function trim_errors( array $errors ): array {
        if ( count( $errors ) > self::MAX_ERRORS_RETAINED ) {
            return array_slice( $errors, -self::MAX_ERRORS_RETAINED );
        }

        return $errors;
    }

    /**
     * Localized fallback returned by `get_default_wishlist_name()` when the
     * source plugin doesn't expose a configured default.
     *
     * @since 1.0.6
     * @access protected
     * @return string
     */
    protected function default_wishlist_fallback(): string {
        return __( 'My wishlist', 'saveto-wishlist-lite-for-woocommerce' );
    }
}
