<?php

/**
 * @package    Joomla.Administrator
 * @subpackage com_users
 *
 * @copyright  (C) 2022 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Component\Users\Administrator\Model;

use Joomla\CMS\Date\Date;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\User\User;
use Joomla\Component\Users\Administrator\Helper\Mfa as MfaHelper;
use Joomla\Database\ParameterType;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Multi-factor Authentication Methods list page's model
 *
 * @since 4.2.0
 */
class MethodsModel extends BaseDatabaseModel
{
    /**
     * Returns a list of all available MFA methods and their currently active records for a given user.
     *
     * @param   User|null  $user  The user object. Skip to use the current user.
     *
     * @return  array
     * @throws  \Exception
     *
     * @since 4.2.0
     */
    public function getMethods(?User $user = null): array
    {
        if (\is_null($user)) {
            $user = $this->getCurrentUser();
        }

        if ($user->guest) {
            return [];
        }

        // Get an associative array of MFA Methods
        $rawMethods = MfaHelper::getMfaMethods();
        $methods    = [];

        foreach ($rawMethods as $method) {
            $method['active']         = [];
            $methods[$method['name']] = $method;
        }

        // Put the user MFA records into the Methods array
        $userMfaRecords = MfaHelper::getUserMfaRecords($user->id);

        if (!empty($userMfaRecords)) {
            foreach ($userMfaRecords as $record) {
                if (!isset($methods[$record->method])) {
                    continue;
                }

                $methods[$record->method]->addActiveMethod($record);
            }
        }

        return $methods;
    }

    /**
     * Delete all Multi-factor Authentication Methods for the given user.
     *
     * @param   User|null  $user  The user object to reset MFA for. Null to use the current user.
     *
     * @return  void
     * @throws  \Exception
     *
     * @since 4.2.0
     */
    public function deleteAll(?User $user = null): void
    {
        // Make sure we have a user object
        if (\is_null($user)) {
            $user = $this->getCurrentUser() ?: Factory::getApplication()->getIdentity();
        }

        // If the user object is a guest (who can't have MFA) we stop with an error
        if ($user->guest) {
            throw new \RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
        }

        $db    = $this->getDatabase();
        $query = $db->getQuery(true)
            ->delete($db->quoteName('#__user_mfa'))
            ->where($db->quoteName('user_id') . ' = :user_id')
            ->bind(':user_id', $user->id, ParameterType::INTEGER);
        $db->setQuery($query)->execute();
    }

    /**
     * Format a relative timestamp. It deals with timestamps today and yesterday in a special manner. Example returns:
     * Yesterday, 13:12
     * Today, 08:33
     * January 1, 2015
     *
     * @param   ?string  $dateTimeText  The database time string to use, e.g. "2017-01-13 13:25:36"
     *
     * @return  string  The formatted, human-readable date
     * @throws  \Exception
     *
     * @since 4.2.0
     */
    public function formatRelative(?string $dateTimeText): string
    {
        if (empty($dateTimeText)) {
            return Text::_('JNEVER');
        }

        // The timestamp is given in UTC. Make sure Joomla! parses it as such.
        $utcTimeZone = new \DateTimeZone('UTC');
        $jDate       = new Date($dateTimeText, $utcTimeZone);
        $unixStamp   = $jDate->toUnix();
        $app         = Factory::getApplication();

        // I'm pretty sure we didn't have MFA in Joomla back in 1970 ;)
        if ($unixStamp < 0) {
            return Text::_('JNEVER');
        }

        // I need to display the date in the user's local timezone. That's how you do it.
        $user   = $this->getCurrentUser();
        $userTZ = $user->getParam('timezone', $app->get('offset', 'UTC'));
        $tz     = new \DateTimeZone($userTZ);
        $jDate->setTimezone($tz);

        // Default format string: way in the past, the time of the day is not important
        $formatString    = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_PAST');
        $containerString = Text::_('COM_USERS_MFA_LBL_PAST');

        // If the timestamp is within the last 72 hours we may need a special format
        if ($unixStamp > (time() - (72 * 3600))) {
            // Is this timestamp today?
            $jNow = new Date();
            $jNow->setTimezone($tz);
            $checkNow  = $jNow->format('Ymd', true);
            $checkDate = $jDate->format('Ymd', true);

            if ($checkDate == $checkNow) {
                $formatString    = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_TODAY');
                $containerString = Text::_('COM_USERS_MFA_LBL_TODAY');
            } else {
                // Is this timestamp yesterday?
                $jYesterday = clone $jNow;
                $jYesterday->setTime(0, 0, 0);
                $oneSecond = new \DateInterval('PT1S');
                $jYesterday->sub($oneSecond);
                $checkYesterday = $jYesterday->format('Ymd', true);

                if ($checkDate == $checkYesterday) {
                    $formatString    = Text::_('COM_USERS_MFA_LBL_DATE_FORMAT_YESTERDAY');
                    $containerString = Text::_('COM_USERS_MFA_LBL_YESTERDAY');
                }
            }
        }

        return \sprintf($containerString, $jDate->format($formatString, true));
    }

    /**
     * Set the user's "don't show this again" flag.
     *
     * @param   User  $user  The user to check
     * @param   bool  $flag  True to set the flag, false to unset it (it will be set to 0, actually)
     *
     * @return  void
     *
     * @since 4.2.0
     */
    public function setFlag(User $user, bool $flag = true): void
    {
        $db         = $this->getDatabase();
        $profileKey = 'mfa.dontshow';
        $query      = $db->getQuery(true)
            ->select($db->quoteName('profile_value'))
            ->from($db->quoteName('#__user_profiles'))
            ->where($db->quoteName('user_id') . ' = :user_id')
            ->where($db->quoteName('profile_key') . ' = :profileKey')
            ->bind(':user_id', $user->id, ParameterType::INTEGER)
            ->bind(':profileKey', $profileKey, ParameterType::STRING);

        try {
            $result = $db->setQuery($query)->loadResult();
        } catch (\Exception) {
            return;
        }

        $exists = !\is_null($result);

        $object = (object) [
            'user_id'       => $user->id,
            'profile_key'   => 'mfa.dontshow',
            'profile_value' => ($flag ? 1 : 0),
            'ordering'      => 1,
        ];

        if (!$exists) {
            $db->insertObject('#__user_profiles', $object);
        } else {
            $db->updateObject('#__user_profiles', $object, ['user_id', 'profile_key']);
        }
    }
}
