<?php
/**
 * WP admin dashboard widget — surfaces Backreload status on `/wp-admin/`.
 *
 * Goal: a one-glance answer to "is my site backed up, and is the system
 * healthy?" without making the admin navigate into the plugin first. The
 * widget renders a small status panel covering:
 *
 *   1. Last backup     — when, status, size, kind (manual / scheduled)
 *   2. Next scheduled  — when + which schedule
 *   3. Destinations    — count by health (connected / paused / error)
 *
 * Plus a deep-link to the plugin's own Dashboard so the admin is one
 * click away from real action.
 *
 * Deliberately renders server-side PHP (no React, no REST round-trips,
 * no JS payload). Dashboard widgets fire on every wp-admin pageload that
 * shows the dashboard; loading a 200KB bundle just to render four lines
 * of text would be hostile to that performance budget.
 *
 * @package Backreload
 */

namespace Backreload;

use Backreload\Core\Scheduler;
use Backreload\Models\BackupRecord;
use Backreload\Models\DestinationRecord;

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

final class DashboardWidget {

	const WIDGET_ID = 'backreload_dashboard_widget';

	private Plugin $plugin;

	public function __construct( Plugin $plugin ) {
		$this->plugin = $plugin;
	}

	public function init(): void {
		add_action( 'wp_dashboard_setup', [ $this, 'register' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );
	}

	/**
	 * Enqueue the widget's CSS via the WordPress API (a dependency-only style
	 * handle + wp_add_inline_style) instead of echoing a <style> tag inside
	 * the widget body. Only on the dashboard screen, where the widget renders.
	 *
	 * @param string $hook Current admin page hook.
	 */
	public function enqueue( string $hook ): void {
		if ( 'index.php' !== $hook || ! current_user_can( 'manage_options' ) ) {
			return;
		}
		wp_register_style( 'backreload-dashboard-widget', false, [], BACKRELOAD_VERSION );
		wp_enqueue_style( 'backreload-dashboard-widget' );
		wp_add_inline_style( 'backreload-dashboard-widget', self::widget_css() );
	}

	/**
	 * Widget CSS, scoped under .backreload-dw. Kept self-contained (no dependency on
	 * the React app stylesheet, which is scoped under #backreload-root).
	 * Brand green: #17cc98.
	 *
	 * @return string CSS (no <style> wrapper — enqueued via wp_add_inline_style).
	 */
	private static function widget_css(): string {
		return '
		.backreload-dw{font-size:13px;line-height:1.45;}
		.backreload-dw-banner{display:flex;align-items:center;gap:.4rem;padding:.5rem .65rem;border-radius:6px;font-weight:600;font-size:12.5px;margin-bottom:.65rem;}
		.backreload-dw-banner .dashicons{font-size:16px;width:16px;height:16px;}
		.backreload-dw-banner.ok{background:#e6f7f1;color:#0f8a63;}
		.backreload-dw-banner.warn{background:#fcf3e3;color:#8a6d1a;}
		.backreload-dw-banner.err{background:#fbeaea;color:#b32d2e;}
		.backreload-dw-stats{display:flex;gap:.5rem;margin-bottom:.65rem;}
		.backreload-dw-stat{flex:1;background:#f6f7f7;border-radius:6px;padding:.5rem .6rem;min-width:0;}
		.backreload-dw-stat b{display:block;font-size:17px;color:#1d2327;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
		.backreload-dw-stat span{display:block;font-size:10.5px;color:#646970;text-transform:uppercase;letter-spacing:.02em;font-weight:600;margin-top:2px;}
		.backreload-dw-row{display:flex;align-items:flex-start;gap:.5rem;padding:.45rem 0;border-bottom:1px solid #f1f3f4;}
		.backreload-dw-row:last-of-type{border-bottom:0;}
		.backreload-dw-icon{flex:0 0 1.25rem;line-height:1.25rem;text-align:center;font-size:1rem;color:#17cc98;}
		.backreload-dw-body{flex:1;min-width:0;}
		.backreload-dw-label{display:block;color:#646970;font-size:11px;text-transform:uppercase;letter-spacing:.02em;font-weight:600;}
		.backreload-dw-value{display:block;color:#1d2327;font-weight:500;margin-top:1px;}
		.backreload-dw-muted{color:#646970;font-weight:400;font-size:12px;}
		.backreload-dw-success{color:#118a3d;}
		.backreload-dw-danger{color:#b32d2e;}
		.backreload-dw-warn{color:#996800;}
		.backreload-dw-cta{margin-top:.75rem;display:flex;gap:.5rem;flex-wrap:wrap;}
		.backreload-dw-dot{display:inline-block;width:.5rem;height:.5rem;border-radius:50%;margin-right:.25rem;vertical-align:1px;}
		.backreload-dw-dot.ok{background:#118a3d;}.backreload-dw-dot.warn{background:#dba617;}.backreload-dw-dot.err{background:#b32d2e;}';
	}

	/**
	 * Register the widget. Capability-gated to `manage_options` so it
	 * doesn't leak operational status to subscribers/contributors whose
	 * dashboard view we don't want this data on.
	 */
	public function register(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}
		wp_add_dashboard_widget(
			self::WIDGET_ID,
			__( 'BackReload', 'backreload' ),
			[ $this, 'render' ]
		);
	}

	public function render(): void {
		// Defensive: WP's dashboard sometimes invokes widget callbacks in
		// contexts where capability assumptions don't hold (admin-ajax
		// previews on multisite, etc.). Re-check before reading our data.
		if ( ! current_user_can( 'manage_options' ) ) {
			echo '<p>' . esc_html__( 'Insufficient permissions.', 'backreload' ) . '</p>';
			return;
		}

		$last  = $this->last_backup();
		$next  = $this->next_scheduled();
		$dests = $this->destination_summary();
		$stats = $this->stats();
		$plugin_url  = admin_url( 'admin.php?page=backreload' );
		$backups_url = admin_url( 'admin.php?page=backreload-backups' );

		// Overall health, in priority order: a hard failure or a destination
		// in error needs attention; otherwise a missing backup / destination /
		// schedule means setup isn't finished; otherwise we're protected.
		if ( $dests['error'] > 0 || ( $last && 'failed' === $last['status'] ) ) {
			$health = [ 'err', __( 'Backups need attention', 'backreload' ), 'dashicons-warning' ];
		} elseif ( ! $last || 0 === $dests['total'] || ! $next ) {
			$health = [ 'warn', __( 'Finish setting up backups', 'backreload' ), 'dashicons-info-outline' ];
		} else {
			$health = [ 'ok', __( 'Your site is protected', 'backreload' ), 'dashicons-shield' ];
		}

		// Widget CSS is enqueued via enqueue() / wp_add_inline_style().
		echo '<div class="backreload-dw">';

		// ── Health banner ──────────────────────────────────────────────
		printf(
			'<div class="backreload-dw-banner %1$s"><span class="dashicons %2$s"></span>%3$s</div>',
			esc_attr( $health[0] ),
			esc_attr( $health[2] ),
			esc_html( $health[1] )
		);

		// ── Stat tiles ─────────────────────────────────────────────────
		echo '<div class="backreload-dw-stats">';
		printf(
			'<div class="backreload-dw-stat"><b>%s</b><span>%s</span></div>',
			esc_html( number_format_i18n( $stats['total'] ) ),
			esc_html__( 'Backups', 'backreload' )
		);
		printf(
			'<div class="backreload-dw-stat"><b>%s</b><span>%s</span></div>',
			esc_html( $stats['size'] > 0 ? size_format( $stats['size'], 1 ) : '0 B' ),
			esc_html__( 'Stored', 'backreload' )
		);
		if ( $stats['total'] > 0 ) {
			printf(
				'<div class="backreload-dw-stat"><b>%s</b><span>%s</span></div>',
				esc_html( $stats['rate'] . '%' ),
				esc_html__( 'Success', 'backreload' )
			);
		}
		echo '</div>';

		// ── Last backup row ────────────────────────────────────────────
		echo '<div class="backreload-dw-row">';
		if ( $last ) {
			$icon = 'success' === $last['status'] ? '<span class="dashicons dashicons-yes backreload-dw-success"></span>'
				 : ( in_array( $last['status'], [ 'failed', 'cancelled' ], true )
					 ? '<span class="dashicons dashicons-warning backreload-dw-danger"></span>'
					 : '<span class="dashicons dashicons-clock backreload-dw-warn"></span>' );
			$age = human_time_diff( strtotime( $last['created_at'] ) + 0, time() );
			/* translators: %s: a human-friendly time interval like "2 hours" */
			$ago = sprintf( __( '%s ago', 'backreload' ), $age );
			echo '<span class="backreload-dw-icon">' . wp_kses_post( $icon ) . '</span>';
			echo '<span class="backreload-dw-body">';
			echo '<span class="backreload-dw-label">' . esc_html__( 'Last backup', 'backreload' ) . '</span>';
			echo '<span class="backreload-dw-value">' . esc_html( $ago );
			echo '<span class="backreload-dw-muted"> · ' . esc_html( ucfirst( $last['type'] ) );
			if ( $last['size'] > 0 ) {
				echo ' · ' . esc_html( size_format( (int) $last['size'], 1 ) );
			}
			echo '</span></span>';
			if ( 'success' !== $last['status'] ) {
				$status_class = 'failed' === $last['status'] ? 'backreload-dw-danger' : 'backreload-dw-warn';
				echo '<span class="backreload-dw-muted"> · <span class="' . esc_attr( $status_class ) . '">' . esc_html( ucfirst( $last['status'] ) ) . '</span></span>';
			}
			echo '</span>';
		} else {
			echo '<span class="backreload-dw-icon"><span class="dashicons dashicons-backup backreload-dw-muted"></span></span>';
			echo '<span class="backreload-dw-body">';
			echo '<span class="backreload-dw-label">' . esc_html__( 'Last backup', 'backreload' ) . '</span>';
			echo '<span class="backreload-dw-value backreload-dw-muted">' . esc_html__( 'No backups yet', 'backreload' ) . '</span>';
			echo '</span>';
		}
		echo '</div>';

		// ── Next scheduled row ─────────────────────────────────────────
		echo '<div class="backreload-dw-row">';
		echo '<span class="backreload-dw-icon"><span class="dashicons dashicons-calendar-alt"></span></span>';
		echo '<span class="backreload-dw-body">';
		echo '<span class="backreload-dw-label">' . esc_html__( 'Next scheduled', 'backreload' ) . '</span>';
		if ( $next ) {
			$when_human = human_time_diff( time(), $next['when'] );
			if ( $next['when'] > time() ) {
				/* translators: %s: a human-friendly time interval like "6 hours" */
				$in_text = sprintf( __( 'in %s', 'backreload' ), $when_human );
			} else {
				/* translators: %s: a human-friendly time interval like "6 hours" */
				$in_text = sprintf( __( 'overdue (%s)', 'backreload' ), $when_human );
			}
			echo '<span class="backreload-dw-value">' . esc_html( $in_text );
			echo '<span class="backreload-dw-muted"> · ' . esc_html( $next['label'] ) . '</span>';
			echo '</span>';
		} else {
			echo '<span class="backreload-dw-value backreload-dw-muted">' . esc_html__( 'No active schedule', 'backreload' ) . '</span>';
		}
		echo '</span></div>';

		// ── Destinations row ───────────────────────────────────────────
		echo '<div class="backreload-dw-row">';
		echo '<span class="backreload-dw-icon"><span class="dashicons dashicons-cloud"></span></span>';
		echo '<span class="backreload-dw-body">';
		echo '<span class="backreload-dw-label">' . esc_html__( 'Destinations', 'backreload' ) . '</span>';
		if ( 0 === $dests['total'] ) {
			echo '<span class="backreload-dw-value backreload-dw-muted">' . esc_html__( 'None configured', 'backreload' ) . '</span>';
		} else {
			$parts = [];
			if ( $dests['connected'] > 0 ) {
				$parts[] = '<span class="backreload-dw-dot ok"></span>' . sprintf(
					/* translators: %d: number of destinations */
					esc_html( _n( '%d connected', '%d connected', $dests['connected'], 'backreload' ) ),
					(int) $dests['connected']
				);
			}
			if ( $dests['paused'] > 0 ) {
				$parts[] = '<span class="backreload-dw-dot warn"></span>' . sprintf(
					/* translators: %d: number of destinations */
					esc_html( _n( '%d paused', '%d paused', $dests['paused'], 'backreload' ) ),
					(int) $dests['paused']
				);
			}
			if ( $dests['error'] > 0 ) {
				$parts[] = '<span class="backreload-dw-dot err"></span>' . sprintf(
					/* translators: %d: number of destinations */
					esc_html( _n( '%d in error', '%d in error', $dests['error'], 'backreload' ) ),
					(int) $dests['error']
				);
			}
			echo '<span class="backreload-dw-value">' . wp_kses_post( implode( ' &nbsp;·&nbsp; ', $parts ) ) . '</span>';
		}
		echo '</span></div>';

		// ── Footer CTAs ────────────────────────────────────────────────
		echo '<div class="backreload-dw-cta">';
		printf(
			'<a href="%s" class="button button-primary">%s</a>',
			esc_url( $plugin_url ),
			esc_html__( 'Open BackReload', 'backreload' )
		);
		printf(
			'<a href="%s" class="button">%s</a>',
			esc_url( $backups_url ),
			esc_html__( 'Backups', 'backreload' )
		);
		echo '</div>';

		echo '</div>';
	}

	/* ──────────────────────  Data gathering  ─────────────────────────── */

	/**
	 * Most-recent backup row regardless of status. Excludes the
	 * `migrating` kind because those are internal one-shot migration
	 * artifacts, not historical backups the admin would think of when
	 * reading "last backup".
	 *
	 * @return array<string,mixed>|null
	 */
	private function last_backup(): ?array {
		global $wpdb;
		$table = $wpdb->prefix . 'backreload_backups';
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix (never user input), cannot be bound via prepare(); reads the plugin's own table, result is transient and uncacheable.
		$row = $wpdb->get_row(
			"SELECT type, status, size, created_at, kind FROM `{$table}`
			 WHERE kind != 'migrating' OR kind IS NULL
			 ORDER BY created_at DESC LIMIT 1",
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		return $row ?: null;
	}

	/**
	 * Aggregate backup stats for the summary tiles: total count, bytes
	 * stored, and success rate (%). Excludes internal `migrating` artifacts,
	 * matching last_backup(). Degrades to zeros if the table isn't ready.
	 *
	 * @return array{total:int,size:int,ok:int,rate:int}
	 */
	private function stats(): array {
		global $wpdb;
		$table = $wpdb->prefix . 'backreload_backups';
		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from $wpdb->prefix (never user input), cannot be bound via prepare(); reads the plugin's own table, result is transient and uncacheable.
		$row = $wpdb->get_row(
			"SELECT COUNT(*) AS total,
			        COALESCE(SUM(size),0) AS size,
			        SUM(CASE WHEN status='success' THEN 1 ELSE 0 END) AS ok
			 FROM `{$table}`
			 WHERE kind != 'migrating' OR kind IS NULL",
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$total = (int) ( $row['total'] ?? 0 );
		$ok    = (int) ( $row['ok'] ?? 0 );
		return [
			'total' => $total,
			'size'  => (int) ( $row['size'] ?? 0 ),
			'ok'    => $ok,
			'rate'  => $total > 0 ? (int) round( $ok / $total * 100 ) : 0,
		];
	}

	/**
	 * Earliest wp-cron event for an enabled schedule, plus a human-readable
	 * label. Returns null when no enabled schedules exist OR cron has no
	 * events queued (e.g. cron flushed and not yet resynced).
	 *
	 * @return array{when:int,label:string}|null
	 */
	private function next_scheduled(): ?array {
		try {
			$scheduler = $this->plugin->get( 'scheduler' );
		} catch ( \Throwable $e ) {
			return null;
		}
		if ( ! $scheduler instanceof Scheduler ) {
			return null;
		}

		$earliest = null;
		$label    = '';
		foreach ( $scheduler->all() as $s ) {
			if ( empty( $s['enabled'] ) ) continue;
			$when = wp_next_scheduled( Scheduler::HOOK_RUN, [ (string) ( $s['id'] ?? '' ) ] );
			if ( ! $when ) continue;
			if ( null === $earliest || $when < $earliest ) {
				$earliest = (int) $when;
				$freq     = (string) ( $s['frequency'] ?? '' );
				$type     = (string) ( $s['type'] ?? 'full' );
				// e.g. "Daily full at 03:00" / "Hourly database"
				$label = trim( ucfirst( $freq ) . ' ' . $type );
				if ( in_array( $freq, [ 'daily', 'weekly', 'monthly' ], true ) && ! empty( $s['time_of_day'] ) ) {
					$label .= ' at ' . $s['time_of_day'];
				}
			}
		}
		if ( null === $earliest ) {
			return null;
		}
		return [ 'when' => $earliest, 'label' => $label ];
	}

	/**
	 * Bucketed destination counts. `connected` = healthy + not paused.
	 * Anything paused counts toward `paused` regardless of underlying
	 * status (the user explicitly told us to skip it). Status `error`
	 * is the destination's last-known transport-failure state.
	 *
	 * @return array{total:int,connected:int,paused:int,error:int}
	 */
	private function destination_summary(): array {
		$out = [ 'total' => 0, 'connected' => 0, 'paused' => 0, 'error' => 0 ];
		try {
			$rows = DestinationRecord::all();
		} catch ( \Throwable $e ) {
			return $out; // DB not yet provisioned, etc.
		}
		foreach ( $rows as $d ) {
			$out['total']++;
			if ( ! empty( $d->paused ) ) {
				$out['paused']++;
				continue;
			}
			if ( 'error' === $d->status ) {
				$out['error']++;
				continue;
			}
			if ( 'connected' === $d->status ) {
				$out['connected']++;
			}
		}
		return $out;
	}
}
