<?php

declare(strict_types=1);

namespace apexl\Io\modules\user\services;

use apexl\Io\modules\user\enums\permissions\Common;
use apexl\Io\modules\user\interfaces\PermissionInterface;
use apexl\Utils\Class\ClassBasename;

/**
 * Builds the JSON body PermissionMiddleware returns for a denied data route.
 * Pure — no container, no DB — so it is unit-testable on its own.
 */
final class PermissionDenialPayload
{
    /** Message name/id. Constant so repeat denials collapse to one client toast. */
    public const string MESSAGE_KEY = 'permission_denied';

    /**
     * @param PermissionInterface|list<PermissionInterface>|null $permission route gate (array = any-of)
     * @return array{success:bool,error:string,permission?:list<string>,messages:array<string,array<string,mixed>>}
     */
    public static function build(
        PermissionInterface|array|null $permission,
        bool $isLoggedIn,
        ?Permissions $registry = null,
    ): array {
        $grantable = self::grantable($permission);

        $body = [
            'success' => false,
            'error'   => self::MESSAGE_KEY,
        ];

        // Nothing about the permission model is disclosed to a caller we can't
        // identify — an anonymous 403 says only that sign-in is required.
        if ($isLoggedIn && $grantable !== []) {
            $body['permission'] = array_map(self::key(...), $grantable);
        }

        $body['messages'] = [
            self::MESSAGE_KEY => [
                'content' => self::content($permission, $isLoggedIn, $registry),
                'type'    => 'error',
                'id'      => self::MESSAGE_KEY,
                'icon'    => 'lock',
                'timeout' => null,
            ],
        ];

        return $body;
    }

    /**
     * Human-readable sentence shown to the user.
     *
     * @param PermissionInterface|list<PermissionInterface>|null $permission
     */
    public static function content(
        PermissionInterface|array|null $permission,
        bool $isLoggedIn,
        ?Permissions $registry = null,
    ): string {
        $list = self::normalise($permission);

        if (!$isLoggedIn || in_array(Common::IS_LOGGED_IN, $list, true)) {
            return 'You need to be signed in to do that. Sign in and try again.';
        }

        $grantable = self::grantable($permission);
        if ($grantable === []) {
            return 'That action is not available to your account. If you need it, ask an administrator.';
        }

        $labels = array_map(fn(PermissionInterface $p) => '"' . self::label($p, $registry) . '"', $grantable);

        return sprintf(
            'You don\'t have permission to do that. Ask an administrator to add %s to your role.',
            self::joinNaturally($labels),
        );
    }

    /**
     * The permission's admin-UI friendly name when it is registered, else a
     * humanised form of the enum case (never the raw FQCN).
     */
    public static function label(PermissionInterface $permission, ?Permissions $registry = null): string
    {
        try {
            $name = trim((string) ($registry?->getRegistration($permission)?->name ?? ''));
            if ($name !== '') {
                return $name;
            }
        } catch (\Throwable) {
            // Registry unavailable/unpopulated — fall through to the enum label.
        }

        $case  = ucwords(strtolower(str_replace('_', ' ', $permission->name)));
        $group = ClassBasename::classBasename($permission::class);

        // "Common" is the framework's catch-all enum — naming it tells the user
        // nothing, so the bare case reads better ("View", not "Common: View").
        return $group === 'Common' ? $case : sprintf('%s: %s', $group, $case);
    }

    /** Wire form of the permission — matches IsPermissionTrait::jsonSerialize(). */
    public static function key(PermissionInterface $permission): string
    {
        return sprintf('%s__%s', ClassBasename::classBasename($permission::class), $permission->name);
    }

    /**
     * @param PermissionInterface|list<PermissionInterface>|null $permission
     * @return list<PermissionInterface>
     */
    private static function normalise(PermissionInterface|array|null $permission): array
    {
        if ($permission === null) return [];
        $list = is_array($permission) ? $permission : [$permission];

        return array_values(array_filter($list, fn($p) => $p instanceof PermissionInterface));
    }

    /**
     * The subset a user could actually be granted — pseudo-permissions are gate
     * mechanics, and naming one would only confuse ("ask for 'Can Do Nothing'").
     *
     * @param PermissionInterface|list<PermissionInterface>|null $permission
     * @return list<PermissionInterface>
     */
    private static function grantable(PermissionInterface|array|null $permission): array
    {
        return array_values(array_filter(
            self::normalise($permission),
            fn(PermissionInterface $p) => $p !== Common::CAN_DO_NOTHING
                && $p !== Common::ALLOW_ALL
                && $p !== Common::IS_LOGGED_IN,
        ));
    }

    /** @param list<string> $parts */
    private static function joinNaturally(array $parts): string
    {
        if (count($parts) <= 1) return $parts[0] ?? '';
        $last = array_pop($parts);

        return implode(', ', $parts) . ' or ' . $last;
    }
}
