<?php

/**
 * Accumulates safety findings across Plugin Builder auto-retry attempts.
 *
 * @package LolaCore\PluginBuilder\Generation
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\PluginBuilder\Generation;

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

/**
 * Tracks fatal safety findings and detects repeated fatal codes between reviews.
 */
final class RetryLedger {

	/** @var array<string, array{attempt: int, severity: string, code: string, message: string, line?: int}> */
	private array $entries = array();

	/** @var array<int, string>|null */
	private ?array $priorReviewFatalCodes = null;

	/** @var array<int, string>|null */
	private ?array $lastReviewFatalCodes = null;

	/**
	 * @param array{
	 *   safety_findings?: array<int, array{severity?: string, code?: string, message?: string, line?: int}>
	 * } $review
	 */
	public function addFromReview( array $review, int $attemptIndex ): void {
		$fatals = $this->extractFatalFindings($review);

		foreach ($fatals as $finding) {
			$key = $this->dedupeKey($finding);
			if (isset($this->entries[ $key ])) {
				continue;
			}

			$this->entries[ $key ] = array(
				'attempt'  => $attemptIndex,
				'severity' => (string) ( $finding['severity'] ?? 'fatal' ),
				'code'     => (string) ( $finding['code'] ?? 'unknown' ),
				'message'  => (string) ( $finding['message'] ?? '' ),
				'line'     => isset($finding['line']) ? (int) $finding['line'] : 0,
			);
		}

		$this->priorReviewFatalCodes = $this->lastReviewFatalCodes;
		$this->lastReviewFatalCodes  = $this->extractFatalCodes($review);
	}

	public function isStuckRepeatingFatals(): bool {
		if ($this->priorReviewFatalCodes === null || $this->lastReviewFatalCodes === null) {
			return false;
		}

		if ($this->priorReviewFatalCodes === array() || $this->lastReviewFatalCodes === array()) {
			return false;
		}

		return $this->normalizeCodes($this->priorReviewFatalCodes) === $this->normalizeCodes($this->lastReviewFatalCodes);
	}

	/**
	 * @return array<int, string>
	 */
	public function getLastFatalCodes(): array {
		return $this->lastReviewFatalCodes ?? array();
	}

	/**
	 * @return array<int, string>
	 */
	public function getUniqueFatalCodes(): array {
		$codes = array();
		foreach ($this->entries as $entry) {
			$codes[] = $entry['code'];
		}

		return $this->normalizeCodes($codes);
	}

	public function countEntries(): int {
		return count($this->entries);
	}

	/**
	 * Next attempt index to use when appending findings from a new review pass.
	 */
	public function getNextAttemptIndex(): int {
		$max = -1;
		foreach ($this->entries as $entry) {
			$max = max($max, (int) ( $entry['attempt'] ?? 0 ));
		}

		return $max + 1;
	}

	/**
	 * Serialize ledger state for persistence on a failed async job.
	 *
	 * @return array{
	 *   entries: array<int, array{attempt: int, severity: string, code: string, message: string, line?: int}>,
	 *   prior_fatal_codes: array<int, string>|null,
	 *   last_fatal_codes: array<int, string>|null
	 * }
	 */
	public function toSnapshot(): array {
		return array(
			'entries'           => array_values($this->entries),
			'prior_fatal_codes' => $this->priorReviewFatalCodes,
			'last_fatal_codes'  => $this->lastReviewFatalCodes,
		);
	}

	/**
	 * Restore a ledger saved via {@see toSnapshot()}.
	 *
	 * @param array<string, mixed> $snapshot
	 */
	public static function fromSnapshot( array $snapshot ): self {
		$ledger = new self();
		$ledger->loadSnapshot($snapshot);

		return $ledger;
	}

	/**
	 * @param array<string, mixed> $snapshot
	 */
	private function loadSnapshot( array $snapshot ): void {
		$this->entries = array();

		if (isset($snapshot['entries']) && is_array($snapshot['entries'])) {
			foreach ($snapshot['entries'] as $entry) {
				if (! is_array($entry)) {
					continue;
				}

				$key = $this->dedupeKey(
					array(
						'code'    => (string) ( $entry['code'] ?? 'unknown' ),
						'message' => (string) ( $entry['message'] ?? '' ),
						'line'    => (int) ( $entry['line'] ?? 0 ),
					)
				);
				$this->entries[ $key ] = array(
					'attempt'  => (int) ( $entry['attempt'] ?? 0 ),
					'severity' => (string) ( $entry['severity'] ?? 'fatal' ),
					'code'     => (string) ( $entry['code'] ?? 'unknown' ),
					'message'  => (string) ( $entry['message'] ?? '' ),
					'line'     => (int) ( $entry['line'] ?? 0 ),
				);
			}
		}

		$prior = $snapshot['prior_fatal_codes'] ?? null;
		$last  = $snapshot['last_fatal_codes'] ?? null;

		$this->priorReviewFatalCodes = is_array($prior) ? $this->normalizeCodes($prior) : null;
		$this->lastReviewFatalCodes  = is_array($last) ? $this->normalizeCodes($last) : null;
	}

	public function formatForPrompt(): string {
		if ($this->entries === array()) {
			return '';
		}

		$lines = array( 'ACCUMULATED BLOCKING SAFETY FINDINGS (do not repeat any of these):' );

		foreach ($this->entries as $entry) {
			$linePart = $entry['line'] > 0 ? ' (line ' . $entry['line'] . ')' : '';
			$lines[]  = sprintf(
				'- [attempt %d] [fatal] %s%s: %s',
				$entry['attempt'],
				$entry['code'],
				$linePart,
				$entry['message']
			);
		}

		return implode("\n", $lines);
	}

	/**
	 * @param array{safety_findings?: array<int, array{severity?: string, code?: string, message?: string, line?: int}>} $review
	 * @return array<int, array{severity: string, code: string, message: string, line?: int}>
	 */
	private function extractFatalFindings( array $review ): array {
		if (empty($review['safety_findings']) || ! is_array($review['safety_findings'])) {
			return array();
		}

		$fatals = array();
		foreach ($review['safety_findings'] as $finding) {
			$severity = (string) ( $finding['severity'] ?? '' );
			if ($severity !== 'fatal' && $severity !== 'error') {
				continue;
			}

			$fatals[] = array(
				'severity' => $severity !== '' ? $severity : 'fatal',
				'code'     => (string) ( $finding['code'] ?? 'unknown' ),
				'message'  => (string) ( $finding['message'] ?? '' ),
				'line'     => isset($finding['line']) ? (int) $finding['line'] : 0,
			);
		}

		return $fatals;
	}

	/**
	 * @param array{safety_findings?: array<int, array{severity?: string, code?: string}>} $review
	 * @return array<int, string>
	 */
	private function extractFatalCodes( array $review ): array {
		$codes = array();
		foreach ($this->extractFatalFindings($review) as $finding) {
			$codes[] = $finding['code'];
		}

		return $this->normalizeCodes($codes);
	}

	/**
	 * @param array{code: string, message: string, line?: int} $finding
	 */
	private function dedupeKey( array $finding ): string {
		$line = isset($finding['line']) ? (int) $finding['line'] : 0;
		$msg  = substr((string) ( $finding['message'] ?? '' ), 0, 80);

		return $finding['code'] . '|' . $line . '|' . md5($msg);
	}

	/**
	 * @param array<int, string> $codes
	 * @return array<int, string>
	 */
	private function normalizeCodes( array $codes ): array {
		$codes = array_values(array_unique($codes));
		sort($codes);

		return $codes;
	}
}
