<?php
/**
 * Audit Trail — tamper-evident change log for content.
 *
 * Keeps an APPEND-ONLY timeline of post-status transitions (and any
 * filter-configured meta keys) per post, rendered as a vertical timeline meta
 * box on the editor. Entries are only ever appended — never overwritten — so
 * the history cannot be silently rewritten. Records who changed what, and when.
 *
 * Generic by design (no external dependencies):
 *   - Tracks the content post types — filter `platform_helm_audit_post_types`.
 *   - Optionally tracks meta keys — filter `platform_helm_audit_meta_keys`.
 *   - Display labels are filterable (`platform_helm_audit_field_label`,
 *     `platform_helm_audit_value_label`).
 *
 * SECURITY:
 *   - We never audit our own meta key, so the write path can't recurse.
 *   - Read-only meta box; no writes happen from render.
 *
 * @package Platform\Helm\Modules\Audit_Trail
 */

declare( strict_types = 1 );

namespace Platform\Helm\Modules\Audit_Trail;

use Platform\Helm\Contracts\ModuleInterface;
use Platform\Helm\Core\PostTypes;
use WP_Post;
use WP_User;

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

final class AuditTrailModule implements ModuleInterface {

	/** Hidden meta key where the append-only trail lives. Never audited itself. */
	private const AUDIT_META_KEY = '_platform_helm_audit_trail';

	private const META_BOX_ID   = 'platform_helm_audit_trail';
	private const MAX_DISPLAYED = 100;
	private const MAX_STORED     = 500;

	/* =====================================================================
	 * ModuleInterface
	 * ===================================================================== */

	public function id(): string {
		return 'audit_trail';
	}

	public function name(): string {
		return __( 'Audit Trail', 'platform-helm' );
	}

	public function description(): string {
		return __( 'Keeps a tamper-evident, append-only timeline of status changes (and chosen meta fields) for your content, shown right on the editor — who changed what, and when.', 'platform-helm' );
	}

	public function default_enabled(): bool {
		return false;
	}

	public function boot(): void {
		add_action( 'transition_post_status', [ $this, 'on_status_transition' ], 10, 3 );
		add_action( 'update_post_meta', [ $this, 'on_meta_update' ], 10, 4 );
		add_action( 'add_post_meta', [ $this, 'on_meta_add' ], 10, 3 );
		add_action( 'add_meta_boxes', [ $this, 'register_meta_box' ] );
	}

	/* =====================================================================
	 * TRACKED TARGETS (filterable)
	 * ===================================================================== */

	/** @return string[] */
	private function tracked_post_types(): array {
		/** @var mixed $types */
		$types = apply_filters( 'platform_helm_audit_post_types', PostTypes::content() );
		return is_array( $types ) ? array_values( array_filter( array_map( 'strval', $types ) ) ) : [];
	}

	/** @return string[] Meta keys to audit (besides post status). */
	private function tracked_meta_keys(): array {
		/** @var mixed $keys */
		$keys = apply_filters( 'platform_helm_audit_meta_keys', [] );
		$keys = is_array( $keys ) ? array_values( array_filter( array_map( 'strval', $keys ) ) ) : [];
		// Never audit our own key — prevents the write path from recursing.
		return array_values( array_diff( $keys, [ self::AUDIT_META_KEY ] ) );
	}

	private function is_tracked_post( int $post_id ): bool {
		if ( $post_id <= 0 ) {
			return false;
		}
		return in_array( get_post_type( $post_id ), $this->tracked_post_types(), true );
	}

	/* =====================================================================
	 * CAPTURE
	 * ===================================================================== */

	/**
	 * @param mixed $new_status
	 * @param mixed $old_status
	 * @param mixed $post
	 */
	public function on_status_transition( $new_status, $old_status, $post ): void {
		if ( ! $post instanceof WP_Post ) {
			return;
		}
		if ( ! in_array( $post->post_type, $this->tracked_post_types(), true ) ) {
			return;
		}
		$new = (string) $new_status;
		$old = (string) $old_status;
		if ( $new === $old || in_array( $new, [ 'auto-draft', 'inherit' ], true ) ) {
			return;
		}
		// Treat brand-new posts as starting from "(new)".
		$from = in_array( $old, [ '', 'new', 'auto-draft' ], true ) ? '' : $old;
		$this->log_entry( (int) $post->ID, 'status', '', $from, $new );
	}

	/**
	 * Fires before the DB UPDATE, so get_post_meta() still holds the old value.
	 *
	 * @param mixed $meta_id
	 * @param mixed $object_id
	 * @param mixed $meta_key
	 * @param mixed $new_value
	 */
	public function on_meta_update( $meta_id, $object_id, $meta_key, $new_value ): void {
		$key = is_string( $meta_key ) ? $meta_key : '';
		if ( ! in_array( $key, $this->tracked_meta_keys(), true ) ) {
			return;
		}
		$post_id = (int) $object_id;
		if ( ! $this->is_tracked_post( $post_id ) ) {
			return;
		}
		$from = $this->stringify( get_post_meta( $post_id, $key, true ) );
		$to   = $this->stringify( $new_value );
		if ( $from === $to ) {
			return;
		}
		$this->log_entry( $post_id, 'meta', $key, $from, $to );
	}

	/**
	 * @param mixed $object_id
	 * @param mixed $meta_key
	 * @param mixed $meta_value
	 */
	public function on_meta_add( $object_id, $meta_key, $meta_value ): void {
		$key = is_string( $meta_key ) ? $meta_key : '';
		if ( ! in_array( $key, $this->tracked_meta_keys(), true ) ) {
			return;
		}
		$post_id = (int) $object_id;
		if ( ! $this->is_tracked_post( $post_id ) ) {
			return;
		}
		$to = $this->stringify( $meta_value );
		if ( '' === $to ) {
			return;
		}
		$this->log_entry( $post_id, 'meta', $key, '', $to );
	}

	/* =====================================================================
	 * WRITE PATH
	 * ===================================================================== */

	private function log_entry( int $post_id, string $type, string $key, string $from, string $to ): void {
		$user = wp_get_current_user();
		$real = $user instanceof WP_User && $user->exists();

		$entry = [
			'type'      => $type,
			'key'       => $key,
			'from'      => $from,
			'to'        => $to,
			'user_id'   => $real ? (int) $user->ID : 0,
			'user_name' => $real ? (string) $user->display_name : __( 'System', 'platform-helm' ),
			'timestamp' => time(),
		];

		$trail = get_post_meta( $post_id, self::AUDIT_META_KEY, true );
		if ( ! is_array( $trail ) ) {
			$trail = [];
		}
		$trail[] = $entry;
		// Cap stored history so the meta row can't grow unbounded.
		if ( count( $trail ) > self::MAX_STORED ) {
			$trail = array_slice( $trail, -self::MAX_STORED );
		}
		// Writes to AUDIT_META_KEY (never an audited key) so this can't recurse.
		update_post_meta( $post_id, self::AUDIT_META_KEY, $trail );
	}

	/** @param mixed $value */
	private function stringify( $value ): string {
		if ( is_null( $value ) ) {
			return '';
		}
		if ( is_scalar( $value ) ) {
			return (string) $value;
		}
		$encoded = wp_json_encode( $value );
		return is_string( $encoded ) ? $encoded : '';
	}

	/* =====================================================================
	 * LABELS
	 * ===================================================================== */

	/** @param array<string,mixed> $entry */
	private function field_label( array $entry ): string {
		if ( 'status' === ( $entry['type'] ?? '' ) ) {
			return __( 'Status', 'platform-helm' );
		}
		$key   = (string) ( $entry['key'] ?? '' );
		$label = apply_filters( 'platform_helm_audit_field_label', $key, $key );
		return is_string( $label ) && '' !== $label ? $label : $key;
	}

	/** @param array<string,mixed> $entry */
	private function value_label( array $entry, string $raw ): string {
		if ( 'status' === ( $entry['type'] ?? '' ) ) {
			return $this->status_label( $raw );
		}
		if ( '' === $raw ) {
			return __( '(empty)', 'platform-helm' );
		}
		$label = apply_filters( 'platform_helm_audit_value_label', $raw, $raw, (string) ( $entry['key'] ?? '' ) );
		return is_string( $label ) && '' !== $label ? $label : $raw;
	}

	private function status_label( string $status ): string {
		if ( '' === $status ) {
			return __( '(new)', 'platform-helm' );
		}
		$obj = get_post_status_object( $status );
		if ( $obj && isset( $obj->label ) && '' !== (string) $obj->label ) {
			return (string) $obj->label;
		}
		return ucfirst( str_replace( [ '-', '_' ], ' ', $status ) );
	}

	/* =====================================================================
	 * META BOX
	 * ===================================================================== */

	public function register_meta_box(): void {
		foreach ( $this->tracked_post_types() as $post_type ) {
			add_meta_box(
				self::META_BOX_ID,
				__( 'Audit Trail', 'platform-helm' ),
				[ $this, 'render_meta_box' ],
				$post_type,
				'normal',
				'default'
			);
		}
	}

	public function render_meta_box( WP_Post $post ): void {
		echo $this->inline_css(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static, self-contained stylesheet.

		$trail = get_post_meta( $post->ID, self::AUDIT_META_KEY, true );
		$trail = is_array( $trail ) ? $trail : [];

		if ( empty( $trail ) ) {
			echo '<div class="platform-helm-audit-trail"><div class="pha-empty"><div class="pha-empty-icon">📋</div><div class="pha-empty-title">' . esc_html__( 'No changes recorded yet', 'platform-helm' ) . '</div><div class="pha-empty-sub">' . esc_html__( 'Status and field changes for this post will appear here as they happen.', 'platform-helm' ) . '</div></div></div>';
			return;
		}

		$total = count( $trail );
		$trail = array_reverse( $trail );
		if ( $total > self::MAX_DISPLAYED ) {
			$trail = array_slice( $trail, 0, self::MAX_DISPLAYED );
		}

		echo '<div class="platform-helm-audit-trail">';
		echo '<div class="pha-header"><span class="pha-count-pill">' . esc_html(
			sprintf( /* translators: %s: number of entries */ _n( '%s entry', '%s entries', $total, 'platform-helm' ), number_format_i18n( $total ) )
		) . '</span></div>';
		echo '<ol class="pha-timeline">';
		$first = true;
		foreach ( $trail as $entry ) {
			if ( is_array( $entry ) ) {
				$this->render_entry( $entry, $first );
				$first = false;
			}
		}
		echo '</ol></div>';
	}

	/** @param array<string,mixed> $entry */
	private function render_entry( array $entry, bool $is_latest ): void {
		$from_raw   = isset( $entry['from'] ) && is_string( $entry['from'] ) ? $entry['from'] : '';
		$to_raw     = isset( $entry['to'] ) && is_string( $entry['to'] ) ? $entry['to'] : '';
		$user_name  = isset( $entry['user_name'] ) && is_string( $entry['user_name'] ) ? $entry['user_name'] : __( 'Unknown', 'platform-helm' );
		$timestamp  = isset( $entry['timestamp'] ) ? (int) $entry['timestamp'] : 0;
		$is_initial = ( '' === $from_raw );

		$from_label  = $this->value_label( $entry, $from_raw );
		$to_label    = $this->value_label( $entry, $to_raw );
		$field_label = $this->field_label( $entry );

		$age_text   = $timestamp > 0 ? sprintf( /* translators: %s: time diff */ __( '%s ago', 'platform-helm' ), human_time_diff( $timestamp, time() ) ) : __( 'unknown time', 'platform-helm' );
		$exact_text = $timestamp > 0 ? wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $timestamp ) : '';
		$dt_attr    = $timestamp > 0 ? gmdate( 'c', $timestamp ) : '';

		echo '<li class="pha-entry' . ( $is_latest ? ' pha-entry--latest' : '' ) . '">';
		echo '<div class="pha-dot"></div>';
		echo '<div class="pha-card">';
		echo '<div class="pha-card-row pha-card-row--main"><div class="pha-transition">';
		echo '<span class="pha-field">' . esc_html( $field_label ) . '</span>';
		echo '<span class="pha-status ' . ( $is_initial ? 'pha-status--initial' : 'pha-status--from' ) . '">' . esc_html( $from_label ) . '</span>';
		echo '<span class="pha-arrow" aria-hidden="true">→</span>';
		echo '<span class="pha-status pha-status--to">' . esc_html( $to_label ) . '</span>';
		echo '</div>';
		echo '<time class="pha-time"' . ( '' !== $dt_attr ? ' datetime="' . esc_attr( $dt_attr ) . '"' : '' ) . ( '' !== $exact_text ? ' title="' . esc_attr( $exact_text ) . '"' : '' ) . '>' . esc_html( $age_text ) . '</time>';
		echo '</div>';
		echo '<div class="pha-card-row pha-card-row--meta"><span class="pha-actor-label">' . esc_html__( 'by', 'platform-helm' ) . '</span> <span class="pha-actor">' . esc_html( $user_name ) . '</span>';
		if ( '' !== $exact_text ) {
			echo ' <span class="pha-divider">·</span> <span class="pha-exact">' . esc_html( $exact_text ) . '</span>';
		}
		echo '</div></div></li>';
	}

	/* =====================================================================
	 * STYLES (scoped to .platform-helm-audit-trail)
	 * ===================================================================== */

	private function inline_css(): string {
		ob_start();
		?>
<style id="platform-helm-audit-trail-styles">
.platform-helm-audit-trail{--pha-brand:#0047AB;--pha-tint:#EAF1FB;--pha-text:#0F1A33;--pha-text2:#5C6B82;--pha-muted:#8A95A8;--pha-bg:#fff;--pha-soft:#F7F9FC;--pha-border:#E5EAF2;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;color:var(--pha-text);line-height:1.45;padding:4px 0 8px}
.platform-helm-audit-trail *{box-sizing:border-box}
.platform-helm-audit-trail .pha-header{display:flex;justify-content:flex-end;margin-bottom:14px}
.platform-helm-audit-trail .pha-count-pill{font-size:11px;font-weight:600;color:var(--pha-brand);background:var(--pha-tint);padding:4px 10px;border-radius:999px}
.platform-helm-audit-trail .pha-timeline{list-style:none;margin:0;padding:0;position:relative}
.platform-helm-audit-trail .pha-timeline::before{content:"";position:absolute;left:9px;top:16px;bottom:16px;width:2px;background:linear-gradient(180deg,var(--pha-border) 0%,var(--pha-border) 90%,transparent 100%)}
.platform-helm-audit-trail .pha-entry{position:relative;padding-left:32px;padding-bottom:14px;margin:0}
.platform-helm-audit-trail .pha-entry:last-child{padding-bottom:0}
.platform-helm-audit-trail .pha-dot{position:absolute;left:0;top:12px;width:20px;height:20px;border-radius:50%;background:var(--pha-bg);border:2px solid var(--pha-border);z-index:1}
.platform-helm-audit-trail .pha-entry--latest .pha-dot{background:var(--pha-brand);border-color:var(--pha-brand);box-shadow:0 0 0 4px var(--pha-tint)}
.platform-helm-audit-trail .pha-card{background:var(--pha-bg);border:1px solid var(--pha-border);border-radius:8px;padding:12px 14px;box-shadow:0 1px 2px rgba(15,26,51,.04),0 1px 3px rgba(15,26,51,.06)}
.platform-helm-audit-trail .pha-entry--latest .pha-card{border-color:#C9D8EF;box-shadow:0 1px 3px rgba(0,71,171,.08),0 4px 12px rgba(0,71,171,.06)}
.platform-helm-audit-trail .pha-card-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.platform-helm-audit-trail .pha-card-row--main{justify-content:space-between;margin-bottom:4px}
.platform-helm-audit-trail .pha-card-row--meta{font-size:11px;color:var(--pha-text2)}
.platform-helm-audit-trail .pha-transition{display:inline-flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:13px}
.platform-helm-audit-trail .pha-field{font-size:10px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--pha-muted)}
.platform-helm-audit-trail .pha-status{display:inline-flex;align-items:center;padding:3px 10px;border-radius:6px;font-weight:600;font-size:12px;background:var(--pha-soft);color:var(--pha-text);border:1px solid var(--pha-border)}
.platform-helm-audit-trail .pha-status--initial{background:transparent;border:1px dashed var(--pha-border);color:var(--pha-muted);font-style:italic;font-weight:500}
.platform-helm-audit-trail .pha-status--to{background:var(--pha-tint);color:var(--pha-brand);border-color:#C9D8EF}
.platform-helm-audit-trail .pha-arrow{color:var(--pha-muted);font-weight:600;font-size:14px}
.platform-helm-audit-trail .pha-time{font-size:11px;color:var(--pha-text2);font-variant-numeric:tabular-nums;white-space:nowrap}
.platform-helm-audit-trail .pha-actor-label{color:var(--pha-muted)}
.platform-helm-audit-trail .pha-actor{font-weight:600;color:var(--pha-text)}
.platform-helm-audit-trail .pha-divider{color:var(--pha-muted)}
.platform-helm-audit-trail .pha-exact{font-variant-numeric:tabular-nums}
.platform-helm-audit-trail .pha-empty{padding:28px 20px;text-align:center;background:var(--pha-soft);border:1px dashed var(--pha-border);border-radius:8px}
.platform-helm-audit-trail .pha-empty-icon{font-size:24px;margin-bottom:8px;opacity:.7}
.platform-helm-audit-trail .pha-empty-title{font-size:13px;font-weight:600;color:var(--pha-text);margin-bottom:4px}
.platform-helm-audit-trail .pha-empty-sub{font-size:12px;color:var(--pha-text2)}
</style>
		<?php
		return (string) ob_get_clean();
	}
}
