<?php
/**
 * API key manager for Mail Doctor integrations.
 *
 * @package MailDoctor
 */

namespace MailDoctor;

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

/**
 * Stores and validates Mail Doctor REST API keys.
 */
class API_Keys {


	/**
	 * Option key where API credentials are stored.
	 */
	const OPTION_KEY = 'mail_doctor_api_keys';

	/**
	 * Supported scope identifiers.
	 *
	 * @var string[]
	 */
	protected static $default_scopes = array(
		'settings.manage',
		'logs.read',
		'queue.write',
		'status.read',
		'all',
	);

	/**
	 * Return all stored API key records.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function all(): array {
		$keys = get_option( self::OPTION_KEY, array() );

		if ( ! is_array( $keys ) ) {
			return array();
		}

		return $keys;
	}

	/**
	 * Retrieve a single key by identifier.
	 *
	 * @param string $id Key identifier.
	 * @return array<string, mixed>|null
	 */
	public static function get( string $id ): ?array {
		$keys = self::all();

		return $keys[ $id ] ?? null;
	}

	/**
	 * Persist key collection.
	 *
	 * @param array<string, array<string, mixed>> $keys Key map.
	 * @return void
	 */
	protected static function save( array $keys ): void {
		update_option( self::OPTION_KEY, $keys );
	}

	/**
	 * Generate a new API key.
	 *
	 * @param string   $label   Friendly label.
	 * @param string[] $scopes  Scope list.
	 * @param int      $user_id Creator user ID.
	 * @return array<string, mixed> Contains id, secret, and entry metadata.
	 */
	public static function create( string $label, array $scopes, int $user_id = 0 ): array {
		$keys = self::all();

		$id     = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : strtolower( wp_generate_password( 32, false, false ) );
		$secret = self::generate_secret();

		$entry = array(
			'label'       => self::sanitize_label( $label ),
			'created_at'  => time(),
			'created_by'  => $user_id > 0 ? $user_id : 0,
			'last_used'   => null,
			'last_ip'     => null,
			'scopes'      => self::normalize_scopes( $scopes ),
			'secret_hash' => wp_hash_password( $secret ),
			'revoked'     => false,
		);

		$keys[ $id ] = $entry;
		self::save( $keys );

		return array(
			'id'     => $id,
			'secret' => $secret,
			'entry'  => $entry,
		);
	}

	/**
	 * Revoke (delete) an API key.
	 *
	 * @param string $id Key identifier.
	 * @return bool
	 */
	public static function revoke( string $id ): bool {
		$keys = self::all();

		if ( ! isset( $keys[ $id ] ) ) {
			return false;
		}

		unset( $keys[ $id ] );
		self::save( $keys );

		return true;
	}

	/**
	 * Validate API secret and return matching key metadata.
	 *
	 * @param string $secret Incoming secret token.
	 * @return array<string, mixed>|null
	 */
	public static function validate_secret( string $secret ): ?array {
		$secret = trim( (string) $secret );

		if ( '' === $secret ) {
			return null;
		}

		$keys = self::all();

		foreach ( $keys as $id => $entry ) {
			if ( ! empty( $entry['revoked'] ) ) {
				continue;
			}

			$hash = $entry['secret_hash'] ?? '';

			if ( '' === $hash ) {
				continue;
			}

			if ( wp_check_password( $secret, $hash ) ) {
				$entry['id'] = $id;

				return $entry;
			}
		}

		return null;
	}

	/**
	 * Determine whether an API key grants the provided scope.
	 *
	 * @param array<string, mixed> $key    Key metadata.
	 * @param string               $scope Requested scope.
	 * @return bool
	 */
	public static function has_scope( array $key, string $scope ): bool {
		$scope = sanitize_key( $scope );

		if ( '' === $scope ) {
			return false;
		}

		$scopes = array();

		if ( isset( $key['scopes'] ) && is_array( $key['scopes'] ) ) {
			$scopes = array_map( 'sanitize_key', $key['scopes'] );
		}

		if ( in_array( 'all', $scopes, true ) ) {
			return true;
		}

		return in_array( $scope, $scopes, true );
	}

	/**
	 * Update last-used timestamp for a key.
	 *
	 * @param string $id Key identifier.
	 * @param string $ip Caller IP address (optional).
	 * @return void
	 */
	public static function touch( string $id, string $ip = '' ): void {
		$keys = self::all();

		if ( ! isset( $keys[ $id ] ) ) {
			return;
		}

		$keys[ $id ]['last_used'] = time();

		if ( '' !== $ip ) {
			$keys[ $id ]['last_ip'] = sanitize_text_field( $ip );
		}

		self::save( $keys );
	}

	/**
	 * Record failed authentication attempt.
	 *
	 * @param string $reason Description/reason for failure.
	 * @return void
	 */
	public static function record_failure( string $reason ): void {
		$reason = sanitize_text_field( $reason );

		do_action( 'mail_doctor_api_key_failure', $reason );
	}

	/**
	 * Return normalised scope list.
	 *
	 * @param string[] $scopes Raw scopes.
	 * @return string[]
	 */
	public static function normalize_scopes( array $scopes ): array {
		$scopes = array_map( function($s) { return preg_replace('/[^a-z0-9_\-\.]/', '', strtolower($s)); }, $scopes );
		$scopes = array_filter(
			$scopes,
			static function ( $scope ) {
				return '' !== $scope;
			}
		);

		$scopes = array_unique( $scopes );
		$scopes = array_values( array_intersect( $scopes, self::$default_scopes ) );

		if ( empty( $scopes ) ) {
			$scopes[] = 'status.read';
		}

		return $scopes;
	}

	/**
	 * Generate a random secret string.
	 *
	 * @return string
	 */
	protected static function generate_secret(): string {
		try {
			return bin2hex( random_bytes( 20 ) );
		} catch ( \Exception $exception ) {
			return wp_generate_password( 40, false, false );
		}
	}

	/**
	 * Sanitise a key label.
	 *
	 * @param string $label Raw label.
	 * @return string
	 */
	protected static function sanitize_label( string $label ): string {
		$label = sanitize_text_field( $label );

		if ( '' !== $label ) {
			return $label;
		}

		/* translators: API key fallback label. */
		return __( 'Untitled API key', 'mail-doctor' );
	}
}
