<?php
/**
 * Recording session diagnostics.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\Recording;

use ScheduledChanges\Domain\Recording\RecordingCleanupStatus;
use ScheduledChanges\Domain\Recording\RecordingSession;
use ScheduledChanges\Support\Json;

/**
 * Builds a persisted developer-facing summary of recording safety signals.
 */
final class RecordingDiagnosticBuilder {
	/**
	 * @param array<string,mixed> $current_snapshot Current shadow snapshot.
	 * @param array<string,mixed> $source_snapshot Current source snapshot.
	 * @param array<string,mixed> $diff Recording diff.
	 * @return array<string,mixed>
	 */
	public function build(
		RecordingSession $session,
		array $current_snapshot,
		array $source_snapshot,
		array $diff,
		?string $cleanup_status = null
	): array {
		$warnings              = $this->warnings( $session->baselineSnapshot(), $current_snapshot );
		$diff_warnings         = array_values( array_filter( $diff['warnings'] ?? array(), 'is_array' ) );
		$changes               = array_values( array_filter( $diff['changes'] ?? array(), 'is_array' ) );
		$unsupported_meta      = array_filter(
			$diff_warnings,
			static fn ( array $warning ): bool => 'unsupported_metadata_changed' === (string) ( $warning['code'] ?? '' )
		);
		$non_selectable        = array_filter(
			$changes,
			static fn ( array $change ): bool => true !== ( $change['selectable'] ?? false )
		);
		$unsupported_providers = array();

		foreach ( $warnings as $warning ) {
			$provider_id = trim( (string) ( $warning['provider_id'] ?? '' ) );

			if ( '' !== $provider_id ) {
				$unsupported_providers[] = $provider_id;
			}
		}

		$unsupported_providers = array_values( array_unique( $unsupported_providers ) );
		sort( $unsupported_providers );

		return array(
			'generated_at_utc'                 => gmdate( 'c' ),
			'session_uuid'                     => $session->uuid()->value(),
			'source_id'                        => $session->sourceId(),
			'workspace_id'                     => $session->workspaceId(),
			'subtype'                          => $this->subtype( $session->baselineSnapshot() ),
			'active_providers'                 => $this->providerIds( $session->baselineSnapshot(), $current_snapshot ),
			'unsupported_providers'            => $unsupported_providers,
			'snapshot_warning_count'           => count( $warnings ),
			'diff_warning_count'               => count( $diff_warnings ),
			'selected_change_count'            => count( is_array( $diff['selected_change_ids'] ?? null ) ? $diff['selected_change_ids'] : array() ),
			'ignored_unsupported_change_count' => count( $unsupported_meta ) + count( $non_selectable ),
			'unknown_changed_meta_detected'    => array() !== $unsupported_meta,
			'source_mutation_detected'         => $this->sourceMutationDetected( $session->baselineSnapshot(), $source_snapshot ),
			'cleanup_result'                   => $cleanup_status ?? $session->cleanupStatus(),
		);
	}

	/**
	 * Detects changes to baseline source values or source variation topology.
	 *
	 * @param array<string,mixed> $baseline_snapshot Baseline source snapshot.
	 * @param array<string,mixed> $source_snapshot Current source snapshot.
	 */
	public function sourceMutationDetected( array $baseline_snapshot, array $source_snapshot ): bool {
		$baseline = $this->comparableObjects( $baseline_snapshot );
		$current  = $this->comparableObjects( $source_snapshot );

		if ( array_keys( $baseline ) !== array_keys( $current ) ) {
			return true;
		}

		foreach ( $baseline as $reference => $baseline_object ) {
			$current_object = $current[ $reference ] ?? null;

			if ( ! is_array( $current_object ) ) {
				return true;
			}

			foreach ( $baseline_object['providers'] as $provider_id => $values ) {
				if (
					! array_key_exists( $provider_id, $current_object['providers'] )
					|| Json::canonicalEncode( $values ) !== Json::canonicalEncode( $current_object['providers'][ $provider_id ] )
				) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * @param array<string,mixed> ...$snapshots Snapshot documents.
	 * @return string[]
	 */
	private function providerIds( array ...$snapshots ): array {
		$ids = array();

		foreach ( $snapshots as $snapshot ) {
			foreach ( $this->objects( $snapshot ) as $object ) {
				$ids = array_merge(
					$ids,
					array_map( 'strval', array_keys( is_array( $object['providers'] ?? null ) ? $object['providers'] : array() ) )
				);
			}
		}

		$ids = array_values( array_unique( $ids ) );
		sort( $ids );

		return $ids;
	}

	/**
	 * @param array<string,mixed> ...$snapshots Snapshot documents.
	 * @return array<int,array<string,mixed>>
	 */
	private function warnings( array ...$snapshots ): array {
		$warnings = array();

		foreach ( $snapshots as $snapshot ) {
			foreach ( $this->objects( $snapshot ) as $object ) {
				$warnings = array_merge(
					$warnings,
					array_values( array_filter( $object['warnings'] ?? array(), 'is_array' ) )
				);
			}
		}

		return $warnings;
	}

	/**
	 * @param array<string,mixed> $snapshot Snapshot document.
	 */
	private function subtype( array $snapshot ): string {
		foreach ( $this->objects( $snapshot ) as $object ) {
			return (string) ( $object['subtype'] ?? $object['target_type'] ?? '' );
		}

		return '';
	}

	/**
	 * @param array<string,mixed> $snapshot Snapshot document.
	 * @return array<int,array<string,mixed>>
	 */
	private function objects( array $snapshot ): array {
		return array_values( array_filter( $snapshot['objects'] ?? array(), 'is_array' ) );
	}

	/**
	 * @param array<string,mixed> $snapshot Snapshot document.
	 * @return array<string,array{providers:array<string,array<string,mixed>>}>
	 */
	private function comparableObjects( array $snapshot ): array {
		$objects = array();

		foreach ( $this->objects( $snapshot ) as $object ) {
			$reference = (string) ( $object['target_reference'] ?? $object['reference'] ?? '' );

			if ( '' === $reference ) {
				continue;
			}

			$providers = array();

			foreach ( is_array( $object['providers'] ?? null ) ? $object['providers'] : array() as $provider_id => $provider ) {
				if ( ! is_array( $provider ) ) {
					continue;
				}

				$providers[ (string) $provider_id ] = is_array( $provider['values'] ?? null )
					? $provider['values']
					: array();
			}

			ksort( $providers );
			$objects[ $reference ] = array( 'providers' => $providers );
		}

		ksort( $objects );

		return $objects;
	}
}
