<?php
/**
 * Class SteamGuard
 *
 * @created      20.06.2023
 * @author       smiley <smiley@chillerlan.net>
 * @copyright    2023 smiley
 * @license      MIT
 *
 * @noinspection PhpComposerExtensionStubsInspection
 */
declare(strict_types=1);

namespace chillerlan\Authenticator\Authenticators;

use chillerlan\Authenticator\Common\Base64;
use RuntimeException;
use SensitiveParameter;
use function curl_close;
use function curl_exec;
use function curl_getinfo;
use function curl_init;
use function curl_setopt_array;
use function floor;
use function intdiv;
use function is_array;
use function json_decode;
use function sprintf;
use function time;
use const CURLOPT_HTTPHEADER;
use const CURLOPT_POST;
use const CURLOPT_POSTFIELDS;
use const CURLOPT_RETURNTRANSFER;

/**
 * /data/data/com.valvesoftware.android.steam.community/f/Steamguard-STEAMID64
 *
 * @see https://help.steampowered.com/en/faqs/view/7EFD-3CAE-64D3-1C31
 * @see https://github.com/SoftCreatR/php-steam-guard
 */
final class SteamGuard extends TOTP{

	private const steamCodeChars = '23456789BCDFGHJKMNPQRTVWXY';
	private const steamTimeURL   = 'https://api.steampowered.com/ITwoFactorService/QueryTime/v0001';

	/**
	 * @inheritDoc
	 */
	public function setSecret(#[SensitiveParameter] string $encodedSecret):AuthenticatorInterface{
		$this->secret = Base64::decode($this->checkEncodedSecret($encodedSecret));

		return $this;
	}

	/**
	 * @inheritDoc
	 */
	public function getSecret():string{

		if($this->secret === null){
			throw new RuntimeException('No secret set');
		}

		return Base64::encode($this->secret);
	}

	/**
	 * @inheritDoc
	 * @codeCoverageIgnore
	 */
	public function createSecret(int|null $length = null):string{
		throw new RuntimeException('Not implemented');
	}

	/**
	 * @inheritDoc
	 */
	public function getCounter(int|null $data = null):int{
		// the period is fixed to 30 seconds for Steam Guard
		$this->options->period = 30;

		return parent::getCounter($data);
	}

	/**
	 * @inheritDoc
	 */
	public function getHMAC(int $counter):string{
		// algorithm is fixed to sha1 for Steam Guard
		$this->options->algorithm = self::ALGO_SHA1;

		return parent::getHMAC($counter);
	}

	/**
	 * @inheritDoc
	 */
	public function getOTP(#[SensitiveParameter] int $code):string{
		$str = '';
		$len = 26; // strlen($this::steamCodeChars)

		// length is fixed to 5 for Steam
		for($i = 0; $i < 5; $i++){
			$str  .= $this::steamCodeChars[($code % $len)];
			$code  = intdiv($code, $len);
		}

		return $str;
	}

	/**
	 * @inheritDoc
	 * @throws \RuntimeException
	 */
	public function getServerTime():int{

		if($this->options->forceTimeRefresh === false && $this->serverTime !== 0){
			return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
		}

		$this->serverTime      = 0;
		$this->lastRequestTime = 0;

		$options = [
			CURLOPT_RETURNTRANSFER => true,
			CURLOPT_POST           => true,
			CURLOPT_POSTFIELDS     => 'steamid=0',
			CURLOPT_HTTPHEADER     => [sprintf('User-Agent: %s', $this::userAgent)],
		];

		$ch = curl_init($this::steamTimeURL);

		// it's almost impossible to run into this, but hey, phpstan happy
		if($ch === false){
			throw new RuntimeException('curl_init error'); // @codeCoverageIgnore
		}

		curl_setopt_array($ch, $options);

		$response = curl_exec($ch);
		$info     = curl_getinfo($ch);

		curl_close($ch);

		if($info['http_code'] !== 200 || $response === false){
			// I'm not going to investigate the error further as this shouldn't happen usually
			throw new RuntimeException(sprintf('Steam API request error: HTTP/%s', $info['http_code'])); // @codeCoverageIgnore
		}

		$json = json_decode($response, true);

		if(!is_array($json) || !isset($json['response']['server_time'])){
			throw new RuntimeException('Unable to decode Steam API response'); // @codeCoverageIgnore
		}

		$this->serverTime      = (int)$json['response']['server_time'];
		$this->lastRequestTime = (time() - (int)floor($info['total_time']));

		return $this->getAdjustedTime($this->serverTime, $this->lastRequestTime);
	}

}
