<?php

declare(strict_types=1);
/**
 * Copyright 2022-2025 FOSSBilling
 * Copyright 2011-2021 BoxBilling, Inc.
 * SPDX-License-Identifier: Apache-2.0.
 *
 * @copyright FOSSBilling (https://www.fossbilling.org)
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
 */

/**
 * Clients API methods.
 */

namespace Box\Mod\Client\Api;

use FOSSBilling\Security\RandomizedTimeFloor;
use FOSSBilling\Tools;
use FOSSBilling\Validation\Api\RequiredParams;

class Guest extends \Api_Abstract
{
    /**
     * Client signup action.
     *
     * @optional string $last_name - last name
     * @optional string $aid - Alternative id. Usually used by import tools.
     * @optional string $gender - Gender - values: male|female|nonbinary|other
     * @optional string $country - Country
     * @optional string $city - city
     * @optional string $birthday - Birthday
     * @optional string $type - Identifies client type: company or individual
     * @optional string $company - Company
     * @optional string $company_vat - Company VAT number
     * @optional string $company_number - Company number
     * @optional string $address_1 - Address line 1
     * @optional string $address_2 - Address line 2
     * @optional string $postcode - zip or postcode
     * @optional string $state - country state
     * @optional string $phone - Phone number
     * @optional string $phone_cc - Phone country code
     * @optional string $document_type - Related document type, ie: passport, driving license
     * @optional string $document_nr - Related document number, ie: passport number: LC45698122
     * @optional string $notes - Notes about client. Visible for admin only
     * @optional string $custom_1 - Custom field 1
     * @optional string $custom_2 - Custom field 2
     * @optional string $custom_3 - Custom field 3
     * @optional string $custom_4 - Custom field 4
     * @optional string $custom_5 - Custom field 5
     * @optional string $custom_6 - Custom field 6
     * @optional string $custom_7 - Custom field 7
     * @optional string $custom_8 - Custom field 8
     * @optional string $custom_9 - Custom field 9
     * @optional string $custom_10 - Custom field 10
     */
    #[RequiredParams(['email' => 'Email required', 'first_name' => 'First name required', 'password' => 'Password required', 'password_confirm' => 'Password confirmation required'])]
    public function create($data = []): int
    {
        $this->getDi()['rate_limiter']->consumeOrThrow('client_signup', (string) $this->getIp());

        $config = $this->getDi()['mod_config']('client');

        if (isset($config['disable_signup']) && $config['disable_signup']) {
            throw new \FOSSBilling\InformationException('New registrations are temporarily disabled');
        }

        $this->getDi()['validator']->passwordsMatch($data);

        $this->getService()->checkExtraRequiredFields($data);
        $this->getService()->checkCustomFields($data);

        $this->getDi()['validator']->isPasswordStrong($data['password']);
        $service = $this->getService();

        $email = $data['email'] ?? null;
        $email = $this->getDi()['tools']->validateAndSanitizeEmail($email);
        $email = strtolower(trim((string) $email));
        if ($service->clientAlreadyExists($email)) {
            throw new \FOSSBilling\InformationException('This email address is already registered.');
        }

        $client = $service->guestCreateClient($data);

        if (isset($config['require_email_confirmation']) && (bool) $config['require_email_confirmation']) {
            $service->sendEmailConfirmationForClient($client);
        }

        if (Tools::normalizeBoolean($config['auto_login_after_signup'] ?? true, true)) {
            try {
                $this->login(['email' => $client->email, 'password' => $data['password']]);
            } catch (\Throwable $e) {
                error_log($e->getMessage());
            }
        }

        return (int) $client->id;
    }

    /**
     * Client login action.
     *
     * @return array - session data
     *
     * @throws \FOSSBilling\InformationException
     */
    #[RequiredParams(['email' => 'Email required', 'password' => 'Password required'])]
    public function login($data)
    {
        $startedAt = microtime(true);

        try {
            $this->getDi()['tools']->validateAndSanitizeEmail($data['email'], true, false);

            $event_params = $data;
            $event_params['ip'] = $this->ip;
            $this->getDi()['events_manager']->fire(['event' => 'onBeforeClientLogin', 'params' => $event_params]);

            $service = $this->getService();
            $client = $service->authorizeClient($data['email'], $data['password']);

            if (!$client instanceof \Model_Client) {
                $this->getDi()['events_manager']->fire(['event' => 'onEventClientLoginFailed', 'params' => $event_params]);

                throw new \FOSSBilling\InformationException('Please check your login details.', [], 401);
            }

            $this->getDi()['events_manager']->fire(['event' => 'onAfterClientLogin', 'params' => ['id' => $client->id, 'ip' => $this->ip]]);

            $oldSession = $this->getDi()['session']->getId();
            $this->getDi()['session']->regenerateId();
            $result = $service->toSessionArray($client);
            $this->getDi()['session']->set('client_id', $client->id);

            $this->getDi()['logger']->info('Client #%s logged in', $client->id);
            $this->getDi()['session']->delete('redirect_uri');

            if (!headers_sent() && !empty($client->lang)) {
                setcookie('fb_locale', (string) $client->lang, ['expires' => strtotime('+1 month'), 'path' => '/']);
            }

            $this->getDi()['mod_service']('cart')->transferFromOtherSession($oldSession);

            return $result;
        } finally {
            RandomizedTimeFloor::apply($startedAt);
        }
    }

    /**
     * Password reset confirmation email will be sent to email.
     *
     * @throws \FOSSBilling\Exception
     */
    #[RequiredParams(['email' => 'Email required'])]
    public function reset_password($data): bool
    {
        $startedAt = microtime(true);

        try {
            $this->getDi()['events_manager']->fire(['event' => 'onBeforePasswordResetClient']);
            $service = $this->getDi()['mod_service']('client');

            // Sanitize email
            $data['email'] = $this->getDi()['tools']->validateAndSanitizeEmail($data['email']);

            $ipLimit = $this->getDi()['rate_limiter']->consume('client_password_reset_ip', (string) $this->getIp());
            if ($ipLimit->isLimited()) {
                $this->getDi()['logger']->setChannel('security')->info('Client password reset rate limited from IP %s: email %s', $this->getIp(), $data['email']);

                return true;
            }

            $emailLimit = $this->getDi()['rate_limiter']->consume('client_password_reset_email', (string) $data['email']);
            if ($emailLimit->isLimited()) {
                $this->getDi()['logger']->setChannel('security')->info('Client password reset rate limited for email %s from IP %s', $data['email'], $this->getIp());

                return true;
            }

            $this->checkPasswordResetCaptcha($data);

            $this->getDi()['events_manager']->fire(['event' => 'onBeforeGuestPasswordResetRequest', 'params' => $data]);

            // Fetch the client by email
            $c = $this->getDi()['db']->findOne('Client', 'email = ?', [$data['email']]);
            if (!$c instanceof \Model_Client) {
                $this->getDi()['logger']->setChannel('security')->info('Client password reset requested for unknown email %s from IP %s', $data['email'], $this->getIp());

                return true;
            }

            if ($c->status !== \Model_Client::ACTIVE) {
                $this->getDi()['logger']->setChannel('security')->info('Client password reset requested for ineligible client #%s from IP %s: email %s, account status %s', $c->id, $this->getIp(), $data['email'], $c->status);

                return true;
            }

            $hash = $service->createPasswordResetRequestForClient($c);
            $service->sendPasswordResetRequestEmailForClient($c, $hash);

            $this->getDi()['logger']->setChannel('security')->info('Client password reset email queued for client #%s from IP %s: email %s', $c->id, $this->getIp(), $data['email']);

            return true;
        } finally {
            RandomizedTimeFloor::apply($startedAt, 300, 450);
        }
    }

    private function checkPasswordResetCaptcha(array $data): void
    {
        $extensionService = $this->getDi()['mod_service']('extension');
        if (!$extensionService->isExtensionActive('mod', 'antispam')) {
            return;
        }

        $this->getDi()['mod_service']('Antispam')->checkCaptcha($data);
    }

    #[RequiredParams(['hash' => 'No Hash provided', 'password' => 'Password required', 'password_confirm' => 'Password confirmation required'])]
    public function update_password($data): bool
    {
        $startedAt = microtime(true);

        try {
            $this->getDi()['rate_limiter']->consumeOrThrow('client_password_reset_confirm_post_ip', (string) $this->getIp());

            $this->getDi()['events_manager']->fire(['event' => 'onBeforeClientProfilePasswordReset', 'params' => $data['hash']]);

            $this->getDi()['validator']->passwordsMatch($data);
            $this->getDi()['validator']->isPasswordStrong($data['password']);

            $reset = $this->getDi()['db']->findOne('ClientPasswordReset', 'hash = ?', [$data['hash']]);
            if (!$reset instanceof \Model_ClientPasswordReset) {
                $this->getDi()['logger']->setChannel('security')->info('Client password reset confirmation failed from IP %s: reset token not found', $this->getIp());

                throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
            }

            if (strtotime($reset->created_at) - time() + 900 < 0) {
                $this->getDi()['logger']->setChannel('security')->info('Client password reset confirmation failed for client #%s from IP %s: reset token expired', $reset->client_id, $this->getIp());

                throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
            }

            $c = $this->getDi()['db']->getExistingModelById('Client', $reset->client_id, 'Client not found');
            if ($c->status !== \Model_Client::ACTIVE) {
                $this->getDi()['logger']->setChannel('security')->info('Client password reset confirmation failed for client #%s from IP %s: account status %s', $c->id, $this->getIp(), $c->status);

                throw new \FOSSBilling\InformationException('The link has expired or you have already reset your password.');
            }

            $c->pass = $this->getDi()['password']->hashIt($data['password']);
            $this->getDi()['db']->store($c);

            $this->getDi()['logger']->setChannel('security')->info('Client password reset completed for client #%s from IP %s', $c->id, $this->getIp());

            // send email
            $email = [];
            $email['to_client'] = $c->id;
            $email['code'] = 'mod_client_password_reset_information';
            $emailService = $this->getDi()['mod_service']('email');
            $emailService->sendTemplate($email);

            $this->getDi()['db']->trash($reset);
            $this->getDi()['events_manager']->fire(['event' => 'onAfterClientProfilePasswordReset', 'params' => ['id' => $c->id]]);

            return true;
        } finally {
            RandomizedTimeFloor::apply($startedAt);
        }
    }

    /**
     * Check if given vat number is valid EU country VAT number
     * This method uses http://isvat.appspot.com/ method to validate VAT.
     *
     * @return bool true if VAT is valid, false if not
     */
    #[RequiredParams(['country' => 'Country code', 'vat' => 'Country VAT is required'])]
    public function is_vat($data): bool
    {
        $cc = $data['country'];
        $vatnum = $data['vat'];

        // @todo add new service provider https://vatlayer.com/ check
        //         $url    = 'http://isvat.appspot.com/' . rawurlencode($cc) . '/' . rawurlencode($vatnum) . '/';
        return true;
    }

    /**
     * List of required fields for client registration.
     */
    public function required()
    {
        $config = $this->getDi()['mod_config']('client');

        return $config['required'] ?? [];
    }

    /**
     * Array of custom fields for client registration.
     */
    public function custom_fields()
    {
        $config = $this->getDi()['mod_config']('client');

        return $config['custom_fields'] ?? [];
    }

    public function is_email_validation_required(): bool
    {
        $config = $this->getDi()['mod_config']('client');

        return (bool) ($config['require_email_confirmation'] ?? false);
    }
}
