<?php

namespace Core\Objects\TwoFactor;

use Base32\Base32;
use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions;
use Core\Driver\SQL\SQL;
use Core\Objects\Context;
use Core\Objects\DatabaseEntity\Attribute\Visibility;
use Core\Objects\DatabaseEntity\TwoFactorToken;

class TimeBasedTwoFactorToken extends TwoFactorToken {

  const TYPE = "totp";

  #[Visibility(Visibility::NONE)]
  private string $secret;

  public function __construct(string $secret) {
    parent::__construct(self::TYPE);
    $this->secret = $secret;
  }

  protected function readData(string $data) {
    $this->secret = $data;
  }

  public function getUrl(Context $context): string {
    $otpType = self::TYPE;
    $name = rawurlencode($context->getUser()->getUsername());
    $settings = $context->getSettings();
    $urlArgs = [
      "secret" => $this->secret,
      "issuer" => $settings->getSiteName(),
    ];

    $urlArgs = http_build_query($urlArgs);
    return "otpauth://$otpType/$name?$urlArgs";
  }

  public function generateQRCode(Context $context) {
    $options = new QROptions(['outputType' => QRCode::OUTPUT_IMAGE_PNG, "imageBase64" => false]);
    $qrcode = new QRCode($options);
    return $qrcode->render($this->getUrl($context));
  }

  public function generate(?int $at = null, int $length = 6, int $period = 30): string {
    if ($at === null) {
      $at = time();
    }

    $seed = intval($at / $period);
    $secret =  Base32::decode($this->secret);
    $hmac = hash_hmac('sha1', pack("J", $seed), $secret, true);
    $offset = ord($hmac[-1]) & 0xF;
    $code = (unpack("N", substr($hmac, $offset, 4))[1] & 0x7fffffff) % intval(pow(10, $length));
    return substr(str_pad(strval($code), $length, "0", STR_PAD_LEFT), -1 * $length);
  }

  public function verify(string $code, int $numCodes = 2): bool {
    $now = time();
    $length = 6;
    $period = 30;

    // verify the last $numCodes codes
    for ($i = 0; $i < max(1, $numCodes); $i++) {
      if ($this->generate($now - $period * $i, $length, $period) === $code) {
        return true;
      }
    }

    return false;
  }

  public function getData(): string {
    return $this->secret;
  }
}