<?php
/**
 * Import/export Excel de pendientes de compras del panel personal.
 *
 * @package Logistica
 */

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

/**
 * Exporta e importa cotizaciones/compras del panel.
 */
class Logistica_User_Panel_Compras_Excel {

	/**
	 * @return array<string, string>
	 */
	public static function columns() {
		return array(
			'producto_ofrecido' => __( 'Producto ofrecido', 'logistica' ),
			'referencia'        => __( 'Referencia', 'logistica' ),
			'precio'            => __( 'Precio', 'logistica' ),
			'cantidad'          => __( 'Cantidad', 'logistica' ),
			'unidad_medida'     => __( 'Unidad de medida', 'logistica' ),
			'fecha_requerida'   => __( 'Fecha requerida', 'logistica' ),
			'fecha_solicitud'   => __( 'Fecha solicitud', 'logistica' ),
			'status'            => __( 'Estado', 'logistica' ),
			'priority'          => __( 'Prioridad', 'logistica' ),
			'escala'            => __( 'Escala', 'logistica' ),
			'empresa'           => __( 'Empresa', 'logistica' ),
			'fecha'             => __( 'Fecha', 'logistica' ),
			'documento_numero'  => __( 'Nº documento', 'logistica' ),
		);
	}

	/**
	 * @param array<int, array<string, mixed>> $compras     Compras.
	 * @param array<int, array<string, mixed>> $proveedores Proveedores.
	 * @return void
	 */
	public static function download( $compras, $proveedores = array() ) {
		$rows = self::prepare_export_rows( $compras, $proveedores );

		if ( class_exists( 'ZipArchive', false ) ) {
			self::download_xlsx( $rows );
		}

		self::download_csv( $rows );
	}

	/**
	 * @param array<int, array<string, mixed>> $compras     Compras.
	 * @param array<int, array<string, mixed>> $proveedores Proveedores.
	 * @return array<int, array<string, string>>
	 */
	public static function prepare_export_rows( $compras, $proveedores = array() ) {
		$by_id = array();
		foreach ( $proveedores as $proveedor ) {
			if ( ! is_array( $proveedor ) ) {
				continue;
			}
			$id = (string) ( $proveedor['id'] ?? '' );
			if ( '' !== $id ) {
				$by_id[ $id ] = $proveedor;
			}
		}

		$rows = array();
		foreach ( $compras as $compra ) {
			if ( ! is_array( $compra ) ) {
				continue;
			}

			$status_key   = sanitize_key( (string) ( $compra['status'] ?? 'pendiente' ) );
			$priority_key = sanitize_key( (string) ( $compra['priority'] ?? 'media' ) );
			if ( ! isset( Logistica_User_Panel::get_priorities()[ $priority_key ] ) ) {
				$priority_key = 'media';
			}
			$prov_id = (string) ( $compra['proveedor_id'] ?? '' );

			$rows[] = array(
				'producto_ofrecido' => (string) ( $compra['producto_ofrecido'] ?? $compra['item'] ?? '' ),
				'referencia'        => (string) ( $compra['referencia'] ?? '' ),
				'precio'            => (string) ( $compra['precio'] ?? '' ),
				'cantidad'          => (string) ( $compra['cantidad'] ?? '' ),
				'unidad_medida'     => (string) ( $compra['unidad_medida'] ?? '' ),
				'fecha_requerida'   => (string) ( $compra['fecha_requerida'] ?? '' ),
				'fecha_solicitud'   => (string) ( $compra['fecha_solicitud'] ?? '' ),
				'status'            => Logistica_User_Panel::get_compra_statuses()[ $status_key ] ?? $status_key,
				'priority'          => Logistica_User_Panel::get_priorities()[ $priority_key ] ?? $priority_key,
				'escala'            => (string) ( $compra['escala'] ?? $compra['quantity'] ?? '' ),
				'empresa'           => (string) ( $by_id[ $prov_id ]['nombre'] ?? '' ),
				'fecha'             => (string) ( $compra['fecha'] ?? '' ),
				'documento_numero'  => (string) ( $compra['documento_numero'] ?? '' ),
			);
		}

		return $rows;
	}

	/**
	 * @param array $file $_FILES row.
	 * @return array<int, array<string, string>>|\WP_Error
	 */
	public static function parse_upload( $file ) {
		if ( empty( $file['tmp_name'] ) || ! is_uploaded_file( $file['tmp_name'] ) ) {
			return new WP_Error( 'file', __( 'No se recibió ningún archivo.', 'logistica' ) );
		}

		$content  = file_get_contents( $file['tmp_name'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$filename = strtolower( (string) ( $file['name'] ?? '' ) );

		if ( false === $content ) {
			return new WP_Error( 'file', __( 'No se pudo leer el archivo.', 'logistica' ) );
		}

		return self::parse_file( $content, $filename );
	}

	/**
	 * @param string $content Contenido.
	 * @param string $filename Nombre archivo.
	 * @return array<int, array<string, string>>|\WP_Error
	 */
	private static function parse_file( $content, $filename ) {
		$extension = strtolower( pathinfo( $filename, PATHINFO_EXTENSION ) );

		if ( in_array( $extension, array( 'xlsx', 'xlsm' ), true ) ) {
			return self::parse_xlsx( $content );
		}

		return self::parse_csv( $content );
	}

	/**
	 * @param string $content CSV.
	 * @return array<int, array<string, string>>|\WP_Error
	 */
	private static function parse_csv( $content ) {
		$content = preg_replace( '/^\xEF\xBB\xBF/', '', $content );
		$lines   = preg_split( '/\r\n|\r|\n/', $content );
		if ( ! is_array( $lines ) || count( $lines ) < 2 ) {
			return new WP_Error( 'empty', __( 'El archivo no contiene filas de datos.', 'logistica' ) );
		}

		$delimiter = str_contains( (string) $lines[0], ';' ) ? ';' : ',';
		$header    = str_getcsv( array_shift( $lines ), $delimiter );
		$map       = self::map_headers( $header );

		if ( ! in_array( 'producto_ofrecido', $map, true ) ) {
			return new WP_Error( 'headers', __( 'La plantilla debe incluir la columna Producto ofrecido.', 'logistica' ) );
		}

		$rows = array();
		foreach ( $lines as $line ) {
			if ( '' === trim( $line ) ) {
				continue;
			}

			$cells = str_getcsv( $line, $delimiter );
			$row   = self::cells_to_row( $cells, $map );
			if ( '' !== ( $row['producto_ofrecido'] ?? '' ) ) {
				$rows[] = $row;
			}
		}

		return $rows;
	}

	/**
	 * @param string $content Binario xlsx.
	 * @return array<int, array<string, string>>|\WP_Error
	 */
	private static function parse_xlsx( $content ) {
		if ( ! class_exists( 'ZipArchive', false ) ) {
			return new WP_Error( 'zip', __( 'El servidor no puede leer archivos .xlsx. Guarda como CSV e importa de nuevo.', 'logistica' ) );
		}

		$tmp = wp_tempnam( 'compras-import.xlsx' );
		if ( ! $tmp || false === file_put_contents( $tmp, $content ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
			return new WP_Error( 'tmp', __( 'No se pudo procesar el archivo subido.', 'logistica' ) );
		}

		$zip = new ZipArchive();
		if ( true !== $zip->open( $tmp ) ) {
			@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			return new WP_Error( 'xlsx', __( 'No se pudo abrir el archivo .xlsx.', 'logistica' ) );
		}

		$sheet_xml = $zip->getFromName( 'xl/worksheets/sheet1.xml' );
		$shared    = $zip->getFromName( 'xl/sharedStrings.xml' );
		$zip->close();
		@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged

		if ( false === $sheet_xml || '' === trim( $sheet_xml ) ) {
			return new WP_Error( 'xlsx', __( 'No se encontró la hoja de datos en el archivo .xlsx.', 'logistica' ) );
		}

		$shared_strings = self::parse_shared_strings( false === $shared ? '' : $shared );
		$sheet          = simplexml_load_string( $sheet_xml );
		if ( false === $sheet ) {
			return new WP_Error( 'xlsx', __( 'No se pudo interpretar la hoja del archivo .xlsx.', 'logistica' ) );
		}

		$sheet->registerXPathNamespace( 'm', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' );
		$row_nodes = $sheet->xpath( '//m:sheetData/m:row' );
		if ( empty( $row_nodes ) ) {
			return new WP_Error( 'xlsx', __( 'El archivo .xlsx no contiene filas de datos.', 'logistica' ) );
		}

		$map  = array();
		$rows = array();

		foreach ( $row_nodes as $row_node ) {
			$row_node->registerXPathNamespace( 'm', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' );
			$cells = array();
			foreach ( $row_node->xpath( 'm:c' ) as $cell ) {
				$ref   = (string) $cell['r'];
				$col   = preg_replace( '/\d+/', '', $ref );
				$value = self::xlsx_cell_value( $cell, $shared_strings );
				$cells[ self::column_index( $col ) ] = $value;
			}

			if ( empty( $cells ) ) {
				continue;
			}

			ksort( $cells );
			$values = array_values( $cells );

			if ( empty( $map ) ) {
				$candidate_map = self::map_headers( $values );
				if ( in_array( 'producto_ofrecido', $candidate_map, true ) ) {
					$map = $candidate_map;
				}
				continue;
			}

			$row = self::cells_to_row( $values, $map );
			if ( '' !== ( $row['producto_ofrecido'] ?? '' ) ) {
				$rows[] = $row;
			}
		}

		if ( ! in_array( 'producto_ofrecido', $map, true ) ) {
			return new WP_Error( 'headers', __( 'La plantilla debe incluir la columna Producto ofrecido.', 'logistica' ) );
		}

		return $rows;
	}

	/**
	 * @param array<int, string> $header Encabezados.
	 * @return array<int, string>
	 */
	private static function map_headers( $header ) {
		$aliases = array(
			'producto_ofrecido' => array( 'producto ofrecido', 'producto', 'item', 'descripcion producto' ),
			'referencia'        => array( 'referencia', 'ref' ),
			'precio'            => array( 'precio', 'valor', 'costo' ),
			'cantidad'          => array( 'cantidad', 'cant', 'qty', 'quantity' ),
			'unidad_medida'     => array( 'unidad de medida', 'unidad', 'und', 'uom', 'medida' ),
			'fecha_requerida'   => array( 'fecha requerida', 'requerida', 'fecha req', 'req' ),
			'fecha_solicitud'   => array( 'fecha solicitud', 'solicitud', 'fecha sol', 'sol' ),
			'status'            => array( 'estado', 'status' ),
			'priority'          => array( 'prioridad', 'priority' ),
			'escala'            => array( 'escala' ),
			'empresa'           => array( 'empresa', 'proveedor', 'nombre de la empresa', 'nombre empresa' ),
			'fecha'             => array( 'fecha' ),
			'documento_numero'  => array( 'n documento', 'nº documento', 'no documento', 'numero documento', 'número documento', 'documento', 'doc', 'proforma', 'factura', 'cotizacion', 'cotización' ),
		);

		$map = array();
		foreach ( $header as $index => $label ) {
			$normalized = self::normalize_header( $label );
			foreach ( $aliases as $key => $options ) {
				if ( in_array( $normalized, $options, true ) ) {
					$map[ $index ] = $key;
					break;
				}
			}
		}

		return $map;
	}

	/**
	 * @param string $label Etiqueta.
	 * @return string
	 */
	private static function normalize_header( $label ) {
		$label = remove_accents( strtolower( trim( (string) $label ) ) );
		return preg_replace( '/\s+/', ' ', $label );
	}

	/**
	 * @param array<int, string> $cells Celdas.
	 * @param array<int, string> $map   Mapa columna.
	 * @return array<string, string>
	 */
	private static function cells_to_row( $cells, $map ) {
		$row = array(
			'producto_ofrecido' => '',
			'referencia'        => '',
			'precio'            => '',
			'cantidad'          => '',
			'unidad_medida'     => '',
			'fecha_requerida'   => '',
			'fecha_solicitud'   => '',
			'status'            => '',
			'priority'          => '',
			'escala'            => '',
			'empresa'           => '',
			'fecha'             => '',
			'documento_numero'  => '',
		);

		foreach ( $map as $index => $key ) {
			if ( isset( $cells[ $index ] ) ) {
				$row[ $key ] = trim( (string) $cells[ $index ] );
			}
		}

		return $row;
	}

	/**
	 * @param array<int, array<string, string>> $rows Filas exportables.
	 * @return void
	 */
	private static function download_csv( $rows ) {
		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="compras-panel-' . wp_date( 'Y-m-d' ) . '.csv"' );

		$handle = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		if ( false === $handle ) {
			exit;
		}

		fwrite( $handle, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
		fputcsv( $handle, array_values( self::columns() ), ';' );

		foreach ( $rows as $row ) {
			$line = array();
			foreach ( array_keys( self::columns() ) as $key ) {
				$line[] = $row[ $key ] ?? '';
			}
			fputcsv( $handle, $line, ';' );
		}

		fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
		exit;
	}

	/**
	 * @param array<int, array<string, string>> $rows Filas exportables.
	 * @return void
	 */
	private static function download_xlsx( $rows ) {
		$sheet_xml = self::build_sheet_xml( $rows );
		$filename  = 'compras-panel-' . wp_date( 'Y-m-d' ) . '.xlsx';
		$tmp       = wp_tempnam( 'compras-panel.xlsx' );

		if ( ! $tmp ) {
			return;
		}

		$zip = new ZipArchive();
		if ( true !== $zip->open( $tmp, ZipArchive::OVERWRITE ) ) {
			@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
			return;
		}

		$zip->addFromString(
			'[Content_Types].xml',
			'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
			. '<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
			. '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>'
			. '<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>'
			. '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>'
			. '</Types>'
		);
		$zip->addFromString(
			'_rels/.rels',
			'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
			. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>'
			. '</Relationships>'
		);
		$zip->addFromString(
			'xl/workbook.xml',
			'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
			. '<sheets><sheet name="Compras" sheetId="1" r:id="rId1"/></sheets>'
			. '</workbook>'
		);
		$zip->addFromString(
			'xl/_rels/workbook.xml.rels',
			'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
			. '<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>'
			. '<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>'
			. '</Relationships>'
		);
		$zip->addFromString( 'xl/styles.xml', self::build_styles_xml() );
		$zip->addFromString( 'xl/worksheets/sheet1.xml', $sheet_xml );
		$zip->close();

		nocache_headers();
		header( 'Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' );
		header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
		header( 'Content-Length: ' . (string) filesize( $tmp ) );
		readfile( $tmp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
		@unlink( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		exit;
	}

	/**
	 * @param array<int, array<string, string>> $rows Filas exportables.
	 * @return string
	 */
	private static function build_sheet_xml( $rows ) {
		$columns    = self::columns();
		$keys       = array_keys( $columns );
		$last_col   = self::column_letter( count( $keys ) - 1 );
		$count      = count( $rows );
		$user       = wp_get_current_user();
		$user_label = $user && $user->exists() ? $user->display_name : __( 'Usuario', 'logistica' );

		$title    = __( 'PENDIENTES DE COMPRAS — Inicio — JB Planeación', 'logistica' );
		$subtitle = sprintf(
			/* translators: 1: export date/time 2: record count 3: user name */
			__( 'Exportado el %1$s · %2$d registro(s) · %3$s', 'logistica' ),
			wp_date( 'd/m/Y H:i' ),
			$count,
			$user_label
		);

		$sheet_rows   = array();
		$sheet_rows[] = self::build_row_xml( 1, array( $title ), array( 1 ), $keys, 26 );
		$sheet_rows[] = self::build_row_xml( 2, array( $subtitle ), array( 2 ), $keys, 20 );
		$sheet_rows[] = self::build_row_xml( 3, array_values( $columns ), array_fill( 0, count( $keys ), 3 ), $keys, 22 );

		$row_number = 4;
		foreach ( $rows as $index => $row ) {
			$cells = array();
			foreach ( $keys as $key ) {
				$cells[] = $row[ $key ] ?? '';
			}
			$style        = 0 === ( $index % 2 ) ? 4 : 0;
			$sheet_rows[] = self::build_row_xml( $row_number, $cells, array_fill( 0, count( $keys ), $style ), $keys, 18 );
			++$row_number;
		}

		$merge = '<mergeCells count="2">'
			. '<mergeCell ref="A1:' . $last_col . '1"/>'
			. '<mergeCell ref="A2:' . $last_col . '2"/>'
			. '</mergeCells>';

		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
			. '<sheetViews><sheetView workbookViewId="0">'
			. '<pane ySplit="3" topLeftCell="A4" activePane="bottomLeft" state="frozen"/>'
			. '<selection pane="bottomLeft" activeCell="A4" sqref="A4"/>'
			. '</sheetView></sheetViews>'
			. self::build_column_widths_xml()
			. '<sheetData>' . implode( '', $sheet_rows ) . '</sheetData>'
			. $merge
			. '</worksheet>';
	}

	/**
	 * @return string
	 */
	private static function build_column_widths_xml() {
		$widths = array(
			'producto_ofrecido' => 28,
			'referencia'        => 16,
			'precio'            => 14,
			'fecha_requerida'   => 14,
			'fecha_solicitud'   => 14,
			'status'            => 20,
			'priority'          => 12,
			'escala'            => 12,
			'empresa'           => 24,
			'fecha'             => 14,
			'documento_numero'  => 18,
		);
		$xml    = '<cols>';
		$index  = 0;

		foreach ( array_keys( self::columns() ) as $key ) {
			$width = $widths[ $key ] ?? 16;
			$xml  .= '<col min="' . ( $index + 1 ) . '" max="' . ( $index + 1 ) . '" width="' . $width . '" customWidth="1"/>';
			++$index;
		}

		$xml .= '</cols>';
		return $xml;
	}

	/**
	 * @return string
	 */
	private static function build_styles_xml() {
		return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
			. '<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
			. '<fonts count="4">'
			. '<font><sz val="11"/><color theme="1"/><name val="Calibri"/><family val="2"/></font>'
			. '<font><b/><sz val="15"/><color rgb="FFFFFFFF"/><name val="Calibri"/><family val="2"/></font>'
			. '<font><sz val="10"/><color rgb="FF0F766E"/><name val="Calibri"/><family val="2"/></font>'
			. '<font><b/><sz val="11"/><color rgb="FFFFFFFF"/><name val="Calibri"/><family val="2"/></font>'
			. '</fonts>'
			. '<fills count="6">'
			. '<fill><patternFill patternType="none"/></fill>'
			. '<fill><patternFill patternType="gray125"/></fill>'
			. '<fill><patternFill patternType="solid"><fgColor rgb="FF0F766E"/><bgColor indexed="64"/></patternFill></fill>'
			. '<fill><patternFill patternType="solid"><fgColor rgb="FFECFDF5"/><bgColor indexed="64"/></patternFill></fill>'
			. '<fill><patternFill patternType="solid"><fgColor rgb="FF0B5E58"/><bgColor indexed="64"/></patternFill></fill>'
			. '<fill><patternFill patternType="solid"><fgColor rgb="FFF8FAFC"/><bgColor indexed="64"/></patternFill></fill>'
			. '</fills>'
			. '<borders count="2">'
			. '<border><left/><right/><top/><bottom/><diagonal/></border>'
			. '<border>'
			. '<left style="thin"><color rgb="FFCBD5E1"/></left>'
			. '<right style="thin"><color rgb="FFCBD5E1"/></right>'
			. '<top style="thin"><color rgb="FFCBD5E1"/></top>'
			. '<bottom style="thin"><color rgb="FFCBD5E1"/></bottom>'
			. '<diagonal/>'
			. '</border>'
			. '</borders>'
			. '<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>'
			. '<cellXfs count="5">'
			. '<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
			. '<xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>'
			. '<xf numFmtId="0" fontId="2" fillId="3" borderId="0" xfId="0" applyFont="1" applyFill="1" applyAlignment="1"><alignment horizontal="left" vertical="center" wrapText="1"/></xf>'
			. '<xf numFmtId="0" fontId="3" fillId="4" borderId="1" xfId="0" applyFont="1" applyFill="1" applyBorder="1" applyAlignment="1"><alignment horizontal="center" vertical="center" wrapText="1"/></xf>'
			. '<xf numFmtId="0" fontId="0" fillId="5" borderId="1" xfId="0" applyFill="1" applyBorder="1" applyAlignment="1"><alignment vertical="top" wrapText="1"/></xf>'
			. '</cellXfs>'
			. '</styleSheet>';
	}

	/**
	 * @param int                  $row_number Fila.
	 * @param array<int, mixed>    $cells      Valores.
	 * @param array<int, int>      $styles     Estilos por celda.
	 * @param array<int, string>   $keys       Claves columna.
	 * @param int                  $height     Altura fila (pt).
	 * @return string
	 */
	private static function build_row_xml( $row_number, $cells, $styles, $keys, $height = 0 ) {
		$height_attr = $height > 0 ? ' ht="' . (int) $height . '" customHeight="1"' : '';
		$xml         = '<row r="' . (int) $row_number . '"' . $height_attr . '>';
		$cols        = count( $keys );

		for ( $index = 0; $index < $cols; $index++ ) {
			$value = $cells[ $index ] ?? ( 0 === $index ? ( $cells[0] ?? '' ) : '' );
			$style = (int) ( $styles[ $index ] ?? ( $styles[0] ?? 0 ) );
			$ref   = self::column_letter( $index ) . (int) $row_number;
			$xml  .= self::build_cell_xml( $ref, $value, $style );
		}

		$xml .= '</row>';
		return $xml;
	}

	/**
	 * @param string $ref   Referencia celda.
	 * @param mixed  $value Valor.
	 * @param int    $style Índice estilo.
	 * @return string
	 */
	private static function build_cell_xml( $ref, $value, $style ) {
		return '<c r="' . $ref . '" s="' . (int) $style . '" t="inlineStr"><is><t xml:space="preserve">' . self::xml_escape( (string) $value ) . '</t></is></c>';
	}

	/**
	 * @param int $index Indice base 0.
	 * @return string
	 */
	private static function column_letter( $index ) {
		$index  = max( 0, (int) $index );
		$letter = '';

		do {
			$letter = chr( 65 + ( $index % 26 ) ) . $letter;
			$index  = (int) floor( $index / 26 ) - 1;
		} while ( $index >= 0 );

		return $letter;
	}

	/**
	 * @param string $letters Letras columna.
	 * @return int
	 */
	private static function column_index( $letters ) {
		$letters = strtoupper( (string) $letters );
		$index   = 0;
		$length  = strlen( $letters );

		for ( $i = 0; $i < $length; $i++ ) {
			$index = ( $index * 26 ) + ( ord( $letters[ $i ] ) - 64 );
		}

		return max( 0, $index - 1 );
	}

	/**
	 * @param string $xml Shared strings.
	 * @return array<int, string>
	 */
	private static function parse_shared_strings( $xml ) {
		if ( '' === trim( $xml ) ) {
			return array();
		}

		$doc = simplexml_load_string( $xml );
		if ( false === $doc ) {
			return array();
		}

		$doc->registerXPathNamespace( 'm', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' );
		$items = $doc->xpath( '//m:si' );
		$out   = array();

		foreach ( $items as $item ) {
			$item->registerXPathNamespace( 'm', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' );
			$text_nodes = $item->xpath( './/m:t' );
			$text       = '';
			if ( is_array( $text_nodes ) ) {
				foreach ( $text_nodes as $node ) {
					$text .= (string) $node;
				}
			}
			$out[] = $text;
		}

		return $out;
	}

	/**
	 * @param SimpleXMLElement  $cell Celda.
	 * @param array<int,string> $shared_strings Strings compartidos.
	 * @return string
	 */
	private static function xlsx_cell_value( $cell, $shared_strings ) {
		$type = (string) $cell['t'];
		if ( 'inlineStr' === $type ) {
			$cell->registerXPathNamespace( 'm', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' );
			$nodes = $cell->xpath( './/m:t' );
			if ( is_array( $nodes ) && isset( $nodes[0] ) ) {
				return (string) $nodes[0];
			}
			return '';
		}

		$value = (string) $cell->v;
		if ( 's' === $type ) {
			$index = (int) $value;
			return $shared_strings[ $index ] ?? '';
		}

		return $value;
	}

	/**
	 * @param string $value Texto.
	 * @return string
	 */
	private static function xml_escape( $value ) {
		return htmlspecialchars( $value, ENT_QUOTES | ENT_XML1, 'UTF-8' );
	}
}
