<?php

namespace App\Filament\Central\Resources;

use App\Filament\Central\Resources\TenantResource\Pages\CreateTenant;
use App\Filament\Central\Resources\TenantResource\Pages\EditTenant;
use App\Filament\Central\Resources\TenantResource\Pages\ListTenants;
use App\Filament\Central\Resources\TenantResource\RelationManagers\AuditLogsRelationManager;
use App\Filament\Central\Resources\TenantResource\RelationManagers\BranchesRelationManager;
use App\Filament\Central\Resources\TenantResource\RelationManagers\PaymentMethodsRelationManager;
use App\Filament\Central\Resources\TenantResource\RelationManagers\RolesRelationManager;
use App\Filament\Central\Resources\TenantResource\RelationManagers\TillsRelationManager;
use App\Filament\Central\Resources\TenantResource\RelationManagers\UsersRelationManager;
use App\Models\Central\Tenant;
use App\Models\Permission;
use App\Models\User;
use App\Services\AuditLog;
use App\Services\CurrentContext;
use App\Services\Storage\StorageConfig;
use App\Services\Tenancy\TestInstanceService;
use Filament\Actions\Action;
use Filament\Actions\EditAction;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;

/**
 * Ported from App\Http\Controllers\Central\TenantController +
 * TenantEntitlementController (docs/migration-plan.md Phase 10).
 *
 * Lives on the central (landlord) panel only — the `central` guard, no tenant
 * scoping. Every cross-tenant read or write goes through
 * `CurrentContext::runForTenant()`, which is this codebase's own idiom for
 * operating inside another tenant's scope (and the correct replacement for the
 * dead `$tenant->run()` calls, see Decisions Log #15/#61).
 */
class TenantResource extends Resource
{
    protected static ?string $model = Tenant::class;

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

    protected static ?string $recordTitleAttribute = 'name';

    protected static ?int $navigationSort = 1;

    /**
     * The Tenants section shows live environments only. Test copies live under
     * their own TestInstanceResource.
     */
    public static function getEloquentQuery(): Builder
    {
        return parent::getEloquentQuery()->live();
    }

    public static function getRelations(): array
    {
        return [
            UsersRelationManager::class,
            RolesRelationManager::class,
            BranchesRelationManager::class,
            TillsRelationManager::class,
            PaymentMethodsRelationManager::class,
            AuditLogsRelationManager::class,
        ];
    }

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            Section::make('Tenant Details')->schema([
                TextInput::make('name')
                    ->label('Company Name')
                    ->required()
                    ->maxLength(255)
                    // Client-side only: derives slug/admin fields as the operator
                    // types, with no Livewire round-trip (fields stay
                    // wire:model-deferred, so writing straight to the DOM + firing
                    // a native `input` event updates Alpine's entangled state
                    // locally). Uses fixed DOM ids + document.getElementById
                    // rather than Alpine $refs, since the Name field and the
                    // admin fields sit in separate Sections/Alpine scopes and
                    // $refs does not cross that boundary. Only wired up on
                    // create — edit has no admin fields and shouldn't clobber
                    // an existing slug.
                    ->extraInputAttributes(fn (string $operation): array => $operation === 'create' ? [
                        'x-on:input' => <<<'JS'
                            let name = $event.target.value;
                            let slug = name.toLowerCase().trim()
                                .replace(/[^a-z0-9]+/g, '-')
                                .replace(/(^-+|-+$)/g, '');
                            let apply = (id, value) => {
                                let el = document.getElementById(id);
                                if (! el) return;
                                el.value = value;
                                el.dispatchEvent(new Event('input'));
                            };
                            apply('tenant-slug-field', slug);
                            apply('tenant-admin-name-field', name ? name + ' Admin' : '');
                            apply('tenant-admin-email-field', slug ? 'admin@' + slug + '.com' : '');
                        JS,
                    ] : []),
                TextInput::make('slug')
                    ->label(static::slugLabel())
                    ->required()
                    ->maxLength(63)
                    ->alphaDash()
                    // Path tenancy: a slug shares the first-URL-segment
                    // namespace with real top-level paths, so a reserved slug
                    // is rejected here rather than at the router
                    // (docs/path-based-tenancy-plan.md Phase 5).
                    ->rules([
                        fn (?Tenant $record) => Rule::unique('tenants', 'slug')->ignore($record?->id),
                        Rule::notIn(config('tenancy.reserved_slugs', [])),
                    ])
                    ->suffix(static::slugSuffix())
                    ->helperText(fn (): string => static::slugHelperText())
                    ->extraInputAttributes(['id' => 'tenant-slug-field'])
                    // Str::lower() on save, matching the controller.
                    ->dehydrateStateUsing(fn (?string $state): string => Str::lower((string) $state)),

                // Only collected when provisioning a brand-new tenant; the owner
                // account is created by TenantProvisioner, not by this form.
                TextInput::make('admin_name')
                    ->label('Owner Name')
                    ->required()
                    ->maxLength(255)
                    ->visible(fn (string $operation): bool => $operation === 'create')
                    ->extraInputAttributes(['id' => 'tenant-admin-name-field']),
                TextInput::make('admin_email')
                    ->label('Owner Email')
                    ->email()
                    ->required()
                    ->maxLength(255)
                    ->visible(fn (string $operation): bool => $operation === 'create')
                    ->extraInputAttributes(['id' => 'tenant-admin-email-field']),
                Toggle::make('enable_test_instance')
                    ->label('Enable Test Instance')
                    ->default(false)
                    ->columnSpanFull()
                    // Only collected when provisioning a brand-new tenant; the
                    // test copy is allocated by TestInstanceService after the
                    // live tenant and its owner are provisioned.
                    ->visible(fn (string $operation): bool => $operation === 'create')
                    ->helperText(fn (Get $get): string => sprintf(
                        'Also provisions an isolated copy on "%s", seeded with the current live state. Test data never affects the live tenant.',
                        static::domainFor(Str::lower((string) $get('slug')).'-test'),
                    )),
            ])->columns(2),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('name')->searchable()->sortable(),
                TextColumn::make('slug')->badge()->searchable(),
                TextColumn::make('status')
                    ->badge()
                    ->color(fn (?string $state): string => match ($state) {
                        Tenant::STATUS_ACTIVE => 'success',
                        Tenant::STATUS_TRIAL => 'info',
                        Tenant::STATUS_SUSPENDED => 'danger',
                        default => 'gray',
                    }),
                TextColumn::make('test_instance')
                    ->label('Test Instance')
                    ->state(fn (Tenant $record): string => $record->testInstance ? 'Enabled' : 'Not enabled')
                    ->badge()
                    ->color(fn (Tenant $record): string => $record->testInstance ? 'warning' : 'gray')
                    ->description(fn (Tenant $record): ?string => $record->testInstance?->slug)
                    ->placeholder('Not enabled')
                    ->toggleable(),
                TextColumn::make('users_count')
                    ->label('Users')
                    ->state(fn (Tenant $record): int => static::userCountFor($record)),

                // Every tenant shares one bucket, so "who is consuming more" is
                // the question worth answering here. Sortable on the cached
                // column (not the formatted string) so the heaviest tenants can
                // be brought to the top; refreshed by `storage:recalculate-usage`
                // and kept roughly live by TenantStorage on each write/delete.
                TextColumn::make('storage_used_bytes')
                    ->label('Storage used')
                    ->sortable()
                    ->formatStateUsing(fn ($state): string => static::formatBytes((int) $state))
                    ->description(fn (Tenant $record): ?string => static::quotaShare($record))
                    ->color(fn (Tenant $record): ?string => match (true) {
                        ($pct = static::quotaPercent($record)) === null => null,
                        $pct >= 25.0 => 'danger',
                        $pct >= 10.0 => 'warning',
                        default => null,
                    })
                    ->summarize(Sum::make()
                        ->label('Total')
                        ->formatStateUsing(fn ($state): string => static::formatBytes((int) $state))),
                TextColumn::make('storage_files_count')
                    ->label('Files')
                    ->sortable()
                    ->numeric()
                    ->toggleable(),
                TextColumn::make('storage_calculated_at')
                    ->label('Usage as of')
                    ->since()
                    ->placeholder('never')
                    ->tooltip(fn (Tenant $record): ?string => $record->storage_calculated_at?->toDayDateTimeString())
                    ->toggleable(isToggledHiddenByDefault: true),

                TextColumn::make('created_at')->date()->sortable(),
            ])
            ->defaultSort('name')
            ->filters([
                Filter::make('search')
                    ->label('Search')
                    ->schema([
                        TextInput::make('value')
                            ->label('Name or subdomain')
                            ->placeholder('e.g. Acme or acme')
                            ->columnSpanFull(),
                    ])
                    ->query(fn (Builder $query, array $data): Builder => $query->when(
                        $data['value'] ?? null,
                        fn (Builder $q, $value) => $q->where(function (Builder $qq) use ($value): void {
                            $qq->where('name', 'like', "%{$value}%")
                                ->orWhere('slug', 'like', "%{$value}%");
                        }),
                    )),
                SelectFilter::make('status')
                    ->label('Status')
                    ->options(collect(Tenant::STATUSES)->mapWithKeys(
                        fn (string $status): array => [$status => Str::headline($status)],
                    )->all()),
            ])
            ->recordActions([
                EditAction::make(),

                Action::make('suspend')
                    ->color('danger')
                    ->requiresConfirmation()
                    ->visible(fn (Tenant $record): bool => ! $record->isSuspended())
                    ->action(function (Tenant $record): void {
                        $record->update(['status' => Tenant::STATUS_SUSPENDED]);
                        static::audit($record, 'tenant.suspended');
                        Notification::make()->title("{$record->name} suspended.")->success()->send();
                    }),

                Action::make('resume')
                    ->color('success')
                    ->requiresConfirmation()
                    ->visible(fn (Tenant $record): bool => $record->isSuspended())
                    ->action(function (Tenant $record): void {
                        $record->update(['status' => Tenant::STATUS_ACTIVE]);
                        static::audit($record, 'tenant.resumed');
                        Notification::make()->title("{$record->name} reactivated.")->success()->send();
                    }),

                Action::make('impersonate')
                    ->label('Auto-impersonate')
                    ->icon(Heroicon::OutlinedUserCircle)
                    ->requiresConfirmation()
                    ->modalDescription('Opens a support session as this tenant\'s administrator. The hand-off is written to their audit trail.')
                    ->action(fn (Tenant $record) => static::startImpersonation($record)),

                Action::make('viewCredentials')
                    ->label('Manual — View credentials')
                    ->icon(Heroicon::OutlinedKey)
                    ->requiresConfirmation()
                    ->modalDescription('Generates a fresh one-time password for the tenant owner and shows the sign-in details.')
                    ->action(fn (Tenant $record) => static::viewOwnerCredentials($record)),

                Action::make('openTestInstance')
                    ->label('Open Test Instance')
                    ->icon(Heroicon::OutlinedBeaker)
                    ->visible(fn (Tenant $record): bool => $record->testInstance !== null)
                    ->url(fn (Tenant $record): string => static::tenantUrl($record->testInstance))
                    ->openUrlInNewTab(),

                Action::make('createTestInstance')
                    ->label('Create Test Instance')
                    ->icon(Heroicon::OutlinedBeaker)
                    ->color('warning')
                    ->visible(fn (Tenant $record): bool => $record->testInstance === null)
                    ->requiresConfirmation()
                    ->modalHeading('Create a test instance?')
                    ->modalIcon(Heroicon::OutlinedBeaker)
                    ->modalIconColor('warning')
                    ->modalDescription(fn (Tenant $record): string => sprintf(
                        'An isolated copy of this tenant will be provisioned on "%s", seeded with the current live data (%s users, %s branches). Changes inside it never affect the live tenant.',
                        static::domainFor(TestInstanceService::testSlugFor($record)),
                        static::userCountFor($record),
                        (int) DB::table('warehouses')->where('tenant_id', $record->id)->count(),
                    ))
                    ->modalSubmitActionLabel('Create test instance')
                    ->action(function (Tenant $record): void {
                        try {
                            $test = app(TestInstanceService::class)->create(
                                $record,
                                Auth::guard('central')->id(),
                            );
                        } catch (\Throwable $e) {
                            Notification::make()
                                ->title('Could not create test instance')
                                ->body($e->getMessage())
                                ->danger()
                                ->persistent()
                                ->send();

                            return;
                        }

                        static::audit($record, 'tenant.test_instance_provisioned', [
                            'test_tenant_id' => $test->id,
                            'test_slug' => $test->slug,
                        ]);

                        Notification::make()
                            ->title('Test instance created')
                            ->body(sprintf('Isolated copy provisioned at "%s". It was seeded with the current live state.', $test->url()))
                            ->success()
                            ->persistent()
                            ->send();
                    }),
            ]);
    }

    /**
     * Human-readable byte size. Tenants on a shared bucket span a very wide
     * range — an empty trial next to a chain with years of product images — so
     * the unit is picked per value rather than fixed at MB.
     */
    public static function formatBytes(int $bytes): string
    {
        return match (true) {
            $bytes >= 1073741824 => number_format($bytes / 1073741824, 2).' GB',
            $bytes >= 1048576 => number_format($bytes / 1048576, 1).' MB',
            $bytes >= 1024 => number_format($bytes / 1024).' KB',
            default => $bytes.' B',
        };
    }

    /**
     * This tenant's share of the PLATFORM quota, or null when no quota is set.
     * The quota is shared, so one tenant's growth is every other tenant's
     * problem — which is the reason to surface the percentage at all.
     */
    public static function quotaPercent(Tenant $tenant): ?float
    {
        $quota = app(StorageConfig::class)->quotaBytes();

        if ($quota === null || $quota <= 0) {
            return null;
        }

        return ((int) $tenant->storage_used_bytes) / $quota * 100;
    }

    public static function quotaShare(Tenant $tenant): ?string
    {
        $percent = static::quotaPercent($tenant);

        if ($percent === null) {
            return null;
        }

        return number_format($percent, 1).'% of platform quota';
    }

    /**
     * Ported from Central\ImpersonationController::start().
     *
     * The landlord lives on the central domain with the `central` guard, so a
     * tenant `web` session cookie cannot be minted here. A short-lived
     * single-use token is issued instead and the operator is handed off to the
     * tenant subdomain, where the tenant-side ImpersonationController consumes
     * it (already ported in Phase 0). Every hand-off is audited.
     */
    public static function startImpersonation(Tenant $tenant): void
    {
        $context = app(CurrentContext::class);

        $owner = $context->runForTenant($tenant, fn () => User::query()
            ->where('status', User::STATUS_ACTIVE)
            ->whereHas('roles', fn ($q) => $q->where('is_full_admin', true)->where('is_active', true))
            ->first());

        if (! $owner) {
            Notification::make()->title('This tenant has no active administrator to impersonate.')->danger()->send();

            return;
        }

        $token = Str::random(48);
        $admin = Auth::guard('central')->user();

        Cache::put("impersonate:{$token}", [
            'user_id' => $owner->id,
            'tenant_id' => $tenant->id,
            'by_email' => $admin?->email,
        ], now()->addMinutes(2));

        $context->runForTenant($tenant, fn () => AuditLog::record(
            'tenant.impersonation_started',
            $owner,
            ['central_admin' => $admin?->email],
        ));

        Notification::make()
            ->title('Impersonation link ready')
            ->body('Valid for 2 minutes, single use.')
            ->actions([
                Action::make('open')
                    ->label('Open session')
                    ->url($tenant->url('/impersonate/'.$token))
                    ->openUrlInNewTab(),
            ])
            ->success()
            ->persistent()
            ->send();
    }

    /**
     * The tenant's own URL — delegates to {@see Tenant::url()} so every
     * builder observes the active tenancy routing (path: "/<slug>/app" on
     * the single host; subdomain: "<scheme>://<slug>.<app_domain>" with any
     * non-standard port preserved), docs/path-based-tenancy-plan.md Phase 4.
     */
    public static function tenantUrl(Tenant $tenant): string
    {
        return $tenant->url();
    }

    /**
     * Users belonging to a tenant. Counted outside tenant scope (this is the
     * landlord panel) via an explicit tenant_id filter, matching the source's
     * grouped `DB::table('users')` count.
     */
    public static function userCountFor(Tenant $tenant): int
    {
        // DB::table(), not Eloquent: the landlord panel has no tenant bound, so
        // TenantScope would (correctly) refuse a User query here. The source
        // counts the same way, via a grouped DB::table('users') query.
        return (int) DB::table('users')
            ->whereNull('deleted_at')
            ->where('tenant_id', $tenant->id)
            ->count();
    }

    /**
     * Display form of a tenant's URL for exactly this slug — a host label in
     * subdomain mode, the "/<slug>/app" path on the single host in path mode.
     */
    public static function domainFor(string $slug): string
    {
        return config('tenancy.routing') === 'path'
            ? url('/'.$slug.'/app')
            : $slug.'.'.config('tenancy.app_domain');
    }

    /**
     * The slug field's label: "Subdomain" vs. "Workspace", aligned with the
     * active routing strategy.
     */
    public static function slugLabel(): string
    {
        return config('tenancy.routing') === 'path' ? 'Workspace Slug' : 'Subdomain';
    }

    /**
     * What the slug field shows after the input: the "/app" suffix that always
     * follows the workspace segment in path mode, or ".domain" in subdomain
     * mode.
     */
    public static function slugSuffix(): string
    {
        return config('tenancy.routing') === 'path'
            ? '/app'
            : '.'.config('tenancy.app_domain');
    }

    public static function slugHelperText(): string
    {
        return config('tenancy.routing') === 'path'
            ? sprintf('This workspace will be served at "%s/<slug>/app" on this domain.', config('tenancy.app_domain'))
            : sprintf('This tenant will be served on a subdomain like "acme.%s".', config('tenancy.app_domain'));
    }

    /**
     * Ported from TenantEntitlementController::update(): sync the ceiling,
     * flush the tenant's entitlement cache AND every tenant user's cached
     * permission set (the ceiling caps them all), then audit.
     *
     * @param  array<int, string>  $permissionNames
     */
    public static function updateEntitlements(Tenant $tenant, array $permissionNames): void
    {
        $ids = Permission::query()->whereIn('name', $permissionNames)->pluck('id')->all();

        $tenant->entitlements()->sync($ids);
        $tenant->flushEntitlementCache();

        app(CurrentContext::class)->runForTenant($tenant, function (): void {
            User::query()->pluck('id')->each(function ($id): void {
                Cache::forget("user:{$id}:permissions");
                Cache::forget("user:{$id}:full_admin");
            });
        });

        static::audit($tenant, 'tenant.entitlements_updated', ['count' => count($ids)]);

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

    /**
     * Ported from TenantController::resetAdminPassword().
     */
    public static function resetAdminPassword(Tenant $tenant): void
    {
        $password = 'password';
        $context = app(CurrentContext::class);

        $owner = $context->runForTenant($tenant, fn () => User::query()
            ->where('status', User::STATUS_ACTIVE)
            ->whereHas('roles', fn ($q) => $q->where('is_full_admin', true)->where('is_active', true))
            ->first());

        if (! $owner) {
            Notification::make()->title('No active administrator found for this tenant.')->danger()->send();

            return;
        }

        $context->runForTenant($tenant, function () use ($owner, $password): void {
            $owner->update([
                'password' => Hash::make($password),
                'must_change_password' => true,
            ]);
            $owner->flushPermissionCache();
        });

        static::audit($tenant, 'tenant.admin_password_reset', ['admin_email' => $owner->email]);

        Notification::make()
            ->title("Password for {$owner->email} reset to \"{$password}\".")
            ->body('The administrator must change it at next login.')
            ->success()
            ->persistent()
            ->send();
    }

    /**
     * The "Manual — View credentials" impersonation alternative: instead of a
     * one-click hand-off, generate a fresh one-time password for the tenant
     * owner and surface the sign-in details (URL, email, password) exactly
     * once, forcing a change at the owner's next login.
     */
    public static function viewOwnerCredentials(Tenant $tenant): void
    {
        $password = Str::password(14);
        $context = app(CurrentContext::class);

        $owner = $context->runForTenant($tenant, fn () => User::query()
            ->where('status', User::STATUS_ACTIVE)
            ->whereHas('roles', fn ($q) => $q->where('is_full_admin', true)->where('is_active', true))
            ->first());

        if (! $owner) {
            Notification::make()->title('This tenant has no active administrator.')->danger()->send();

            return;
        }

        $context->runForTenant($tenant, function () use ($owner, $password): void {
            $owner->update([
                'password' => Hash::make($password),
                'must_change_password' => true,
            ]);
            $owner->flushPermissionCache();
        });

        static::audit($tenant, 'tenant.owner_credentials_generated', ['admin_email' => $owner->email]);

        Notification::make()
            ->title('Owner credentials ready')
            ->body(sprintf(
                "URL: %s\nEmail: %s\nPassword: %s\n\nThe owner must change this password at their next login. This is shown once.",
                static::tenantUrl($tenant),
                $owner->email,
                $password,
            ))
            ->actions([
                Action::make('open')
                    ->label('Open sign-in page')
                    ->url(static::tenantUrl($tenant))
                    ->openUrlInNewTab(),
            ])
            ->success()
            ->persistent()
            ->send();
    }

    /**
     * Record a landlord action into the affected tenant's audit trail, noting
     * the central operator (who is not a tenant user, so actor_id stays null).
     * Ported verbatim from TenantController::audit().
     *
     * @param  array<string, mixed>  $context
     */
    public static function audit(Tenant $tenant, string $action, array $context = []): void
    {
        $admin = Auth::guard('central')->user();
        $context['central_admin'] = $admin?->email;

        app(CurrentContext::class)->runForTenant($tenant, fn () => AuditLog::record($action, $tenant, $context));
    }

    public static function getPages(): array
    {
        return [
            'index' => ListTenants::route('/'),
            'create' => CreateTenant::route('/create'),
            'edit' => EditTenant::route('/{record}/edit'),
        ];
    }
}
