<?php
/**
 * WP-CLI commands for Backreload.
 *
 * Why this exists: shared-hosting PHP timeouts kill long-running web requests
 * regardless of how many set_time_limit(0) calls you make. Running the engine
 * via WP-CLI bypasses the web request lifecycle entirely — no timeout, no
 * loopback request issues, no wp-cron quirks. The recommended setup for
 * sites > ~10 GB is:
 *
 *   * * * * * cd /path/to/site && wp backreload tick >/dev/null 2>&1
 *
 * Where `tick` (a thin wrapper around running due schedules) is fired by the
 * host's real cron rather than WordPress's traffic-driven pseudo-cron.
 *
 * Usage examples:
 *   wp backreload list
 *   wp backreload create --type=full
 *   wp backreload run bk_abc123
 *   wp backreload resume bk_abc123
 *   wp backreload cancel bk_abc123
 *   wp backreload delete bk_abc123 --yes
 *
 * @package Backreload\CLI
 */

namespace Backreload\CLI;

use Backreload\Models\BackupRecord;

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

class CliCommands {

	/**
	 * List existing backups.
	 *
	 * ## OPTIONS
	 *
	 * [--type=<type>]
	 * : Filter by backup type. Accepted: full, database, files.
	 *
	 * [--status=<status>]
	 * : Filter by status. Accepted: pending, running, success, failed, cancelled.
	 *
	 * [--limit=<n>]
	 * : Max rows to show. Default 25.
	 *
	 * [--format=<format>]
	 * : Output format. Accepted: table, json, yaml, csv. Default table.
	 *
	 * @when after_wp_load
	 */
	public function list( $args, $assoc ): void {
		$type   = $assoc['type']   ?? null;
		$status = $assoc['status'] ?? null;
		$limit  = (int) ( $assoc['limit'] ?? 25 );
		$format = $assoc['format'] ?? 'table';

		$rows = [];
		foreach ( BackupRecord::all( max( 1, $limit ), 0, $type ) as $rec ) {
			if ( $status && $rec->status !== $status ) continue;
			$rows[] = [
				'id'         => $rec->ext_id,
				'type'       => $rec->type,
				'status'     => $rec->status,
				'size'       => size_format( $rec->size ),
				'parts'      => count( $rec->files ),
				'created_at' => $rec->created_at,
			];
		}
		\WP_CLI\Utils\format_items( $format, $rows, [ 'id', 'type', 'status', 'size', 'parts', 'created_at' ] );
	}

	/**
	 * Create AND run a new backup. Blocking — sits in the WP-CLI process until
	 * the engine returns. Use this from a host cron to bypass PHP timeouts.
	 *
	 * ## OPTIONS
	 *
	 * [--type=<type>]
	 * : full | database | files. Default full.
	 *
	 * [--components=<list>]
	 * : Override components, comma-separated. e.g. "db,plugins,themes".
	 *
	 * [--destinations=<list>]
	 * : Override destinations, comma-separated ext_ids. Defaults to all connected.
	 *
	 * @when after_wp_load
	 */
	public function create( $args, $assoc ): void {
		$this->prepare_long_run();

		$type = $assoc['type'] ?? 'full';
		$components   = isset( $assoc['components'] )
			? array_filter( array_map( 'trim', explode( ',', $assoc['components'] ) ) )
			: null;
		$destinations = isset( $assoc['destinations'] )
			? array_filter( array_map( 'trim', explode( ',', $assoc['destinations'] ) ) )
			: null;

		$engine = backreload()->get( 'engine' );
		$rec    = $engine->create_pending( array_filter( [
			'type'         => $type,
			'components'   => $components,
			'destinations' => $destinations,
		] ) );
		\WP_CLI::line( "Created backup {$rec->ext_id}, executing…" );

		$engine->execute( $rec );
		$this->report_outcome( $rec );
	}

	/**
	 * Execute an existing pending/running backup. Used when the record was
	 * already created (e.g. via the dashboard) and you want WP-CLI to drive
	 * the execution.
	 *
	 * ## OPTIONS
	 *
	 * <id>
	 * : Backup ID (ext_id) to run.
	 *
	 * @when after_wp_load
	 */
	public function run( $args, $assoc ): void {
		$this->prepare_long_run();
		[ $id ] = $args;

		$rec = BackupRecord::find( $id );
		if ( ! $rec ) {
			\WP_CLI::error( "Backup not found: {$id}" );
		}
		if ( ! in_array( $rec->status, [ 'pending', 'running' ], true ) ) {
			\WP_CLI::error( "Backup is '{$rec->status}', can't run. Use `resume` for failed/cancelled backups." );
		}

		\WP_CLI::line( "Running backup {$rec->ext_id} (status: {$rec->status})…" );
		backreload()->get( 'engine' )->execute( $rec );
		$this->report_outcome( $rec );
	}

	/**
	 * Resume a failed or cancelled backup. The engine reuses already-built
	 * archive parts and already-completed uploads — only does what's left.
	 *
	 * ## OPTIONS
	 *
	 * <id>
	 * : Backup ID (ext_id) to resume.
	 *
	 * @when after_wp_load
	 */
	public function resume( $args, $assoc ): void {
		$this->prepare_long_run();
		[ $id ] = $args;

		$rec = BackupRecord::find( $id );
		if ( ! $rec ) {
			\WP_CLI::error( "Backup not found: {$id}" );
		}
		if ( ! in_array( $rec->status, [ 'failed', 'cancelled' ], true ) ) {
			\WP_CLI::error( "Only failed or cancelled backups can be resumed. Current state: {$rec->status}" );
		}
		if ( empty( $rec->files ) ) {
			\WP_CLI::error( 'No prior progress recorded; start a fresh backup instead.' );
		}

		$rec->status       = 'pending';
		$rec->error        = null;
		$rec->completed_at = null;
		$rec->save();

		\WP_CLI::line( "Resuming backup {$rec->ext_id} (" . count( $rec->files ) . ' part(s) already built)…' );
		backreload()->get( 'engine' )->execute( $rec );
		$this->report_outcome( $rec );
	}

	/**
	 * Cancel a running backup. Engine will exit at its next checkpoint.
	 *
	 * ## OPTIONS
	 *
	 * <id>
	 * : Backup ID (ext_id) to cancel.
	 *
	 * @when after_wp_load
	 */
	public function cancel( $args, $assoc ): void {
		[ $id ] = $args;

		$rec = BackupRecord::find( $id );
		if ( ! $rec ) {
			\WP_CLI::error( "Backup not found: {$id}" );
		}
		if ( ! in_array( $rec->status, [ 'pending', 'running' ], true ) ) {
			\WP_CLI::error( "Backup is '{$rec->status}', nothing to cancel." );
		}
		BackupRecord::flag_cancel( $rec->ext_id );
		\WP_CLI::success( "Cancellation flagged for {$id}; engine will stop at next checkpoint." );
	}

	/**
	 * Delete a backup record AND its archive files on every destination.
	 *
	 * ## OPTIONS
	 *
	 * <id>
	 * : Backup ID (ext_id) to delete.
	 *
	 * [--keep-remote]
	 * : Only remove the local DB record; leave the archive files (and
	 *   manifest) on their destinations. NOTE: a subsequent `sync` will
	 *   re-discover those files via their manifest and re-import the backup
	 *   as a fresh record. Use only when you intend to keep the files.
	 *
	 * [--yes]
	 * : Skip the confirmation prompt.
	 *
	 * @when after_wp_load
	 */
	public function delete( $args, $assoc ): void {
		[ $id ] = $args;

		$rec = BackupRecord::find( $id );
		if ( ! $rec ) {
			\WP_CLI::error( "Backup not found: {$id}" );
		}
		\WP_CLI\Utils\get_flag_value( $assoc, 'yes' ) || \WP_CLI::confirm( "Delete backup {$id}?" );

		// Default to the same behaviour as the REST DELETE path:
		// delete_with_remote_files() removes the parts AND the manifest.json
		// sibling from every destination. The old CLI called the row-only
		// delete(), which left the files + manifest behind — and the next
		// `sync` then re-imported the backup from its lingering manifest,
		// producing a zombie record. --keep-remote opts back into row-only.
		if ( \WP_CLI\Utils\get_flag_value( $assoc, 'keep-remote' ) ) {
			$rec->delete();
			\WP_CLI::success( "Deleted record {$id} (remote files kept)." );
			return;
		}
		$rec->delete_with_remote_files();
		\WP_CLI::success( "Deleted {$id} and its files on all destinations." );
	}

	/* ───────────────────────── internals ───────────────────────── */

	/** Long-running commands shouldn't be interrupted by time or memory caps. */
	private function prepare_long_run(): void {
		// phpcs:disable Squiz.PHP.DiscouragedFunctions.Discouraged -- Long-running backup/restore needs an extended time/memory limit, scoped to the engine's own execution.
		@set_time_limit( 0 );
		@ini_set( 'memory_limit', '512M' );
		// phpcs:enable Squiz.PHP.DiscouragedFunctions.Discouraged
		// Don't let an early disconnect (TTY closed, SSH dropped) abort a backup
		// that's already pushing bytes to a destination.
		@ignore_user_abort( true );
	}

	/** Emit a final summary line and exit with a status-appropriate code. */
	private function report_outcome( BackupRecord $rec ): void {
		// Re-load to capture engine's final writes.
		$fresh = BackupRecord::find( $rec->ext_id ) ?? $rec;

		switch ( $fresh->status ) {
			case 'success':
				\WP_CLI::success(
					"{$fresh->ext_id}. " . size_format( $fresh->size ) .
					' across ' . count( $fresh->files ) . " part(s) in {$fresh->duration}s"
				);
				return;
			case 'cancelled':
				\WP_CLI::warning( "{$fresh->ext_id}: cancelled by user" );
				return;
			case 'failed':
				\WP_CLI::error( "{$fresh->ext_id}: failed: " . ( $fresh->error ?: 'unknown error' ) );
				return; // WP_CLI::error exits with code 1
			default:
				\WP_CLI::line( "{$fresh->ext_id}: current status: {$fresh->status}" );
		}
	}
}
