<?php

namespace App\Filament\Central\Resources\TenantResource\RelationManagers;

use App\Filament\Resources\RoleResource;
use App\Filament\Support\PermissionMatrix;
use App\Models\Role;
use App\Models\User;
use App\Services\AuditLog;
use App\Services\CurrentContext;
use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use Illuminate\Validation\Rule;

/**
 * Full role management for a tenant, from the landlord panel
 * (docs/migration-plan.md Phase 12). Mirrors the tenant-side RoleResource's
 * surface — create / edit / duplicate / toggle-active / delete — scoped to the
 * tenant being managed, so the landlord can stand up a tenant's RBAC without
 * impersonating just to open its Roles page.
 *
 * The same two invariants that hold tenant-side hold here:
 *  - a role created from a UI is never system-defined or full-admin
 *    (RoleController::store() hardcodes both to false — only the seeder mints
 *    those), and a full-admin role is always active and its grant list is not
 *    editable;
 *  - a role can't be deleted while it is_system or still has members
 *    (RolePolicy::delete()).
 *
 * Unlike the tenant panel there is no per-model policy (see
 * BranchesRelationManager) — authorization on the landlord panel is "an
 * authenticated central admin, full stop" — but Role is tenant-scoped, so every
 * mutating action runs inside CurrentContext::runForTenant() and lands in the
 * tenant's audit trail flagged with `via: master_control` (actor_id stays null —
 * the operator is not a tenant user), the same idiom TenantResource::audit()
 * uses. The permission matrix is capped at the tenant's entitlement ceiling via
 * the picker's explicit ->tenantId(), mirroring UsersRelationManager.
 */
class RolesRelationManager extends RelationManager
{
    protected static string $relationship = 'roles';

    protected static ?string $title = 'Roles';

    protected static ?string $recordTitleAttribute = 'name';

    /** Landlord panel — no per-model policy, see BranchesRelationManager. */
    protected static bool $shouldSkipAuthorization = true;

    public function form(Schema $schema): Schema
    {
        return $schema->components([]);
    }

    /**
     * @return array<int, Component>
     */
    protected function roleFormSchema(): array
    {
        return [
            Section::make('Role')->schema([
                TextInput::make('name')
                    ->required()
                    ->maxLength(120)
                    // Unique within this tenant's roles — `roles` holds every
                    // tenant's rows, so the scoped rule replaces the
                    // single-tenant `unique:roles,name` the source assumed.
                    ->rule(fn (?Role $record) => Rule::unique('roles', 'name')
                        ->where('tenant_id', $this->getOwnerRecord()->getKey())
                        ->ignore($record?->id)),
                Textarea::make('description')->maxLength(255)->columnSpanFull(),
                Toggle::make('is_active')
                    ->default(true)
                    // Ported rule: an is_full_admin role is always active —
                    // RoleController::update() forces true regardless of input.
                    ->disabled(fn (?Role $record): bool => (bool) $record?->is_full_admin)
                    ->dehydrateStateUsing(fn (?Role $record, $state) => $record?->is_full_admin ? true : $state),
                Toggle::make('is_all_branches')
                    ->label('All-Branch Access (role-wide)')
                    ->helperText('Every active holder of this role sees every branch, in addition to whatever is separately granted at the user level.'),
            ])->columns(2),

            Section::make('Permissions')
                ->description('A full-admin role bypasses every permission check, so its grant list is not editable here.')
                ->visible(fn (?Role $record): bool => ! $record?->is_full_admin)
                ->schema([
                    PermissionMatrix::checkboxList('permissions')
                        ->limitedToTenantEntitlement()
                        // Auth::user() here is a CentralAdmin, which has no
                        // tenant_id of its own — cap the matrix at the tenant
                        // actually being edited instead.
                        ->tenantId(fn (): int => $this->getOwnerRecord()->getKey())
                        ->helperText('Permissions this role grants its members, capped at the tenant\'s entitlement ceiling.'),
                ]),
        ];
    }

    public function table(Table $table): Table
    {
        return $table
            ->modifyQueryUsing(fn ($query) => $query->withCount(['users', 'permissions']))
            ->columns([
                TextColumn::make('name')->searchable()->sortable(),
                TextColumn::make('description')->placeholder('—')->limit(60),
                IconColumn::make('is_system')->boolean()->label('System'),
                IconColumn::make('is_full_admin')->boolean()->label('Full admin'),
                IconColumn::make('is_active')->boolean(),
                TextColumn::make('users_count')->label('Members'),
                TextColumn::make('permissions_count')->label('Permissions'),
                TextColumn::make('updated_at')->dateTime()->sortable()->toggleable(),
            ])
            ->defaultSort('name')
            ->headerActions([
                Action::make('create')
                    ->label('Create role')
                    ->icon(Heroicon::OutlinedPlus)
                    ->modalHeading(fn (): string => 'Create role for '.$this->getOwnerRecord()->name)
                    ->schema($this->roleFormSchema())
                    ->action(function (array $data): void {
                        $this->createRole($data);
                    }),
            ])
            ->recordActions([
                Action::make('edit')
                    ->label('Edit')
                    ->icon(Heroicon::OutlinedPencilSquare)
                    ->schema($this->roleFormSchema())
                    ->fillForm(fn (Role $record): array => [
                        'permissions' => $record->permissions()->pluck('permissions.name')->all(),
                    ])
                    ->action(function (Role $record, array $data): void {
                        $this->updateRole($record, $data);
                    }),

                Action::make('duplicate')
                    ->label('Duplicate')
                    ->icon(Heroicon::OutlinedDocumentDuplicate)
                    ->requiresConfirmation()
                    ->visible(fn (Role $record): bool => ! $record->is_system)
                    ->action(fn (Role $record) => $this->duplicateRole($record)),

                Action::make('toggleActive')
                    ->label(fn (Role $record): string => $record->is_active ? 'Deactivate' : 'Activate')
                    ->color(fn (Role $record): string => $record->is_active ? 'danger' : 'success')
                    ->requiresConfirmation()
                    ->visible(fn (Role $record): bool => ! $record->is_full_admin)
                    ->action(fn (Role $record) => $this->toggleActive($record)),

                Action::make('delete')
                    ->label('Delete')
                    ->color('danger')
                    ->requiresConfirmation()
                    ->action(fn (Role $record) => $this->deleteRole($record)),
            ]);
    }

    /**
     * Ported from CreateRole (mutateFormDataBeforeCreate + afterCreate): a role
     * created through the UI is never system-defined or full-admin, and its
     * permission grants are synced after creation.
     */
    private function createRole(array $data): void
    {
        // Livewire syncs an all-unchecked PermissionPicker back as `false`
        // rather than `[]` — `?? []` alone doesn't catch that.
        $permissionNames = is_array($data['permissions'] ?? null) ? $data['permissions'] : [];
        unset($data['permissions']);

        $data['is_system'] = false;
        $data['is_full_admin'] = false;

        $this->runForTenant(function () use ($data, $permissionNames): void {
            $role = Role::create($data);
            $role->permissions()->sync(RoleResource::permissionIdsFor($permissionNames));
            RoleResource::flushRoleMembers($role);

            AuditLog::record('role.created', $role, [
                'name' => $role->name,
                'permissions_count' => count($permissionNames),
                'permissions' => $permissionNames,
                'via' => 'master_control',
            ]);
        });

        Notification::make()->title('Role created.')->success()->send();
    }

    /**
     * Ported from EditRole (mutateFormDataBeforeSave + afterSave): permissions
     * are computed, so the change flows to every member at once — no sync
     * happens (or is meaningful) for a full-admin role.
     */
    private function updateRole(Role $record, array $data): void
    {
        $oldPermissionNames = $record->permissions()->pluck('permissions.name')->all();
        $newPermissionNames = is_array($data['permissions'] ?? null) ? $data['permissions'] : [];
        unset($data['permissions']);

        $this->runForTenant(function () use ($record, $data, $oldPermissionNames, $newPermissionNames): void {
            // Belt-and-braces: the toggle is already forced true for full-admin
            // roles in the schema; hold it here too in case the payload is hand-crafted.
            $data['is_active'] = $record->is_full_admin ? true : ($data['is_active'] ?? $record->is_active);

            $record->update($data);

            if (! $record->is_full_admin) {
                $record->permissions()->sync(RoleResource::permissionIdsFor($newPermissionNames));
            }

            RoleResource::flushRoleMembers($record);

            AuditLog::record('role.updated', $record, [
                'added' => array_values(array_diff($newPermissionNames, $oldPermissionNames)),
                'removed' => array_values(array_diff($oldPermissionNames, $newPermissionNames)),
                'via' => 'master_control',
            ]);
        });

        Notification::make()->title('Role updated.')->success()->send();
    }

    /**
     * Ported from RoleResource::duplicate() — the copy is never system,
     * full-admin or all-branch, and carries this tenant's scoping with it.
     */
    private function duplicateRole(Role $record): void
    {
        $copyName = null;

        $this->runForTenant(function () use ($record, &$copyName): void {
            $copy = $record->replicate(['is_system']);

            // The list query's withCount(['users', 'permissions']) attaches
            // computed users_count/permissions_count attributes that replicate()
            // would otherwise persist as real columns.
            unset($copy->users_count, $copy->permissions_count);

            $copy->name = RoleResource::uniqueCopyName($record->name);
            $copy->is_system = false;
            $copy->is_full_admin = false;
            // Mirrors is_full_admin: duplicating an elevated role must not silently
            // mint another one — granting All-Branch Access is a deliberate act.
            $copy->is_all_branches = false;
            $copy->save();

            $copy->permissions()->sync($record->permissions()->pluck('permissions.id')->all());

            AuditLog::record('role.duplicated', $copy, [
                'name' => $copy->name,
                'source' => $record->name,
                'source_role_id' => $record->id,
                'via' => 'master_control',
            ]);

            $copyName = $copy->name;
        });

        Notification::make()->title("Role duplicated as \"{$copyName}\".")->success()->send();
    }

    /**
     * Ported from RoleResource::toggleActive(): flip is_active, then flush every
     * member's permission cache — activating/deactivating a role changes every
     * holder's effective permissions immediately.
     */
    private function toggleActive(Role $record): void
    {
        $nowActive = false;

        $this->runForTenant(function () use ($record, &$nowActive): void {
            $record->update(['is_active' => ! $record->is_active]);
            RoleResource::flushRoleMembers($record);

            AuditLog::record('role.toggled_active', $record, [
                'name' => $record->name,
                'is_active' => $record->is_active,
                'via' => 'master_control',
            ]);

            $nowActive = $record->is_active;
        });

        Notification::make()->title($nowActive ? 'Role activated.' : 'Role deactivated.')->success()->send();
    }

    /**
     * Ported guards from RolePolicy::delete(): a system role can't be deleted,
     * and neither can a role that still has members — checked over BOTH the
     * legacy singular role_id column and the user_roles pivot, matching
     * RoleResource::flushRoleMembers()'s dual lookup.
     */
    private function deleteRole(Role $record): void
    {
        $deleted = $this->runForTenant(function () use ($record): bool {
            if ($record->is_system) {
                Notification::make()->title('A system role cannot be deleted.')->danger()->send();

                return false;
            }

            $hasMembers = $record->users()->exists()
                || User::query()->whereHas('roles', fn ($q) => $q->where('roles.id', $record->id))->exists();

            if ($hasMembers) {
                Notification::make()->title('Cannot delete: role still has members assigned.')->danger()->send();

                return false;
            }

            AuditLog::record('role.deleted', $record, [
                'name' => $record->name,
                'via' => 'master_control',
            ]);

            $record->delete();

            return true;
        });

        if ($deleted) {
            Notification::make()->title('Role deleted.')->success()->send();
        }
    }

    /**
     * AuditLog::record() (and any tenant-scoped write) needs a bound tenant —
     * this panel has none by default (see class docblock).
     */
    private function runForTenant(\Closure $callback): mixed
    {
        return app(CurrentContext::class)->runForTenant($this->getOwnerRecord(), $callback);
    }
}
