<?php
/**
 * File Path: includes/import/class-mbs-epub-look-inside-extractor.php
 *
 * EPUB Importer -- Look Inside Extractor
 *
 * Locates the copyright / colophon page and Look Inside candidates in an EPUB
 * spine. Receives an open ZipArchive, OPF directory prefix, and the pre-built
 * spine map and TOC label map from MBS_EPUB_OPF_Reader via MBS_EPUB_Parser.
 * Modifies the data array in place via extract().
 *
 * @package MyBookShowroom
 * @since   1.2.0
 */

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

/**
 * Class MBS_EPUB_Look_Inside_Extractor
 *
 * Extracts copyright-page fields and Look Inside candidates from an EPUB
 * archive. Receives an open ZipArchive and opf_dir from MBS_EPUB_Parser;
 * never opens or closes the ZIP handle.
 *
 * @since 1.2.0
 */
class MBS_EPUB_Look_Inside_Extractor { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/** @var ZipArchive Open archive handle (owned by MBS_EPUB_Parser). */
	private ZipArchive $zip;

	/** @var string Directory prefix for resolving manifest hrefs. */
	private string $opf_dir;

	// -- Public API ----------------------------------------------------------

	/**
	 * Constructor.
	 *
	 * @since 1.2.0
	 * @param ZipArchive $zip     Open archive handle.
	 * @param string     $opf_dir Directory prefix for manifest hrefs (may be empty).
	 */
	public function __construct( ZipArchive $zip, string $opf_dir ) {
		$this->zip     = $zip;
		$this->opf_dir = $opf_dir;
	}

	/**
	 * Extract copyright-page fields and Look Inside candidates into $data.
	 *
	 * @since  1.2.0
	 * @param  array  $spine_map     Spine id-to-href map from MBS_EPUB_OPF_Reader.
	 * @param  array  $toc_label_map TOC href-basename-to-label map from MBS_EPUB_OPF_Reader.
	 * @param  array &$data          Data array from MBS_EPUB_OPF_Reader; modified in place.
	 * @return void
	 */
	public function extract( array $spine_map, array $toc_label_map, array &$data ): void {
		// Copyright page -- best-effort; failure is non-fatal.
		$cp_path = $this->find_copyright_page( $spine_map );
		if ( false !== $cp_path ) {
			$cp_html = $this->zip->getFromName( $cp_path );
			if ( false !== $cp_html ) {
				$this->parse_copyright_page( $cp_html, $data );
			}
		}

		// Look Inside candidates -- best-effort; failure is non-fatal.
		$this->find_look_inside_candidates( $spine_map, $toc_label_map, $data );
	}

	// -- Private helpers -----------------------------------------------------

	/**
	 * Locate the copyright / colophon document in the EPUB spine.
	 * Implements the heuristic priority defined in EPUB-001 ss7a.
	 *
	 * @param  array $spine_map Spine id-to-href map.
	 * @return string|false ZIP-relative path to the copyright document, or false.
	 */
	private function find_copyright_page( array $spine_map ): string|false {
		if ( empty( $spine_map ) ) {
			return false;
		}

		$copyright_keywords = [ 'copyright', 'colophon', 'rights' ];
		$cover_seen         = false;

		foreach ( $spine_map as $idref => $href ) {
			$base = strtolower( basename( $href ) );

			// Heuristic 1: filename contains a copyright keyword.
			foreach ( $copyright_keywords as $kw ) {
				if ( str_contains( $base, $kw ) ) {
					return $this->opf_dir . $href;
				}
			}

			if ( str_contains( $base, 'cover' ) ) {
				$cover_seen = true;
			}

			// Heuristic 2: spine doc after cover that contains copyright markers.
			if ( $cover_seen ) {
				$full_path = $this->opf_dir . $href;
				$content   = $this->zip->getFromName( $full_path );
				if ( false !== $content ) {
					if ( str_contains( $content, 'Copyright' ) || str_contains( $content, 'PUBLISHED BY' ) ) {
						return $full_path;
					}
				}
			}
		}

		return false;
	}

	/**
	 * Parse copyright / colophon XHTML and extract CIP block fields.
	 * Populates $data as a fallback source per EPUB-001 ss7.
	 *
	 * @param  string $html Raw XHTML bytes.
	 * @param  array &$data Data array to populate.
	 * @return void
	 */
	private function parse_copyright_page( string $html, array &$data ): void {
		$text = preg_replace( '/<(?:p|div|h[1-6]|li|tr|td|th|br|blockquote|section|article|header|footer)(?:\s[^>]*)?>/', "\n", $html );
		$text = wp_strip_all_tags( $text );
		$text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );

		// Copyright year
		if ( '' === $data['copyright_year'] ) {
			if ( preg_match( '/Copyright[^0-9]{0,20}(\d{4}(?:[\s,]+\d{4})*)\s+by/i', $text, $m ) ) {
				preg_match_all( '/(\d{4})/', $m[1], $yrs );
				if ( ! empty( $yrs[1] ) ) {
					$data['copyright_year'] = (string) max( array_map( 'intval', $yrs[1] ) );
				}
			}
		}

		// Publisher (fallback)
		if ( '' === $data['publisher'] ) {
			if ( preg_match( '/PUBLISHED BY[:\s]*([^,\n]+)/i', $text, $m ) ) {
				$pub = trim( preg_replace( '/[*]+/', '', $m[1] ) );
				if ( '' !== $pub ) {
					$data['publisher'] = sanitize_text_field( $pub );
				}
			}
		}

		// Publisher address (preview only)
		if ( '' === $data['pub_address'] ) {
			if ( preg_match( '/PUBLISHED BY[^,\n]+,\s*([^\n]+)/i', $text, $m ) ) {
				$addr = trim( $m[1] );
				if ( preg_match( '/^(.+?\d{5}(?:[- ]\d{4})?)/s', $addr, $am ) ) {
					$addr = rtrim( $am[1], '. ' );
				}
				if ( '' !== $addr ) {
					$data['pub_address'] = sanitize_text_field( $addr );
				}
			}
		}

		// LCCN (fallback)
		if ( '' === $data['lccn'] ) {
			if ( preg_match( '/LCCN[:\s]*(\d{7,10})/i', $text, $m ) ) {
				$data['lccn'] = $m[1];
			}
		}

		// LC record URL (preview only)
		if ( '' === $data['lc_url'] ) {
			if ( preg_match( '#(https?://lccn\.loc\.gov/\S+)#i', $text, $m ) ) {
				$data['lc_url'] = esc_url_raw( $m[1] );
			}
		}

		// All ISBNs from copyright page: "978-x-xxxxx-xxx-x (label)"
		if ( preg_match_all( '/(978[-\d]+)\s*\(([^)]+)\)/i', $text, $matches, PREG_SET_ORDER ) ) {
			foreach ( $matches as $match ) {
				$isbn  = MBS_ISBN_Helper::normalize_isbn( $match[1] );
				$label = trim( $match[2] );
				if ( ! $isbn ) {
					continue;
				}
				$ll = strtolower( $label );
				// Ebook ISBN fallback
				if ( '' === $data['isbn13'] ) {
					if ( str_contains( $ll, 'ebk' ) || str_contains( $ll, 'ebook' )
					     || str_contains( $ll, 'epub' ) || str_contains( $ll, 'digital' ) ) {
						$data['isbn13'] = $isbn;
						continue;
					}
				}
				// Skip duplicates already in other_isbns
				$dup = false;
				foreach ( $data['other_isbns'] as $ex ) {
					if ( $ex['isbn'] === $isbn ) {
						$dup = true;
						break;
					}
				}
				if ( ! $dup ) {
					$data['other_isbns'][] = [ 'isbn' => $isbn, 'label' => $label ];
				}
			}
		}

		// Trim size (preview only -- no direct MBS post meta field)
		if ( '' === $data['trim_size'] ) {
			if ( preg_match( '/(\d+(?:\.\d+)?)\s*in\.\s*[xX×]\s*(\d+(?:\.\d+)?)\s*in\./iu', $text, $m ) ) {
				$data['trim_size'] = $m[1] . ' x ' . $m[2] . ' in.';
			}
		}

		// Page count
		if ( '' === $data['page_count'] ) {
			if ( preg_match( '/(\d+)\s*p\.\s+\d/i', $text, $m ) ) {
				$data['page_count'] = $m[1];
			}
		}

		// Series + book number (fallback)
		if ( '' === $data['series_name'] ) {
			if ( preg_match( '/book\s+(\d+)\s+of\s+([^.]+?)(?:\s+\w+\s+[Ee]dition|\.)/i', $text, $m ) ) {
				$data['series_num'] = $m[1];
				$raw_series = trim( $m[2] );
				// Strip trailing edition suffix per EPUB-001 ss7d.
				$raw_series = trim( preg_replace( '/\s+\w+\s+[Ee]dition$/i', '', $raw_series ) );
				if ( '' !== $raw_series ) {
					$data['series_name'] = $raw_series;
				}
			}
		}

		// Cover artist (preview only -- no MBS metadata field)
		if ( '' === $data['cover_artist'] ) {
			if ( preg_match( '/[Cc]over\s+(?:design|art|illustration|image)\s+by\s+([^(\n.]+)/i', $text, $m ) ) {
				$data['cover_artist'] = sanitize_text_field( trim( $m[1] ) );
			}
		}

		// Title fallback: first short non-copyright line of the page.
		if ( '' === $data['title'] ) {
			$lines = array_filter( array_map( 'trim', preg_split( '/\r?\n/', $text ) ) );
			foreach ( $lines as $line ) {
				if ( false !== stripos( $line, 'copyright' ) ) {
					break;
				}
				if ( strlen( $line ) > 3 && strlen( $line ) < 120 ) {
					$data['title'] = sanitize_text_field( $line );
					break;
				}
			}
		}
	}

	/**
	 * Detect Look Inside candidates from the spine and store sanitized HTML in $data.
	 * Finds up to 2 candidates: a Prologue and a Chapter 1 (first match of each).
	 *
	 * @since  1.3.0
	 * @param  array  $spine_map     Spine id-to-href map.
	 * @param  array  $toc_label_map TOC href-basename-to-label map.
	 * @param  array &$data          Data array; 'look_inside_candidates' key populated.
	 * @return void
	 */
	private function find_look_inside_candidates( array $spine_map, array $toc_label_map, array &$data ): void {
		if ( empty( $spine_map ) ) {
			return;
		}

		// Definitively non-prose entries. Intentionally excludes prologue, preface,
		// foreword, and introduction so those entries remain detectable.
		$exclude_pattern = '/^(copyright|dedication|colophon|acknowledgments?|about the author|'
			. 'find .+ online|newsletter|more .+ books|reader.?s guide|translation key|'
			. 'glossary|appendix|bibliography|index|notes|afterword|conclusion|'
			. 'discussion questions|inspired by|half.?title|title.?page|epigraph|'
			. 'list of illustrations|list of maps|list of tables|list of figures|'
			. 'cover|table of contents|contents)$/iu';

		$found_prologue = false;
		$found_chapter1 = false;

		foreach ( $spine_map as $idref => $href ) {
			if ( $found_prologue && $found_chapter1 ) {
				break;
			}

			$label = $toc_label_map[ basename( $href ) ] ?? '';

			// Skip definitively non-prose entries.
			if ( '' !== $label && preg_match( $exclude_pattern, $label ) ) {
				continue;
			}

			// Minimum word count check.
			$raw_html = $this->extract_spine_doc_html( $href );
			if ( str_word_count( wp_strip_all_tags( $raw_html ) ) < 200 ) {
				continue;
			}

			// Detect Prologue candidate.
			$is_prologue = preg_match( '/\bprologue\b|\bprol\.?\b|\bpreface\b/i', $label );
			// Filename fallback: label empty but filename suggests prologue.
			if ( ! $is_prologue && '' === $label ) {
				$is_prologue = preg_match( '/prol(ogue)?|preface/i', pathinfo( basename( $href ), PATHINFO_FILENAME ) );
			}
			if ( ! $found_prologue && $is_prologue ) {
				$data['look_inside_candidates'][] = [
					'label' => 'Prologue',
					'html'  => MBS_Look_Inside_Sanitizer::clean( MBS_Look_Inside_Sanitizer::truncate_at_chapter_boundary( $raw_html ), true, MBS_LOOK_INSIDE_WORD_LIMIT ),
				];
				$found_prologue = true;
				continue;
			}

			// Detect Chapter 1 candidate (word-boundary anchors prevent false
			// matches on "Chapter 10", "Chapter 11", etc.).
			// Also matches bare "1" or "01" labels (common in professional EPUBs).
			$is_chapter1 = preg_match( '/\bch(apter)?\.?\s*(1|01|one)\b/i', $label )
				|| preg_match( '/^(1|01)\.?$/', trim( $label ) );
			// Filename fallback: label empty but filename suggests chapter 1.
			if ( ! $is_chapter1 && '' === $label ) {
				$is_chapter1 = preg_match( '/ch(apter)?[-_.\s]*(01?|one)\b/i', pathinfo( basename( $href ), PATHINFO_FILENAME ) );
			}
			if ( ! $found_chapter1 && $is_chapter1 ) {
				$data['look_inside_candidates'][] = [
					'label' => 'Chapter 1',
					'html'  => MBS_Look_Inside_Sanitizer::clean( MBS_Look_Inside_Sanitizer::truncate_at_chapter_boundary( $raw_html ), true, MBS_LOOK_INSIDE_WORD_LIMIT ),
				];
				$found_chapter1 = true;
			}
		}

		// Last-resort fallback: label heuristics found nothing -- grab the first
		// spine document that passes the word-count and exclusion checks.
		if ( empty( $data['look_inside_candidates'] ) ) {
			foreach ( $spine_map as $idref => $href ) {
				$label    = $toc_label_map[ basename( $href ) ] ?? '';
				if ( '' !== $label && preg_match( $exclude_pattern, $label ) ) {
					continue;
				}
				$raw_html = $this->extract_spine_doc_html( $href );
				if ( str_word_count( wp_strip_all_tags( $raw_html ) ) >= 200 ) {
					$data['look_inside_candidates'][] = [
						'label' => '' !== $label ? $label : 'Look Inside',
						'html'  => MBS_Look_Inside_Sanitizer::clean( MBS_Look_Inside_Sanitizer::truncate_at_chapter_boundary( $raw_html ), true, MBS_LOOK_INSIDE_WORD_LIMIT ),
					];
					break;
				}
			}
		}
	}

	/**
	 * Extract body innerHTML from a spine XHTML document.
	 *
	 * @since  1.3.0
	 * @param  string $href Manifest href relative to the OPF directory.
	 * @return string Body innerHTML, or empty string on failure.
	 */
	private function extract_spine_doc_html( string $href ): string {
		$raw = $this->zip->getFromName( $this->opf_dir . $href );
		if ( false === $raw ) {
			return '';
		}
		if ( ! preg_match( '/<body[^>]*>(.*?)<\/body>/is', $raw, $m ) ) {
			return '';
		}
		return $m[1];
	}
}
