<?php
/**
 * File Path: includes/import/class-mbs-importer-mybooktable.php
 *
 * MyBookTable Direct Importer -- My Book Showroom
 *
 * Converts a MyBookTable JSON export directly into WordPress posts and
 * MBS database rows, bypassing the CSV pipeline entirely.
 *
 * DISTINCT from the existing MBS_MyBookTable_Importer (class-mbs-mybooktable-importer.php)
 * which converts MBT JSON to an intermediate CSV. This class writes directly to the
 * WordPress database using the MBS DAOs via MBS_Book_Hydrator (IEF-C-002).
 *
 * Field mapping (confirmed against real MBT export files):
 *   title             -> post_title
 *   name              -> post_name (URL slug)
 *   content           -> post_content
 *   excerpt           -> mbs_teaser postmeta (HTML stripped + entities decoded;
 *                        falls back to stripped content if excerpt is empty)
 *   authors[]         -> mbs_author taxonomy terms (per-book array)
 *   series[]          -> mbs_series taxonomy terms (per-book array)
 *   series_order      -> mbs_format_rows.series_number
 *   genres[]          -> mbs_genre taxonomy terms
 *   tags[]            -> mbs_tag taxonomy terms
 *   tags "Coming Soon!" -> mbs_coming_soon postmeta = '1' (tag stripped from taxonomy)
 *   unique_id_isbn    -> mbs_format_rows.isbn13
 *   unique_id_asin    -> mbs_format_rows.asin
 *   price             -> mbs_format_rows.price
 *   publisher_name    -> mbs_publisher postmeta (PLAN-M-001: not publisher)
 *   publisher_url     -> mbs_publisher_url postmeta
 *   publication_year  -> mbs_format_rows.pub_date (year integer)
 *   book_length       -> mbs_format_rows.page_length
 *   display_mode      -> mbs_landing_page_mode postmeta ('1' when display_mode === 'landingpage')
 *   image_src         -> featured image via MBS_Import_Media_Sideloader::sideload() (IEF-C-002)
 *   buybuttons[].url  -> book_buy_links via MBS_Book_Hydrator / MBS_Buy_Link_DAO::save()
 *   buybuttons[].text -> book_buy_links.link_label
 *
 * IEF-C-002 refactor (Sprint IEF-XLSX Phase 7):
 *   import_book() now delegates to MBS_Book_Hydrator::hydrate() via
 *   build_book_object_from_mbt(). The following methods have been removed:
 *   resolve_taxonomy_term_ids(), save_mbt_post_meta(), save_mbt_format_rows(),
 *   save_mbt_buy_links(). Cover sideloading is performed post-hydration.
 *
 * Settings mapping (settings export only -- selectively applied, never overwrites
 * an existing non-empty MBS value):
 *   amazon_buybutton_affiliate_code -> mbs_setting_affiliate_amazon
 *   itunes_affiliate_token          -> mbs_setting_affiliate_apple
 *   indiebound_affiliate_id         -> mbs_setting_affiliate_bookshop
 *   powells_partner_id              -> mbs_setting_affiliate_powells
 *   linkshare_affiliate_code        -> mbs_setting_affiliate_bn (Rakuten/LinkShare powers B&N)
 *   posts_per_page                  -> mbs_setting_formatting_items_per_page
 *
 * Tier: FREE (competitor migration -- no mbs_is_pro() gate).
 *
 * @package    My_Book_Showroom
 * @subpackage Includes/Import
 * @since      1.0.0
 */

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

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

	// RETAILER_MAP removed REF-006 -- use MBS_Importer_MyBookTable_Mapper::RETAILER_MAP.

	// __ Public API _______________________________________________________________

	/**
	 * Import a MyBookTable JSON export directly into WordPress.
	 *
	 * @param string $json_path Absolute path to the MBT JSON export file.
	 * @return array{ imported: int, skipped: int, errors: string[], settings_applied: int }
	 */
	public static function import( string $json_path ): array {
		$result = array(
			'imported'         => 0,
			'skipped'          => 0,
			'errors'           => array(),
			'settings_applied' => 0,
		);

		// __ Parse JSON ___________________________________________________________
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$json = @file_get_contents( $json_path ); // phpcs:ignore -- @ suppresses E_WARNING; false return is handled below
		if ( false === $json ) {
			$result['errors'][] = 'Could not read the export file.';
			MBS_Telemetry_Failure_Tracker::record_import_failure( 'mybooktable' ); // F-006
			return $result;
		}

		$data = json_decode( $json, true );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
			$result['errors'][] = 'File is not valid JSON: ' . json_last_error_msg();
			MBS_Telemetry_Failure_Tracker::record_import_failure( 'mybooktable' ); // F-006
			return $result;
		}

		// __ Settings import (selective) ________________________________________
		// SEC-021: Extract into local variable and unset from $data BEFORE any
		// processing (S14-2 unconditional pre-loop strip). This ensures the MBT
		// api_key and other plaintext credentials are never accessible after this
		// point, even if import_settings() throws or logs its input.
		// Never overwrites an existing non-empty MBS value so a re-import
		// cannot clobber settings the user has already configured in MBS.
		$raw_settings = $data['settings'] ?? [];
		unset( $data['settings'] ); // strip BEFORE any processing (S14-2)
		if ( ! empty( $raw_settings ) && is_array( $raw_settings ) ) {
			$result['settings_applied'] += self::import_settings( $raw_settings );
		}

		if ( empty( $data['books'] ) || ! is_array( $data['books'] ) ) {
			$result['errors'][] = 'No books found in the export file.';
			MBS_Telemetry_Failure_Tracker::record_import_failure( 'mybooktable' ); // F-006
			return $result;
		}

		// __ Pre-create taxonomy terms ____________________________________________
		// Series must be created first so per-book series assignment has existing
		// term IDs to assign. The top-level series array includes slug + parent info.
		$series_slug_to_term_id = self::ensure_series_terms( $data['series'] ?? array() );

		// Pre-create genre terms from the top-level genres array.
		// Items are objects {id, name, slug, description, parent, image} -- not strings.
		if ( ! empty( $data['genres'] ) && is_array( $data['genres'] ) ) {
			foreach ( $data['genres'] as $genre ) {
				$genre_name = is_array( $genre ) ? ( $genre['name'] ?? '' ) : (string) $genre;
				if ( '' !== trim( $genre_name ) ) {
					wp_insert_term( sanitize_text_field( $genre_name ), MBS_TAX_GENRE );
				}
			}
		}

		// Pre-create tag terms from the top-level tags array.
		// Same object structure as genres.
		if ( ! empty( $data['tags'] ) && is_array( $data['tags'] ) ) {
			foreach ( $data['tags'] as $tag ) {
				$tag_name = is_array( $tag ) ? ( $tag['name'] ?? '' ) : (string) $tag;
				if ( '' !== trim( $tag_name ) ) {
					wp_insert_term( sanitize_text_field( $tag_name ), MBS_TAX_TAG );
				}
			}
		}

		// __ Walk books ___________________________________________________________
		foreach ( $data['books'] as $index => $book ) {
			if ( ! is_array( $book ) ) {
				continue;
			}

			$import_result = self::import_book( $book, $series_slug_to_term_id );

			if ( true === $import_result ) {
				$result['imported']++;
			} elseif ( 'skipped' === $import_result ) {
				$result['skipped']++;
			} else {
				$result['errors'][] = sprintf(
					'Book #%d (%s): %s',
					$index,
					esc_html( (string) ( $book['title'] ?? '(untitled)' ) ),
					$import_result
				);
			}
		}

		if ( ! empty( $result['errors'] ) ) {
			MBS_Telemetry_Failure_Tracker::record_import_failure( 'mybooktable', count( $result['errors'] ) ); // F-006
		}
		return $result;
	}

	// __ Private helpers __________________________________________________________

	/**
	 * Selectively apply MBT settings to MBS wp_options.
	 *
	 * Only writes values that have a direct MBS equivalent AND where the
	 * corresponding MBS option is currently empty/unset. A re-import will
	 * never overwrite settings the user has already configured in MBS.
	 *
	 * @param array $settings The 'settings' block from the MBT export.
	 * @return int Number of settings written.
	 */
	private static function import_settings( array $settings ): int {
		$applied = 0;

		// amazon_buybutton_affiliate_code -> mbs_setting_amazon_partner_tag
		// Special case: different option-key pattern, not in AFFILIATE_SETTINGS_MAP.
		$mbt_amazon_tag = trim( (string) ( $settings['amazon_buybutton_affiliate_code'] ?? '' ) );
		if ( '' !== $mbt_amazon_tag && '' === get_option( 'mbs_setting_amazon_partner_tag', '' ) ) {
			update_option( 'mbs_setting_amazon_partner_tag', sanitize_text_field( $mbt_amazon_tag ) );
			$applied++;
		}

		// posts_per_page -> mbs_setting_formatting_items_per_page
		// Special case: requires integer cast + positive-integer guard.
		$mbt_per_page = (int) ( $settings['posts_per_page'] ?? 0 );
		if ( $mbt_per_page > 0 && '' === get_option( 'mbs_setting_formatting_items_per_page', '' ) ) {
			update_option( 'mbs_setting_formatting_items_per_page', $mbt_per_page );
			$applied++;
		}

		// Simple string affiliate codes -- itunes, indiebound, powells, linkshare.
		// Each is written only when the corresponding MBS option is currently empty.
		foreach ( MBS_Importer_MyBookTable_Mapper::AFFILIATE_SETTINGS_MAP as $mbt_key => $mbs_option ) {
			$val = trim( (string) ( $settings[ $mbt_key ] ?? '' ) );
			if ( '' !== $val && '' === get_option( $mbs_option, '' ) ) {
				update_option( $mbs_option, sanitize_text_field( $val ) );
				$applied++;
			}
		}

		return $applied;
	}

	/**
	 * Create or look up mbs_series terms from the top-level series array.
	 *
	 * The top-level array has entries like:
	 *   { "id": 5, "name": "The Dragon Cycle", "slug": "the-dragon-cycle",
	 *     "description": "...", "parent": 0 }
	 *
	 * We create parent-less terms first, then child terms.
	 *
	 * @param array $series_list Top-level series array from the MBT export.
	 * @return array<string, int> Map of MBT slug => WP term_id.
	 */
	private static function ensure_series_terms( array $series_list ): array {
		$slug_to_term_id = array();

		// Sort: parents (parent == 0) before children.
		usort( $series_list, static function ( $a, $b ) {
			return (int) ( $a['parent'] ?? 0 ) <=> (int) ( $b['parent'] ?? 0 );
		} );

		foreach ( $series_list as $series ) {
			if ( ! is_array( $series ) || empty( $series['name'] ) ) {
				continue;
			}

			$name = sanitize_text_field( (string) $series['name'] );
			$slug = sanitize_title( (string) ( $series['slug'] ?? $name ) );

			$args = array(
				'slug'        => $slug,
				'description' => sanitize_textarea_field( (string) ( $series['description'] ?? '' ) ),
			);

			// Resolve parent by slug if present.
			$parent_id = (int) ( $series['parent'] ?? 0 );
			if ( $parent_id > 0 ) {
				// MBT parent IDs are internal to MBT; look up by the slug we created.
				// If not found, insert as top-level rather than silently failing.
				foreach ( $series_list as $candidate ) {
					if ( (int) ( $candidate['id'] ?? -1 ) === $parent_id ) {
						$parent_slug    = sanitize_title( (string) ( $candidate['slug'] ?? $candidate['name'] ?? '' ) );
						$existing_parent = get_term_by( 'slug', $parent_slug, MBS_TAX_SERIES );
						if ( $existing_parent instanceof WP_Term ) {
							$args['parent'] = $existing_parent->term_id;
						}
						break;
					}
				}
			}

			$inserted = wp_insert_term( $name, MBS_TAX_SERIES, $args );

			if ( is_wp_error( $inserted ) ) {
				// Term may already exist -- retrieve existing term_id.
				$existing = get_term_by( 'slug', $slug, MBS_TAX_SERIES );
				if ( $existing instanceof WP_Term ) {
					$slug_to_term_id[ $slug ] = $existing->term_id;
				}
			} else {
				$slug_to_term_id[ $slug ] = (int) $inserted['term_id'];
			}
		}

		return $slug_to_term_id;
	}

	/**
	 * Import a single book from the MBT export.
	 *
	 * Delegates to MBS_Book_Hydrator::hydrate() after building an MBS Book Object
	 * via build_book_object_from_mbt(). Cover sideloading is performed post-hydration.
	 *
	 * @param array                $book              Single book entry from MBT export.
	 * @param array<string, int>   $series_slug_map   Pre-built slug => term_id map.
	 * @return true|'skipped'|string  true = imported, 'skipped' = duplicate, string = error message.
	 */
	private static function import_book( array $book, array $series_slug_map ) {
		$title = sanitize_text_field( (string) ( $book['title'] ?? '' ) );
		if ( '' === $title ) {
			return 'Book has no title -- skipped.';
		}

		// __ Duplicate detection __________________________________________________
		$dup_candidates = array( 'title' => $title );

		$isbn_raw = (string) ( $book['unique_id_isbn'] ?? '' );
		$asin_raw = (string) ( $book['unique_id_asin'] ?? '' );
		// Normalize before duplicate detection so dashed ISBNs and mixed-case
		// ASINs match any previously imported copies.
		$isbn13 = '' !== $isbn_raw ? MBS_ISBN_Helper::normalize_isbn( $isbn_raw ) : '';
		$asin   = '' !== $asin_raw ? MBS_ISBN_Helper::normalize_asin( $asin_raw ) : '';
		if ( '' !== $isbn13 ) {
			$dup_candidates['isbn13'] = $isbn13;
		}
		if ( '' !== $asin ) {
			$dup_candidates['asin'] = $asin;
		}

		$existing_id = MBS_Duplicate_Detector::find_duplicate( $dup_candidates );
		if ( null !== $existing_id ) {
			return 'skipped';
		}

		// __ Build book object and hydrate ________________________________________
		$book_object = self::build_book_object_from_mbt( $book, $series_slug_map, $isbn13, $asin );
		$post_id     = MBS_Book_Hydrator::hydrate( $book_object, array( 'skip_duplicate_check' => true ) );

		if ( is_wp_error( $post_id ) ) {
			return $post_id->get_error_code() === 'duplicate'
				? 'skipped'
				: $post_id->get_error_message();
		}

		// __ Sideload cover image (post-insert) ___________________________________
		// IEF-C-002 / PLAN-R2-M-005: uses unified MBS_Import_Media_Sideloader::sideload().
		if ( ! empty( $book['image_src'] ) ) {
			MBS_Import_Media_Sideloader::sideload( $post_id, esc_url_raw( (string) $book['image_src'] ), 'cover' );
		}

		return true;
	}

	// __ Private mapping helper ___________________________________________________

	/**
	 * Map a single MBT JSON book entry to an MBS Book Object.
	 *
	 * Pure mapping function: no DB reads or writes. Exceptions: none.
	 *
	 * Corrections (Sprint IEF-XLSX Phase 7):
	 *   PLAN-M-001:     uses $book['publisher_name'] (not $book['publisher']).
	 *   PLAN-R2-M-005:  no images key -- cover sideloading is handled post-hydration.
	 *   R4-H-002:       series_number mapped from $book['series_order'] on every format row.
	 *   R4-M-003:       post_name preserved from $book['name'] for deterministic slugs.
	 *   R6-L-002:       no dead images key in returned array (hydrator ignores it).
	 *
	 * Note: mbs_landing_page_mode is included in post_meta for completeness but is
	 * silently skipped by the hydrator (not in whitelist). Flagged for a future
	 * hydrator update.
	 *
	 * @param array              $book            Single book array from MBT export.
	 * @param array<string, int> $series_slug_map Pre-built slug => term_id map from ensure_series_terms().
	 * @param string             $isbn13          Pre-normalized ISBN-13 (may be empty).
	 * @param string             $asin            Pre-normalized ASIN (may be empty).
	 * @return array MBS Book Object ready for MBS_Book_Hydrator::hydrate().
	 * @since 1.0.0
	 */
	private static function build_book_object_from_mbt(
		array  $book,
		array  $series_slug_map,
		string $isbn13 = '',
		string $asin   = ''
	): array {
		$title   = sanitize_text_field( (string) ( $book['title'] ?? '' ) );
		$content = wp_kses_post( (string) ( $book['content'] ?? '' ) );

		// __ Teaser: strip HTML, decode entities, fall back to content ____________
		$excerpt_raw = (string) ( $book['excerpt'] ?? '' );
		$content_raw = (string) ( $book['content'] ?? '' );
		$teaser_html = '' !== trim( $excerpt_raw ) ? $excerpt_raw : $content_raw;
		$teaser      = html_entity_decode( wp_strip_all_tags( $teaser_html ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$teaser      = sanitize_textarea_field( $teaser );

		// __ Coming Soon detection via tag ________________________________________
		$is_coming_soon = '0';
		foreach ( (array) ( $book['tags'] ?? array() ) as $tag ) {
			if ( is_string( $tag ) && 0 === strcasecmp( trim( $tag ), 'Coming Soon!' ) ) {
				$is_coming_soon = '1';
				break;
			}
		}

		// __ Taxonomy names _______________________________________________________
		$author_names = array_values( array_filter( array_map(
			'sanitize_text_field',
			(array) ( $book['authors'] ?? array() )
		) ) );

		$genre_names = array_values( array_filter( array_map(
			'sanitize_text_field',
			(array) ( $book['genres'] ?? array() )
		) ) );

		// Filter "Coming Soon!" out of tags -- it is mapped to mbs_coming_soon instead.
		$tag_names = array_values( array_filter(
			array_map( 'sanitize_text_field', (array) ( $book['tags'] ?? array() ) ),
			static function ( string $t ): bool {
				return 0 !== strcasecmp( trim( $t ), 'Coming Soon!' );
			}
		) );

		// Per-book series are plain strings (not objects -- see ensure_series_terms()
		// for the top-level series array which uses objects).
		$series_names = array();
		foreach ( (array) ( $book['series'] ?? array() ) as $series_name ) {
			if ( ! is_string( $series_name ) || '' === trim( $series_name ) ) {
				continue;
			}
			$series_names[] = sanitize_text_field( $series_name );
		}

		// __ Buy links (book-level) _______________________________________________
		$buy_links = array();
		foreach ( (array) ( $book['buybuttons'] ?? array() ) as $idx => $btn ) {
			$btn_url = esc_url_raw( (string) ( $btn['url'] ?? '' ) );
			if ( '' === $btn_url ) {
				continue;
			}
			$mbt_store        = (string) ( $btn['store'] ?? '' );
			$is_direct_author = ( 'universal' === $mbt_store ) ? 1 : 0;
			$retailer_key     = $is_direct_author
				? ''
				: ( MBS_Importer_MyBookTable_Mapper::RETAILER_MAP[ $mbt_store ] ?? '' );
			if ( '' === $retailer_key && ! $is_direct_author ) {
				continue; // Unknown store -- skip.
			}
			$buy_links[] = array(
				'retailer_key'     => $retailer_key,
				'url'              => $btn_url,
				'link_label'       => sanitize_text_field( (string) ( $btn['text'] ?? '' ) ),
				'is_direct_author' => $is_direct_author,
				'sort_order'       => $idx,
				'link_type'        => 'buy',
				'locale_us'        => '',
				'locale_uk'        => '',
				'locale_ca'        => '',
				'locale_au'        => '',
				'locale_de'        => '',
				'locale_jp'        => '',
			);
		}

		// __ Format rows (ISBN/ASIN decision matrix from save_mbt_format_rows) ___
		// B-prefix ASINs are Kindle/ebook identifiers. Numeric ASINs are physical-
		// book Amazon product IDs (ISBN-10) and belong on the paperback row, not a
		// separate ebook format row.
		$has_isbn      = '' !== $isbn13;
		$has_asin      = '' !== $asin;
		$asin_is_ebook = $has_asin && ( 'B' === strtoupper( $asin[0] ) );

		$pub_year     = (int) ( $book['publication_year'] ?? 0 );
		$pub_date_val = $pub_year > 0 ? (string) $pub_year : '';
		$price_raw    = trim( (string) ( $book['price'] ?? '' ) );
		$price_val    = '' !== $price_raw && is_numeric( $price_raw ) ? (string) $price_raw : '';
		$series_num   = sanitize_text_field( (string) ( $book['series_order'] ?? '' ) );

		$shared_fields = array(
			'series_number' => $series_num,
			'pub_date'      => $pub_date_val,
			'buy_links'     => array(),
		);
		if ( '' !== $price_val ) {
			$shared_fields['price'] = $price_val;
		}

		$format_rows = array();

		if ( $has_isbn ) {
			// Paperback row. Numeric (physical) ASIN is included on this same row;
			// B-prefix (Kindle) ASINs are excluded here and get their own ebook row.
			$pb_row = array_merge( $shared_fields, array(
				'format'      => 'paperback',
				'isbn13'      => $isbn13,
				'page_length' => (int) ( $book['book_length'] ?? 0 ),
			) );
			if ( $has_asin && ! $asin_is_ebook ) {
				$pb_row['asin'] = $asin;
			}
			$format_rows[] = $pb_row;
		}

		if ( $asin_is_ebook ) {
			$format_rows[] = array_merge( $shared_fields, array(
				'format' => 'ebook',
				'asin'   => $asin,
			) );
		}

		if ( ! $has_isbn && ! $asin_is_ebook ) {
			// No ISBN and no Kindle ASIN -- either a numeric physical ASIN or nothing.
			$fallback = array_merge( $shared_fields, array(
				'format'      => 'paperback',
				'page_length' => (int) ( $book['book_length'] ?? 0 ),
			) );
			if ( $has_asin ) {
				$fallback['asin'] = $asin;
			}
			$format_rows[] = $fallback;
		}

		// __ Assemble MBS Book Object _____________________________________________
		return array(
			'post_title'   => $title,
			// R4-M-003: Preserve deterministic slug from MBT's 'name' field.
			// The hydrator does not yet consume post_name, but it is included
			// here for forward-compatibility and to document intent.
			'post_name'    => sanitize_title( (string) ( $book['name'] ?? $title ) ),
			'post_content' => $content,
			'post_status'  => 'publish',
			'post_meta'    => array(
				'mbs_teaser'            => $teaser,
				'mbs_coming_soon'       => $is_coming_soon,
				'mbs_publisher'         => sanitize_text_field( (string) ( $book['publisher_name'] ?? '' ) ),  // PLAN-M-001
				'mbs_publisher_url'     => esc_url_raw( (string) ( $book['publisher_url']           ?? '' ) ),
				// mbs_landing_page_mode is included here but silently skipped by the
				// hydrator's meta whitelist (hydrator update flagged for a future session).
				'mbs_landing_page_mode' => 'landingpage' === (string) ( $book['display_mode'] ?? '' ) ? '1' : '',
			),
			'taxonomies'   => array(
				MBS_TAX_AUTHOR  => $author_names,
				MBS_TAX_GENRE   => $genre_names,
				MBS_TAX_TAG     => $tag_names,
				MBS_TAX_SERIES  => $series_names,
			),
			'classifications' => array(),
			'format_rows'     => $format_rows,
			'book_buy_links'  => $buy_links,
			'endorsements'    => array(),
			'contributors'    => array(),
			'awards'          => array(),
		);
	}
}
