<?php

namespace App\Filament\Resources;

use App\Filament\Forms\Components\PermissionOverridePicker;
use App\Filament\Resources\Concerns\ChecksPermissionEscalation;
use App\Filament\Resources\Concerns\HasStandardTableFeatures;
use App\Filament\Resources\UserResource\Pages\CreateUser;
use App\Filament\Resources\UserResource\Pages\EditUser;
use App\Filament\Resources\UserResource\Pages\ListUsers;
use App\Filament\Resources\UserResource\Pages\ViewUser;
use App\Filament\Support\Perm;
use App\Models\Concerns\HasPermissions;
use App\Models\Permission;
use App\Models\Role;
use App\Models\Sales\Till;
use App\Models\User;
use App\Services\AuditLog;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section as FormSection;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;

/**
 * Ported from App\Http\Controllers\UserManagement\UserManagementUserController
 * (docs/migration-plan.md §4 — see RoleResource's docblock for why this
 * Resource, uniquely, defers to a real Policy instead of HasModulePermissions).
 *
 * Deliberately does NOT use HasModulePermissions: record-level rules (a user
 * may always edit themselves; a non-full-admin actor may only touch a target
 * whose permissions they themselves hold in full; nobody may delete
 * themselves) live in UserPolicy, already registered via Gate::policy() in
 * AppServiceProvider, and Filament's default canX() resolve through it.
 *
 * The three high-trust verbs kept as separate grantable permissions in the
 * source (unlock / reset_password / set_pin — see permissions.md) are NOT
 * expressible through UserPolicy (which only knows view/create/update/delete),
 * so each of those three row actions carries its own explicit
 * Perm::can('user_management_users', '<verb>') check IN ADDITION to the
 * policy-backed 'update' check — exactly mirroring the source, where the verb
 * is a route-middleware gate (`can_do:user_management_users.unlock`) layered
 * on top of the controller's `$this->authorize('update', $user)`.
 */
class UserResource extends Resource
{
    use ChecksPermissionEscalation;
    use HasStandardTableFeatures;

    protected static ?string $model = User::class;

    protected static string|\BackedEnum|null $navigationIcon = Heroicon::OutlinedUsers;

    protected static string|\UnitEnum|null $navigationGroup = 'Administration';

    protected static ?int $navigationSort = 0;

    protected static ?string $recordTitleAttribute = 'name';

    /**
     * Same reasoning as TillResource's override: `user_management_users` has no
     * `export` action of its own, so there is no `.export` permission for the
     * trait's default to require — and this Resource declares no
     * $permissionModuleKey at all, by the design noted above. Deferring to
     * canViewAny() keeps the gate where the rest of this Resource's rules
     * already live (UserPolicy::viewAny() => user_management_users.access).
     */
    public static function canExport(): bool
    {
        return static::canViewAny();
    }

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            FormSection::make('Profile')->schema([
                TextInput::make('name')->required()->maxLength(120),
                TextInput::make('email')
                    ->required()
                    ->email()
                    ->maxLength(255)
                    ->rule(fn (?User $record) => Rule::unique('users', 'email')->ignore($record?->id)),
                TextInput::make('phone')->tel()->maxLength(20),
                Select::make('status')
                    ->options(array_combine(User::STATUSES, User::STATUSES))
                    ->required()
                    ->default(User::STATUS_ACTIVE),
                TextInput::make('password')
                    ->password()
                    ->revealable()
                    ->confirmed()
                    ->required(fn (string $operation): bool => $operation === 'create')
                    ->dehydrated(fn (?string $state): bool => filled($state))
                    ->helperText(fn (string $operation): string => $operation === 'create'
                        ? 'Any password is accepted — no complexity requirements.'
                        : 'Leave blank to keep the current password.'),
                TextInput::make('password_confirmation')
                    ->password()
                    ->revealable()
                    ->required(fn (string $operation): bool => $operation === 'create')
                    ->dehydrated(false),
                Toggle::make('must_change_password')
                    ->label('Require password change on first login')
                    ->helperText('When enabled, the user must set a new password the next time they log in.')
                    ->default(true),
            ])->columns(2),

            FormSection::make('Roles')
                ->description('Where this user\'s permissions come from. Editing a role later moves everyone holding it, including this user.')
                ->schema([
                    Select::make('role_ids')
                        ->label('Roles')
                        ->multiple()
                        ->options(fn (): Collection => Role::query()
                            ->where('is_active', true)
                            ->when(! (Auth::user()?->isFullAdmin() ?? false), fn ($q) => $q->where('is_full_admin', false))
                            ->orderBy('name')
                            ->pluck('name', 'id'))
                        ->searchable()
                        ->preload()
                        // Live so the matrix below reshades to the new roles'
                        // grants immediately. It only changes what the matrix
                        // *shows*: the overrides themselves are stored
                        // independently of the roles.
                        ->live()
                        ->rule(static::roleEscalationRule()),
                ]),

            FormSection::make('Permission overrides')
                ->description('Exceptions for this user only. Everything else follows the roles above.')
                ->schema([
                    PermissionOverridePicker::make('permission_overrides')
                        ->label('Permissions')
                        ->rolesStatePath('role_ids')
                        ->rule(static::permissionOverrideEscalationRule()),
                ]),

            FormSection::make('Access')->schema([
                Toggle::make('is_all_branches')
                    ->label('All-Branch Access')
                    ->helperText('Sees every branch in this tenant, without being made a full admin.')
                    ->live()
                    ->rule(static::allBranchAccessEscalationRule()),
                Select::make('warehouses')
                    ->relationship('warehouses', 'name')
                    ->multiple()
                    ->searchable()
                    ->preload()
                    ->visible(function (Get $get): bool {
                        if ($get('is_all_branches')) {
                            return false;
                        }

                        $roleIds = array_values(array_unique(array_map('intval', $get('role_ids') ?? [])));
                        if ($roleIds === []) {
                            return true;
                        }

                        return Role::query()->whereIn('id', $roleIds)->where('is_full_admin', true)->doesntExist();
                    }),
                Select::make('tills')
                    ->relationship('tills', 'name')
                    ->getOptionLabelFromRecordUsing(fn (Till $till): string => $till->name.($till->branch?->name ? ' — '.$till->branch->name : ''))
                    ->multiple()
                    ->searchable()
                    ->preload(),
            ])->columns(2),
        ]);
    }

    public static function table(Table $table): Table
    {
        return static::standardTableDefaults($table)
            ->recordUrl(static::standardRecordUrl())
            ->emptyStateHeading('No users yet')
            ->emptyStateDescription('Add a user and assign them a role and a till.')
            ->columns([
                TextColumn::make('name')->searchable()->sortable(),
                TextColumn::make('email')->searchable()->sortable(),
                TextColumn::make('phone')->placeholder('—'),
                TextColumn::make('status')
                    ->badge()
                    ->formatStateUsing(fn (string $state): string => ucfirst($state))
                    ->color(fn (string $state): string => match ($state) {
                        User::STATUS_ACTIVE => 'success',
                        User::STATUS_SUSPENDED => 'danger',
                        User::STATUS_INACTIVE => 'gray',
                        default => 'gray',
                    }),
                TextColumn::make('roles.name')->label('Roles')->badge()->placeholder('—'),
                IconColumn::make('locked')
                    ->label('Locked')
                    ->boolean()
                    ->state(fn (User $record): bool => $record->isLocked()),
                TextColumn::make('last_login_at')->dateTime()->placeholder('Never')->toggleable(),
            ])
            ->defaultSort('name')
            ->filters([
                SelectFilter::make('roles')
                    ->relationship('roles', 'name')
                    ->multiple(),
                SelectFilter::make('status')->options(array_combine(User::STATUSES, User::STATUSES)),
            ])
            ->recordActions([
                Action::make('view')
                    ->url(fn (User $record): string => ViewUser::getUrl(['record' => $record]))
                    ->visible(fn (User $record): bool => static::canView($record)),
                EditAction::make(),

                Action::make('suspend')
                    ->color('danger')
                    ->requiresConfirmation()
                    ->visible(fn (User $record): bool => $record->status !== User::STATUS_SUSPENDED
                        && (Auth::user()?->can('update', $record) ?? false))
                    ->action(fn (User $record) => static::setStatus($record, User::STATUS_SUSPENDED, 'user.suspended', 'User suspended.')),
                Action::make('reactivate')
                    ->color('success')
                    ->requiresConfirmation()
                    ->visible(fn (User $record): bool => $record->status !== User::STATUS_ACTIVE
                        && (Auth::user()?->can('update', $record) ?? false))
                    ->action(fn (User $record) => static::setStatus($record, User::STATUS_ACTIVE, 'user.reactivated', 'User reactivated.')),
                Action::make('deactivate')
                    ->color('gray')
                    ->requiresConfirmation()
                    ->visible(fn (User $record): bool => $record->status !== User::STATUS_INACTIVE
                        && (Auth::user()?->can('update', $record) ?? false))
                    ->action(fn (User $record) => static::setStatus($record, User::STATUS_INACTIVE, 'user.deactivated', 'User set to inactive.')),

                Action::make('unlock')
                    ->icon(Heroicon::OutlinedLockOpen)
                    ->requiresConfirmation()
                    ->visible(fn (User $record): bool => $record->isLocked()
                        && Perm::can('user_management_users', 'unlock')
                        && (Auth::user()?->can('update', $record) ?? false))
                    ->action(function (User $record): void {
                        $record->forceFill(['locked_until' => null, 'failed_login_count' => 0])->save();
                        AuditLog::record('user.unlocked', $record);
                        Notification::make()->title('User unlocked.')->success()->send();
                    }),

                Action::make('resetPassword')
                    ->label('Reset password')
                    ->icon(Heroicon::OutlinedKey)
                    ->color('warning')
                    ->requiresConfirmation()
                    ->modalDescription('Generates a new one-time password and forces a change at next login.')
                    ->visible(fn (User $record): bool => Perm::can('user_management_users', 'reset_password')
                        && (Auth::user()?->can('update', $record) ?? false))
                    ->action(fn (User $record) => static::resetPassword($record)),

                Action::make('setPin')
                    ->label('Set PIN')
                    ->icon(Heroicon::OutlinedKey)
                    ->visible(fn (User $record): bool => Perm::can('user_management_users', 'set_pin')
                        && (Auth::user()?->can('update', $record) ?? false))
                    ->schema([
                        TextInput::make('pin')
                            ->label('PIN')
                            ->numeric()
                            ->rule('digits_between:4,6')
                            ->helperText('4–6 digits. Leave blank to clear the PIN.'),
                    ])
                    ->action(function (User $record, array $data): void {
                        $pin = filled($data['pin'] ?? null) ? (string) $data['pin'] : null;

                        $record->forceFill(['pin_hash' => $pin !== null ? Hash::make($pin) : null])->save();

                        AuditLog::record($pin !== null ? 'user.pin_set' : 'user.pin_cleared', $record);

                        Notification::make()->title($pin !== null ? 'PIN set.' : 'PIN cleared.')->success()->send();
                    }),

                DeleteAction::make()
                    ->before(function (User $record): void {
                        $record->flushPermissionCache();
                        AuditLog::record('user.deleted', $record, ['name' => $record->name, 'email' => $record->email]);
                    }),
            ])
            // Gated only by `.bulk_delete`, applied uniformly to all three
            // actions — matches BulkActionRequest::authorize() exactly (the
            // source does not require `.edit` separately for the suspend/
            // reactivate bulk actions, only the one bulk permission).
            ->toolbarActions([
                BulkAction::make('bulkSuspend')
                    ->label('Suspend selected')
                    ->color('danger')
                    ->requiresConfirmation()
                    ->authorize(fn (): bool => Perm::can('user_management_users', 'bulk_delete'))
                    ->action(fn (Collection $records) => static::bulkApply($records, fn (User $u) => static::setStatus($u, User::STATUS_SUSPENDED, 'user.suspended', null, 'bulk'))),
                BulkAction::make('bulkReactivate')
                    ->label('Reactivate selected')
                    ->color('success')
                    ->requiresConfirmation()
                    ->authorize(fn (): bool => Perm::can('user_management_users', 'bulk_delete'))
                    ->action(fn (Collection $records) => static::bulkApply($records, fn (User $u) => static::setStatus($u, User::STATUS_ACTIVE, 'user.reactivated', null, 'bulk'))),
                BulkAction::make('bulkDelete')
                    ->label('Delete selected')
                    ->color('danger')
                    ->requiresConfirmation()
                    ->authorize(fn (): bool => Perm::can('user_management_users', 'bulk_delete'))
                    ->action(fn (Collection $records) => static::bulkApply($records, function (User $u): void {
                        $u->flushPermissionCache();
                        AuditLog::record('user.deleted', $u, ['name' => $u->name, 'email' => $u->email, 'via' => 'bulk']);
                        $u->delete();
                    })),
            ]);
    }

    /**
     * Ported from bulkAction(): the acting user is silently excluded from
     * their own bulk operation, and every remaining target must individually
     * pass the same ability the single-record action would require — applied
     * all-or-nothing (matching the source's abort-on-first-failure), not
     * partially applied to whichever targets happen to pass.
     */
    public static function bulkApply(Collection $records, \Closure $apply): void
    {
        $actor = Auth::user();
        $targets = $records->reject(fn (User $u) => $actor && $u->is($actor));

        foreach ($targets as $target) {
            if (! ($actor?->can('update', $target) ?? false)) {
                Notification::make()->title("You do not have permission to modify \"{$target->name}\".")->danger()->send();

                return;
            }
        }

        $targets->each($apply);

        Notification::make()->title('Bulk action applied to '.$targets->count().' user(s).')->success()->send();
    }

    public static function setStatus(User $user, string $status, string $auditAction, ?string $successMessage, string $via = ''): void
    {
        $user->update(['status' => $status, 'updated_by' => Auth::id()]);
        $user->flushPermissionCache();

        AuditLog::record($auditAction, $user, $via !== '' ? ['via' => $via] : []);

        if ($successMessage !== null) {
            Notification::make()->title($successMessage)->success()->send();
        }
    }

    /**
     * Ported from resetPassword(): generates a random 14-character password
     * server-side and shows it once via a persistent notification — Filament
     * has no JSON-response equivalent, so this follows the same one-time-
     * reveal convention already established by TenantResource::resetAdminPassword().
     */
    public static function resetPassword(User $user): void
    {
        $password = Str::password(14);

        $user->forceFill([
            'password' => Hash::make($password),
            'failed_login_count' => 0,
            'locked_until' => null,
            'must_change_password' => true,
        ])->save();

        AuditLog::record('user.password_reset_by_admin', $user);

        Notification::make()
            ->title("Password for {$user->email} reset.")
            ->body("New password: {$password}\n\nThe user must change it at next login. This is shown once.")
            ->success()
            ->persistent()
            ->send();
    }

    /**
     * @param  array<int, string>  $permissionNames
     * @return array<int, int>
     */
    public static function permissionIdsFor(array $permissionNames): array
    {
        return Permission::query()->whereIn('name', $permissionNames)->pluck('id')->all();
    }

    /**
     * Assign roles, then store the per-user exception layer exactly as the
     * administrator set it — nothing is derived from the roles here.
     *
     * `$overrides` is the {@see PermissionOverridePicker} state:
     * `['grant' => [...], 'revoke' => [...]]`. A permission in neither list is
     * absent from `user_permission_overrides` altogether, which is what makes it
     * inherited: {@see HasPermissions::computeEffectivePermissionNames()}
     * resolves it through the roles on every read, so a later edit to a role
     * moves this user with it. A permission in either list gets a pivot row and
     * from then on ignores what the roles say.
     *
     * The previous implementation took the user's whole *effective* set and
     * derived "minimal" exceptions by diffing it against the roles' union. That
     * had no way to tell a deliberate deviation from a stale form, so changing a
     * user's role wrote an allow override for every permission of the old role
     * and a deny override for every permission of the new one — the role change
     * did nothing and the user was pinned to their old permissions for good.
     *
     * `grant`/`revoke` is the administrator-facing vocabulary; the pivot keeps
     * the storage-level `allow`/`deny` it has always used, so nothing below the
     * form layer changes.
     *
     * `$grantorId` is the tenant-user id stamped onto the override pivots
     * (granted_by → users). Defaults to Auth::id() for the tenant panel; the
     * master control passes null because its actor is a CentralAdmin on the
     * `central` guard, whose id must never be written into a users FK.
     *
     * @param  array<int, int>  $roleIds
     * @param  array{grant?: array<int, string>, revoke?: array<int, string>}|mixed  $overrides
     * @return array{allow: array<int, string>, deny: array<int, string>}
     */
    public static function applyRolesAndOverrides(User $user, array $roleIds, mixed $overrides, ?int $grantorId = null): array
    {
        $user->roles()->sync($roleIds);

        $overrides = PermissionOverridePicker::normalize($overrides);
        $grantorId ??= Auth::id();

        $idByName = Permission::query()
            ->whereIn('name', array_merge($overrides['grant'], $overrides['revoke']))
            ->pluck('id', 'name');

        // Existing rows keep their original granted_by/granted_at for as long as
        // the override itself is unchanged — restamping every save would erase
        // who first pinned a permission and when, which is the only reason those
        // columns exist.
        $existing = $user->permissionOverrides()->get()
            ->keyBy(fn (Permission $permission): int => $permission->getKey());

        $sync = [];
        foreach (['grant' => 'allow', 'revoke' => 'deny'] as $formKey => $storedType) {
            foreach ($overrides[$formKey] as $name) {
                if (! isset($idByName[$name])) {
                    continue;
                }

                $id = $idByName[$name];
                $prior = $existing->get($id);
                $unchanged = $prior && $prior->pivot->type === $storedType;

                $sync[$id] = [
                    'type' => $storedType,
                    'granted_by' => $unchanged ? $prior->pivot->granted_by : $grantorId,
                    'granted_at' => $unchanged ? $prior->pivot->granted_at : now(),
                ];
            }
        }

        $user->permissionOverrides()->sync($sync);
        $user->flushPermissionCache();

        return ['allow' => $overrides['grant'], 'deny' => $overrides['revoke']];
    }

    public static function getPages(): array
    {
        return [
            'index' => ListUsers::route('/'),
            'create' => CreateUser::route('/create'),
            'view' => ViewUser::route('/{record}'),
            'edit' => EditUser::route('/{record}/edit'),
        ];
    }
}
