<?php
/**
 * File Path: includes/helpers/class-mbs-isbn-helper.php
 *
 * ISBN / ASIN Helper
 *
 * Pure-static helpers for ISBN and ASIN normalisation, validation, conversion,
 * and display formatting.  Extracted from MBS_Formatting in R-08 (2026-04-25).
 *
 * R-08 also implements isbn10_to_isbn13() which was called by normalize_isbn()
 * but was never defined anywhere in the codebase (latent fatal error on any
 * ISBN-10 input).  See AUDIT-HISTORY entry R-08 / CQ-10.
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class MBS_ISBN_Helper { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/**
	 * Format a normalised ISBN-13 string for display (e.g. 978-0-306-40615-7).
	 *
	 * D-006: Validates the check digit using the same mod-10 algorithm as
	 * normalize_isbn() so a display/storage mismatch is impossible — any string
	 * that would be rejected by normalize_isbn() is also rejected here.
	 *
	 * @param  string $isbn  Raw or normalised ISBN string.
	 * @return string        Hyphenated ISBN-13, or '' if invalid.
	 */
	public static function format_isbn( string $isbn ): string {
		$clean = preg_replace( '/[\s\-]/', '', $isbn );
		if ( strlen( $clean ) !== 13 || ! preg_match( '/^97[89]/', $clean ) ) {
			return '';
		}

		// D-006: Validate check digit — mirrors the ISBN-V-4 logic in normalize_isbn().
		$sum = 0;
		for ( $i = 0; $i < 12; $i++ ) {
			$sum += (int) $clean[ $i ] * ( ( $i % 2 === 0 ) ? 1 : 3 );
		}
		if ( ( ( 10 - ( $sum % 10 ) ) % 10 ) !== (int) $clean[12] ) {
			return '';
		}

		// FV-011: 3-1-2-6-1 segmentation matches spec example and JS formatter.
		// Pattern: prefix(3) – group(1) – publisher(2) – title(6) – check(1).
		return substr( $clean, 0, 3 ) . '-'
			. substr( $clean, 3, 1 ) . '-'
			. substr( $clean, 4, 2 ) . '-'
			. substr( $clean, 6, 6 ) . '-'
			. substr( $clean, 12, 1 );
	}

	/**
	 * Normalize ISBN for storage (canonical method).
	 *
	 * FV-014: ISBN-10 inputs (10 digits after stripping hyphens/spaces) are
	 * automatically converted to ISBN-13 via isbn10_to_isbn13().  This is
	 * intentional user-friendly behavior — the field stores only ISBN-13 but
	 * accepts a pasted ISBN-10 gracefully.  The admin field hint should surface
	 * this: "ISBN-10 values are automatically converted to ISBN-13."
	 *
	 * @param  string $isbn  Raw ISBN input (ISBN-10 or ISBN-13, with or without hyphens/spaces).
	 * @return string|false  Canonical 13-digit ISBN-13 string, or false if invalid.
	 */
	public static function normalize_isbn( string $isbn ): string|false {
		$clean = preg_replace( '/[\s\-]/', '', $isbn );
		if ( strlen( $clean ) === 10 ) {
			return self::isbn10_to_isbn13( $clean );
		}
		if ( strlen( $clean ) === 13 && preg_match( '/^97[89]\d{10}$/', $clean ) ) {
			// ISBN-V-4: Validate check digit using mod-10 algorithm before accepting.
			$sum = 0;
			for ( $i = 0; $i < 12; $i++ ) {
				$sum += (int) $clean[ $i ] * ( ( $i % 2 === 0 ) ? 1 : 3 );
			}
			$expected_check = ( 10 - ( $sum % 10 ) ) % 10;
			if ( $expected_check !== (int) $clean[12] ) {
				return false; // Invalid check digit.
			}
			return $clean;
		}
		return false;
	}

	/**
	 * Clean ISBN for metabox save (alias for normalize_isbn).
	 *
	 * @param  string $isbn  Raw ISBN input.
	 * @return string|false  Normalised ISBN-13, or false if invalid.
	 */
	public static function clean_isbn( string $isbn ): string|false {
		return self::normalize_isbn( $isbn );
	}

	/**
	 * Convert an ISBN-13 (978-prefix only) to its ISBN-10 equivalent.
	 *
	 * Returns false for any ISBN-13 with a 979 prefix — those have no ISBN-10
	 * counterpart.  Input must be exactly 13 digits starting with "978".
	 *
	 * @param  string $isbn13  Normalised 13-digit ISBN-13 string.
	 * @return string|false    10-character ISBN-10 (check digit may be 'X'), or false.
	 */
	public static function isbn13_to_isbn10( string $isbn13 ): string|false {
		$clean = preg_replace( '/[\s\-]/', '', $isbn13 );
		if ( strlen( $clean ) !== 13 || ! str_starts_with( $clean, '978' ) ) {
			return false;
		}
		$base = substr( $clean, 3, 9 );
		$sum  = 0;
		for ( $i = 0; $i < 9; $i++ ) {
			$sum += (int) $base[ $i ] * ( 10 - $i );
		}
		$check       = ( 11 - ( $sum % 11 ) ) % 11;
		$check_digit = ( $check === 10 ) ? 'X' : (string) $check;
		return $base . $check_digit;
	}

	/**
	 * Normalize an ASIN for storage (FV-005).
	 *
	 * Rules:
	 *  – Strip any character that is not A–Z, a–z, or 0–9.
	 *  – Uppercase the result.
	 *  – Return false if the result is not exactly 10 characters.
	 *
	 * Mirrors the client-side validation in child-admin-validation.js (bindASINValidation).
	 *
	 * @param  string $asin  Raw ASIN input from user or import source.
	 * @return string|false  Normalised 10-character uppercase ASIN, or false if invalid.
	 */
	public static function normalize_asin( string $asin ): string|false {
		$clean = strtoupper( preg_replace( '/[^A-Za-z0-9]/', '', $asin ) );
		return ( strlen( $clean ) === 10 ) ? $clean : false;
	}

	// ── Private helpers ───────────────────────────────────────────────────────

	/**
	 * Convert a valid ISBN-10 string to its ISBN-13 equivalent.
	 *
	 * CQ-10 (R-08, 2026-04-25): This method was called by normalize_isbn() but
	 * was never defined anywhere in the codebase — any ISBN-10 input would have
	 * triggered a fatal error.  Implemented here with the correct algorithm.
	 *
	 * Algorithm:
	 *  1. Strip the ISBN-10 check digit to get the 9-digit body.
	 *  2. Prepend "978" → 12-digit string.
	 *  3. Compute the ISBN-13 mod-10 check digit.
	 *  4. Return the 13-digit result, or false if the ISBN-10 check digit is invalid.
	 *
	 * @param  string $isbn10  10-character ISBN-10 (digits only, check digit may be 'X').
	 * @return string|false    13-digit ISBN-13, or false if invalid.
	 */
	private static function isbn10_to_isbn13( string $isbn10 ): string|false {
		$clean = strtoupper( preg_replace( '/[\s\-]/', '', $isbn10 ) );
		if ( strlen( $clean ) !== 10 ) {
			return false;
		}

		// Validate the ISBN-10 check digit before converting.
		$sum = 0;
		for ( $i = 0; $i < 9; $i++ ) {
			if ( ! ctype_digit( $clean[ $i ] ) ) {
				return false;
			}
			$sum += (int) $clean[ $i ] * ( 10 - $i );
		}
		$last          = $clean[9];
		$check_val     = ( 'X' === $last ) ? 10 : ( ctype_digit( $last ) ? (int) $last : -1 );
		if ( $check_val < 0 ) {
			return false;
		}
		if ( ( $sum + $check_val ) % 11 !== 0 ) {
			return false; // Invalid ISBN-10 check digit.
		}

		// Build the 13-digit body (978 + 9-digit body) and compute ISBN-13 check digit.
		$body = '978' . substr( $clean, 0, 9 );
		$sum13 = 0;
		for ( $i = 0; $i < 12; $i++ ) {
			$sum13 += (int) $body[ $i ] * ( ( $i % 2 === 0 ) ? 1 : 3 );
		}
		$check13 = ( 10 - ( $sum13 % 10 ) ) % 10;

		return $body . (string) $check13;
	}
}
