<?php
/**
 * Link Architect Diagnostics.
 *
 * Self-contained logger that does not rely on PHP's error_log being
 * configured. Writes to a file in wp-content/uploads (always writable)
 * AND keeps an in-memory checkpoint list so the catch block in render()
 * can show the trace inline.
 *
 * Also registers a shutdown handler so uncatchable fatals (memory,
 * compile errors, parse errors that slip past lint) still get a record.
 *
 * @package Platform\Helm\Modules\Link_Architect
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\Link_Architect;

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

final class Diagnostics {

	private static ?string $log_file_cache = null;
	private static bool $shutdown_registered = false;

	/** @var array<int,array<string,mixed>> */
	private static array $checkpoints = [];

	/**
	 * Drop a checkpoint into the log.
	 *
	 * In-memory storage is always on (cheap, used by the inline error panel).
	 * Disk writes only happen when PLATFORM_HELM_LK_DEBUG is defined and true
	 * — leave it off in production to avoid per-request file I/O.
	 *
	 * To enable disk logging, add this to wp-config.php:
	 *     define( 'PLATFORM_HELM_LK_DEBUG', true );
	 *
	 * @param mixed $data Optional structured detail.
	 */
	public static function checkpoint( string $message, mixed $data = null ): void {
		self::$checkpoints[] = [
			'time' => microtime( true ),
			'msg'  => $message,
			'data' => $data,
		];

		// Skip disk write unless explicitly enabled.
		if ( ! defined( 'PLATFORM_HELM_LK_DEBUG' ) || ! PLATFORM_HELM_LK_DEBUG ) {
			return;
		}

		$line = sprintf(
			"[%s] %s%s\n",
			gmdate( 'Y-m-d H:i:s' ),
			$message,
			null === $data ? '' : ' | ' . ( is_string( $data ) ? $data : (string) wp_json_encode( $data ) )
		);

		// Silently best-effort. If the file isn't writable we still have
		// the in-memory copy for the inline panel.
		@file_put_contents( self::log_file(), $line, FILE_APPEND | LOCK_EX );
	}

	/**
	 * @return array<int,array<string,mixed>>
	 */
	public static function get_checkpoints(): array {
		return self::$checkpoints;
	}

	public static function log_exception( \Throwable $e ): void {
		self::checkpoint( 'EXCEPTION CAUGHT', [
			'class' => get_class( $e ),
			'msg'   => $e->getMessage(),
			'file'  => $e->getFile(),
			'line'  => $e->getLine(),
			'trace' => $e->getTraceAsString(),
		] );
	}

	/**
	 * Catch uncatchable fatals via shutdown handler. Idempotent.
	 */
	public static function register_shutdown(): void {
		if ( self::$shutdown_registered ) {
			return;
		}
		self::$shutdown_registered = true;

		register_shutdown_function( static function (): void {
			$error = error_get_last();
			if ( null === $error ) {
				return;
			}
			$fatal_types = [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR ];
			if ( ! in_array( (int) ( $error['type'] ?? 0 ), $fatal_types, true ) ) {
				return;
			}
			self::checkpoint( 'SHUTDOWN FATAL', [
				'type' => (int) $error['type'],
				'msg'  => (string) ( $error['message'] ?? '' ),
				'file' => (string) ( $error['file'] ?? '' ),
				'line' => (int) ( $error['line'] ?? 0 ),
			] );
		} );
	}

	/**
	 * Resolve the log file path. Falls back to a temp path if uploads
	 * isn't available.
	 */
	public static function log_file(): string {
		if ( null !== self::$log_file_cache ) {
			return self::$log_file_cache;
		}
		$basedir = '';
		if ( function_exists( 'wp_get_upload_dir' ) ) {
			$uploads = wp_get_upload_dir();
			if ( is_array( $uploads ) && ! empty( $uploads['basedir'] ) ) {
				$basedir = (string) $uploads['basedir'];
			}
		}
		if ( '' === $basedir && defined( 'ABSPATH' ) ) {
			$basedir = rtrim( ABSPATH, '/\\' ) . '/wp-content/uploads';
		}
		if ( '' === $basedir ) {
			$basedir = sys_get_temp_dir();
		}
		self::$log_file_cache = $basedir . '/platform-helm-link-architect-diag.log';
		return self::$log_file_cache;
	}

	public static function clear_log(): void {
		@unlink( self::log_file() );
	}

	public static function read_log( int $tail_bytes = 100000 ): string {
		$file = self::log_file();
		if ( ! file_exists( $file ) ) {
			return '(no log file yet — visit the Link Architect page first to generate one)';
		}
		$size = (int) ( filesize( $file ) ?: 0 );
		if ( $size <= $tail_bytes ) {
			$content = file_get_contents( $file );
			return false === $content ? '(cannot read log file)' : $content;
		}
		$fh = fopen( $file, 'rb' );
		if ( false === $fh ) {
			return '(cannot open log file)';
		}
		fseek( $fh, -$tail_bytes, SEEK_END );
		$content = (string) fread( $fh, $tail_bytes );
		fclose( $fh );
		return $content;
	}
}
