<?php
/**
 * GTIN / barcode validation.
 *
 * Barnet's `pid` field is documented as "Product barcode", but in practice it
 * sometimes holds a real UPC/EAN and sometimes a synthesised value (commonly the
 * SKU repeated twice, e.g. cspcid 952580 -> pid "952580952580"). Passing an
 * invalid barcode to Google Merchant Center as a GTIN gets the product
 * DISAPPROVED, whereas honestly declaring identifier_exists=false merely costs
 * some visibility. So every pid must be validated before it is used.
 *
 * Valid GTIN lengths are 8, 12, 13 and 14. The final digit is a modulo-10 check
 * digit computed from the rest.
 */

defined( 'ABSPATH' ) || exit;

class BW_Barnet_GTIN {

	public const STATUS_VALID    = 'valid';
	public const STATUS_REPAIRED = 'repaired';
	public const STATUS_INVALID  = 'invalid';
	public const STATUS_EMPTY    = 'empty';

	/**
	 * Validate a candidate barcode, attempting a leading-zero repair.
	 *
	 * An 11-digit value that becomes valid when padded to 12 is the classic
	 * signature of a barcode stored numerically somewhere upstream, dropping its
	 * leading zero. Those are recoverable rather than junk, so we report them
	 * separately instead of discarding them.
	 *
	 * @return array{status:string,gtin:string,reason:string}
	 */
	public static function evaluate( $raw ): array {
		$code = preg_replace( '/\D/', '', (string) $raw );

		if ( '' === $code ) {
			return array(
				'status' => self::STATUS_EMPTY,
				'gtin'   => '',
				'reason' => 'no digits present',
			);
		}

		if ( self::check_digit_ok( $code ) ) {
			return array(
				'status' => self::STATUS_VALID,
				'gtin'   => $code,
				'reason' => 'check digit correct',
			);
		}

		// Try restoring stripped leading zeros up to each legal GTIN length.
		foreach ( array( 8, 12, 13, 14 ) as $length ) {
			if ( strlen( $code ) < $length ) {
				$padded = str_pad( $code, $length, '0', STR_PAD_LEFT );
				if ( self::check_digit_ok( $padded ) ) {
					return array(
						'status' => self::STATUS_REPAIRED,
						'gtin'   => $padded,
						'reason' => sprintf( 'valid once padded to %d digits (leading zero stripped upstream)', $length ),
					);
				}
			}
		}

		return array(
			'status' => self::STATUS_INVALID,
			'gtin'   => '',
			'reason' => self::explain_failure( $code ),
		);
	}

	/**
	 * True when the value is a well-formed GTIN of a legal length.
	 */
	public static function check_digit_ok( string $code ): bool {
		if ( ! ctype_digit( $code ) || ! in_array( strlen( $code ), array( 8, 12, 13, 14 ), true ) ) {
			return false;
		}

		$digits = array_map( 'intval', str_split( $code ) );
		$check  = array_pop( $digits );
		$sum    = 0;

		// Weights alternate 3,1 reading right-to-left from the end of the body.
		foreach ( array_reverse( $digits ) as $i => $digit ) {
			$sum += $digit * ( 0 === $i % 2 ? 3 : 1 );
		}

		return ( ( 10 - $sum % 10 ) % 10 ) === $check;
	}

	private static function explain_failure( string $code ): string {
		$len = strlen( $code );
		if ( ! in_array( $len, array( 8, 12, 13, 14 ), true ) ) {
			return sprintf( '%d digits (valid GTINs are 8, 12, 13 or 14)', $len );
		}
		return 'check digit does not match';
	}

	/**
	 * Heuristic: does this pid look like the SKU simply repeated?
	 * Useful for reporting, since it points at a specific upstream data habit
	 * rather than at random corruption.
	 */
	public static function looks_like_doubled_sku( $pid, $cspcid ): bool {
		$pid    = preg_replace( '/\D/', '', (string) $pid );
		$cspcid = preg_replace( '/\D/', '', (string) $cspcid );
		return '' !== $cspcid && $pid === $cspcid . $cspcid;
	}
}
