<?php
/**
 * File Path: includes/import/class-mbs-mybooktable-importer.php
 *
 * MyBookTable Import Adapter
 *
 * Converts a MyBookTable PRO backup JSON export into an MBS-compatible CSV
 * file that can be fed directly into the existing CSV import pipeline.
 *
 * Supported MyBookTable export versions: 3.x
 *
 * Field mapping:
 *   MBT title            → MBS title
 *   MBT excerpt          → MBS teaser  (HTML stripped, entities decoded)
 *   MBT content          → MBS teaser  (fallback if excerpt is empty)
 *   MBT publisher_name   → MBS publisher
 *   MBT publication_year → MBS pub_date
 *   MBT unique_id_isbn   → MBS isbn13
 *   MBT unique_id_asin   → MBS asin
 *   MBT price            → MBS price
 *   MBT sample_url       → (not imported; Look Inside requires HTML prose, not a URL)
 *   MBT authors[0]       → MBS author
 *   MBT series[0]        → MBS series  (HTML entities decoded)
 *   MBT series_order     → MBS series_number
 *   MBT book_length      → MBS page_length
 *   MBT image_src        → MBS cover_image_url (deferred sideload)
 *   MBT tags "Coming Soon!" → MBS coming_soon = 1
 *   (no equivalent)      → MBS format  (always empty — MBT has no format concept)
 *   (no equivalent)      → MBS cover_image_file (always empty for remote imports)
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

// Ensure ACCEPTED_COLUMNS is available.
require_once dirname( __FILE__ ) . '/csv/class-mbs-child-importer-csv-helpers.php';

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

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

	/** Minimum version prefix this parser has been validated against. */
	const SUPPORTED_VERSION_PREFIX = '3.';

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

	// ── Public API ────────────────────────────────────────────────────────────

	/**
	 * Detect whether a file appears to be a MyBookTable JSON export.
	 *
	 * Reads the first 2 KB and checks for the characteristic top-level keys.
	 * Does not perform full JSON validation — that is deferred to to_csv_path().
	 *
	 * @param string $file_path Absolute path to the uploaded file.
	 * @return bool
	 */
	public static function is_mybooktable_json( string $file_path ): bool {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		$fh = fopen( $file_path, 'r' );
		if ( ! $fh ) {
			return false;
		}
		// UNREGFREE-10: read 16 KB instead of 2 KB. The "books" key can appear
		// beyond the 2 KB mark when the export contains many series entries
		// (confirmed: real export has "books" at byte 2609).
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
		$head = fread( $fh, 16384 );
		fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- WP_Filesystem migration deferred to PC-FILESYS sprint; PHP native file ops are safe here (temp files in get_temp_dir(), never inside wp-content)

		// MBT exports are JSON objects starting with {"version":...,"authors":...,"series":...,"books":...}
		return false !== strpos( $head, '"version"' )
			&& false !== strpos( $head, '"books"' )
			&& false !== strpos( $head, '"series"' )
			&& str_starts_with( ltrim( (string) $head ), '{' );
	}

	/**
	 * Convert a MyBookTable JSON export to an MBS-compatible CSV file.
	 *
	 * The generated CSV has a UTF-8 BOM and column headers that exactly match
	 * MBS_Child_Importer_CSV_Helpers::ACCEPTED_COLUMNS. After this method
	 * returns, the caller can feed the CSV path directly into the existing
	 * MBS CSV import pipeline (parse_headers → column mapping → import).
	 *
	 * @param string $json_path Absolute path to the MyBookTable JSON export.
	 * @param string $temp_dir  Directory in which to write the output CSV.
	 * @return string|\WP_Error Absolute path to the generated CSV, or WP_Error.
	 */
	public static function to_csv_path( string $json_path, string $temp_dir ): string|\WP_Error {
		// ── Read and parse JSON ────────────────────────────────────────────────
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$json = file_get_contents( $json_path );
		if ( false === $json ) {
			return new WP_Error( 'mbt_read_failed', 'Could not read the MyBookTable export file.' );
		}

		$data = json_decode( $json, true );
		if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
			return new WP_Error( 'mbt_json_invalid', 'The file is not valid JSON: ' . json_last_error_msg() );
		}

		if ( empty( $data['books'] ) || ! is_array( $data['books'] ) ) {
			return new WP_Error( 'mbt_no_books', 'No books were found in the MyBookTable export file.' );
		}

		// Warn (but continue) if the version is not in the supported range.
		$version = (string) ( $data['version'] ?? '' );
		if ( ! empty( $version ) && ! str_starts_with( $version, self::SUPPORTED_VERSION_PREFIX ) ) {
			// Not fatal — the format may still be compatible.
			// The admin will see any parse errors in the column-mapping preview.
		}

		// ── Write CSV ─────────────────────────────────────────────────────────
		$csv_path = trailingslashit( $temp_dir ) . 'mbt_' . uniqid() . '.csv';

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		$fh = fopen( $csv_path, 'w' );
		if ( ! $fh ) {
			return new WP_Error( 'mbt_write_failed', 'Could not create temporary CSV file for import.' );
		}

		// UTF-8 BOM — ensures Excel opens the file with correct encoding.
		fwrite( $fh, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- WP_Filesystem migration deferred to PC-FILESYS sprint; PHP native file ops are safe here (temp files in get_temp_dir(), never inside wp-content)

		// Header row: ACCEPTED_COLUMNS + private passthrough columns.
		// _buy_links is not in ACCEPTED_COLUMNS (hidden from the column-mapping UI)
		// but is passed through to insert_book() via the _ -prefix passthrough rule.
		$headers = array_merge( MBS_Child_Importer_CSV_Helpers::ACCEPTED_COLUMNS, array( '_buy_links' ) );
		fputcsv( $fh, $headers );

		foreach ( $data['books'] as $book ) {
			if ( ! is_array( $book ) ) {
				continue;
			}
			fputcsv( $fh, self::book_to_row( $book ) );
		}

		fclose( $fh ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- WP_Filesystem migration deferred to PC-FILESYS sprint; PHP native file ops are safe here (temp files in get_temp_dir(), never inside wp-content)

		return $csv_path;
	}

	/**
	 * Read affiliate codes from a MyBookTable settings export and write them
	 * to MBS affiliate settings using mbs_update_setting().
	 *
	 * Only non-empty codes are written so existing MBS settings are never
	 * overwritten with blank values. Returns a summary array so the caller
	 * can surface a notice to the admin.
	 *
	 * MBT setting → MBS affiliate key mapping:
	 *   amazon_buybutton_affiliate_code → amazon
	 *   linkshare_affiliate_code        → bn    (Barnes & Noble via Rakuten/LinkShare)
	 *   (no MBT equivalent)             → kobo
	 *   (no MBT equivalent)             → audible
	 *
	 * @param string $json_path Absolute path to the MyBookTable JSON export.
	 * @return array<string,string> Map of MBS affiliate key → code that was written.
	 *                              Empty array if file is unreadable or has no codes.
	 */
	public static function apply_affiliate_settings( string $json_path ): array {
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$json = file_get_contents( $json_path );
		if ( false === $json ) {
			return array();
		}

		$data = json_decode( $json, true );
		if ( JSON_ERROR_NONE !== json_last_error() || ! isset( $data['settings'] ) ) {
			return array();
		}

		$settings = $data['settings'];

		// MBT setting key → MBS affiliate retailer key.
		$mapping = array(
			'amazon_buybutton_affiliate_code' => 'amazon',
			'linkshare_affiliate_code'        => 'bn',
		);

		$written = array();
		foreach ( $mapping as $mbt_key => $mbs_key ) {
			$code = trim( (string) ( $settings[ $mbt_key ] ?? '' ) );
			if ( '' !== $code ) {
				mbs_update_setting( 'affiliate', $mbs_key, $code );
				$written[ $mbs_key ] = $code;
			}
		}

		return $written;
	}

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

	/**
	 * Map a single MyBookTable book object to an MBS CSV row array.
	 *
	 * Column order: ACCEPTED_COLUMNS then private passthrough columns:
	 *   title, teaser, publisher, pub_date, coming_soon,
	 *   isbn13, asin, format, price,
	 *   author, series, series_number, genre, tags,
	 *   page_length, cover_image_url, cover_image_file,
	 *   _buy_links (JSON — private passthrough, not in ACCEPTED_COLUMNS)
	 *
	 * @param array $book A single book entry from the MBT JSON export.
	 * @return string[]   Row values in ACCEPTED_COLUMNS order.
	 */
	private static function book_to_row( array $book ): array {

		// title ────────────────────────────────────────────────────────────────
		$title = trim( (string) ( $book['title'] ?? '' ) );

		// teaser: prefer excerpt; fall back to content ─────────────────────────
		$teaser_raw = ! empty( $book['excerpt'] ) ? $book['excerpt'] : ( $book['content'] ?? '' );
		$teaser     = self::clean_text( (string) $teaser_raw );

		// publisher ────────────────────────────────────────────────────────────
		$publisher = trim( (string) ( $book['publisher_name'] ?? '' ) );

		// pub_date: MBT stores only the year ──────────────────────────────────
		$pub_date = trim( (string) ( $book['publication_year'] ?? '' ) );

		// coming_soon + tags: separate "coming soon" tag from real tags ────────
		// MBT tags array carries both content advisors and status markers.
		// Any tag that contains "coming soon" (case-insensitive) maps to the
		// coming_soon flag; all remaining tags map to the mbs_tag taxonomy.
		$coming_soon  = '';
		$real_tags    = array();
		foreach ( (array) ( $book['tags'] ?? array() ) as $tag ) {
			$tag_str = trim( (string) $tag );
			if ( '' === $tag_str ) {
				continue;
			}
			if ( false !== stripos( $tag_str, 'coming soon' ) ) {
				$coming_soon = '1';
			} else {
				$real_tags[] = $tag_str;
			}
		}
		$tags = implode( '|', $real_tags );

		// isbn13 ───────────────────────────────────────────────────────────────
		$isbn13 = trim( (string) ( $book['unique_id_isbn'] ?? '' ) );

		// asin ─────────────────────────────────────────────────────────────────
		$asin = trim( (string) ( $book['unique_id_asin'] ?? '' ) );

		// format: not available in MBT exports ────────────────────────────────
		$format = '';

		// price ────────────────────────────────────────────────────────────────
		$price = trim( (string) ( $book['price'] ?? '' ) );

		// author: MBT 3.x always exports an empty authors array; use first item
		// if ever populated in future versions ────────────────────────────────
		$authors = (array) ( $book['authors'] ?? array() );
		$author  = ! empty( $authors ) ? trim( (string) ( $authors[0] ?? '' ) ) : '';

		// series: first element; decode HTML entities (e.g. &amp; → &) ────────
		$series_arr = (array) ( $book['series'] ?? array() );
		$series     = '';
		if ( ! empty( $series_arr ) ) {
			$series = self::clean_text( (string) ( $series_arr[0] ?? '' ) );
		}

		// series_number ────────────────────────────────────────────────────────
		$series_number = trim( (string) ( $book['series_order'] ?? '' ) );

		// genre: all elements from MBT genres array, pipe-delimited ─────────
		// MBT books frequently carry 2+ genres (e.g. Military|Romance).
		$genres_arr  = array_filter( array_map( 'trim', (array) ( $book['genres'] ?? array() ) ) );
		$genre       = implode( '|', $genres_arr );

		// page_length ──────────────────────────────────────────────────────────
		$page_length = trim( (string) ( $book['book_length'] ?? '' ) );

		// cover_image_url: full remote URL — deferred sideload after import ───
		$cover_image_url = trim( (string) ( $book['image_src'] ?? '' ) );

		// cover_image_file: not applicable for remote-URL imports ─────────────
		$cover_image_file = '';

		// _buy_links: private passthrough column — JSON-encoded array of buy
		// link objects, one per MBT buybutton entry. Passed to insert_book()
		// which decodes and saves each via MBS_Buy_Link_DAO::save().
		//
		// MBT → MBS retailer key mapping:
		//   amazon    → 'amazon'
		//   bnn       → 'bn'       (Barnes & Noble)
		//   ibooks    → 'apple'    (Apple Books)
		//   kobo      → 'kobo'
		//   googleplay→ 'google'
		//   universal → null, is_direct_author = 1 (direct/custom link)
		$buy_links_data = array();
		foreach ( (array) ( $book['buybuttons'] ?? array() ) as $btn ) {
			$btn_url = trim( (string) ( $btn['url'] ?? '' ) );
			if ( '' === $btn_url ) {
				continue; // Skip buttons with no URL.
			}
			$mbt_store        = (string) ( $btn['store'] ?? '' );
			$retailer_key     = MBS_Importer_MyBookTable_Mapper::RETAILER_MAP[ $mbt_store ] ?? null;
			$is_direct_author = ( 'universal' === $mbt_store ) ? 1 : 0;
			$buy_links_data[] = array(
				'retailer_key'     => $retailer_key,
				'url'              => $btn_url,
				'is_direct_author' => $is_direct_author,
			);
		}
		$buy_links = ! empty( $buy_links_data ) ? wp_json_encode( $buy_links_data ) : '';

		return array(
			$title, $teaser, $publisher, $pub_date, $coming_soon,
			$isbn13, $asin, $format, $price,
			$author, $series, $series_number, $genre, $tags,
			$page_length, $cover_image_url, $cover_image_file,
			$buy_links,
		);
	}

	/**
	 * Strip HTML tags, decode HTML entities, and normalise whitespace.
	 *
	 * Used for teaser (excerpt/content) and series name fields that arrive
	 * from MBT with HTML encoding.
	 *
	 * @param string $raw Raw HTML-encoded string.
	 * @return string     Clean plain text.
	 */
	private static function clean_text( string $raw ): string {
		// Strip HTML tags first (handles <div>, <a>, <br>, etc.).
		$clean = wp_strip_all_tags( $raw );
		// Decode HTML entities (&amp; → &, &nbsp; → space, etc.).
		$clean = html_entity_decode( $clean, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		// Normalise runs of whitespace (including \r\n from MBT exports) to single spaces.
		$clean = preg_replace( '/\s+/', ' ', $clean ) ?? $clean;
		return trim( $clean );
	}
}
