<?php
/**
 * OAuth 2.1 data store — clients and tokens (auth codes, access, refresh).
 *
 * Part of AI-9 (OAuth 2.1 + DCR for MCP). Hand-rolled on top of the vendored
 * Firebase\JWT; this class owns persistence + revocability. All token values
 * are stored hashed at rest.
 *
 * @package AtlasAiDev\Awfah
 * @subpackage OAuth
 */

declare(strict_types=1);

namespace AtlasAiDev\Awfah\OAuth;

defined( 'ABSPATH' ) || exit;

/**
 * Class OAuth_Repository
 */
class OAuth_Repository {

	/**
	 * DB schema version. Bump to trigger dbDelta on the next load.
	 */
	const DB_VERSION        = '2';
	const DB_VERSION_OPTION = 'awfah_oauth_db_version';

	/**
	 * Registered OAuth clients table (without prefix).
	 */
	const CLIENTS_TABLE = 'awfah_oauth_clients';

	/**
	 * Issued grants table: authorization codes, access + refresh tokens.
	 */
	const TOKENS_TABLE = 'awfah_oauth_tokens';

	/**
	 * Fully-qualified clients table name.
	 *
	 * @return string
	 */
	public static function clients_table(): string {
		global $wpdb;
		return $wpdb->prefix . self::CLIENTS_TABLE;
	}

	/**
	 * Fully-qualified tokens table name.
	 *
	 * @return string
	 */
	public static function tokens_table(): string {
		global $wpdb;
		return $wpdb->prefix . self::TOKENS_TABLE;
	}

	/**
	 * Create tables once, or when the schema version changes.
	 *
	 * @return void
	 */
	public static function maybe_create_tables(): void {
		if ( (string) get_option( self::DB_VERSION_OPTION ) === self::DB_VERSION ) {
			return;
		}
		self::create_tables();
		update_option( self::DB_VERSION_OPTION, self::DB_VERSION, false );
	}

	/**
	 * Create the OAuth tables via dbDelta.
	 *
	 * @return void
	 */
	public static function create_tables(): void {
		global $wpdb;
		$charset = $wpdb->get_charset_collate();
		$clients = self::clients_table();
		$tokens  = self::tokens_table();

		$sql_clients = "CREATE TABLE {$clients} (
			client_id varchar(191) NOT NULL,
			client_secret_hash varchar(255) NOT NULL DEFAULT '',
			client_name varchar(191) NOT NULL DEFAULT '',
			redirect_uris longtext NOT NULL,
			grant_types varchar(191) NOT NULL DEFAULT 'authorization_code refresh_token',
			scope text NOT NULL,
			token_endpoint_auth_method varchar(40) NOT NULL DEFAULT 'none',
			created datetime NOT NULL,
			created_by_ip varchar(100) NOT NULL DEFAULT '',
			PRIMARY KEY  (client_id)
		) {$charset};";

		$sql_tokens = "CREATE TABLE {$tokens} (
			id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
			token_hash varchar(255) NOT NULL,
			type varchar(10) NOT NULL,
			client_id varchar(191) NOT NULL DEFAULT '',
			user_id bigint(20) unsigned NOT NULL DEFAULT 0,
			scope text NOT NULL,
			code_challenge varchar(255) NOT NULL DEFAULT '',
			code_challenge_method varchar(10) NOT NULL DEFAULT '',
			redirect_uri text NOT NULL,
			parent_jti varchar(64) NOT NULL DEFAULT '',
			expires datetime NOT NULL,
			revoked tinyint(1) NOT NULL DEFAULT 0,
			created datetime NOT NULL,
			PRIMARY KEY  (id),
			KEY token_hash (token_hash),
			KEY type (type),
			KEY client_id (client_id),
			KEY expires (expires)
		) {$charset};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql_clients );
		dbDelta( $sql_tokens );
	}

	/**
	 * Hash a token/secret value for at-rest storage.
	 *
	 * @param string $value Raw value.
	 * @return string
	 */
	public static function hash( string $value ): string {
		// Keyed hash tied to the site secret; constant-time compared on lookup.
		return hash_hmac( 'sha256', $value, wp_salt( 'auth' ) );
	}

	/* --------------------------------------------------------------------- *
	 * Clients
	 * --------------------------------------------------------------------- */

	/**
	 * Insert a registered client.
	 *
	 * @param array $client Client fields.
	 * @return bool
	 */
	public static function insert_client( array $client ): bool {
		global $wpdb;
		return false !== $wpdb->insert(
			self::clients_table(),
			array(
				'client_id'                  => $client['client_id'],
				'client_secret_hash'         => $client['client_secret_hash'] ?? '',
				'client_name'                => $client['client_name'] ?? '',
				'redirect_uris'              => wp_json_encode( $client['redirect_uris'] ?? array() ),
				'grant_types'                => $client['grant_types'] ?? 'authorization_code refresh_token',
				'scope'                      => $client['scope'] ?? '',
				'token_endpoint_auth_method' => $client['token_endpoint_auth_method'] ?? 'none',
				'created'                    => current_time( 'mysql', true ),
				'created_by_ip'              => $client['created_by_ip'] ?? '',
			),
			array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
		);
	}

	/**
	 * Fetch a client by id.
	 *
	 * @param string $client_id Client id.
	 * @return array|null
	 */
	public static function get_client( string $client_id ): ?array {
		global $wpdb;
		$row = $wpdb->get_row(
			$wpdb->prepare( 'SELECT * FROM ' . self::clients_table() . ' WHERE client_id = %s', $client_id ),
			ARRAY_A
		);
		if ( ! $row ) {
			return null;
		}
		$row['redirect_uris'] = json_decode( (string) $row['redirect_uris'], true ) ?: array();
		return $row;
	}

	/**
	 * List all registered clients (newest first).
	 *
	 * @return array
	 */
	public static function list_clients(): array {
		global $wpdb;
		$rows = $wpdb->get_results( 'SELECT client_id, client_name, scope, created FROM ' . self::clients_table() . ' ORDER BY created DESC', ARRAY_A );
		return is_array( $rows ) ? $rows : array();
	}

	/**
	 * Delete a client and all of its tokens.
	 *
	 * @param string $client_id Client id.
	 * @return void
	 */
	public static function delete_client( string $client_id ): void {
		global $wpdb;
		$wpdb->delete( self::clients_table(), array( 'client_id' => $client_id ), array( '%s' ) );
		$wpdb->delete( self::tokens_table(), array( 'client_id' => $client_id ), array( '%s' ) );
	}

	/* --------------------------------------------------------------------- *
	 * Tokens / codes
	 * --------------------------------------------------------------------- */

	/**
	 * Store a grant row (authorization code, access, or refresh token).
	 *
	 * @param array $token Token fields. `token` is the raw value (hashed here).
	 * @return bool
	 */
	public static function insert_token( array $token ): bool {
		global $wpdb;
		return false !== $wpdb->insert(
			self::tokens_table(),
			array(
				'token_hash'            => self::hash( (string) $token['token'] ),
				'type'                  => $token['type'],
				'client_id'             => $token['client_id'] ?? '',
				'user_id'               => (int) ( $token['user_id'] ?? 0 ),
				'scope'                 => $token['scope'] ?? '',
				'code_challenge'        => $token['code_challenge'] ?? '',
				'code_challenge_method' => $token['code_challenge_method'] ?? '',
				'redirect_uri'          => $token['redirect_uri'] ?? '',
				'parent_jti'            => $token['parent_jti'] ?? '',
				'expires'               => $token['expires'],
				'revoked'               => 0,
				'created'               => current_time( 'mysql', true ),
			),
			array( '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s' )
		);
	}

	/**
	 * Look up a non-revoked, non-expired grant by raw value + type.
	 *
	 * @param string $raw_value Raw token/code value.
	 * @param string $type      One of code|access|refresh.
	 * @return array|null
	 */
	public static function find_active( string $raw_value, string $type ): ?array {
		global $wpdb;
		$row = $wpdb->get_row(
			$wpdb->prepare(
				'SELECT * FROM ' . self::tokens_table() . ' WHERE token_hash = %s AND type = %s AND revoked = 0 AND expires > %s',
				self::hash( $raw_value ),
				$type,
				current_time( 'mysql', true )
			),
			ARRAY_A
		);
		return $row ?: null;
	}

	/**
	 * Revoke a single grant row by id.
	 *
	 * @param int $id Row id.
	 * @return void
	 */
	public static function revoke( int $id ): void {
		global $wpdb;
		$wpdb->update( self::tokens_table(), array( 'revoked' => 1 ), array( 'id' => $id ), array( '%d' ), array( '%d' ) );
	}

	/**
	 * Revoke a grant by its raw value (any type).
	 *
	 * @param string $raw_value Raw token value.
	 * @return void
	 */
	public static function revoke_by_value( string $raw_value ): void {
		global $wpdb;
		$wpdb->update( self::tokens_table(), array( 'revoked' => 1 ), array( 'token_hash' => self::hash( $raw_value ) ), array( '%d' ), array( '%s' ) );
	}

	/**
	 * Delete expired/revoked rows (housekeeping).
	 *
	 * @return void
	 */
	public static function cleanup(): void {
		global $wpdb;
		$wpdb->query(
			$wpdb->prepare(
				'DELETE FROM ' . self::tokens_table() . ' WHERE revoked = 1 OR expires < %s',
				current_time( 'mysql', true )
			)
		);
	}

	/**
	 * Scope of a client's most recent active (non-revoked, unexpired) access token —
	 * i.e. what the connection can actually do now, which may be narrower than the
	 * client's registered scope. Returns null if the client has no active token.
	 *
	 * @param string $client_id Client id.
	 * @return string|null
	 */
	public static function latest_token_scope( string $client_id ): ?string {
		global $wpdb;
		$scope = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT scope FROM ' . self::tokens_table() . " WHERE client_id = %s AND type = 'access' AND revoked = 0 AND expires > %s ORDER BY created DESC LIMIT 1",
				$client_id,
				current_time( 'mysql', true )
			)
		);
		return ( null === $scope ) ? null : (string) $scope;
	}

	/**
	 * Revoke every issued token/code (global kill switch). Pro / admin "revoke all".
	 *
	 * @return void
	 */
	public static function revoke_all(): void {
		global $wpdb;
		$wpdb->query( 'UPDATE ' . self::tokens_table() . ' SET revoked = 1 WHERE revoked = 0' );
	}

	/**
	 * Revoke every token/code issued to a specific WordPress user.
	 *
	 * @param int $user_id WP user id.
	 * @return void
	 */
	public static function revoke_for_user( int $user_id ): void {
		global $wpdb;
		$wpdb->update( self::tokens_table(), array( 'revoked' => 1 ), array( 'user_id' => $user_id ), array( '%d' ), array( '%d' ) );
	}
}
