<?php
/**
 * TI WooCommerce Wishlist Importer class.
 *
 * @package SaveToWishlist\Classes\Importers
 * @since 1.0.4
 */

namespace SaveToWishlist\Classes\Importers;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

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

/**
 * Class TI_Importer
 *
 * Imports wishlists from TI WooCommerce Wishlist.
 *
 * @since 1.0.4
 */
class TI_Importer extends Abstract_Importer {

    /**
     * TI lists table name (without prefix).
     */
    const TI_LISTS_TABLE = 'tinvwl_lists';

    /**
     * TI items table name (without prefix).
     */
    const TI_ITEMS_TABLE = 'tinvwl_items';

    /**
     * TI plugin files (relative to plugins dir).
     *
     * Lists both the free and Premium variants so either activation flips the
     * importer on.
     */
    const TI_PLUGIN_FILES = array(
        'ti-woocommerce-wishlist/ti-woocommerce-wishlist.php',
        'ti-woocommerce-wishlist-premium/ti-woocommerce-wishlist-premium.php',
    );

    /**
     * Batch size for processing.
     */
    const BATCH_SIZE = 50;

    /**
     * Get source name.
     *
     * @since 1.0.4
     * @access public
     * @return string
     */
    public function get_source_name(): string {
        return 'TI WooCommerce Wishlist';
    }

    /**
     * Get source slug.
     *
     * @since 1.0.4
     * @access public
     * @return string
     */
    public function get_source_slug(): string {
        return 'ti';
    }

    /**
     * Get source icon.
     *
     * Heart-with-pulse variant so TI is visually distinguishable from the
     * other heart-based importer entries.
     *
     * @since 1.0.4
     * @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"/><path d="M3.22 12H9.5l.5-1 2 4.5 2-7 1.5 3.5h5.27"/></svg>';
    }

    /**
     * Check if source is active.
     *
     * Returns true only when the TI source plugin is installed and active —
     * leftover wishlist tables alone are not enough to surface the importer.
     *
     * @since 1.0.4
     * @access public
     * @return bool
     */
    public function is_source_active(): bool {
        return $this->is_plugin_installed_and_active( self::TI_PLUGIN_FILES );
    }

    /**
     * Get source version.
     *
     * @since 1.0.4
     * @access public
     * @return string
     */
    public function get_source_version(): string {
        if ( ! function_exists( 'get_plugin_data' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        foreach ( self::TI_PLUGIN_FILES as $plugin_file ) {
            $absolute_path = WP_PLUGIN_DIR . '/' . $plugin_file;
            if ( file_exists( $absolute_path ) ) {
                $plugin_data = get_plugin_data( $absolute_path );
                return $plugin_data['Version'] ?? 'Unknown';
            }
        }

        return 'Unknown';
    }

    /**
     * Get source data count.
     *
     * @since 1.0.4
     * @access public
     * @return array
     */
    public function get_source_data_count(): array {
        global $wpdb;

        if ( ! $this->is_source_active() ) {
            return array(
                'countFromSource' => 0,
                'wishlists'       => 0,
                'items'           => 0,
                'users'           => 0,
                'guests'          => 0,
            );
        }

        // phpcs:disable

        $lists_table       = $wpdb->prefix . self::TI_LISTS_TABLE;
        $items_table       = $wpdb->prefix . self::TI_ITEMS_TABLE;
        $collections_table = Helper_Database::get_collections_table_name();

        $wishlists = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*)
                FROM {$lists_table} l
                LEFT JOIN {$collections_table} c ON l.share_key = c.url_code
                WHERE l.author IS NOT NULL
                AND l.author > %d
                AND c.id IS NULL",
                0
            )
        );

        $guests = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$lists_table} WHERE author IS NULL OR author = %d", 0 ) );

        $items = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*)
                FROM {$items_table} i
                INNER JOIN {$lists_table} l ON i.wishlist_id = l.ID
                LEFT JOIN {$collections_table} c ON l.share_key = c.url_code
                WHERE l.author IS NOT NULL
                AND l.author > %d
                AND c.id IS NULL",
                0
            )
        );

        $users = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(DISTINCT l.author)
                FROM {$lists_table} l
                LEFT JOIN {$collections_table} c ON l.share_key = c.url_code
                WHERE l.author IS NOT NULL
                AND l.author > %d
                AND c.id IS NULL",
                0
            )
        );

        $count_from_source = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$lists_table}" );

        // phpcs:enable

        return array(
            'countFromSource' => $count_from_source,
            'wishlists'       => $wishlists,
            'items'           => $items,
            'users'           => $users,
            'guests'          => $guests,
        );
    }

    /**
     * Get preview data.
     *
     * @since 1.0.4
     * @access public
     * @return array
     */
    public function get_preview_data(): array {
        global $wpdb;

        if ( ! $this->is_source_active() ) {
            return array();
        }

        // phpcs:disable

        $lists_table       = $wpdb->prefix . self::TI_LISTS_TABLE;
        $items_table       = $wpdb->prefix . self::TI_ITEMS_TABLE;
        $collections_table = Helper_Database::get_collections_table_name();

        $wishlists = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT l.*
                FROM {$lists_table} l
                LEFT JOIN {$collections_table} c ON l.share_key = c.url_code
                WHERE l.author IS NOT NULL
                AND l.author > %d
                AND c.id IS NULL
                ORDER BY l.date DESC
                LIMIT %d",
                0,
                5
            )
        );

        // phpcs:enable

        if ( empty( $wishlists ) ) {
            return array();
        }

        $wishlist_ids = array_map( static fn( $w ) => (int) $w->ID, $wishlists );
        $user_ids     = array_unique( array_map( static fn( $w ) => (int) $w->author, $wishlists ) );

        $item_counts = $this->get_item_counts_for_wishlists( $wishlist_ids );
        $user_map    = $this->get_user_map( $user_ids );

        $preview = array();
        foreach ( $wishlists as $wishlist ) {
            $author_id = (int) $wishlist->author;
            $user      = $user_map[ $author_id ] ?? null;
            $preview[] = array(
                'id'         => $wishlist->ID,
                'name'       => ! empty( $wishlist->title ) ? $wishlist->title : $this->get_default_wishlist_name(),
                'user_id'    => $author_id,
                'user_email' => $user ? $user->user_email : '',
                'item_count' => (int) ( $item_counts[ (int) $wishlist->ID ] ?? 0 ),
                'created_at' => $wishlist->date,
                'privacy'    => $wishlist->status,
            );
        }

        return $preview;
    }

    /**
     * Fetch item counts for a set of wishlist IDs in a single query.
     *
     * @param int[] $wishlist_ids List of source wishlist IDs to count items for.
     * @since 1.0.4
     * @access protected
     * @return array<int,int> Map of wishlist_id => count.
     */
    protected function get_item_counts_for_wishlists( array $wishlist_ids ): array {
        if ( empty( $wishlist_ids ) ) {
            return array();
        }

        global $wpdb;

        $items_table  = $wpdb->prefix . self::TI_ITEMS_TABLE;
        $placeholders = implode( ',', array_fill( 0, count( $wishlist_ids ), '%d' ) );

        // phpcs:disable

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT wishlist_id, COUNT(*) AS item_count FROM {$items_table} WHERE wishlist_id IN ({$placeholders}) GROUP BY wishlist_id",
                ...$wishlist_ids
            )
        );

        // phpcs:enable

        $map = array();
        foreach ( $rows as $row ) {
            $map[ (int) $row->wishlist_id ] = (int) $row->item_count;
        }

        return $map;
    }

    /**
     * Fetch all wishlist items for a batch of wishlist IDs in a single query.
     *
     * Results are grouped by wishlist_id and preserve the original
     * ORDER BY ID ASC ordering within each wishlist.
     *
     * @param int[] $wishlist_ids List of source wishlist IDs whose items should be fetched.
     * @since 1.0.4
     * @access protected
     * @return array<int,array> Map of wishlist_id => list of item rows.
     */
    protected function get_items_for_wishlists( array $wishlist_ids ): array {
        if ( empty( $wishlist_ids ) ) {
            return array();
        }

        global $wpdb;

        $items_table  = $wpdb->prefix . self::TI_ITEMS_TABLE;
        $placeholders = implode( ',', array_fill( 0, count( $wishlist_ids ), '%d' ) );

        // phpcs:disable

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$items_table} WHERE wishlist_id IN ({$placeholders}) ORDER BY wishlist_id ASC, ID ASC",
                ...$wishlist_ids
            )
        );

        // phpcs:enable

        $map = array();
        foreach ( $rows as $row ) {
            $map[ (int) $row->wishlist_id ][] = $row;
        }

        return $map;
    }

    /**
     * Look up existing collection url_codes for a specific set of TI share_keys.
     *
     * Used to batch the per-wishlist `get_collection()` check in
     * `process_wishlists_batch()` into a single `WHERE url_code IN (...)` query.
     * The LEFT JOIN in `get_import_batch()` already filters imported rows; this
     * second check guards against the race window between fetch and insert.
     *
     * @param string[] $keys TI wishlist share_keys to look up as existing SaveTo url_codes.
     * @since 1.0.6
     * @access protected
     * @return array<string,true>
     */
    protected function get_existing_url_codes_for_keys( array $keys ): array {
        $keys = array_values( array_unique( array_filter( array_map( 'strval', $keys ), 'strlen' ) ) );
        if ( empty( $keys ) ) {
            return array();
        }

        global $wpdb;

        $collections_table = Helper_Database::get_collections_table_name();
        $placeholders      = implode( ',', array_fill( 0, count( $keys ), '%s' ) );

        // phpcs:disable

        $codes = $wpdb->get_col(
            $wpdb->prepare(
                "SELECT url_code FROM {$collections_table} WHERE url_code IN ({$placeholders})",
                ...$keys
            )
        );

        // phpcs:enable

        return empty( $codes ) ? array() : array_fill_keys( $codes, true );
    }

    /**
     * Get import batch.
     *
     * Cursor-based pagination: `$offset` is the highest source ID already seen
     * (0 on first call). The LEFT JOIN already excludes imported wishlists, so
     * an OFFSET would skip past unimported rows once the filter shrinks.
     *
     * @param int $offset Highest source wishlist ID already processed (cursor).
     * @param int $limit Limit.
     * @since 1.0.4
     * @access public
     * @return array
     */
    public function get_import_batch( int $offset, int $limit ): array {
        global $wpdb;

        if ( ! $this->is_source_active() ) {
            return array();
        }

        // phpcs:disable

        $lists_table       = $wpdb->prefix . self::TI_LISTS_TABLE;
        $collections_table = Helper_Database::get_collections_table_name();

        $wishlists = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prepare(
                "SELECT l.*
                FROM {$lists_table} l
                LEFT JOIN {$collections_table} c ON l.share_key = c.url_code
                WHERE l.author IS NOT NULL
                AND l.author > %d
                AND c.id IS NULL
                AND l.ID > %d
                ORDER BY l.ID ASC
                LIMIT %d",
                0,
                $offset,
                $limit
            )
        );

        // phpcs:enable

        return $wishlists;
    }

    /**
     * Import data.
     *
     * @param array $options Import options.
     * @since 1.0.4
     * @access public
     * @return array
     */
    public function import( array $options ): array {
        $this->clear_import_log();

        if ( ! $this->is_source_active() ) {
            return array(
                'success' => false,
                'message' => __( 'TI WooCommerce Wishlist is not installed or active.', 'saveto-wishlist-lite-for-woocommerce' ),
            );
        }

        $is_batch_request = isset( $options['batch_number'] ) && isset( $options['job_id'] );

        if ( $is_batch_request ) {
            return $this->import_batch( $options );
        }

        $this->set_time_limit_with_fallback();

        $stats = array(
            'wishlists_imported' => 0,
            'items_imported'     => 0,
            'wishlists_skipped'  => 0,
            'items_skipped'      => 0,
            'users_affected'     => 0,
            'errors'             => array(),
        );

        $processed_users = array();
        $last_id         = 0;

        while ( true ) {
            $wishlists = $this->get_import_batch( $last_id, self::BATCH_SIZE );
            if ( empty( $wishlists ) ) {
                break;
            }

            $batch_result    = $this->process_wishlists_batch( $wishlists, $options, $processed_users, $stats );
            $stats           = $batch_result['stats'];
            $processed_users = $batch_result['processed_users'];

            $last_id = (int) end( $wishlists )->ID;
        }

        $this->clear_import_caches( array_keys( $processed_users ) );

        return array(
            'success' => true,
            'stats'   => $stats,
            'log'     => $this->get_import_log(),
        );
    }

    /**
     * Import a single batch (for AJAX processing).
     *
     * @param array $options Import options including batch_number and job_id.
     * @since 1.0.4
     * @access public
     * @return array
     */
    public function import_batch( array $options ): array {
        $job_id       = $options['job_id'] ?? '';
        $batch_number = (int) ( $options['batch_number'] ?? 0 );

        if ( empty( $job_id ) ) {
            return array(
                'success' => false,
                'message' => __( 'Invalid job ID.', 'saveto-wishlist-lite-for-woocommerce' ),
            );
        }

        $progress = get_transient( STWLITE_IMPORT_PROGRESS_GROUP . $job_id );

        if ( false === $progress && 0 === $batch_number ) {
            $counts          = $this->get_source_data_count();
            $total_wishlists = $counts['wishlists'];
            $total_batches   = ceil( $total_wishlists / self::BATCH_SIZE );

            $progress = array(
                'user_id'            => get_current_user_id(),
                'job_id'             => $job_id,
                'status'             => 'processing',
                'current_batch'      => 0,
                'total_batches'      => $total_batches,
                'total_wishlists'    => $total_wishlists,
                'wishlists_imported' => 0,
                'items_imported'     => 0,
                'wishlists_skipped'  => 0,
                'items_skipped'      => 0,
                'users_affected'     => 0,
                'processed_users'    => array(),
                'errors'             => array(),
                'last_id'            => 0,
                'started_at'         => current_time( 'mysql' ),
            );
        } elseif ( false === $progress ) {
            return array(
                'success' => false,
                'message' => __( 'Import job not found or expired.', 'saveto-wishlist-lite-for-woocommerce' ),
            );
        }

        $last_id   = (int) ( $progress['last_id'] ?? 0 );
        $wishlists = $this->get_import_batch( $last_id, self::BATCH_SIZE );

        if ( empty( $wishlists ) ) {
            $progress['status']       = 'completed';
            $progress['completed_at'] = current_time( 'mysql' );

            $this->clear_import_caches( array_keys( $progress['processed_users'] ) );

            set_transient( STWLITE_IMPORT_PROGRESS_GROUP . $job_id, $progress, HOUR_IN_SECONDS );

            return array(
                'success'  => true,
                'complete' => true,
                'progress' => $progress,
            );
        }

        $stats           = array(
            'wishlists_imported' => $progress['wishlists_imported'],
            'items_imported'     => $progress['items_imported'],
            'wishlists_skipped'  => $progress['wishlists_skipped'],
            'items_skipped'      => $progress['items_skipped'],
            'users_affected'     => $progress['users_affected'],
            'errors'             => $progress['errors'],
        );
        $processed_users = $progress['processed_users'];

        $batch_result = $this->process_wishlists_batch( $wishlists, $options, $processed_users, $stats );

        $progress['current_batch']      = $batch_number;
        $progress['wishlists_imported'] = $batch_result['stats']['wishlists_imported'];
        $progress['items_imported']     = $batch_result['stats']['items_imported'];
        $progress['wishlists_skipped']  = $batch_result['stats']['wishlists_skipped'];
        $progress['items_skipped']      = $batch_result['stats']['items_skipped'];
        $progress['users_affected']     = $batch_result['stats']['users_affected'];
        $progress['processed_users']    = $batch_result['processed_users'];
        $progress['errors']             = $batch_result['stats']['errors'];
        $progress['last_id']            = (int) end( $wishlists )->ID;
        $progress['percentage']         = ( $progress['total_batches'] > 0 ) ? round( ( ( $batch_number + 1 ) / $progress['total_batches'] ) * 100, 2 ) : 100;

        set_transient( STWLITE_IMPORT_PROGRESS_GROUP . $job_id, $progress, HOUR_IN_SECONDS );

        return array(
            'success'  => true,
            'complete' => false,
            'progress' => $progress,
        );
    }

    /**
     * Process a batch of wishlists.
     *
     * @param array $wishlists Wishlists to process.
     * @param array $options Import options.
     * @param array $processed_users Already processed user IDs.
     * @param array $stats Current stats.
     * @since 1.0.4
     * @access protected
     * @return array Updated stats and processed_users.
     */
    protected function process_wishlists_batch( array $wishlists, array $options, array $processed_users, array $stats ): array {
        $preserve_default = $options['preserve_default'] ?? true;
        $preserve_privacy = $options['preserve_privacy'] ?? true;

        $batch_wishlist_ids = array_map( static fn( $w ) => (int) $w->ID, $wishlists );
        $items_by_wishlist  = $this->get_items_for_wishlists( $batch_wishlist_ids );

        // Batch the existing-collection check (LEFT JOIN in get_import_batch already
        // filters; this guards the race window between fetch and insert).
        $share_keys     = array_values( array_filter( array_map( static fn( $w ) => (string) $w->share_key, $wishlists ), 'strlen' ) );
        $existing_codes = $this->get_existing_url_codes_for_keys( $share_keys );

        foreach ( $wishlists as $ti_wishlist ) {
            if ( ! $this->validate_user( (int) $ti_wishlist->author ) ) {
                ++$stats['wishlists_skipped'];
                $this->log( sprintf( 'Skipped wishlist ID %d - user does not exist', $ti_wishlist->ID ), 'warning' );
                continue;
            }

            $wishlist_name = $ti_wishlist->title;
            if ( empty( $wishlist_name ) ) {
                $wishlist_name = $this->get_default_wishlist_name();
            }

            $items = $items_by_wishlist[ (int) $ti_wishlist->ID ] ?? array();

            $item_mapping = array();
            foreach ( $items as $index => $item ) {
                $product_id   = (int) ( $item->variation_id > 0 ? $item->variation_id : $item->product_id );
                $product_data = $this->resolve_product_variation( $product_id );

                if ( 0 === $product_data['product_id'] ) {
                    ++$stats['items_skipped'];
                    continue;
                }

                $item_mapping[] = array(
                    'product_id'   => $product_data['product_id'],
                    'variation_id' => $product_data['variation_id'],
                    'quantity'     => (int) $item->quantity,
                    'sort_order'   => $index,
                );
            }

            if ( isset( $existing_codes[ (string) $ti_wishlist->share_key ] ) ) {
                ++$stats['wishlists_skipped'];
                $this->log( sprintf( 'Collection already exists for wishlist ID %d', $ti_wishlist->ID ), 'warning' );
                continue;
            }

            $collection_data = array(
                'user_id'     => (int) $ti_wishlist->author,
                'name'        => $wishlist_name,
                'url_code'    => ! empty( $ti_wishlist->share_key ) ? $ti_wishlist->share_key : $this->generate_unique_url_code(),
                'status'      => 'publish',
                'is_default'  => $preserve_default ? ( 'default' === $ti_wishlist->type ) : false,
                'is_public'   => $preserve_privacy ? $this->map_privacy_string( (string) $ti_wishlist->status ) : 0,
                'description' => '',
                'items'       => $item_mapping,
            );

            $collections       = Collections::instance();
            $collection_result = $collections->save_collection( $collection_data );

            if ( empty( $collection_result ) || is_wp_error( $collection_result ) ) {
                ++$stats['wishlists_skipped'];
                $error_msg         = is_wp_error( $collection_result ) ? $collection_result->get_error_message() : 'Unknown error';
                $stats['errors'][] = sprintf( 'Failed to create collection for wishlist ID %d: %s', $ti_wishlist->ID, $error_msg );
                $this->log( sprintf( 'Failed to create collection for wishlist ID %d', $ti_wishlist->ID ), 'error' );
                continue;
            }

            $target_collection_id = isset( $collection_result->id ) ? (int) $collection_result->id : 0;

            if ( ! $target_collection_id ) {
                ++$stats['wishlists_skipped'];
                $this->log( sprintf( 'Failed to retrieve collection ID from result for wishlist ID %d', $ti_wishlist->ID ), 'error' );
                continue;
            }

            $author_id = (int) $ti_wishlist->author;
            if ( ! isset( $processed_users[ $author_id ] ) ) {
                $processed_users[ $author_id ] = true;
                ++$stats['users_affected'];
            }

            $stats['items_imported'] += count( $item_mapping );

            ++$stats['wishlists_imported'];
        }

        $stats['errors'] = $this->trim_errors( $stats['errors'] );

        return array(
            'stats'           => $stats,
            'processed_users' => $processed_users,
        );
    }

    /**
     * Map TI privacy string to SaveTo numeric privacy.
     *
     * TI: 'public', 'share' -> visible; 'private' -> hidden.
     * SaveTo: 1=public, 0=private.
     *
     * @param string $ti_privacy TI privacy value.
     * @since 1.0.4
     * @access protected
     * @return int
     */
    protected function map_privacy_string( string $ti_privacy ): int {
        return ( 'private' === $ti_privacy ) ? 0 : 1;
    }

    /**
     * Get TI's configured "Default wishlist name" with a safe fallback.
     *
     * Mirrors the YITH importer pattern: pull the source plugin's configured
     * default title, and fall back to a localized string if unavailable.
     *
     * @since 1.0.6
     * @access protected
     * @return string
     */
    protected function get_default_wishlist_name(): string {
        if ( function_exists( 'tinv_get_option' ) ) {
            $default = tinv_get_option( 'general', 'default_title' );
            if ( ! empty( $default ) ) {
                return (string) $default;
            }
        }

        return $this->default_wishlist_fallback();
    }
}
