<?php
/**
 * Copyright © Simplyin. All rights reserved.
 */
declare(strict_types=1);

namespace Simplyin\Payment\Service;

use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Model\QuoteIdMaskFactory;
use Magento\Sales\Api\OrderRepositoryInterface;
use Psr\Log\LoggerInterface;

/**
 * Ownership guard for checkout entities (quote / order).
 *
 * Prevents IDOR on the endpoints that act on a caller-supplied quoteId /
 * magentoOrderId (payment + pickup-point, REST controllers + GraphQL resolvers):
 * an authenticated customer may only act on an entity that belongs to them.
 *
 * Identity is passed IN by the caller (transport-agnostic) — the REST frontend
 * reads it from the cookie session, GraphQL from the query $context — because
 * Magento's GraphQL area is stateless and CustomerSession is NOT reliably
 * populated there. A null customerId means "no authenticated customer" (guest or
 * the not-yet-logged-in new-account flow): such a caller may act only on a
 * guest-owned entity (owner id 0, reached via its unguessable masked id) and is
 * denied on any entity that belongs to a registered customer — otherwise an
 * unauthenticated request could act on a victim's quote/order by supplying its
 * guessable numeric id (IDOR). A missing / unresolvable / non-existent entity id
 * is NOT a violation (the flow surfaces its own not-found/validation error).
 */
class CheckoutOwnershipGuard
{
    /**
     * @var CartRepositoryInterface
     */
    private $quoteRepository;

    /**
     * @var QuoteIdMaskFactory
     */
    private $quoteIdMaskFactory;

    /**
     * @var OrderRepositoryInterface
     */
    private $orderRepository;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * @param CartRepositoryInterface $quoteRepository
     * @param QuoteIdMaskFactory $quoteIdMaskFactory
     * @param OrderRepositoryInterface $orderRepository
     * @param LoggerInterface $logger
     */
    public function __construct(
        CartRepositoryInterface $quoteRepository,
        QuoteIdMaskFactory $quoteIdMaskFactory,
        OrderRepositoryInterface $orderRepository,
        LoggerInterface $logger
    ) {
        $this->quoteRepository = $quoteRepository;
        $this->quoteIdMaskFactory = $quoteIdMaskFactory;
        $this->orderRepository = $orderRepository;
        $this->logger = $logger;
    }

    /**
     * True when the quote may be acted on by the given customer.
     *
     * @param int|null $customerId Authenticated customer id, or null for a guest / no customer
     * @param mixed $quoteId Numeric quote id or masked guest quote id
     * @return bool
     */
    public function isQuoteOwnedBy(?int $customerId, $quoteId): bool
    {
        // No entity supplied — nothing to enforce (the flow surfaces its own error).
        if (empty($quoteId)) {
            return true;
        }

        // An unauthenticated caller may reach a cart ONLY through its unguessable
        // masked id (the capability token). A raw numeric quote id from a null caller
        // is an enumeration attempt against guest carts — deny it outright.
        if ($customerId === null && is_numeric($quoteId)) {
            return false;
        }

        $numericId = $this->resolveQuoteId($quoteId);
        if ($numericId === null) {
            // Unresolvable id — let the flow surface its own "invalid quote" error.
            return true;
        }

        try {
            $quote = $this->quoteRepository->get((int)$numericId);
        } catch (\Exception $e) {
            // Non-existent quote is not an ownership violation.
            $this->logger->warning('SimplyIN OwnershipGuard: quote load failed: ' . $e->getMessage());
            return true;
        }

        return $this->ownerAllows($customerId, (int)$quote->getCustomerId());
    }

    /**
     * True when the Magento order may be acted on by the given customer.
     *
     * @param int|null $customerId Authenticated customer id, or null for a guest / no customer
     * @param mixed $magentoOrderId Magento order entity id
     * @return bool
     */
    public function isOrderOwnedBy(?int $customerId, $magentoOrderId): bool
    {
        if (empty($magentoOrderId)) {
            return true;
        }

        try {
            $order = $this->orderRepository->get((int)$magentoOrderId);
        } catch (\Exception $e) {
            $this->logger->warning('SimplyIN OwnershipGuard: order load failed: ' . $e->getMessage());
            return true;
        }

        return $this->ownerAllows($customerId, (int)$order->getCustomerId());
    }

    /**
     * Decide whether a caller identity may act on an entity with the given owner.
     *
     * A registered customer may act only on their own entity. An unauthenticated
     * caller (null customerId — guest or the not-yet-logged-in new-account flow)
     * may act only on a guest-owned entity (owner id 0); it must NOT reach an
     * entity that belongs to a registered customer, even when supplying that
     * entity's (guessable, sequential) numeric id — that would be an IDOR.
     *
     * @param int|null $customerId
     * @param int $ownerId Entity owner customer id (0 when guest-owned)
     * @return bool
     */
    private function ownerAllows(?int $customerId, int $ownerId): bool
    {
        if ($customerId === null) {
            return $ownerId === 0;
        }

        return $ownerId === $customerId;
    }

    /**
     * Resolve a (possibly masked) quote id to a numeric quote id.
     *
     * @param mixed $quoteId
     * @return mixed|null Numeric quote id, or null when a masked id cannot be resolved
     */
    private function resolveQuoteId($quoteId)
    {
        if (!is_numeric($quoteId)) {
            $mask = $this->quoteIdMaskFactory->create()->load($quoteId, 'masked_id');
            return $mask->getQuoteId() ?: null;
        }

        return $quoteId;
    }
}
