<?php
/**
 * Pushes WooCommerce products to Google Sheets.
 *
 * @package StoreSheet
 */

declare( strict_types=1 );

namespace StoreSheet\Sync\WcToSheets;

defined( 'ABSPATH' ) || exit;

use StoreSheet\API\GoogleSheetsClient;
use StoreSheet\Sync\RowMapper;

class ProductPusher {

	private const TAB = 'Products';

	private GoogleSheetsClient $client;
	private RowMapper $mapper;

	public function __construct( GoogleSheetsClient $client, RowMapper $mapper ) {
		$this->client = $client;
		$this->mapper = $mapper;
	}

	/**
	 * Push a single product by ID.
	 *
	 * @param int $product_id
	 * @return true|\WP_Error
	 */
	public function push( int $product_id ) {
		$product = wc_get_product( $product_id );
		if ( ! $product instanceof \WC_Product ) {
			return new \WP_Error( 'invalid_product', "Product {$product_id} not found." );
		}

		$row_index = $this->get_row_index( $product_id );

		/**
		 * Filter whether a WC→Sheets push should be skipped, to avoid clobbering a
		 * Sheets-side edit. Default: skip when the row is locked (_ss_locked = TRUE).
		 * The Pro build's ConflictResolver overrides this per the conflict_strategy
		 * setting (wc_wins / sheets_wins / newest_wins).
		 *
		 * @param bool              $skip       Whether to skip the push.
		 * @param string            $tab        Sheets tab name.
		 * @param int|null          $row_index  1-based row index, or null for a new (not-yet-mapped) row.
		 * @param \WC_DateTime|null $wc_updated The WC object's last-modified time.
		 * @param string            $resource   Resource type slug (product|order|coupon|variation).
		 */
		if ( apply_filters( 'storesheet_skip_push', $this->is_locked( $row_index ), self::TAB, $row_index, $product->get_date_modified(), 'product' ) ) {
			storesheet()->log( "Product {$product_id} conflict skip — not overwriting the Sheets row.", 'debug' );
			return true;
		}

		$row = $this->mapper->product_to_row( $product );

		if ( $row_index ) {
			$result = $this->client->batch_update(
				array(
					array(
						'range'  => self::TAB . '!A' . $row_index . ':U' . $row_index,
						'values' => array( $row ),
					),
				)
			);
		} else {
			$result = $this->client->append( self::TAB . '!A:U', array( $row ) );
			if ( ! is_wp_error( $result ) ) {
				$new_index = $this->parse_appended_row_index( $result );
				if ( $new_index ) {
					$this->save_row_index( $product_id, $new_index );
				}
			}
		}

		if ( is_wp_error( $result ) ) {
			$this->log_sync( $product_id, 'failed', $result->get_error_message() );
			return $result;
		}

		$this->log_sync( $product_id, 'success' );
		return true;
	}

	/**
	 * Push all published products — used for initial backfill.
	 *
	 * @param int $batch_size Max products per call (Sheets batchUpdate limit: 100 rows safe).
	 * @param int $offset
	 * @return int|\WP_Error Number of products pushed.
	 */
	public function push_all( int $batch_size = 100, int $offset = 0 ) {
		$product_ids = wc_get_products(
			array(
				'limit'   => $batch_size,
				'offset'  => $offset,
				'status'  => array( 'publish', 'draft', 'private' ),
				'orderby' => 'ID',
				'order'   => 'ASC',
				'return'  => 'ids',
			)
		);

		if ( empty( $product_ids ) || ! is_array( $product_ids ) ) {
			return 0;
		}

		// Route each product through the single-item push so the row map is
		// honoured — existing rows are updated in place, only genuinely new
		// products are appended. A blind append() here duplicated every row on
		// each scheduled run (row_map is authoritative; see CLAUDE.md).
		$pushed = 0;
		foreach ( $product_ids as $product_id ) {
			$result = $this->push( (int) $product_id );
			if ( is_wp_error( $result ) ) {
				return $result;
			}
			++$pushed;
		}

		return $pushed;
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------

	/**
	 * Whether the mapped Sheets row is locked (_ss_locked = TRUE, column P).
	 *
	 * @param int|null $row_index 1-based row index, or null for an unmapped row.
	 */
	private function is_locked( ?int $row_index ): bool {
		if ( ! $row_index ) {
			return false;
		}

		$result = $this->client->get_values( self::TAB . '!P' . $row_index );
		if ( is_wp_error( $result ) ) {
			return false;
		}

		$value = strtoupper( trim( $result['values'][0][0] ?? '' ) );
		return 'TRUE' === $value;
	}

	private function get_row_index( int $product_id ): ?int {
		global $wpdb;
		$settings = get_option( 'storesheet_settings', array() );
		$sheet_id = $settings['spreadsheet_id'] ?? '';

		$row = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prepare(
				"SELECT row_index FROM {$wpdb->prefix}storesheet_row_map WHERE tab_name = %s AND resource = 'product' AND resource_id = %d AND sheet_id = %s LIMIT 1",
				self::TAB,
				$product_id,
				$sheet_id
			)
		);

		return $row ? (int) $row : null;
	}

	private function save_row_index( int $product_id, int $row_index ): void {
		global $wpdb;
		$settings = get_option( 'storesheet_settings', array() );
		$sheet_id = $settings['spreadsheet_id'] ?? '';

		$wpdb->replace( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prefix . 'storesheet_row_map',
			array(
				'tab_name'    => self::TAB,
				'resource'    => 'product',
				'resource_id' => $product_id,
				'row_index'   => $row_index,
				'sheet_id'    => $sheet_id,
			),
			array( '%s', '%s', '%d', '%d', '%s' )
		);
	}

	private function parse_appended_row_index( array $result ): ?int {
		// updatedRange looks like "Products!A5:U5" — extract row number.
		$range = $result['updates']['updatedRange'] ?? '';
		if ( ! $range ) {
			return null;
		}
		if ( preg_match( '/(\d+)$/', $range, $m ) ) {
			return (int) $m[1];
		}
		return null;
	}

	private function log_sync( int $product_id, string $status, string $message = '' ): void {
		global $wpdb;
		$wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$wpdb->prefix . 'storesheet_sync_log',
			array(
				'direction'   => 'wc_to_sheets',
				'resource'    => 'product',
				'resource_id' => $product_id,
				'tab_name'    => self::TAB,
				'status'      => $status,
				'message'     => $message,
			),
			array( '%s', '%s', '%d', '%s', '%s', '%s' )
		);
	}
}
