<?php
/**
 * Google Drive destination.
 *
 * Stores backup parts inside a configurable folder in the user's Drive
 * (default: "BackReload" at the root of My Drive). Each backup gets its own
 * `bk_<ext_id>/` subfolder mirroring the layout we use on Local / FTP / S3
 * destinations so the cross-destination "browse and import" flow works.
 *
 * Implementation lives in GoogleClient (a thin HTTP wrapper around the
 * Drive REST API v3). This class just maps the Destination interface onto
 * the client's calls — and caches the resolved folder ids so we don't
 * re-traverse the tree on every upload.
 *
 * OAuth setup (handled by OauthController):
 *   1. User creates an OAuth client in Google Cloud Console
 *   2. Pastes Client ID + Secret into the destination form
 *   3. Clicks Save → popup opens to Google for consent
 *   4. We receive tokens via the /oauth/google/callback REST route
 *   5. Tokens are persisted into the destination config (encrypted at rest)
 *
 * @package Backreload\Destinations
 */

namespace Backreload\Destinations;

use Backreload\Models\DestinationRecord;

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

class GoogleDestination extends AbstractDestination {

	public static function type(): string  { return 'google'; }
	public static function label(): string { return 'Google Drive'; }

	public static function config_schema(): array {
		return [
			[
				'key'      => 'client_id',
				'label'    => 'Client ID',
				'type'     => 'text',
				'required' => true,
				'help'     => 'From Google Cloud Console → APIs & Services → Credentials → OAuth 2.0 Client. See setup help below.',
			],
			[ 'key' => 'client_secret', 'label' => 'Client secret', 'type' => 'password', 'required' => true, 'secret' => true,
			  'help' => 'See setup help below.' ],
			[
				'key'     => 'folder',
				'label'   => 'Folder name',
				'type'    => 'text',
				'default' => 'BackReload',
				'help'    => 'A folder of this name is created in your Drive root. Backups land inside, grouped into bk_* subfolders.',
			],
		];
	}

	/**
	 * Step-by-step "where do I get these credentials?" doc for the
	 * destination form. Catalog endpoint passes this through to React,
	 * which renders it as a collapsible help block below the OAuth fields.
	 *
	 * Content is server-controlled HTML — limited to <strong>, <code>, and
	 * <a> tags by convention so the React renderer can safely emit them
	 * via dangerouslySetInnerHTML. We never interpolate user input here.
	 *
	 * Callouts surface values the user has to paste into Google's console
	 * (redirect URI, JavaScript origin) — separated from the prose so the
	 * UI can render them as copy-friendly code boxes.
	 */
	public static function setup_help(): array {
		$callback = rest_url( 'backreload/v1/oauth/google/callback' );
		$origin   = untrailingslashit( home_url() );
		return [
			'title'    => 'Setup help: where do I get the Client ID and Client Secret?',
			'provider' => 'Google Drive',
			'steps' => [
				'Open <a href="https://console.cloud.google.com" target="_blank" rel="noopener">Google Cloud Console</a> and create (or pick) a project.',
				'Enable the <strong>Drive API</strong> for that project (APIs &amp; Services → Library → search "Google Drive API" → Enable).',
				'Configure the <strong>OAuth consent screen</strong> (APIs &amp; Services → OAuth consent screen): pick <strong>External</strong> user type, fill in the app name + your email, save. Under <strong>Test users</strong> add your own Google account so sign-in works while the app is in testing mode.',
				'Open <strong>APIs &amp; Services → Credentials → + Create Credentials → OAuth client ID</strong>. Application type: <strong>Web application</strong>. Name it anything.',
				'In <strong>Authorized JavaScript origins</strong>, add the origin shown in the box below (no path, no trailing slash).',
				'In <strong>Authorized redirect URIs</strong>, add the full URL shown in the box below.',
				'Click <strong>Create</strong>. A modal shows your <strong>Client ID</strong> (ends with <code>.apps.googleusercontent.com</code>) and <strong>Client Secret</strong> (starts with <code>GOCSPX-</code>). Copy both into the fields above, save the destination, then click "Sign in with Google".',
			],
			'callouts' => [
				[
					'label' => 'Authorized JavaScript origin',
					'value' => $origin,
					'help'  => 'Paste exactly. Origin only, no path, no trailing slash.',
				],
				[
					'label' => 'Authorized redirect URI',
					'value' => $callback,
					'help'  => 'Paste exactly into the Authorized redirect URIs list.',
				],
			],
		];
	}

	/**
	 * Test the connection. Refreshes the token if needed, then calls /about.
	 * Throws on any failure so the caller can surface the real reason.
	 */
	public function test( DestinationRecord $dest ): bool {
		$this->ensure_authorized( $dest );
		$client = new GoogleClient( $dest );
		return $client->test_connection();
	}

	/**
	 * Upload a backup part. The `$remote_name` typically comes in as
	 * `bk_<ext_id>/plugins-1.zip` — we split it on the slash, resolve the
	 * `bk_<ext_id>` subfolder (creating it under the configured root folder
	 * if missing), then upload the file inside.
	 *
	 * Returns a synthetic "drive id" string so the engine has a remote
	 * identifier to persist into the backup record's files[] entry.
	 */
	public function upload( DestinationRecord $dest, string $local_path, string $remote_name ): string {
		$this->ensure_authorized( $dest );
		$client    = new GoogleClient( $dest );
		$root_id   = $this->resolve_root_folder( $client, $dest );
		[ $sub, $file_name ] = $this->split_remote_name( $remote_name );
		$parent_id = $sub
			? $client->find_or_create_folder( $sub, $root_id )
			: $root_id;

		// If a file of the same name already exists in this folder, delete it
		// first so the upload replaces rather than creating a Drive revision.
		// Revisions silently consume quota and the user doesn't see them in
		// the UI — fresher to delete-then-write.
		$existing = $client->find_file( $file_name, $parent_id );
		if ( $existing ) {
			$client->delete_file( $existing );
		}

		$client->upload( $local_path, $file_name, $parent_id );
		return $remote_name;
	}

	/**
	 * Download a previously uploaded part.
	 */
	public function download( DestinationRecord $dest, string $remote_name, string $local_path ): bool {
		$this->ensure_authorized( $dest );
		$client    = new GoogleClient( $dest );
		$root_id   = $this->resolve_root_folder( $client, $dest );
		[ $sub, $file_name ] = $this->split_remote_name( $remote_name );
		$parent_id = $sub
			? $client->find_or_create_folder( $sub, $root_id )
			: $root_id;
		$file_id   = $client->find_file( $file_name, $parent_id );
		if ( ! $file_id ) {
			return false;
		}
		return $client->download_file( $file_id, $local_path );
	}

	public function delete( DestinationRecord $dest, string $remote_name ): bool {
		$this->ensure_authorized( $dest );
		$client  = new GoogleClient( $dest );
		$root_id = $this->resolve_root_folder( $client, $dest );
		[ $sub, $file_name ] = $this->split_remote_name( $remote_name );
		$parent_id = $sub
			? $client->find_or_create_folder( $sub, $root_id )
			: $root_id;
		$file_id   = $client->find_file( $file_name, $parent_id );
		if ( ! $file_id ) {
			return true; // already gone
		}
		return $client->delete_file( $file_id );
	}

	/**
	 * List backups present in the configured folder. Walks each `bk_*`
	 * subfolder, returns its parts + manifest (if any), in the shape the
	 * import-from-destination UI expects.
	 *
	 * Reads manifest.json directly into memory rather than via a temp file
	 * because manifests are small (a few KB) and one round trip is cheaper
	 * than the download_file → fopen → unlink dance.
	 */
	public function list_backups( DestinationRecord $dest ): array {
		$this->ensure_authorized( $dest );
		$client  = new GoogleClient( $dest );
		$root_id = $this->resolve_root_folder( $client, $dest );

		$subfolders = $client->list_children( $root_id, true );
		$out        = [];
		foreach ( $subfolders as $sub ) {
			if ( strpos( (string) $sub['name'], 'bk_' ) !== 0 ) {
				continue;
			}
			$children = $client->list_children( $sub['id'], false );
			$parts    = [];
			$manifest = null;
			foreach ( $children as $c ) {
				if ( \Backreload\Core\Manifest::FILENAME === $c['name'] ) {
					// Stream the small manifest into a temp file, parse, drop it.
					$tmp = self::temp_path( 'backreload-manifest' );
					if ( $client->download_file( $c['id'], $tmp ) ) {
						$manifest = \Backreload\Core\Manifest::read( $tmp );
					}
					if ( is_file( $tmp ) ) {
						wp_delete_file( $tmp );
					}
					continue;
				}
				$parts[] = [
					'name' => $sub['name'] . '/' . $c['name'],
					'size' => (int) $c['size'],
				];
			}
			if ( empty( $parts ) && null === $manifest ) {
				continue;
			}
			$out[] = [
				'ext_id'       => (string) $sub['name'],
				'has_manifest' => null !== $manifest,
				'manifest'     => $manifest,
				'parts'        => $parts,
			];
		}
		return $out;
	}

	/**
	 * Drive's storage quota reported in the same shape as Local. `total`
	 * will be 0 for unlimited workspace accounts, which the UI handles by
	 * hiding the bar.
	 */
	public function usage( DestinationRecord $dest ): array {
		if ( empty( $dest->config['access_token'] ) ) {
			return [ 'used' => 0, 'free' => 0, 'total' => 0 ];
		}
		try {
			$client = new GoogleClient( $dest );
			return $client->quota();
		} catch ( \Throwable $e ) {
			return [ 'used' => 0, 'free' => 0, 'total' => 0 ];
		}
	}

	/**
	 * Verify the destination has been through OAuth. Without this guard a
	 * user could save credentials, skip the popup, and then trigger an
	 * obscure null-token error deep in the upload path. Better to fail
	 * fast with a clear message.
	 */
	private function ensure_authorized( DestinationRecord $dest ): void {
		if ( empty( $dest->config['access_token'] ) ) {
			throw new \RuntimeException( esc_html( 'Google Drive destination has not completed OAuth. Open the destination on the Destinations page and click "Sign in with Google".' ) );
		}
	}

	/**
	 * Resolve (and cache for the rest of this PHP request) the Drive id of
	 * the configured root folder. The id is small enough to cache as an
	 * object property; we re-resolve on the next request rather than
	 * persisting because folder ids can change if the user renames or
	 * moves the folder in Drive's UI between PHP requests.
	 */
	private ?string $root_folder_id_cache = null;
	private function resolve_root_folder( GoogleClient $client, DestinationRecord $dest ): string {
		if ( null !== $this->root_folder_id_cache ) {
			return $this->root_folder_id_cache;
		}
		$name = (string) ( $dest->config['folder'] ?? 'BackReload' );
		if ( '' === $name ) {
			$name = 'BackReload';
		}
		$this->root_folder_id_cache = $client->find_or_create_folder( $name );
		return $this->root_folder_id_cache;
	}

	/**
	 * Split `bk_<ext_id>/plugins-1.zip` into [ 'bk_<ext_id>', 'plugins-1.zip' ].
	 * If the remote name has no slash, returns [ '', $name ] and the file
	 * lands directly in the configured root folder.
	 *
	 * @return array{0:string,1:string}
	 */
	private function split_remote_name( string $remote_name ): array {
		$pos = strpos( $remote_name, '/' );
		if ( false === $pos ) {
			return [ '', $remote_name ];
		}
		return [
			substr( $remote_name, 0, $pos ),
			substr( $remote_name, $pos + 1 ),
		];
	}
}
