<?php
namespace Site\Lib;

class Auth {
  private int $id;

  /**
   * 性別： -1 = 不明, 0 = 男性, 1 = 女性
   * ロール： -1 = BAN, 0 = ユーザー, 1 = スタッフ
   */
  private \stdClass $user;
  private \stdClass $pubUser;
  private ?string $token;

  protected string $dataDir = ROOT."/data/user/";
  protected string $assDir = ROOT."/public/static/user/";
  protected int $minPassLen = 20;
  protected int $tokenDuration = 31536000; // 1年間
  protected string|int|null $algo = PASSWORD_ARGON2ID;

  public function __construct(?string $username = null) {
    if (!AUTH_ENABLED) return;
    $secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
    $this->token = \getcookie('kerozen');
    $this->id = $this->getUserId($username, $this->token);
    if ($this->id > 0) $this->user = $this->getUserData();
  }

  public function getLoggedInUser(): \stdClass|null {
    if ($this->id === 0) return null;
    $this->pubUser = $this->user;
    unset($this->pubUser->password);
    unset($this->pubUser->tokens);
    return $this->pubUser;
  }

  public function getUser(string $name): \stdClass|null {
    $user = $this->getUserDataFromName($name);
    if (!$user) return null;
    unset($user->password);
    unset($user->tokens);
    $myself = $this->getUserData();
    if ($myself->role === 1) {
      unset($user->email);
    }
    $user->name = namecolor($user);
    $user->regDate = date('Y年m月d日', $user->regDate);
    $user->gender = $user->gender === 0 ? '男' : ($user->gender === 1 ? '女' : '不明');
    $user->role = $user->role === 1 ? '管理者' : ($user->role === -1 ? 'BANされた' : 'ユーザー');
    $user->avatar = '/static/user/'.($user->avatar === '' ? 'noicon.png' : $user->username.'/'.$user->avatar);
    $user->altName = $user->displayname ?: $user->username;
    unset($user->username);
    unset($user->displayname);
    unset($user->namecolor);
    return $user;
  }

  public function setToken(string $username, #[SensitiveParameter] string $password): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    $userData = $this->getUserData();
    if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = [];

    $verify = $this->verifyLogin($username, $password);
    if (!$verify->isSuccess) {
      return \Result::error($verify->message);
    }

    $ip = $_SERVER['REMOTE_ADDR'];
    if (!empty($_SERVER['HTTP_X_FORMARDED_FOR'])) {
      $ipList = explode(',', $_SERVER['HTTP_X_FORMARDED_FOR']);
      $ip = trim($ipList[0]);
    }

    $userData = $this->purgeOldTokens($userData);
    $token = bin2hex(random_bytes(64));
    $expire = time() + $this->tokenDuration;
    $newToken = [
      'token' => $token,
      'ip' => $ip,
      'createDate' => time(),
      'expDate' => $expire,
      'lastDate' => time(),
      'userAgent' => $_SERVER['HTTP_USER_AGENT'] ?? '不明'
    ];

    $userData->tokens[] = $newToken;

    $path = $this->dataDir.$this->id.'.'.$userData->username.'.json';
    $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if (file_put_contents($path, $json) === false) {
      return \Result::error('エラー：ユーザーデータの保存に失敗。');
    }

    if (!setcookie('kerozen', $token, [
      'expires' => $expire,
      'path' => '/',
      'domain' => $_SERVER['SERVER_NAME'],
      'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
      'httponly' => true,
      'samesite' => 'Strict'
    ])) {
      return \Result::error('エラー：クッキーを設定に失敗。');
    }

    return \Result::success('ログイン成功');
  }

  public function getToken(): string {
    if (!AUTH_ENABLED) return '';
    $userData = $this->getUserData();
    if (!isset($userData->tokens) || !is_array($userData->tokens)) $userData->tokens = [];
    $userData = $this->purgeOldTokens($userData);
  }

  public function logout(?string $token = null): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    $userData = $this->getUserData();

    if (!$token) {
      $userData->tokens = array_filter($userData->tokens, function($t): bool {
        return ($t->expires ?? 0) > time();
      });
    }

    setcookie('kerozen', null, [
      'expires' => 0,
      'path' => '/',
      'domain' => $_SERVER['SERVER_NAME'],
      'secure' => (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'),
      'httponly' => true,
      'samesite' => 'Strict'
    ]);

    return \Result::success();
  }

  public function isUserExist(?string $username = null): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    if (null === $username) return \Result::Error('エラー：ユーザー名をご入力下さい。');
    $userList = scandir($this->dataDir);

    foreach ($userList as $list) {
      if ($list === '.kara' || $list === '.' || $list === '..') continue;
      $file = str_replace('.json', '', $list);
      $user = explode('.', $file)[1];
      if ($username === $user) return \Result::Success("ユーザー「{$username}」は既に存在します。");
    }

    return \Result::Error("エラー：ユーザー「{$username}」は存在しません。");
  }

  public function mkUser(?string $username = null, #[SensitiveParameter] ?string $password = null, ?string $passwordVerify = null, ?string $email = null): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    if (!AUTH_REGISTER_ENABLED) return \Result::Error('ユーザー登録は無効です。');
    $resUsr = $this->verifyUsername($username);
    $resPwd = $this->verifyPassword($password, $passwordVerify);
    $resEml = $this->verifyEmail($email);

    $err = '';

    if (!$resUsr->isSuccess) $err .= $resUsr->message."<br />";
    if (!$resPwd->isSuccess) $err .= $resPwd->message."<br />";
    if (!$resEml->isSuccess) $err .= $resEml->message."<br />";

    if ($err != '') return \Result::Error($err);
    $err = '';

    if (!mkdir($this->assDir.$username, 0755)) {
      return \Result::Error('エラー：ユーザーのアイコンディレクトリの作成に失敗。');
    }

    $file = scandir($this->dataDir);
    $lastId = 0;
    if ($file) {
      foreach ($file as $f) {
        if ($f === '.kara' || $f === '.' || $f === '..') continue;
        $base = substr($f, 0, -5); // .jsonの削除
        $dot = strpos($base, '.');
        if (!$dot) continue;
        $id = substr($base, 0, $dot);
        if (ctype_digit($id)) {
          if ((int)$id > $lastId) $lastId = $id;
        }
      }
      $lastId++;
    }

    $user = new \stdClass;
    $user->id = $lastId;
    $user->username = $username;
    $user->password = password_hash($password, $algo);
    $user->avatar = '';
    $user->email = $email;
    $user->regDate = time();
    $user->namecolor = '';
    $user->displayname = '';
    $user->gender = -1;
    $user->role = 0;
    $user->tokens = [];

    $path = "{$this->dataDir}{$lastId}.{$username}.json";
    $json = json_encode($user, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if (file_put_contents($path, $json) === false) {
      rmdir($this->assDir.$username);
      return \Result::Error('エラー：ユーザーデータの保存に失敗。');
    }

    return \Result::Success();
  }

  ////////////////////

  private function verifyLogin(string $username, #[SensitiveParameter] string $password): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    $userData = $this->getUserData();
    if ($username !== $userData->username || !password_verify($password, $userData->password)) {
      return \Result::Error('エラー：ユーザー名又はパスワードが一致していません。');
    }

    if (password_needs_rehash($userData->password, $algo)) {
      $userData->password = password_hash($password, $algo);
      $path = $this->dataDir.$this->id.'.'.$userData->username.'.json';
      $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

      if (file_put_contents($path, $json) === false) {
        return \Result::Error('エラー：ユーザーデータの保存に失敗。');
      }
    }

    return \Result::Success();
  }

  private function purgeOldTokens(\stdClass $userData): \stdClass|\Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    // 有効期限切れたトークンの削除
    $out = $userData;
    $out->tokens = array_filter($userData->tokens, function($t): bool {
      return ($t->expDate ?? 0) > time();
    });

    $path = $this->dataDir.$userData->id.'.'.$userData->username.'.json';
    $json = json_encode($userData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    if (file_put_contents($path, $json) === false) {
      return \Result::Error('エラー：ユーザーデータの保存に失敗。');
    }

    return $out;
  }

  private function getUserId(?string $username = null, ?string $token = null): int {
    if (!AUTH_ENABLED) return 0;
    if (!$username && !$token) return 0;
    $file = scandir($this->dataDir);
    $matches = [];
    $id = 0;

    foreach ($file as $f) {
      if ($f === '.kara' || $f === '.' || $f === '..') continue;

      $path = "{$this->dataDir}{$f}";
      if (!is_file($path)) continue;
      $content = file_get_contents($path);
      if ($token && str_contains($content, $token)) {
        $matches[] = $path;
        break;
      } else if (!$token && str_contains($content, '"username": "'.$username.'",')) {
        $matches[] = $path;
        break;
      }
    }
    if (!isset($matches[0])) return 0;
    $id = (int)preg_replace('/^(.*?)\.(.*?)$/', "$1", str_replace($this->dataDir, '', $matches[0]));

    return $id;
  }

  private function getUserData(): \stdClass {
    if (!AUTH_ENABLED) return new \stdClass;
    $file = scandir($this->dataDir);
    $userFile = "";
    $matches = [];

    foreach ($file as $f) {
      if ($f === '.kara' || $f === '.' || $f === '..') continue;
      if (preg_match('/^'.$this->id.'\.(.*?)\.json$/', $f, $matches)) {
        $userFile = $matches[0];
        break;
      }
    }
    $path = $this->dataDir.$userFile;
    unset($file, $userFile);

    if (!file_exists($path) || !is_readable($path)) {
      kys('エラー');
    }

    $fp = fopen($path, 'r');
    assert_not_null($fp);
    if (!$fp) kys('ユーザーが存在しない。');
    $lines = "";
    while (($buf = fgets($fp, 4096)) !== false) $lines .= $buf.PHP_EOL;
    if (!feof($fp)) kys("エラー：不明なエラー");
    fclose($fp);
    unset($path);
    return json_decode($lines);
  }

  private function getUserDataFromName(string $name): \stdClass|null {
    if (!AUTH_ENABLED) return new \stdClass;
    $file = scandir($this->dataDir);
    $userFile = "";
    $matches = [];

    foreach ($file as $f) {
      if ($f === '.kara' || $f === '.' || $f === '..') continue;
      if (preg_match('/^(.*?)\.(.*?)\.json$/', $f, $matches)) {
        if (isset($matches[2]) && $matches[2] === $name) {
          $userFile = $matches[0];
          break;
        }
      }
    }
    if (!$userFile) return null;
    $path = $this->dataDir.$userFile;
    unset($file, $userFile);

    if (!file_exists($path) || !is_readable($path)) {
      kys('エラー');
    }

    $fp = fopen($path, 'r');
    assert_not_null($fp);
    if (!$fp) kys('ユーザーが存在しない。');
    $lines = "";
    while (($buf = fgets($fp, 4096)) !== false) $lines .= $buf.PHP_EOL;
    if (!feof($fp)) kys("エラー：不明なエラー");
    fclose($fp);
    unset($path);
    return json_decode($lines);
  }

  private function isEmailExist(?string $email = null): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    $userList = scandir($this->dataDir);
    $matches = [];

    foreach ($userList as $f) {
      if ($f === '.kara' || $f === '.' || $f === '..') continue;
      $path = "{$this->dataDir}{$f}";
      if (!is_file($path)) continue;
      $content = file_get_contents($path);
      $file = str_replace('.json', '', $f);
      if (str_contains($content, '"email": "'.$email.'",')) {
        $matches[] = $email;
      }
      if (count($matches) > 0) return \Result::Error("ユーザー「{$email}」は既に存在します。");
    }

    return \Result::Success('エラー：メールアドレスが存在しません。');
  }

  private function verifyEmail(?string $email = null): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    if (null === $email || '' === $email) return \Result::Error('エラー：メールアドレスをご入力下さい。');
    if (strpos($email, '@') === false) return \Result::Error('エラー：メールアドレスは不正です。');

    $domain = explode('@', $email)[1];
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
      return \Result::Error('エラー：メールアドレスは不正です。');
    }

    if (!checkdnsrr($domain, 'MX')) {
      return \Result::Error('エラー：メールアドレスは不正です。');
    }

    $res = $this->isEmailExist($email);
    if (!$res->isSuccess) return \Result::Error($res->message);

    return \Result::Success();
  }

  private function verifyUsername(?string $username): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    if (null === $username || '' === $username) return \Result::Error('エラー：ユーザー名をご入力下さい。');
    if (strlen($username) < 6) return \Result::Error('エラー：ユーザー名は6文字以上をご入力下さい。');

    $res = $this->isUserExist($username);
    if ($res->isSuccess) return \Result::Error($res->message);

    $accepted = 'A-Za-z0-9';
    if (preg_match("/[^{$accepted}]/", $username)) {
      return \Result::Error('エラー：ユーザー名に不正な文字が含みます。');
    }

    return \Result::Success();
  }

  private function verifyPassword(#[SensitiveParameter] ?string $password = null, #[SensitiveParameter] ?string $passwordVerify = null): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    if (null === $password || '' === $password) return \Result::Error('エラー：パスワードをご入力下さい。');
    if ($password !== $passwordVerify) return \Result::Error('エラー：パスワードは一致していません。');

    $res = $this->checkPasswordStandards($password);
    if (!$res->isSuccess) {
      return \Result::Error($res->message);
    }

    return \Result::Success();
  }

  private function checkPasswordStandards(#[SensitiveParameter] ?string $password = null): \Result {
    if (!AUTH_ENABLED) return \Result::Error('エラー：認証システムは無効です。');
    if (strlen($password) < $this->minPassLen) return \Result::Error("エラー：パスワードは{$this->minPassLen}以上をご入力下さい。");

    if (!\countmatch($password)) {
      return \Result::Error('エラー：パスワードに不正な文字が含みます。');
    }

    $countUpper = preg_match_all('/[A-Z]/', $password) < 2;
    $countLower = preg_match_all('/[a-z]/', $password) < 2;
    $countDigit = preg_match_all('/[0-9]/', $password) < 2;
    $countSymbol = \count_special_chars($password) < 2;

    if ($countUpper || $countLower || $countDigit || $countSymbol) {
      return \Result::Error('エラー：パスワードは2つ以上の大文字、2つ以上の小文字、2つ以上の数字、及び2つ以上の特別文字を含む事が必須です。');
    }

    return \Result::Success();
  }
};
