<?php

declare(strict_types=1);

namespace App\Policies;

use App\Contracts\OwnedByUser;
use App\Enums\UserRole;
use App\Models\User;
use Illuminate\Auth\Access\Response;

/**
 * Base policy for owner-scoped marketplace resources.
 *
 * Encodes the rules that every such resource shares, so a new policy states
 * only what is genuinely specific to it:
 *
 *  - An account in bad standing can do nothing, whatever it owns. Suspension
 *    has to bite on authorization, not merely on the login screen.
 *
 *  - Support staff may READ anything, because dispute resolution is impossible
 *    otherwise. They may not act on someone else's property.
 *
 *  - Staff do NOT inherit ownership. An administrator editing a seller's listing
 *    goes through an audited admin endpoint, not through the member route. That
 *    is the difference between an action that leaves a trail and one that looks
 *    like the seller did it themselves.
 *
 * Phase 2 resources subclass this and add their own state rules -- a listing
 * that is sold cannot be edited, an offer that expired cannot be accepted --
 * on top of the ownership decision made here.
 */
abstract class OwnershipPolicy
{
    /**
     * Runs before every ability.
     *
     * Returns null to fall through to the individual ability methods. Only
     * outright denials belong here: a blanket allow would silently grant staff
     * every future ability added to a subclass.
     */
    public function before(User $user, string $ability): ?Response
    {
        if (! $user->canAuthenticate()) {
            return Response::deny('This account is not active.');
        }

        return null;
    }

    public function viewAny(User $user): Response
    {
        return Response::allow();
    }

    public function view(User $user, OwnedByUser $resource): Response
    {
        if ($this->owns($user, $resource)) {
            return Response::allow();
        }

        // Support can read for dispute handling; everyone else gets a 404-shaped
        // denial, since confirming a record exists is itself a disclosure.
        if ($user->hasAtLeastRole(UserRole::Support)) {
            return Response::allow();
        }

        return $this->notFound();
    }

    public function create(User $user): Response
    {
        return $user->canTransact()
            ? Response::allow()
            : Response::deny('Confirm your email address before doing this.');
    }

    public function update(User $user, OwnedByUser $resource): Response
    {
        return $this->owns($user, $resource) ? Response::allow() : $this->notFound();
    }

    public function delete(User $user, OwnedByUser $resource): Response
    {
        return $this->owns($user, $resource) ? Response::allow() : $this->notFound();
    }

    /**
     * Whether the account owns the record.
     *
     * An unowned record belongs to nobody, never to everybody -- an unassigned
     * owner must not make a resource world-writable.
     */
    protected function owns(User $user, OwnedByUser $resource): bool
    {
        $ownerId = $resource->ownerId();

        return $ownerId !== null && $ownerId === $user->getKey();
    }

    /**
     * The denial used for someone else's record.
     *
     * Deliberately indistinguishable from a record that does not exist:
     * "forbidden" on a resource you do not own confirms that it exists, which
     * on a marketplace leaks who owns what.
     */
    protected function notFound(): Response
    {
        return Response::denyAsNotFound();
    }
}
