<?php
// Import the PHPMailer class into the global namespace
use PHPMailer\PHPMailer\PHPMailer;

class User extends Model
{
  private bool $_isChanged = false;
  private bool $_autoSave = true;
  private string $_lastError = '';

  public function __construct($id)
  {
    parent::__construct($id);

    // Critical properties
    $this->userName = '';
    $this->password = '';
    $this->callsign = null;
    $this->email = '';
    $this->emailVerified = 0;
    $this->callsignVerified = 0;
  }


  /**
   * User class destructor
   ** Saves changes to the user record if there are any (and autoSave is not disabled)
   */
  public function __destruct()
  {
    if ($this->_autoSave && $this->_isChanged) $this->save();
  }


  /**
   * Set AutoSave
   ** Allows (or prevents) changes to the user record from being saved to the database at
   ** completion of script execution.  Enabled by default.
   *
   * @param string $autoSave (True if changes are to be saved automatically, otherwise false)
   */
  public function setAutoSave($autoSave)
  {
    $this->_autoSave = $autoSave;
  }


  /**
   * Check the username for validity, then set
   ** Has minimum 4 characters in length.
   ** Acceptable characters A-Z, a-z, 0-9, underscore(_), hyphen(-)
   *
   * @param string $username
   * @return boolean (True if username is acceptable and was set, otherwise false)
   */
  public function setUsername($username)
  {
    // Remove whitespace
    $username = trim($username);

    // Test the username and set it if passes
    if (strlen($username) <= 4 || preg_match('/[^a-z_\-0-9]/i', $username) == 0)
    {
      // Username is good, check if it exists
      $user = UserRepository::getInstance()->getObjectByUsername($username);
      if (!$user->isExistingObject())
      {
        $this->userName = $username;
        $this->_isChanged = true;
        return true;
      }

      $this->_lastError = 'Username is already in use.';
    }
    else
    {
      $this->_lastError = 'Username is too short or contains invalid characters. Acceptable characters A-Z, a-z, 0-9, underscore(_), hyphen(-).';
    }
    return false;
  }


  /**
   * Check the password for validity, then set and hash
   ** Has minimum 8 characters in length. Adjust it by modifying {8,}
   ** At least one uppercase English letter. You can remove this condition by removing (?=.*?[A-Z])
   ** At least one lowercase English letter.  You can remove this condition by removing (?=.*?[a-z])
   ** At least one digit. You can remove this condition by removing (?=.*?[0-9])
   ** //At least one special character,  You can remove this condition by removing (?=.*?[#?!@$%^&*-])
   **
   ** "/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/"
   *
   * @param string $password (Plain text password)
   * @return boolean (True if password is acceptable and was set, otherwise false)
   */
  public function setPassword($password)
  {
    // Remove whitespace
    $password = trim($password);

    // Test the password and set it if passes (no )
    if (preg_match("/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$/", $password) == 1)
    {
      // Password is good, hash it
      $this->password = password_hash($password, PASSWORD_ARGON2I);
      $this->_isChanged = true;
      return true;
    }
    else
    {
      $this->_lastError = 'Password does not meet length or complexity requirements.';
    }
    return false;
  }


  /**
   * Check the password against the encrypted password.
   *
   * @param string $password (Plain text password)
   * @return boolean (True if password is correct, otherwise false)
   */
  public function verifyPassword($password)
  {
    if ($this->password && !empty($this->password))
    {
      // Remove whitespace
      $password = trim($password);
      return password_verify($password, $this->password);
    }
    return false;
  }


  /**
   * Check the password against the encrypted password.
   *
   * @return array (Cookie parameters, otherwise false)
   */
  public function generatePersistentAuthToken()
  {
    // Generate a selector (aka database key)
    $selector = generateSecureToken(8); // Returns 16 bytes
    $identifier = generateSecureToken(32); // Returns 64 bytes
    $identifier_hashed = hash('sha256', $identifier);

    $cookie_expiration_time = time() + 7776000; // 90 days

    // Insert into the database
    $pdo = PDOMysqlConnection::getInstance();

    $sql = 'INSERT INTO auth_tokens (user_id, selector, validator, expiration)
            VALUES (?, ?, ?, FROM_UNIXTIME(?))';
    $stmt = $pdo->prepareAndExec($sql, [$this->_id, $selector, $identifier_hashed, $cookie_expiration_time]);

    $id = $pdo->lastInsertId();
    if ($id)
    {
      return
      [
        'id'        => $id,
        'selector'  => $selector,
        'identifier'=> $identifier,
        'expiration'=> $cookie_expiration_time
      ];
    }
    return false;
  }


  /**
   * Validates the user token
   *
   * @param string $selector (Plain text 16 byte selector that identities the user)
   * @param string $identifier (Plain text 32 byte unique identifier that is compared against stored hash)
   * @return array (Cookie parameters, otherwise false)
   */
  public function validatePersistentAuthToken($selector, $identifier)
  {
    $selector = trim($selector);

    // Get the latest token from the database
    $pdo = PDOMysqlConnection::getInstance();

    $sql = 'SELECT validator, UNIX_TIMESTAMP(expiration) AS expires FROM auth_tokens WHERE user_id = ? AND selector = ? ORDER BY id DESC LIMIT 1';
    $stmt = $pdo->prepareAndExec($sql, [$this->_id, $selector]);
    $latestToken = $stmt->fetch(PDO::FETCH_ASSOC);

    // Make sure a token was found and not expired
    if ($latestToken && $latestToken['expires'] > time())
    {
      // Validate the validator against the stored hash
      if (hash_equals(hash('sha256', $identifier), $latestToken['validator']))
      {
        return true;
      }
    }
    $this->_lastError = 'Persistent token not found.';
    return false;
  }


  /**
   * Helper function for changing password by first checking the old password
   * against the encrypted password and setting the new password if the password
   * validates successfully.
   *
   * @param string $oldPassword (Password that is being changed)
   * @param string $newPassword (Password that is being set)
   * @return boolean (True if password is correct, otherwise false)
   */
  public function changePassword($oldPassword, $newPassword)
  {
    // Can't do anything if the password has not been set (hashed)
    if ($this->password == null)
    {
      $this->_lastError = 'Cannot change password, no password is currently set.';
      return false;
    }

    // Remove whitespace
    $oldPassword = trim($oldPassword);
    $newPassword = trim($newPassword);

    // Sanity check
    if (empty($oldPassword) || empty($newPassword))
    {
      $this->_lastError = 'Cannot change password, old password or new password are empty.';
      return false;
    }

    // Verify the old password and set if correct
    if ($this->verifyPassword($oldPassword))
    {
      // setPassword will populate _lastError if the password is invalid
      return $this->setPassword($newPassword);
    }
    else
    {
      $this->_lastError = 'The old password is incorrect.';
    }

    return false;
  }


  /**
   * Check the call for validity, then set
   ** Has minimum 4 characters in length.
   ** Acceptable characters A-Z, 0-9.  Minimum length 3, Maximum length 10.
   *
   * @param string $callsign
   * @return boolean (True if callsign is acceptable and was set, otherwise false)
   */
  public function setCallsign($callsign)
  {
    if (is_null($callsign) || strlen($callsign) == 0)
    {
      $this->callsign = null;
      return true;
    }

    // Remove whitespace & make all uppercase
    $callsign = strtoupper(trim($callsign));

    // Test the password and set it if passes (no )
    if (strlen($callsign) >= 3 && strlen($callsign) <= 10  && preg_match('/[^a-z0-9]/i', $callsign) == 0)
    {
      // Callsign is good, make sure it is not in use by another user
      $user = UserRepository::getInstance()->getObjectByCallsign($callsign);
      if (!$user->isExistingObject())
      {
        if (!$this->isExistingObject() || ($this->isExistingObject() && is_null($this->callsign)))
        $this->callsign = $callsign;
        $this->callsignVerified = 0;
        $this->_isChanged = true;

        // Attempt callsign verification is the e-mail has already been verified
        if ($this->emailVerified)
        {
          $this->completeCallsignVerification();
        }
        return true;
      }

      $this->_lastError = 'Callsign is already in use with another user.';
    }
    else
    {
      $this->_lastError = 'Callsign must be 3-10 characters. Acceptable characters A-Z, 0-9.';
    }
    return false;
  }


  /**
   * Check the email address for validity, then set
   ** Has minimum 4 characters in length.
   ** Acceptable characters A-Z, 0-9.  Minimum length 3, Maximum length 10.
   *
   * @param string $callsign
   * @return boolean (True if email address is acceptable and was set, otherwise false)
   */
  public function setEmail($email)
  {
    // Remove whitespace
    $email = strtolower(trim($email));

    // Test the e-mail address and set it if passes (no )
    // Also make sure it's not the same e-mail which wouldn't need to be saved
    if (filter_var($email, FILTER_VALIDATE_EMAIL) && $email != $this->email)
    {
      // Email is good, make sure it is not in use by another user
      $user = UserRepository::getInstance()->getObjectByEmail($email);
      if (!$user->isExistingObject())
      {
        $this->email = $email;
        $this->_isChanged = true;
        $this->emailVerified = 0;
        return true;
      }

      $this->_lastError = 'E-mail address is already in use with another user.';
    }
    else
    {
      $this->_lastError = 'E-mail address is not valid.';
    }
    return false;
  }


  /**
   * Starts the e-mail verification process
   *
   * @return boolean (True if email with verification code was sent successfully)
   */
  public function startEmailVerification()
  {
    // Do nothing if not enabled in config
    if (getWebsiteConfig('smtp_server_address') == '') return false;

    // Test the e-mail address before allowing the verification flag to be set to true
    if (!isset($this->email) || !filter_var($this->email, FILTER_VALIDATE_EMAIL))
    {
      $this->_lastError = 'Cannot start e-mail verification, e-mail address is not valid.';
      return false;
    }

    // Generate random reset code
    $code = generateSecureCode(6);

    // Code expires in 30 minutes
    $expiration = time() + 1800;

    // Get the site backend database connection
    $pdo = PDOMysqlConnection::getInstance();

    // Save the code
    $sql = 'INSERT INTO email_verification (user_id, code, expiration)
            VALUES (?, ?, FROM_UNIXTIME(?))';

    $stmt = $pdo->prepareAndExec($sql, [$this->_id, $code, $expiration]);
    $id = $pdo->lastInsertId();
    if ($id)
    {
      // Set to unverified
      $this->emailVerified = false;
      $this->_isChanged = true;
      $this->_lastError = $code;

      $confirmation_link_with_code = '';
      $confirmation_link_without_code = '';

      // Send the confirmation e-mail
      $use_secure_smtp = !empty(getWebsiteConfig('smtp_server_password')) ? true : false;

      // Note: passing `true` enables exceptions
      $mail = new PHPMailer(true);
      try
      {
        // PHP_Mailer Server settings
        //$mail->SMTPDebug  = \PHPMailer\PHPMailer\SMTP::DEBUG_SERVER;  // Enable verbose debug output
        $mail->isSMTP();                                              // Send using SMTP
        $mail->Host       = getWebsiteConfig('smtp_server_address');  // Set the SMTP server to send through
        $mail->SMTPAuth   = $use_secure_smtp;                         // Enable SMTP authentication
        $mail->Username   = getWebsiteConfig('smtp_server_username'); // SMTP username
        $mail->Password   = getWebsiteConfig('smtp_server_password'); // SMTP password
        $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;           // Enable implicit TLS encryption
        $mail->Port       = getWebsiteConfig('smtp_server_port');     // TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`

        // Set to & from addresses
        $mail->setFrom(getWebsiteConfig('smtp_from_email'), getWebsiteConfig('smtp_from_email'));
        $mail->addAddress($this->email, $this->userName);

        // Content
        $mail->isHTML(true);                                          // Set email format to HTML
        $mail->Subject = 'Confirm your ' . getWebsiteConfig('title') . ' Account';
        $mail->Body = 'You are receiving this e-mail because an account has been associated with this e-mail address at the <a href="' . getWebsiteConfig('cookie_domain') . '">'
                          . getWebsiteConfig('title') . "</a>\r\nIf you believe that this has been received in error, you may safely disregard this message.\r\n"
                          . "\r\n\r\n<br><br>If you recognize this account, please complete your account activation by confirming your e-mail address.\r\n"
                          . 'Follow this link to <a href="' . getWebsiteConfig('email_link_url') . '/views/validate.php?a=email&code=' . $code . '">verify your account</a>. If asked, enter code <b>' . $code . "</b>\r\n\r\n"
                          . "\r\n" . '<br><br>Questions? Please <a href="' . getWebsiteConfig('email_link_url') . '/views/contact.php">contact us</a>.' . "\r\n";

        $mail->AltBody = 'You are receiving this e-mail because an account has been associated with this e-mail address at the ' . getWebsiteConfig('title')
                          . getWebsiteConfig('cookie_domain') . "\r\nIf you believe that this has been received in error, you may safely disregard this message.\e\n"
                          . "\r\n\r\nIf you recognize this account, please complete your account activation by confirming your e-mail address.\r\n"
                          . "Go to " . getWebsiteConfig('email_link_url') . "/views/validate.php?a=email and enter code $code.\r\n\r\n"
                          . "Questions? Please contact us at " . getWebsiteConfig('email_link_url') . "/views/contact.php\r\n";

        $mail->send();
      }
      catch (Exception $e)
      {
        $this->_lastError = "Activation e-mail could not be sent. Mailer Error: {$mail->ErrorInfo}";
        return false;
      }
      return true;
    }
    $this->_lastError = 'An error occurred in generating or sending the email verification.';
    return false;
  }


  /**
   * Completes e-mail verification.
   ** This is based on whether the user has completed e-mail verification by
   ** following the link in the e-mail or entering the code.
   *
   * @param string $code (Verification code to test)
   * @return boolean (True if email address is verification is complete, otherwise false)
   */
  public function completeEmailVerification($code)
  {
    $code = trim($code);

    // Test the e-mail address before allowing the verification flag to be set to true
    if ($code && (!isset($this->email) || !filter_var($this->email, FILTER_VALIDATE_EMAIL)))
    {
      $this->_lastError = 'Cannot complete e-mail verification, e-mail address is not valid.';
      return false;
    }

    // Get the lastest email verification record for the user
    $sql = 'SELECT code, UNIX_TIMESTAMP(expiration) AS expires FROM email_verification WHERE user_id = ? ORDER BY id DESC LIMIT 1';
    $pdo = PDOMysqlConnection::getInstance();
    $stmt = $pdo->prepareAndExec($sql, [$this->_id]);
    $verification = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($verification)
    {
      // Does the code match, and is it not expired?
      if (time() < $verification['expires'])
      {
        if ($code == $verification['code'])
        {
          $this->emailVerified = true;
          $this->_isChanged = true;
          return true;
        }
        else
        {
          $this->_lastError = 'Cannot complete e-mail verification, verification code is incorrect.';
        }
      }
      else
      {
        $this->_lastError = 'Cannot complete e-mail verification, verification code has expired.';
      }
    }
    else
    {
      $this->_lastError = 'Cannot complete e-mail verification, no verification exists for this user.';
    }

    return false;
  }


  /**
   * Returns the e-mail verification status as text
   *
   * @return string (Verified or Unverified)
   */
  public function getEmailVerificationStatus()
  {
    return $this->emailVerified ? 'Verified' : 'Unverified';
  }



  /**
   * Completes callsign verification.
   ** Contacts QRZ.com and pulls the users profile and verifies the e-mail address.
   *
   * @return boolean (True if callsign is verification is complete, otherwise false)
   */
  public function completeCallsignVerification()
  {
    if (@is_null($this->callsign) || strlen($this->callsign) > 3)
    {
      // E-Mail address must first be verified
      if ($this->emailVerified == 1)
      {
        // Create a new instance of the QRZ helper class
        $qrz = new QRZ();
        if ($qrz->queryCall($this->callsign))
        {
          if (isset($qrz->Callsign) && isset($qrz->Callsign->email))
          {
            if (strtolower($qrz->Callsign->email) == strtolower($this->email))
            {
              $this->callsignVerified = 1;
              $this->_isChanged = true;
              return true;
            }
            else
            {
              $this->_lastError = 'The e-mail address does not match your public QRZ.com profile.';
            }
          }
          else
          {
            $this->_lastError = 'No response or missing data reported by QRZ.com.';
          }
        }
        else
        {
          // Pass the error from QRZ through
          $this->_lastError = $qrz->_lastError;
        }
      }
      else
      {
        $this->_lastError = 'E-mail address must be verified first.';
      }
    }
    else
    {
      $this->_lastError = 'Invalid callsign.';
    }

    return false;
  }


  /**
   * Returns thecallsign verification status as text
   *
   * @return string (Verified or Unverified)
   */
  public function getCallsignVerificationStatus()
  {
    return $this->callsignVerified ? 'Verified' : 'Unverified';
  }


  /**
   * Starts the password recovery process
   *
   * @return boolean (True if email with verification code was sent successfully)
   */
  public function startPasswordRecovery()
  {
    // Do nothing if not enabled in config
    if (getWebsiteConfig('smtp_server_address') == '') return false;

    // Generate random reset code
    $code = generateSecureCode(6);

    // Code expires in 30 minutes
    $expiration = time() + 1800;

    // Get the site backend database connection
    $pdo = PDOMysqlConnection::getInstance();

    // Save the code
    $sql = 'INSERT INTO password_verification (user_id, code, expiration)
            VALUES (?, ?, FROM_UNIXTIME(?))';

    $stmt = $pdo->prepareAndExec($sql, [$this->_id, $code, $expiration]);
    $id = $pdo->lastInsertId();
    if ($id)
    {
      $this->_lastError = $code;

      $confirmation_link_with_code = '';
      $confirmation_link_without_code = '';

      // Send the confirmation e-mail
      $use_secure_smtp = !empty(getWebsiteConfig('smtp_server_password')) ? true : false;

      // Note: passing `true` enables exceptions
      $mail = new PHPMailer(true);
      try
      {
        // PHP_Mailer Server settings
        //$mail->SMTPDebug  = \PHPMailer\PHPMailer\SMTP::DEBUG_SERVER;  // Enable verbose debug output
        $mail->isSMTP();                                              // Send using SMTP
        $mail->Host       = getWebsiteConfig('smtp_server_address');  // Set the SMTP server to send through
        $mail->SMTPAuth   = $use_secure_smtp;                         // Enable SMTP authentication
        $mail->Username   = getWebsiteConfig('smtp_server_username'); // SMTP username
        $mail->Password   = getWebsiteConfig('smtp_server_password'); // SMTP password
        $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;           // Enable implicit TLS encryption
        $mail->Port       = getWebsiteConfig('smtp_server_port');     // TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`

        // Set to & from addresses
        $mail->setFrom(getWebsiteConfig('smtp_from_email'), getWebsiteConfig('smtp_from_email'));
        $mail->addAddress($this->email, $this->userName);

        // Content
        $mail->isHTML(true);                                          // Set email format to HTML
        $mail->Subject = 'Password recovery for your ' . getWebsiteConfig('title') . ' account.';
        $mail->Body = 'You are receiving this e-mail because an account which has been associated with this e-mail address at the <a href="' . getWebsiteConfig('cookie_domain') . '">'
                          . getWebsiteConfig('title') . "</a> has requested a password reset.\r\nIf you believe that this has been received in error, you may safely disregard this message.\r\n"
                          . "\r\n\r\n<br><br>If you recognize this account, please complete your password recovery by visitng the link below.\r\n"
                          . 'Follow this link to <a href="' . getWebsiteConfig('email_link_url') . '/views/validate.php?a=password&code=' . $code . '&email=' . urlencode($this->email) . '">reset your password</a>. If asked, enter code <b>' . $code . "</b>\r\n\r\n"
                          . "\r\n" . '<br><br>Questions? Please <a href="' . getWebsiteConfig('email_link_url') . '/views/contact.php">contact us</a>.' . "\r\n";

        $mail->AltBody = 'You are receiving this e-mail because an account which has been associated with this e-mail address at the ' . getWebsiteConfig('title')
                          . getWebsiteConfig('cookie_domain') . " has requested a password reset.\r\nIf you believe that this has been received in error, you may safely disregard this message.\e\n"
                          . "\r\n\r\nIf you recognize this account, please complete your password recovery by visitng the link below.\r\n"
                          . "Go to " . getWebsiteConfig('email_link_url') . "/views/validate.php?a=pass and enter code $code.\r\n\r\n"
                          . "Questions? Please contact us at " . getWebsiteConfig('email_link_url') . "/views/contact.php\r\n";

        $mail->send();
      }
      catch (Exception $e)
      {
        $this->_lastError = "Password reset e-mail could not be sent. Mailer Error: {$mail->ErrorInfo}";
        return false;
      }
      return true;
    }
    $this->_lastError = 'An error occurred in generating or sending the password reset email.';
    return false;
  }


  /**
   * Completes password recovery process.
   ** This process validates the 6 digit code, and if confirmed
   ** return true to alow the password reset process to be completed.
   *
   * @param string $code (Verification code to test)
   * @param string $newpassword (New password to set if the code matches)
   * @return boolean (True if email address is verification is complete, otherwise false)
   */
  public function completePasswordRecovery($code, $email, $newPassword)
  {
    $code = trim($code);
    $email = trim($email);
    $newPassword = trim($newPassword);

    // Get the lastest email verification record for the user
    $sql = 'SELECT user_id, UNIX_TIMESTAMP(expiration) AS expires FROM password_verification WHERE code = ? ORDER BY id DESC LIMIT 1';
    $pdo = PDOMysqlConnection::getInstance();
    $stmt = $pdo->prepareAndExec($sql, [$code]);
    $verification = $stmt->fetch(PDO::FETCH_ASSOC);

    if ($verification)
    {
      // Does the code match, and is it not expired?
      if (time() < $verification['expires'])
      {
        // Code is valid, load the user record
        $user = UserRepository::getInstance()->getObjectById($verification['user_id']);

        // Set the password if the user account is found
        if ($user->isExistingObject())
        {
          // Compare the e-mail address provided
          if ($email == $user->email)
          {
            // Set the new password if it's valid then delete the reset code so it cannot be used again
            if ($user->setPassword($newPassword))
            {
              $sql = 'DELETE FROM password_verification WHERE user_id = ?';
              $pdo = PDOMysqlConnection::getInstance();
              $stmt = $pdo->prepareAndExec($sql, [$user->_id]);
              return true;
            }
            else
            {
              $this->_lastError = $user->getLastError();
              return false;   // _lastError is set by setPassword();
            }
          }
          else
          {
            $this->_lastError = 'Cannot complete password recovery, e-mail address provided is not on record.';
          }
        }
        else
        {
          $this->_lastError = 'Cannot complete password recovery, verification code is incorrect.';
        }
      }
      else
      {
        $this->_lastError = 'Cannot complete password recovery, verification code has expired.';
      }
    }
    else
    {
      $this->_lastError = 'Cannot complete password recovery, no verification exists for this user.';
    }

    return false;
  }


  /**
   * Save the user record if there are any changes pending
   *
   * @return boolean (True if save was successful, otherwise false)
   */
  public function save()
  {
    // Don't save unless something has been changed
    if ($this->_isChanged === false)
    {
      $this->_lastError = 'No changes have been made to save.';
      return false;
    }

    // Make sure all critical properties are completed
    if ($this->userName == null || $this->password == null
        || $this->email == null)
    {
      $this->_lastError = 'Cannot save, one or more user properties are missing.';
      return false;
    }

    // Get the site backend database connection
    $pdo = PDOMysqlConnection::getInstance();

    // Inserting or saving?
    if ($this->isExistingObject())
    {
      $sql = 'UPDATE users
              SET user_name = ?, password = ?, callsign = ?, email = ?, email_verified = ?, callsign_verified = ?
              WHERE id = ?
              LIMIT 1';
      $stmt = $pdo->prepareAndExec($sql, [$this->userName, $this->password, $this->callsign, $this->email, $this->emailVerified, $this->callsignVerified, $this->_id]);
      if ($stmt)
      {
        $this->_isChanged = false;
        return true;
      }
      $this->_lastError = 'An error occurred saving the user record.';
    }
    else
    {
      $sql = 'INSERT INTO users (user_name, password, callsign, email, email_verified, callsign_verified)
              VALUES (?, ?, ?, ?, ?, ?)';

      $stmt = $pdo->prepareAndExec($sql, [$this->userName, $this->password, $this->callsign, $this->email, $this->emailVerified, $this->callsignVerified]);
      $id = $pdo->lastInsertId();
      if ($id)
      {
        $this->_id = $id;
        $this->_isChanged = false;
        return true;
      }

      $this->_lastError = 'An error occurred creating the user record.';
    }

    return false;
  }


  /**
   * Checks the saved user favorite stations to see if the station is a favorite station
   *
   * @param int $station_id
   * @return boolean (True if station is a favorite, otherwise false)
   */
  public function isStationAFavorite($station_id)
  {
    static $favorites = null;
    static $favoritesTS = null;

    if (!isInt($station_id)) return false;

    // Cache favorites for 30 seconds
    if (is_null($favorites) || (!is_null($favoritesTS) && $favoritesTS < time() - 30))
    {
      $favorites = FavoriteStationRepository::getInstance()->getObjectListByUserId($this->id);
      $favoritesTS = time();
    }

    if (is_null($favorites) || sizeof($favorites) == 0) return false;

    foreach($favorites AS $favorite)
    {
      if ($favorite->stationId == $station_id) return true;
    }

    return false;
  }


  /**
   * Records the a station browse (view) event
   */
  public function recordStationBrowseHistory($station_id)
  {
    if (!isInt($station_id)) return false;

    // Only record history for valid users
    if ($this->isExistingObject())
    {
      // Get the site backend database connection
      $pdo = PDOMysqlConnection::getInstance();

      $sql = 'INSERT INTO station_browse_history SET user_id = ?, station_id = ?, view_time = NOW()';
      $stmt = $pdo->prepareAndExec($sql, [$this->_id, $station_id]);
      if ($stmt)
      {
        return true;
      }
      $this->_lastError = 'An error occurred updating the user station browse history.';
    }
  }


  /**
   * Returns a list of the stations the user has viewed
   *
   * @return Array (Station IDs with Last View Time)
   */
  public function getStationBrowseHistory($iconScale = 16, $limit = 5)
  {
    if (!isInt($limit)) $limit = 5;

    // Only record history for valid users
    if ($this->isExistingObject())
    {
      // Get the site backend database connection
      $pdoMysql = PDOMysqlConnection::getInstance();

      $sql = "SELECT DISTINCT(station_id) FROM station_browse_history WHERE user_id = ? ORDER BY view_time DESC LIMIT $limit";
      $stmt = $pdoMysql->prepareAndExec($sql, [$this->_id]);
      if ($stmt)
      {
        $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $idList = implode(',', array_column($records, 'station_id'));

        // Main Database
        $pdo = PDOConnection::getInstance();

        // Last viewed stations
        $history = [];

        if (count($records) > 0)
        {
          // Get full station details
          $query = $pdo->prepareAndExec
          (
           "SELECT id,source_id,name,station_type_id,latest_confirmed_symbol,latest_confirmed_symbol_table,latest_confirmed_packet_timestamp
            FROM station
            WHERE latest_confirmed_packet_timestamp IS NOT NULL AND ID IN($idList)
            ORDER BY latest_confirmed_packet_timestamp DESC, name ASC;"
          );

          $stationCache = [];
          while ($result = $query->fetch(PDO::FETCH_ASSOC))
          {
            // Symbol
            if (strlen($result['latest_confirmed_symbol']) >= 1 && strlen($result['latest_confirmed_symbol_table']) >= 1) {
                $symbolAsciiValue = ord(substr($result['latest_confirmed_symbol'], 0, 1));
                $symbolTableAsciiValue = ord(substr($result['latest_confirmed_symbol_table'], 0, 1));
            } else {
                // Default values
                $symbolAsciiValue = 125;
                $symbolTableAsciiValue = 47;
            }

            // Symbol URL
            $symbolPath = '/symbols/symbol-' . $symbolAsciiValue . '-' . $symbolTableAsciiValue . '-scale'.$iconScale.'x'.$iconScale.'.svg';
            $isFavorite = $this->isStationAFavorite($result['id']) ? 1 : 0;

            $stationCache[$result['id']] = ['id'=>$result['id'], 'source_id'=>$result['source_id'], 'type'=>$result['station_type_id'], 'text'=>$result['name'], 'icon'=>$symbolPath, 'fav'=>$isFavorite];
          }

          foreach ($records AS $record)
          {
            if (isset($stationCache[$record['station_id']])) $history[] = $stationCache[$record['station_id']];
          }
        }

        return $history;
      }
      $this->_lastError = 'An error occurred updating the user station browse history.';
    }
  }


  /**
   * Delete the user record
   *
   * @return boolean (True if delete was successful, otherwise false)
   */
  public function delete()
  {
    if ($this->isExistingObject())
    {
      // Get the site backend database connection
      $pdo = PDOMysqlConnection::getInstance();

      $sql = 'DELETE FROM users WHERE id = ? LIMIT 1';
      $stmt = $pdo->prepareAndExec($sql, [$this->_id]);
      if ($stmt)
      {
        return true;
      }
      $this->_lastError = 'An error occurred deleting the user record.';
    }
  }


  /**
   * Returns the last error message set during a user action then
   * sets _lastError to an empty string.
   *
   * @return string (Text of the last recorded error)
   */
  public function getLastError()
  {
    $last_error = $this->_lastError;
    $this->_lastError = '';
    return $last_error;
  }
}
