<?php
/**
 * File Path: includes/export/class-mbs-xlsx-builder.php
 *
 * XLSX Builder — Generic OOXML Writer
 *
 * Structure written (no styles or shared-strings table required):
 *   [Content_Types].xml
 *   _rels/.rels
 *   xl/workbook.xml
 *   xl/_rels/workbook.xml.rels
 *   xl/worksheets/sheet1.xml
 *
 * All cell values are written as inline strings (t="inlineStr") so
 * ISBNs, prices, and other numeric-looking values are never subject to
 * Excel's automatic number reformatting.
 *
 * Requires PHP's built-in ZipArchive extension.
 *
 * @package MyBookShowroom
 * @since   1.0.0
 */

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

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

	/**
	 * Build a minimal OOXML XLSX file from a 2-D array of string rows.
	 *
	 * This is the sole public entry point. The caller is responsible for
	 * assembling $rows (including any header row) before calling this method.
	 *
	 * @param array  $rows      2-D array of string arrays; first row = headers.
	 * @param string $dest_path Absolute path where the XLSX file will be written.
	 * @return true|\WP_Error   true on success; WP_Error on failure.
	 */
	public static function build( array $rows, string $dest_path ): true|\WP_Error {
		if ( ! class_exists( 'ZipArchive' ) ) {
			return new WP_Error(
				'no_ziparchive',
				'ZipArchive is not available on this server. Please contact your host to enable the zip extension.'
			);
		}

		$zip = new ZipArchive();
		$rc  = $zip->open( $dest_path, ZipArchive::CREATE );
		if ( true !== $rc ) {
			return new WP_Error(
				'zip_open_failed',
				'Could not create XLSX temp file (ZipArchive error ' . $rc . ').'
			);
		}

		$zip->addFromString( '[Content_Types].xml',        self::xml_content_types() );
		$zip->addFromString( '_rels/.rels',                self::xml_root_rels() );
		$zip->addFromString( 'xl/workbook.xml',            self::xml_workbook() );
		$zip->addFromString( 'xl/_rels/workbook.xml.rels', self::xml_workbook_rels() );
		$zip->addFromString( 'xl/worksheets/sheet1.xml',   self::xml_sheet( $rows ) );

		$zip->close();

		if ( ! file_exists( $dest_path ) ) {
			return new WP_Error( 'xlsx_not_written', 'XLSX file was not written to disk.' );
		}

		return true;
	}

	// ── Multi-sheet public API (1.3.0) ─────────────────────────────────────

	/**
	 * Build a multi-sheet OOXML XLSX file from a named-sheet array.
	 *
	 * @param array  $sheets    ['Sheet Name' => [[row],...]] in tab order.
	 * @param string $dest_path Absolute path where the XLSX will be written.
	 * @return true|WP_Error
	 * @since 1.3.0
	 */
	public static function build_multi_sheet( array $sheets, string $dest_path ): true|WP_Error {
		if ( ! class_exists( 'ZipArchive' ) ) {
			return new WP_Error( 'no_ziparchive', 'ZipArchive is not available on this server.' );
		}
		$zip = new ZipArchive();
		$rc  = $zip->open( $dest_path, ZipArchive::CREATE );
		if ( true !== $rc ) {
			return new WP_Error( 'zip_open_failed', 'Could not create XLSX temp file (error ' . $rc . ').' );
		}
		$sheet_names = array_keys( $sheets );
		$sheet_count = count( $sheet_names );
		$zip->addFromString( '[Content_Types].xml',        self::xml_content_types_multi( $sheet_count ) );
		$zip->addFromString( '_rels/.rels',                self::xml_root_rels() );
		$zip->addFromString( 'xl/workbook.xml',            self::xml_workbook_multi( $sheet_names ) );
		$zip->addFromString( 'xl/_rels/workbook.xml.rels', self::xml_workbook_rels_multi( $sheet_count ) );
		$i = 1;
		foreach ( $sheets as $rows ) {
			$zip->addFromString( 'xl/worksheets/sheet' . $i . '.xml', self::xml_sheet( $rows ) );
			$i++;
		}
		$zip->close();
		if ( ! file_exists( $dest_path ) ) {
			return new WP_Error( 'xlsx_not_written', 'Multi-sheet XLSX file was not written to disk.' );
		}
		return true;
	}

	/**
	 * Generate [Content_Types].xml for a multi-sheet XLSX file.
	 *
	 * @param int $sheet_count Number of worksheets.
	 * @return string XML string.
	 */
	private static function xml_content_types_multi( int $sheet_count ): string {
		$ns_ct = 'http://schemas.openxmlformats.org/package/2006/content-types';
		$parts = '';
		for ( $i = 1; $i <= $sheet_count; $i++ ) {
			$parts .= '<Override PartName="/xl/worksheets/sheet' . $i . '.xml"'
				. ' ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
		}
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<Types xmlns="' . $ns_ct . '">'
			. '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
			. '<Default Extension="xml" ContentType="application/xml"/>'
			. '<Override PartName="/xl/workbook.xml"'
			. ' ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
			. $parts
			. '</Types>';
	}

	/**
	 * Generate xl/workbook.xml for a multi-sheet XLSX file.
	 *
	 * @param string[] $sheet_names Sheet names in tab order.
	 * @return string XML string.
	 */
	private static function xml_workbook_multi( array $sheet_names ): string {
		$ns  = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
		$r   = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
		$sheets = '';
		$i = 1;
		foreach ( $sheet_names as $name ) {
			$safe    = htmlspecialchars( $name, ENT_XML1 | ENT_QUOTES, 'UTF-8' );
			$sheets .= '<sheet name="' . $safe . '" sheetId="' . $i . '" r:id="rId' . $i . '"/>';
			$i++;
		}
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<workbook xmlns="' . $ns . '" xmlns:r="' . $r . '">'
			. '<sheets>' . $sheets . '</sheets>'
			. '</workbook>';
	}

	/**
	 * Generate xl/_rels/workbook.xml.rels for a multi-sheet XLSX file.
	 *
	 * @param int $sheet_count Number of worksheets.
	 * @return string XML string.
	 */
	private static function xml_workbook_rels_multi( int $sheet_count ): string {
		$ns   = 'http://schemas.openxmlformats.org/package/2006/relationships';
		$type = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
		$rels = '';
		for ( $i = 1; $i <= $sheet_count; $i++ ) {
			$rels .= '<Relationship Id="rId' . $i . '" Type="' . $type . '"'
				. ' Target="worksheets/sheet' . $i . '.xml"/>';
		}
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<Relationships xmlns="' . $ns . '">'
			. $rels
			. '</Relationships>';
	}

	// ── Worksheet ─────────────────────────────────────────────────────────────

	/**
	 * Build the worksheet XML for the given rows.
	 *
	 * All cells are written as inline strings. Keeping numeric-looking values
	 * (ISBN-13, price, page count) as strings prevents Excel from reformatting
	 * them (e.g. converting a 13-digit ISBN to scientific notation).
	 *
	 * @param array $rows 2-D array of string arrays.
	 * @return string XML content for xl/worksheets/sheet1.xml.
	 */
	private static function xml_sheet( array $rows ): string {
		$xml  = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n";
		$xml .= '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">' . "\n";
		$xml .= '<sheetData>' . "\n";

		$row_num = 1;
		foreach ( $rows as $row ) {
			$xml    .= '<row r="' . $row_num . '">' . "\n";
			$col_idx = 0;
			foreach ( $row as $value ) {
				$cell_ref = self::col_letter( $col_idx ) . $row_num;
				$escaped  = self::xml_escape( (string) $value );
				$xml     .= '<c r="' . $cell_ref . '" t="inlineStr"><is><t>' . $escaped . '</t></is></c>' . "\n";
				$col_idx++;
			}
			$xml .= '</row>' . "\n";
			$row_num++;
		}

		$xml .= '</sheetData>' . "\n";
		$xml .= '</worksheet>' . "\n";

		return $xml;
	}

	/**
	 * Convert a zero-based column index to an Excel column letter (A … Z, AA … AZ, …).
	 *
	 * @param int $index Zero-based column index.
	 * @return string    Excel column letter(s).
	 */
	private static function col_letter( int $index ): string {
		$result = '';
		$n      = $index + 1; // convert to 1-based
		while ( $n > 0 ) {
			$n--;
			$result = chr( 65 + ( $n % 26 ) ) . $result;
			$n      = intdiv( $n, 26 );
		}
		return $result;
	}

	/**
	 * Escape a raw string value for safe embedding inside OOXML XML.
	 *
	 * Strips control characters (except tab, LF, CR) that are illegal in XML 1.0
	 * before escaping XML special characters.
	 *
	 * @param string $value Raw cell value.
	 * @return string       XML-safe value.
	 */
	private static function xml_escape( string $value ): string {
		// Remove XML 1.0 illegal control characters (U+0000–U+001F except TAB, LF, CR).
		// phpcs:ignore WordPress.PHP.PregQuoteDelimiter -- delimiter not used in pattern
		$value = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value );
		return htmlspecialchars( (string) $value, ENT_XML1 | ENT_QUOTES, 'UTF-8' );
	}

	// ── Static OOXML fragments ────────────────────────────────────────────────

	/** @return string [Content_Types].xml */
	private static function xml_content_types(): string {
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n"
			. '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' . "\n"
			. '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' . "\n"
			. '<Default Extension="xml" ContentType="application/xml"/>' . "\n"
			. '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' . "\n"
			. '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' . "\n"
			. '</Types>' . "\n";
	}

	/** @return string _rels/.rels */
	private static function xml_root_rels(): string {
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n"
			. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' . "\n"
			. '<Relationship Id="rId1"'
			. ' Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"'
			. ' Target="xl/workbook.xml"/>' . "\n"
			. '</Relationships>' . "\n";
	}

	/** @return string xl/workbook.xml */
	private static function xml_workbook(): string {
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n"
			. '<workbook'
			. ' xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"'
			. ' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">' . "\n"
			. '<sheets>' . "\n"
			. '<sheet name="Books" sheetId="1" r:id="rId1"/>' . "\n"
			. '</sheets>' . "\n"
			. '</workbook>' . "\n";
	}

	/** @return string xl/_rels/workbook.xml.rels */
	private static function xml_workbook_rels(): string {
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n"
			. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">' . "\n"
			. '<Relationship Id="rId1"'
			. ' Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"'
			. ' Target="worksheets/sheet1.xml"/>' . "\n"
			. '</Relationships>' . "\n";
	}
}
