<?php

namespace App\Filament\Resources\Concerns;

use Barryvdh\DomPDF\Facade\Pdf;
use Carbon\CarbonInterface;
use Closure;
use Filament\Actions\Action;
use Filament\Actions\ExportAction;
use Filament\Actions\ExportBulkAction;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\StreamedResponse;

/**
 * Every Resource using this trait declares:
 *
 *   protected static string $exporter = \App\Filament\Exports\XxxExporter::class;
 *
 * and adds standardExportActions()/standardExportBulkActions() to its own
 * table()'s ->headerActions()/->toolbarActions() (see ProductResource for the
 * pattern). Excel/CSV go through Filament's own queued export system (hence
 * the Exporter class). PDF is a separate, synchronous action here — Filament's
 * native exporter does not produce PDF — reusing the SAME Exporter's declared
 * columns (name + label) rather than a third, independent column list, and
 * rendering through one shared Blade view
 * (resources/views/filament/exports/table-pdf.blade.php) instead of a bespoke
 * template per Resource.
 *
 * All three exports (Excel/CSV header action, Excel/CSV bulk action, PDF) are
 * gated by {@see canExport()} — `{module}.export` by default. Bulk data
 * extraction is a materially different capability from reading a listing, so
 * `access` alone must never be enough: without this gate any user who could
 * open a listing could download the whole table (and the modules that DID
 * define an `.export` permission only enforced it on their own hand-rolled CSV
 * button, leaving these three as an open side door around it).
 */
trait HasStandardTableFeatures
{
    /**
     * The global table ergonomics every listing shares — applied by the
     * resource's table() so a cashier's filters and search survive a reload,
     * filters apply on demand instead of after every keystroke, and zebra
     * striping keeps the eye on the row.
     */
    public static function standardTableDefaults(Table $table): Table
    {
        return $table
            ->persistFiltersInSession()
            ->persistSearchInSession()
            ->deferFilters()
            ->striped();
    }

    /**
     * The row-wide click target: the whole row navigates to the View page when
     * the resource has one, else to Edit where no View exists. Kills the
     * hunt-for-the-eye-icon problem globally. A resource whose rows must not
     * navigate (e.g. rows that carry destructive quick actions) simply omits
     * this from its table().
     *
     * @return Closure(Model): ?string
     */
    public static function standardRecordUrl(?string $preferredPage = null): Closure
    {
        return function (Model $record) use ($preferredPage): ?string {
            $page = $preferredPage ?? (static::hasPage('view') ? 'view' : 'edit');

            // A resource with neither a View nor an Edit page (read-only
            // listings like Bin Card) simply has no clickable rows.
            if (! static::hasPage($page)) {
                return null;
            }

            return match ($page) {
                'view' => static::canView($record) ? static::getUrl('view', ['record' => $record]) : null,
                'edit' => static::canEdit($record) ? static::getUrl('edit', ['record' => $record]) : null,
                default => null,
            };
        };
    }

    /**
     * @return array<Action>
     */
    public static function standardExportActions(): array
    {
        return [
            ExportAction::make()
                ->exporter(static::exporter())
                ->authorize(fn (): bool => static::canExport()),
            static::pdfExportAction(),
        ];
    }

    /**
     * @return array<ExportBulkAction>
     */
    public static function standardExportBulkActions(): array
    {
        return [
            ExportBulkAction::make()
                ->exporter(static::exporter())
                ->authorize(fn (): bool => static::canExport()),
        ];
    }

    /**
     * The permission every export on this Resource requires. Resources whose
     * module has no `export` action of its own override this to point at the
     * verb that does govern the module (see TillResource).
     */
    public static function canExport(): bool
    {
        return Auth::user()?->hasPermissionTo(static::$permissionModuleKey.'.export') ?? false;
    }

    /**
     * @return class-string<Exporter>
     */
    protected static function exporter(): string
    {
        return static::$exporter;
    }

    public static function pdfExportAction(): Action
    {
        return Action::make('exportPdf')
            ->label('Export PDF')
            ->icon('heroicon-m-document-arrow-down')
            ->color('gray')
            // ->authorize() rather than ->visible(): both hide the button, but
            // only authorize() also makes Filament refuse to mount the action,
            // so a crafted Livewire call cannot reach ->action() either.
            ->authorize(fn (): bool => static::canExport())
            ->action(function (HasTable $livewire): StreamedResponse {
                // Belt and braces at the point the file is actually produced.
                abort_unless(static::canExport(), 403);

                $columns = static::exporter()::getVisibleColumns();
                $records = $livewire->getFilteredSortedTableQuery()->get();

                $headings = array_map(
                    fn (ExportColumn $column): string => $column->getLabel() ?? $column->getName(),
                    $columns,
                );

                $rows = $records->map(fn ($record): array => array_map(
                    fn (ExportColumn $column) => static::formatPdfCellValue(data_get($record, $column->getName())),
                    $columns,
                ))->all();

                $html = view('filament.exports.table-pdf', [
                    'title' => static::getPluralModelLabel(),
                    'headings' => $headings,
                    'rows' => $rows,
                ])->render();

                $pdf = Pdf::loadHTML($html)->setPaper('a4', 'landscape');
                $filename = Str::slug(static::getPluralModelLabel()).'-'.now()->format('Y-m-d-His').'.pdf';

                return response()->streamDownload(
                    fn () => print ($pdf->output()),
                    $filename,
                    ['Content-Type' => 'application/pdf'],
                );
            });
    }

    protected static function formatPdfCellValue(mixed $value): string
    {
        if (is_bool($value)) {
            return $value ? 'Yes' : 'No';
        }

        if ($value instanceof CarbonInterface) {
            return $value->format('Y-m-d H:i');
        }

        if ($value === null || $value === '') {
            return '—';
        }

        return (string) $value;
    }
}
