<?php

namespace App\Livewire\Concerns;

use App\Services\ActivityLogger;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Throwable;

/**
 * The shared list-plus-modal-form behaviour behind every maintenance screen in
 * the panel (products, plans, payment accounts, announcements, staff).
 *
 * Written once so those screens all validate, authorise, log and report errors
 * identically — and so a fix to any of that lands everywhere at once.
 *
 * A component using this trait declares:
 *   modelClass()      the Eloquent model
 *   formFields()      default values for the form
 *   formRules()       validation rules keyed by form field
 *   resourceLabel()   singular human label, e.g. "Plan"
 *
 * and optionally overrides beforeSave(), afterSave() and cannotDeleteReason().
 */
trait ManagesResource
{
    use InteractsWithToasts, WithDataTable;

    /** @var array<string,mixed> */
    public array $form = [];

    public ?int $editingId = null;

    public bool $showForm = false;

    public ?int $deletingId = null;

    /** @return class-string<Model> */
    abstract protected function modelClass(): string;

    /** @return array<string,mixed> */
    abstract protected function formFields(): array;

    /** @return array<string,mixed> */
    abstract protected function formRules(): array;

    abstract protected function resourceLabel(): string;

    /**
     * Whether the signed-in staff member may create / edit / delete here.
     *
     * The panel uses a small role ladder rather than a permission matrix, so
     * this is a plain predicate on the User model.
     */
    public function canManage(): bool
    {
        return (bool) auth()->user()?->canManageCatalog();
    }

    /*
    |--------------------------------------------------------------------------
    | Form lifecycle
    |--------------------------------------------------------------------------
    */

    public function createResource(): void
    {
        $this->authorizeManage();

        $this->resetValidation();
        $this->editingId = null;
        $this->form = $this->formFields();
        $this->showForm = true;
    }

    public function editResource(int $id): void
    {
        $this->authorizeManage();

        $record = $this->modelClass()::findOrFail($id);

        $this->resetValidation();
        $this->editingId = $record->getKey();
        $this->form = $this->fillFormFrom($record);
        $this->showForm = true;
    }

    /** Map a record onto the form array. Overridable for computed fields. */
    protected function fillFormFrom(Model $record): array
    {
        $form = [];

        foreach ($this->formFields() as $field => $default) {
            $value = $record->{$field} ?? null;

            if ($value instanceof \BackedEnum) {
                $value = $value->value;
            }

            if ($value instanceof \DateTimeInterface) {
                $value = $value->format(is_string($default) && strlen($default) === 10 ? 'Y-m-d' : 'Y-m-d\TH:i');
            }

            $form[$field] = $value ?? (is_array($default) ? [] : $value);
        }

        return $form;
    }

    public function saveResource(): void
    {
        $this->authorizeManage();

        $rules = [];

        foreach ($this->formRules() as $field => $rule) {
            $rules['form.'.$field] = $this->resolveUniqueRule($rule);
        }

        $this->validate($rules, [], $this->validationAttributes());

        try {
            $data = $this->beforeSave($this->form);

            if ($this->editingId) {
                $record = $this->modelClass()::findOrFail($this->editingId);
                $record->fill($data)->save();
                $verb = 'updated';
            } else {
                $record = $this->modelClass()::create($data);
                $verb = 'created';
            }

            $this->afterSave($record, (bool) $this->editingId);

            app(ActivityLogger::class)->log(
                Str::snake(class_basename($this->modelClass())).'.'.$verb,
                ucfirst($verb).' '.strtolower($this->resourceLabel()).': '.$this->recordTitle($record),
                $record,
            );

            $this->showForm = false;
            $this->editingId = null;

            $this->toastSuccess($this->resourceLabel().' '.$verb.'.');
        } catch (ValidationException $exception) {
            // A hook rejected the input — let it paint the field, not a toast.
            throw $exception;
        } catch (Throwable $exception) {
            report($exception);

            $this->toastError($exception->getMessage());
        }
    }

    public function confirmDelete(int $id): void
    {
        $this->authorizeManage();
        $this->deletingId = $id;
    }

    public function cancelDelete(): void
    {
        $this->deletingId = null;
    }

    public function deleteResource(): void
    {
        $this->authorizeManage();

        if (! $this->deletingId) {
            return;
        }

        try {
            $record = $this->modelClass()::findOrFail($this->deletingId);

            if ($reason = $this->cannotDeleteReason($record)) {
                $this->toastError($reason, 'Cannot delete');
                $this->deletingId = null;

                return;
            }

            $title = $this->recordTitle($record);

            app(ActivityLogger::class)->log(
                Str::snake(class_basename($this->modelClass())).'.deleted',
                'Deleted '.strtolower($this->resourceLabel()).': '.$title,
                $record,
            );

            $record->delete();

            $this->deletingId = null;
            $this->toastSuccess($this->resourceLabel().' deleted.');
        } catch (Throwable $exception) {
            report($exception);

            $this->deletingId = null;
            $this->toastError($exception->getMessage());
        }
    }

    public function closeForm(): void
    {
        $this->showForm = false;
        $this->editingId = null;
        $this->resetValidation();
    }

    /*
    |--------------------------------------------------------------------------
    | Hooks
    |--------------------------------------------------------------------------
    */

    /** Transform form input before it reaches the model. */
    protected function beforeSave(array $data): array
    {
        return $data;
    }

    protected function afterSave(Model $record, bool $wasUpdate): void
    {
        //
    }

    /**
     * Return a sentence when a record must not be deleted (usually because
     * money or a live tenant depends on it). Returning null allows the delete.
     */
    protected function cannotDeleteReason(Model $record): ?string
    {
        return null;
    }

    protected function recordTitle(Model $record): string
    {
        return (string) ($record->name ?? $record->title ?? $record->getKey());
    }

    /** Apply screen-specific filters to the listing query. */
    protected function applyFilters(Builder $query): Builder
    {
        return $query;
    }

    protected function baseQuery(): Builder
    {
        return $this->modelClass()::query();
    }

    public function rows()
    {
        return $this->applyDataTable($this->applyFilters($this->baseQuery()))
            ->paginate($this->perPage);
    }

    /*
    |--------------------------------------------------------------------------
    | Internals
    |--------------------------------------------------------------------------
    */

    /**
     * Let rules say `unique:table,column` and have "ignore the record being
     * edited" filled in automatically — the single most common source of
     * copy-paste bugs in CRUD screens.
     */
    protected function resolveUniqueRule(mixed $rule): mixed
    {
        if (! is_string($rule) || ! str_contains($rule, 'unique:')) {
            return $rule;
        }

        if (! $this->editingId) {
            return $rule;
        }

        return preg_replace(
            '/unique:([a-z_]+),([a-z_]+)(?![,a-z_])/',
            'unique:$1,$2,'.$this->editingId.',id',
            $rule,
        );
    }

    /** Friendly field names in validation messages. */
    protected function validationAttributes(): array
    {
        $attributes = [];

        foreach (array_keys($this->formFields()) as $field) {
            $attributes['form.'.$field] = Str::of($field)->replace('_id', '')->replace('_', ' ')->lower()->toString();
        }

        return $attributes;
    }

    protected function authorizeManage(): void
    {
        abort_unless($this->canManage(), 403);
    }
}
