<?php
/**
 * CSV Exporter — writes data to a CSV file with streaming.
 *
 * @package RawTypeOE
 */

namespace RawTypeOE\Free;

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Writes export data to a CSV file using fputcsv streaming.
 */
class CSV_Exporter {

	/**
	 * Open file handle for the output CSV.
	 *
	 * @var resource|null
	 */
	private $handle;

	/**
	 * Field delimiter.
	 *
	 * @var string
	 */
	private $delimiter;

	/**
	 * Whether to prepend a UTF-8 BOM for Excel.
	 *
	 * @var bool
	 */
	private $include_bom;

	/**
	 * Absolute path of the output file.
	 *
	 * @var string
	 */
	private $file_path;

	/**
	 * Data rows written so far.
	 *
	 * @var int
	 */
	private $row_count;

	/**
	 * Start a CSV export file.
	 *
	 * Opens the file, writes optional BOM and the header row.
	 *
	 * @param string $file_path Absolute path for the output file.
	 * @param array  $headers   Array of column header strings.
	 * @param array  $options   {
	 *     Optional settings.
	 *     @type string $delimiter CSV delimiter (default: ',').
	 *     @type bool   $bom       Include UTF-8 BOM (default: true).
	 * }
	 * @return bool True on success.
	 */
	public function start( $file_path, $headers, $options = array() ) {
		$this->file_path   = $file_path;
		$this->delimiter   = isset( $options['delimiter'] ) ? $options['delimiter'] : ',';
		$this->include_bom = isset( $options['bom'] ) ? (bool) $options['bom'] : true;
		$this->row_count   = 0;

		// Validate delimiter.
		$allowed_delimiters = array( ',', ';', "\t" );
		if ( ! in_array( $this->delimiter, $allowed_delimiters, true ) ) {
			$this->delimiter = ',';
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
		$this->handle = fopen( $file_path, 'w' );

		if ( false === $this->handle ) {
			return false;
		}

		// Write UTF-8 BOM for Excel compatibility.
		if ( $this->include_bom ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
			fwrite( $this->handle, "\xEF\xBB\xBF" );
		}

		// Write header row.
		fputcsv( $this->handle, $headers, $this->delimiter );

		return true;
	}

	/**
	 * Write a chunk of rows to the CSV file.
	 *
	 * @param array $rows Array of associative arrays (each row is field => value).
	 */
	public function write_chunk( $rows ) {
		if ( null === $this->handle || false === $this->handle ) {
			return;
		}

		foreach ( $rows as $row ) {
			fputcsv( $this->handle, $this->escape_row( array_values( $row ) ), $this->delimiter );
			++$this->row_count;
		}
	}

	/**
	 * Neutralize CSV formula-injection payloads in a row.
	 *
	 * Spreadsheet apps (Excel, LibreOffice, Google Sheets) evaluate any cell
	 * whose first character is `=`, `+`, `-`, `@`, or a leading tab/CR as a
	 * formula. Prefixing the value with a single quote forces the cell to be
	 * treated as literal text while still rendering the original string.
	 *
	 * @param array $row Row values.
	 * @return array
	 */
	private function escape_row( $row ) {
		foreach ( $row as $index => $value ) {
			if ( ! is_scalar( $value ) ) {
				continue;
			}

			$string = (string) $value;
			if ( '' === $string ) {
				continue;
			}

			$first        = $string[0];
			$needs_escape = ( '=' === $first || '@' === $first || "\t" === $first || "\r" === $first );

			// `+`/`-` is only dangerous when followed by non-numeric payload; negative prices stay numeric.
			if ( ! $needs_escape && ( '+' === $first || '-' === $first ) ) {
				$needs_escape = ! is_numeric( $string );
			}

			if ( $needs_escape ) {
				$row[ $index ] = "'" . $string;
			}
		}

		return $row;
	}

	/**
	 * Finish the export — close file and return metadata.
	 *
	 * @return array {
	 *     @type string $file_path Absolute path.
	 *     @type int    $size      File size in bytes.
	 *     @type int    $row_count Number of data rows written.
	 * }
	 */
	public function finish() {
		if ( null !== $this->handle && false !== $this->handle ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
			fclose( $this->handle );
		}

		$size = file_exists( $this->file_path ) ? filesize( $this->file_path ) : 0;

		return array(
			'file_path' => $this->file_path,
			'size'      => $size,
			'row_count' => $this->row_count,
		);
	}
}
