<?php
/**
 * Costudio_Logger — Structured, PSR-3-like logger backed by a database table.
 *
 * Levels: DEBUG, INFO, WARN, ERROR.
 * Each log entry can optionally be correlated to a production_id and agent.
 *
 * Debug-level messages are only recorded when the 'costudio_debug' option is
 * truthy, so production installs stay lightweight by default.
 *
 * @package Contentosapp_Studio
 * @since   3.1.0
 */

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Logger queries on plugin-owned costudio_logs table; $table = $wpdb->prefix . constant, user values pass through $wpdb->prepare placeholders.

class Costudio_Logger {

	const TABLE = 'costudio_logs';

	const LEVEL_DEBUG = 'debug';
	const LEVEL_INFO  = 'info';
	const LEVEL_WARN  = 'warn';
	const LEVEL_ERROR = 'error';

	/** Maximum rows to keep. Older rows are pruned on write. */
	const MAX_ROWS = 20000;

	/** Prune check interval — avoid running DELETE on every write. */
	const PRUNE_INTERVAL = 100;

	/** @var int Simple counter to throttle pruning. */
	private static int $write_count = 0;

	// ── Public API ──────────────────────────────────────────────────────────

	/**
	 * Log a debug message (only recorded when debug mode is on).
	 *
	 * @param string   $message
	 * @param array    $context       Arbitrary key-value pairs.
	 * @param int|null $production_id
	 * @param int|null $agent
	 */
	public static function debug( string $message, array $context = [], ?int $production_id = null, ?int $agent = null ): void {
		if ( ! get_option( 'costudio_debug', false ) ) {
			return;
		}
		self::write( self::LEVEL_DEBUG, $message, $context, $production_id, $agent );
	}

	/**
	 * Log an informational message.
	 */
	public static function info( string $message, array $context = [], ?int $production_id = null, ?int $agent = null ): void {
		self::write( self::LEVEL_INFO, $message, $context, $production_id, $agent );
	}

	/**
	 * Log a warning.
	 */
	public static function warn( string $message, array $context = [], ?int $production_id = null, ?int $agent = null ): void {
		self::write( self::LEVEL_WARN, $message, $context, $production_id, $agent );
	}

	/**
	 * Log an error.
	 */
	public static function error( string $message, array $context = [], ?int $production_id = null, ?int $agent = null ): void {
		self::write( self::LEVEL_ERROR, $message, $context, $production_id, $agent );
	}

	/**
	 * Persist a user-facing pipeline event for a production.
	 */
	public static function pipeline_event( int $production_id, string $message, string $display_level = 'info', ?int $agent = null, ?int $timestamp = null ): void {
		$stored_level = in_array( $display_level, [ self::LEVEL_WARN, self::LEVEL_ERROR ], true )
			? $display_level
			: self::LEVEL_INFO;

		self::write(
			$stored_level,
			$message,
			[
				'source'        => 'pipeline_event',
				'display_level' => $display_level,
				'ts'            => $timestamp ?: time(),
			],
			$production_id,
			$agent
		);
	}

	// ── Query API ───────────────────────────────────────────────────────────

	/**
	 * Get recent log entries with optional filters.
	 *
	 * @param array $args {
	 *   @type string|null $level         Filter by level.
	 *   @type int|null    $production_id Filter by production.
	 *   @type int         $limit         Max rows (default 50).
	 *   @type int         $offset        Pagination offset (default 0).
	 * }
	 * @return array
	 */
	public static function query( array $args = [] ): array {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;

		$where  = [];
		$values = [];

		if ( ! empty( $args['level'] ) ) {
			$where[]  = 'level = %s';
			$values[] = $args['level'];
		}
		if ( ! empty( $args['production_id'] ) ) {
			$where[]  = 'production_id = %d';
			$values[] = (int) $args['production_id'];
		}

		$where_sql = ! empty( $where ) ? 'WHERE ' . implode( ' AND ', $where ) : '';
		$limit     = (int) ( $args['limit'] ?? 50 );
		$offset    = (int) ( $args['offset'] ?? 0 );

		$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY id DESC LIMIT %d OFFSET %d";
		$values[] = $limit;
		$values[] = $offset;

		return $wpdb->get_results( $wpdb->prepare( $sql, ...$values ) );
	}

	/**
	 * Count log entries, optionally filtered.
	 *
	 * @param string|null $level
	 * @return int
	 */
	public static function count( ?string $level = null ): int {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;

		if ( $level ) {
			return (int) $wpdb->get_var( $wpdb->prepare(
				"SELECT COUNT(*) FROM {$table} WHERE level = %s",
				$level
			) );
		}

		return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
	}

	/**
	 * Get persisted user-facing pipeline events for a production.
	 *
	 * @return array<int,array{ts:int,level:string,agent:int|null,msg:string}>
	 */
	public static function query_pipeline_events( int $production_id, int $limit = 200 ): array {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;
		$limit = max( 1, min( 500, $limit ) );

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT message, context_json, agent, created_at FROM {$table}
				WHERE production_id = %d AND context_json LIKE %s
				ORDER BY id DESC LIMIT %d",
				$production_id,
				'%"source":"pipeline_event"%',
				$limit
			)
		);

		if ( empty( $rows ) ) {
			return [];
		}

		$events = [];
		foreach ( array_reverse( $rows ) as $row ) {
			$context = json_decode( (string) ( $row->context_json ?? '' ), true );
			if ( ! is_array( $context ) || ( $context['source'] ?? '' ) !== 'pipeline_event' ) {
				continue;
			}

			$events[] = [
				'ts'    => (int) ( $context['ts'] ?? strtotime( (string) $row->created_at ) ),
				'level' => (string) ( $context['display_level'] ?? self::LEVEL_INFO ),
				'agent' => isset( $row->agent ) ? (int) $row->agent : null,
				'msg'   => (string) $row->message,
			];
		}

		return $events;
	}

	// ── Internal ────────────────────────────────────────────────────────────

	/**
	 * Write a log entry to the database.
	 */
	private static function write( string $level, string $message, array $context, ?int $production_id, ?int $agent ): void {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;

		$wpdb->insert(
			$table,
			[
				'level'         => $level,
				'message'       => mb_substr( $message, 0, 65535 ),
				'context_json'  => ! empty( $context ) ? wp_json_encode( $context ) : null,
				'production_id' => $production_id,
				'agent'         => $agent,
				'created_at'    => current_time( 'mysql' ),
			],
			[ '%s', '%s', '%s', '%d', '%d', '%s' ]
		);

		// Periodic pruning to keep the table bounded.
		self::$write_count++;
		if ( self::$write_count % self::PRUNE_INTERVAL === 0 ) {
			self::prune();
		}
	}

	/**
	 * Remove oldest rows when the table exceeds MAX_ROWS.
	 */
	private static function prune(): void {
		global $wpdb;
		$table = $wpdb->prefix . self::TABLE;
		$count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );

		if ( $count > self::MAX_ROWS ) {
			$delete_count = $count - self::MAX_ROWS;
			$wpdb->query( $wpdb->prepare(
				"DELETE FROM {$table} ORDER BY id ASC LIMIT %d",
				$delete_count
			) );
		}
	}
}
