<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\File_Reader_Interface;

/**
 * CSV file reader utility for reading and parsing CSV content with validation.
 *
 * Supports header detection, blank row skipping, pointer-based reading, and total row counting.
 *
 * @since 1.0.0
 */
class Csv_File_Reader implements File_Reader_Interface {

	/**
	 * The absolute path to the CSV file.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	protected string $file_path;

	/**
	 * Optional configuration arguments.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	protected array $args;

	/**
	 * Internal pointer to track reading position.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	protected int $pointer = 0;

	/**
	 * File handle resource.
	 *
	 * @var resource|null
	 * @since 1.0.0
	 */
	protected $handle = null;

	/**
	 * Header row from the CSV file.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	protected array $headers = [];

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param string $file_path Full path to the CSV file.
	 * @param array  $args      Optional arguments (e.g., expected headers).
	 * @param int    $pointer   Optional pointer position to start reading.
	 */
	public function __construct( string $file_path, array $args = [], int $pointer = 0 ) {
		$this->file_path = $file_path;
		$this->pointer   = $pointer;
		$this->args      = $args;
	}

	/**
	 * Opens the CSV file and reads the header row.
	 *
	 * @since 1.0.0
	 *
	 * @throws Exception If the file could not be opened or header is missing.
	 * @return $this
	 */
	public function open(): self {
		$this->handle = fopen( $this->file_path, 'r' );
		if ( ! is_resource( $this->handle ) ) {
			throw new Exception( Error_Codes::FILE_FAILED_TO_OPEN, __( 'Could not open CSV file.', 'limb-chatbot' ) );
		}
		while ( ( $row = fgetcsv( $this->handle ) ) !== false ) {
			if ( $this->is_blank_row( $row ) ) {
				continue;
			}
			if ( $this->is_header( $row ) ) {
				$this->headers = $row;
				break;
			}
			throw new Exception( Error_Codes::CSV_FILE_MISSING_OR_INVALID_HEADER, __( 'Missing or invalid CSV header row.', 'limb-chatbot' ) );
		}

		return $this;
	}

	/**
	 * Reads rows from the CSV file as associative arrays.
	 *
	 * @since 1.0.0
	 *
	 * @param int      $rows_count   Number of rows to read.
	 * @param int|null $from_pointer Optional start pointer (defaults to current).
	 *
	 * @throws Exception If the file is not open.
	 * @return array<int, array<string, string>> Array of associative row data.
	 */
	public function fetch_rows( int $rows_count, ?int $from_pointer = 0 ): array {
		if ( ! is_resource( $this->handle ) ) {
			throw new Exception( Error_Codes::CSV_FILE_IS_NOT_OPEN, __( 'CSV file is not open.', 'limb-chatbot' ) );
		}
		$current_index = 0;
		$target_index  = $from_pointer ?? $this->pointer;
		while ( ( $row = fgetcsv( $this->handle ) ) !== false ) {
			if ( $this->is_blank_row( $row ) ) {
				continue;
			}
			if ( $current_index < $target_index ) {
				$current_index ++;
				continue;
			}
			$assoc = array_combine( $this->headers, $row );
			if ( $assoc === false ) {
				continue; // silently skip malformed row
			}
			$rows[] = $assoc;
			$current_index ++;
			if ( count( $rows ) >= $rows_count ) {
				break;
			}
		}
		$this->pointer = $current_index;

		return $rows ?? [];
	}

	/**
	 * Returns the total number of data rows in the CSV file (excluding header).
	 *
	 * @since 1.0.0
	 *
	 * @throws Exception If the file is not open.
	 * @return int Total row count.
	 */
	public function get_total_row_count(): int {
		if ( ! is_resource( $this->handle ) ) {
			throw new Exception( Error_Codes::CSV_FILE_IS_NOT_OPEN, __( 'CSV file is not open.', 'limb-chatbot' ) );
		}
		$original_position = ftell( $this->handle );
		rewind( $this->handle );
		$header_skipped = false;
		$count          = 0;
		while ( ( $row = fgetcsv( $this->handle ) ) !== false ) {
			if ( $this->is_blank_row( $row ) ) {
				continue;
			}
			if ( ! $header_skipped && $this->is_header( $row ) ) {
				$header_skipped = true;
				continue;
			}
			$count ++;
		}
		fseek( $this->handle, $original_position );

		return $count;
	}

	/**
	 * Returns the detected headers from the CSV file.
	 *
	 * @since 1.0.0
	 *
	 * @return array<string> CSV header fields.
	 */
	public function get_headers(): array {
		return $this->headers;
	}

	/**
	 * Closes the open file handle if available.
	 *
	 * @since 1.0.0
	 */
	public function close(): void {
		if ( is_resource( $this->handle ) ) {
			fclose( $this->handle );
			$this->handle = null;
		}
	}

	/**
	 * Checks whether the given row is completely empty.
	 *
	 * @since 1.0.0
	 *
	 * @param array $row A CSV row.
	 * @return bool True if the row is blank.
	 */
	protected function is_blank_row( array $row ): bool {
		foreach ( $row as $value ) {
			if ( trim( (string) $value ) !== '' ) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Determines if a row matches expected header columns.
	 *
	 * @since 1.0.0
	 *
	 * @param array $row A CSV row.
	 * @return bool True if the row matches all expected header columns.
	 */
	protected function is_header( array $row ): bool {
		if ( $columns = $this->get_header_columns() ) {
			foreach ( $columns as $column ) {
				$exist = false;
				foreach ( $row as $value ) {
					if ( trim( strtolower( $value ) ) === strtolower( $column ) ) {
						$exist = true;
						break;
					}
				}
				if ( ! $exist ) {
					break;
				}
			}
		}

		return $exist ?? false;
	}

	/**
	 * Returns the expected header column names (if defined).
	 *
	 * @since 1.0.0
	 *
	 * @return array<string>|null List of expected headers, or null if not defined.
	 */
	public function get_header_columns(): ?array {
		return $this->args['header_columns'] ?? null;
	}
}
