<?php
/**
 * Reverse reader for typed JSONL transcript records.
 *
 * @package Niwa_AI
 */

defined( 'ABSPATH' ) || exit;

class Niwa_AI_Admin_Autonomy_Transcript_Reader {

	/**
	 * Read a bounded, authoritative summary of the newest runtime turns.
	 *
	 * Conversation memory is intentionally optimized for semantic recall and
	 * cannot prove whether a tool actually ran. The structured turn_end record
	 * is the source of truth for that question.
	 *
	 * @param string $path  Transcript path.
	 * @param int    $limit Maximum turns.
	 * @return array
	 */
	public static function read_recent_execution_evidence( $path, $limit = 6 ) {
		return self::read_execution_evidence( $path, $limit );
	}

	/**
	 * Read recent execution evidence plus any explicitly requested turn IDs.
	 *
	 * @param string $path               Transcript path.
	 * @param int    $limit              Maximum recent turns.
	 * @param array  $requested_turn_ids Exact structured turn identifiers.
	 * @return array
	 */
	public static function read_execution_evidence( $path, $limit = 6, $requested_turn_ids = array() ) {
		$limit              = max( 1, min( 20, intval( $limit ) ) );
		$requested_turn_ids = array_values(
			array_unique(
				array_filter(
					array_map(
						static function ( $turn_id ) {
							return trim( (string) $turn_id );
						},
						(array) $requested_turn_ids
					)
				)
			)
		);
		$records = self::read_tail_records(
			$path,
			array( 'turn_start', 'turn_end' ),
			empty( $requested_turn_ids ) ? ( $limit * 4 ) + 8 : 800,
			empty( $requested_turn_ids ) ? 33554432 : 67108864,
			false
		);
		$turns   = array();
		$order   = array();

		foreach ( $records as $record ) {
			$turn_id = trim( (string) ( $record['turn_id'] ?? '' ) );
			if ( '' === $turn_id ) {
				continue;
			}
			if ( ! isset( $turns[ $turn_id ] ) ) {
				$turns[ $turn_id ] = array(
					'turn_id'   => $turn_id,
					'prompt'    => '',
					'reply'     => '',
					'completed' => false,
				);
				$order[] = $turn_id;
			}

			if ( 'turn_start' === (string) ( $record['type'] ?? '' ) ) {
				$turns[ $turn_id ]['prompt'] = (string) ( $record['prompt'] ?? '' );
				continue;
			}

			$meta = isset( $record['meta'] ) && is_array( $record['meta'] )
				? $record['meta']
				: array();
			$turns[ $turn_id ]['reply']     = (string) ( $record['reply'] ?? '' );
			$turns[ $turn_id ]['completed'] = true;

			$raw_tools = $meta['executed_tools'] ?? ( $record['executed_tools'] ?? null );
			if ( null !== $raw_tools ) {
				if ( is_string( $raw_tools ) ) {
					$decoded_tools = json_decode( $raw_tools, true );
					$raw_tools     = is_array( $decoded_tools ) ? $decoded_tools : array();
				}
				$existing_tools = (array) ( $turns[ $turn_id ]['executed_tools'] ?? array() );
				$turns[ $turn_id ]['executed_tools'] = array_values(
					array_unique(
						array_filter(
							array_map( 'sanitize_key', array_merge( $existing_tools, (array) $raw_tools ) )
						)
					)
				);
			}

			$raw_write = $meta['successful_write_execution'] ?? ( $record['successful_write_execution'] ?? null );
			if ( null !== $raw_write ) {
				$turns[ $turn_id ]['successful_write_execution'] = ! empty( $turns[ $turn_id ]['successful_write_execution'] )
					|| self::transcript_boolean( $raw_write );
			}

			$completion_state = sanitize_key(
				(string) ( $meta['task_completion_state'] ?? ( $record['task_completion_state'] ?? '' ) )
			);
			if ( '' !== $completion_state ) {
				$turns[ $turn_id ]['task_completion_state'] = $completion_state;
			}
		}

		$evidence = array();
		$included = array();
		foreach ( $requested_turn_ids as $turn_id ) {
			if ( ! isset( $turns[ $turn_id ] ) ) {
				continue;
			}
			$evidence[]          = $turns[ $turn_id ];
			$included[ $turn_id ] = true;
		}
		foreach ( array_slice( $order, -$limit ) as $turn_id ) {
			if ( isset( $included[ $turn_id ] ) ) {
				continue;
			}
			$evidence[] = $turns[ $turn_id ];
		}
		return $evidence;
	}

	/**
	 * Normalize scalar values stored by the transcript sanitizer.
	 *
	 * @param mixed $value Transcript value.
	 * @return bool
	 */
	private static function transcript_boolean( $value ) {
		if ( is_bool( $value ) ) {
			return $value;
		}
		if ( is_numeric( $value ) ) {
			return 0 !== intval( $value );
		}
		return in_array( strtolower( trim( (string) $value ) ), array( '1', 'true', 'yes', 'on' ), true );
	}

	/**
	 * Read the newest matching records without letting unrelated event volume
	 * determine how much conversation survives.
	 *
	 * @param string $path      Transcript path.
	 * @param array  $types     Allowed record types.
	 * @param int    $limit     Maximum matching records.
	 * @param int    $max_bytes Safety bound for one read.
	 * @param bool   $dedupe    Whether duplicate logical dialogue records collapse.
	 * @return array
	 */
	public static function read_tail_records( $path, $types, $limit = 40, $max_bytes = 33554432, $dedupe = true ) {
		$path      = (string) $path;
		$types     = array_fill_keys( array_filter( array_map( 'strval', (array) $types ) ), true );
		$limit     = max( 1, min( 800, intval( $limit ) ) );
		$max_bytes = max( 65536, min( 64 * 1024 * 1024, intval( $max_bytes ) ) );
		if ( '' === $path || empty( $types ) || ! is_file( $path ) ) {
			return array();
		}

		$size = filesize( $path );
		if ( false === $size || $size < 1 ) {
			return array();
		}

		$handle = @fopen( $path, 'rb' );
		if ( ! $handle ) {
			return array();
		}

		$position   = $size;
		$read_total = 0;
		$buffer     = '';
		$records    = array();
		$seen       = array();
		$complete   = false;

		try {
			while ( $position > 0 && $read_total < $max_bytes && ! $complete ) {
				$read_bytes = min( 8192, $position, $max_bytes - $read_total );
				if ( $read_bytes < 1 ) {
					break;
				}

				$position -= $read_bytes;
				if ( 0 !== fseek( $handle, $position ) ) {
					break;
				}

				$chunk = fread( $handle, $read_bytes );
				if ( false === $chunk || '' === $chunk ) {
					break;
				}

				$read_total += $read_bytes;
				$buffer      = $chunk . $buffer;
				while ( false !== ( $newline = strrpos( $buffer, "\n" ) ) ) {
					$line   = trim( substr( $buffer, $newline + 1 ) );
					$buffer = substr( $buffer, 0, $newline );
					if ( '' === $line ) {
						continue;
					}

					$record = json_decode( $line, true );
					$type   = is_array( $record ) ? (string) ( $record['type'] ?? '' ) : '';
					if ( ! isset( $types[ $type ] ) ) {
						continue;
					}

					$identity = $dedupe ? self::dialogue_record_identity( $record ) : '';
					if ( '' !== $identity && isset( $seen[ $identity ] ) ) {
						continue;
					}
					if ( '' !== $identity ) {
						$seen[ $identity ] = true;
					}
					$records[] = $record;
					if ( count( $records ) >= $limit ) {
						$complete = true;
						break;
					}
				}
			}

			if ( ! $complete && 0 === $position && '' !== trim( $buffer ) ) {
				$record = json_decode( trim( $buffer ), true );
				$type   = is_array( $record ) ? (string) ( $record['type'] ?? '' ) : '';
				if ( isset( $types[ $type ] ) ) {
					$identity = $dedupe ? self::dialogue_record_identity( $record ) : '';
					if ( '' === $identity || ! isset( $seen[ $identity ] ) ) {
						$records[] = $record;
					}
				}
			}
		} finally {
			fclose( $handle );
		}

		return array_reverse( array_slice( $records, 0, $limit ) );
	}

	/**
	 * Identify the logical dialogue slot represented by an append-only record.
	 *
	 * Reverse scanning sees the final committed turn_end before its earlier
	 * pre-delivery copy, so one identity both preserves authority and prevents
	 * duplicate physical lines from consuming the dialogue limit.
	 *
	 * @param array $record Transcript record.
	 * @return string
	 */
	private static function dialogue_record_identity( $record ) {
		$type = (string) ( $record['type'] ?? '' );
		if ( ! in_array( $type, array( 'turn_start', 'turn_end' ), true ) ) {
			return '';
		}

		$turn_id = trim( (string) ( $record['turn_id'] ?? '' ) );
		return '' === $turn_id ? '' : $type . ':turn:' . $turn_id;
	}
}
