<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

namespace SimplyIn\Ajax\Handler;

use SimplyIn\Ajax\AjaxActionInterface;
use SimplyIn\Ajax\AjaxRequest;
use SimplyIn\Ajax\Service\CheckoutSessionService;
use SimplyIn\Ajax\Service\CustomerService;
use SimplyIn\Api\SimplyInApiClient;
use SimplyIn\Session\SimplyInSessionInterface;

if (!defined('_PS_VERSION_')) {
    exit;
}

/**
 * Shop-login email-OTP challenge — step 2. Verifies the code; on success the backend returns
 * shopLoginAllowed=true, so we log the customer into the shop account via
 * CustomerService::loginOrCreateByEmail. A wrong / expired code surfaces as isCodeValid=false so
 * the shared pin UI shows the error. Two modes:
 *
 * TOKEN MODE (post-SMS pivot): auth = the checkout authToken; the backend additionally returns
 * fresh SIN tokens, which we persist.
 *
 * EMAIL MODE (email-first): no checkout authToken — the challenge email comes from the session
 * key pinned by SubmitEmailHandler at send time (never from the request). The backend response
 * carries no SIN tokens in this mode and none are stored — the shopper is logged into the shop
 * account only.
 */
final class VerifyShopLoginEmailOtpHandler implements AjaxActionInterface
{
    public function __construct(
        private readonly SimplyInApiClient $api,
        private readonly SimplyInSessionInterface $session,
        private readonly CheckoutSessionService $checkoutSession,
        private readonly CustomerService $customerService,
    ) {
    }

    public function name(): string
    {
        return 'verifyShopLoginEmailOtp';
    }

    /**
     * @return array<string, mixed>
     */
    public function execute(AjaxRequest $request): array
    {
        // EMAIL MODE wins over a stray checkout authToken: a pinned challenge email means the code
        // was e-mailed to that address and can only be validated by-email.
        $challengeEmail = $this->checkoutSession->getShopLoginChallengeEmail();
        if (is_string($challengeEmail) && $challengeEmail !== '') {
            return $this->verifyByChallengeEmail($challengeEmail, $request);
        }

        $authToken = $this->checkoutSession->getAuthToken();
        if (is_string($authToken) && $authToken !== '') {
            return $this->verifyWithCheckoutToken($authToken, $request);
        }

        // Neither a checkout token nor a pending email-first challenge — a server/session problem, not
        // a wrong code, so surface a generic error rather than "Incorrect verification code".
        return ['error' => 'UNAUTHORIZED'];
    }

    /**
     * TOKEN MODE: success = fresh authToken + shopLoginAllowed; the fresh tokens are persisted to
     * the SimplyIN session.
     *
     * @return array<string, mixed>
     */
    private function verifyWithCheckoutToken(string $authToken, AjaxRequest $request): array
    {
        $raw = $this->api->apiCall(
            ['code' => $request->getString('code')],
            'POST',
            'checkout/verifyShopLoginEmailOtpCommerce',
            $authToken,
        );
        $response = is_array($raw) ? $raw : [];

        if (!empty($response['authToken']) && ($response['shopLoginAllowed'] ?? null) === true) {
            $verifiedEmail = $this->verifiedEmailFrom($response);
            if ($verifiedEmail === '') {
                return ['isCodeValid' => false];
            }

            $this->session->set('simplyin_auth_token', (string) $response['authToken']);
            if (isset($response['refreshToken'])) {
                $this->session->set('simplyin_refresh_token', (string) $response['refreshToken']);
            }

            return $this->loginVerifiedCustomer($verifiedEmail, $response);
        }

        return $this->mapFailure($response);
    }

    /**
     * EMAIL MODE (email-first): the verified email is pinned in the session at send time — never
     * read from the request (the front does POST an email param; it is ignored). Success =
     * shopLoginAllowed===true; the backend returns no SIN tokens in this mode and none are stored —
     * the code only proves inbox ownership, so it logs the shopper into the shop account only.
     *
     * @return array<string, mixed>
     */
    private function verifyByChallengeEmail(string $challengeEmail, AjaxRequest $request): array
    {
        $raw = $this->api->apiCall(
            ['email' => $challengeEmail, 'code' => $request->getString('code')],
            'POST',
            'checkout/verifyShopLoginEmailOtpCommerce',
        );
        $response = is_array($raw) ? $raw : [];

        if (($response['shopLoginAllowed'] ?? null) === true) {
            $verifiedEmail = $this->verifiedEmailFrom($response);
            if ($verifiedEmail === '') {
                return ['isCodeValid' => false];
            }

            $this->checkoutSession->clearShopLoginChallengeEmail();

            return $this->loginVerifiedCustomer($verifiedEmail, $response);
        }

        return $this->mapFailure($response);
    }

    /**
     * SECURITY: the login email must come from the backend's verified-user data — never from
     * the client request. Trusting a request-supplied email would let a shopper with a valid
     * code tamper the email and log into a different account. Callers fail closed when this
     * returns '' (backend didn't return the canonical email).
     *
     * @param array<string, mixed> $response
     */
    private function verifiedEmailFrom(array $response): string
    {
        $data = is_array($response['data'] ?? null) ? $response['data'] : [];

        return isset($data['email']) ? (string) $data['email'] : '';
    }

    /**
     * @param array<string, mixed> $response
     *
     * @return array<string, mixed>
     */
    private function loginVerifiedCustomer(string $verifiedEmail, array $response): array
    {
        $loginResult = $this->customerService->loginOrCreateByEmail($verifiedEmail, $response);

        return ['success' => true, 'isCodeValid' => true, 'login_result' => $loginResult];
    }

    /**
     * @param array<string, mixed> $response
     *
     * @return array<string, mixed>
     */
    private function mapFailure(array $response): array
    {
        if (($response['code'] ?? null) === 'TOO_MANY_REQUESTS') {
            return ['code' => 'TOO_MANY_REQUESTS', 'message' => (string) ($response['message'] ?? '')];
        }

        return ['isCodeValid' => false];
    }
}
