<?php
/**
 * File Path: includes/import/class-mbs-importer-json.php
 *
 * JSON Importer (PRO)
 *
 * Imports books from an MBS-format JSON file produced by the settings exporter
 * or by any third-party tool that outputs the canonical MBS candidate shape.
 *
 * Expected JSON structure — array of objects, each matching the candidate shape
 * accepted by MBS_Importer_CSV::insert_book():
 *   [
 *     {
 *       "title": "...",
 *       "author": "...",        // maps to mbs_author taxonomy
 *       "isbn13": "...",
 *       "asin": "...",
 *       "publisher": "...",     // maps to mbs_publisher postmeta
 *       "pub_date": "YYYY-MM-DD",
 *       "format": "...",
 *       "price": "...",
 *       "series": "...",
 *       "series_number": "...",
 *       "page_length": "...",
 *       "teaser": "...",
 *       "look_inside_text": "...",
 *       "post_content": "...",  // book description — stored as post body
 *       "cover_url": "..."      // stored as mbs_import_cover_url for Sprint 9 sideload
 *     },
 *     ...
 *   ]
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

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

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

	/** Maximum upload file size: 5 MB. JSON should never approach this. */
	const MAX_FILE_SIZE = 5242880;

	/**
	 * Import books from an MBS-format JSON file.
	 *
	 * Parses the file, validates structure, and delegates insertion to
	 * MBS_Parent_Importer_CSV::import_candidates() so all books — regardless of
	 * import source — go through the same DAO + taxonomy + duplicate-detection
	 * path (BR-IMP-3, GP-SQL-1, GP-T-1).
	 *
	 * @param string $file_path         Absolute path to the uploaded JSON file.
	 * @param array  $force_insert_keys Title-slug or ISBN-13 keys approved for
	 *                                  force-insert despite duplicate detection.
	 * @return array{
	 *   success: bool,
	 *   inserted: int,
	 *   skipped: int,
	 *   duplicates: array[],
	 *   errors: string[],
	 *   error: string
	 * }
	 */
	public static function import(
		string $file_path,
		array  $force_insert_keys = array()
	): array {
		$result = array(
			'success'    => false,
			'inserted'   => 0,
			'skipped'    => 0,
			'duplicates' => array(),
			'errors'     => array(),
			'error'      => '',
		);

		// File validation.
		$validation = self::validate_file( $file_path );
		if ( true !== $validation ) {
			$result['error'] = $validation;
			return $result;
		}

		$raw  = file_get_contents( $file_path );
		$data = json_decode( $raw, true );

		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $data ) ) {
			$result['error'] = 'Invalid JSON format — file could not be decoded.';
			return $result;
		}

		if ( empty( $data ) ) {
			$result['error'] = 'JSON file contains no books.';
			return $result;
		}

		// Normalise: accept both a flat array and a wrapped {"books": [...]} object.
		if ( isset( $data['books'] ) && is_array( $data['books'] ) ) {
			$data = $data['books'];
		}

		// Build candidate array using the canonical shape expected by insert_book().
		$candidates = array();
		foreach ( $data as $index => $book ) {
			if ( ! is_array( $book ) || empty( $book['title'] ) ) {
				continue;
			}
			$candidates[] = self::normalise_candidate( $book, $index + 1 );
		}

		if ( empty( $candidates ) ) {
			$result['error'] = 'No valid books found in the JSON file (all entries were missing a title).';
			return $result;
		}

		// Delegate to the canonical insert path — same as CSV and API candidates.
		// NEW-M1: This ensures correct DAO + taxonomy writes, not phantom meta keys.
		// M-2:    import_candidates() uses MBS_Duplicate_Detector::check() (batch).
		$csv_result = MBS_Parent_Importer_CSV::import_candidates( $candidates, $force_insert_keys );
		if ( ! empty( $csv_result['errors'] ) ) {
			MBS_Telemetry_Failure_Tracker::record_import_failure( 'json', count( $csv_result['errors'] ) ); // F-006
		}
		return $csv_result;
	}

	// =========================================================================
	// Private helpers
	// =========================================================================

	/**
	 * Validate the uploaded file before parsing.
	 *
	 * @param string $file_path
	 * @return true|string
	 */
	private static function validate_file( string $file_path ): true|string {
		if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
			return 'File not found or not readable.';
		}
		if ( filesize( $file_path ) > self::MAX_FILE_SIZE ) {
			return 'File exceeds the maximum allowed size of 5 MB.';
		}
		return true;
	}

	/**
	 * Normalise a raw JSON book object into the canonical candidate shape.
	 *
	 * Maps common field name variants (e.g. "primary_author", "description")
	 * to the keys expected by MBS_Importer_CSV::insert_book() so that JSON
	 * files exported by third-party tools can be imported without pre-processing.
	 *
	 * @param array $book  Raw JSON object.
	 * @param int   $index 1-based position for error reporting.
	 * @return array Candidate array.
	 */
	private static function normalise_candidate( array $book, int $index ): array {
		return array(
			'_line'        => $index,
			'title'        => $book['title'] ?? '',
			// author / primary_author → 'author' key used by insert_book() taxonomy path
			'author'       => $book['author'] ?? $book['primary_author'] ?? '',
			'isbn13'       => MBS_ISBN_Helper::normalize_isbn( $book['isbn13'] ?? $book['isbn'] ?? '' ) ?: '',
			'asin'         => $book['asin'] ?? '',
			'publisher'    => $book['publisher'] ?? '',
			// pub_date / publication_date → normalised to YYYY-MM-DD when possible
			'pub_date'     => self::normalise_date( $book['pub_date'] ?? $book['publication_date'] ?? '' ),
			'format'       => $book['format'] ?? 'paperback',
			'price'        => (string) ( $book['price'] ?? '' ),
			'series'       => $book['series'] ?? '',
			'series_number'=> (string) ( $book['series_number'] ?? '' ),
			'page_length'  => (string) ( $book['page_length'] ?? '' ),
			'teaser'       => $book['teaser'] ?? '',
			'look_inside_text' => $book['look_inside_text'] ?? '',
			// description / post_content → post body via insert_book() F-2 path
			'post_content' => $book['post_content'] ?? $book['description'] ?? '',
			// cover_url → stored as mbs_import_cover_url for Sprint 9 sideload
			'cover_url'    => $book['cover_url'] ?? $book['thumbnail'] ?? '',
		);
	}

	/**
	 * Attempt to normalise a publication date string to YYYY-MM-DD.
	 * Returns the original string unchanged if it cannot be parsed.
	 *
	 * @param string $raw
	 * @return string
	 */
	private static function normalise_date( string $raw ): string {
		if ( empty( $raw ) ) {
			return '';
		}
		if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $raw ) ) {
			return $raw; // already canonical
		}
		// YYYY-MM or YYYY → pad to first of month / first of year.
		if ( preg_match( '/^(\d{4})-(\d{2})$/', $raw, $m ) ) {
			return $m[1] . '-' . $m[2] . '-01';
		}
		if ( preg_match( '/^(\d{4})$/', $raw, $m ) ) {
			return $m[1] . '-01-01';
		}
		// Let PHP try to parse anything else.
		$ts = strtotime( $raw );
		return $ts ? gmdate( 'Y-m-d', $ts ) : $raw;
	}
}
