<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('listings', function (Blueprint $table) {
            $table->id();

            $table->char('public_id', 26)->unique();

            $table->foreignId('domain_id')->constrained()->cascadeOnDelete();

            /*
             * Both the owning account and the seller profile are stored, and the
             * redundancy is deliberate.
             *
             * `user_id` answers authorization without a relation load, which
             * OwnedByUser requires because ownership is checked on every
             * request. `seller_profile_id` is what the public listing card reads
             * -- handle, rating, badges -- and having it here keeps that query
             * one join rather than two. Both are set together and never
             * independently.
             */
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->foreignId('seller_profile_id')->constrained()->cascadeOnDelete();

            /*
             * Nulled rather than cascaded: retiring a category must not delete
             * the listings filed under it, it must leave them uncategorised for
             * an administrator to refile.
             */
            $table->foreignId('domain_category_id')->nullable()
                ->constrained('domain_categories')->nullOnDelete();

            $table->enum('type', ['fixed_price', 'make_offer', 'auction'])->default('fixed_price');
            $table->enum('status', [
                'draft', 'active', 'under_offer', 'sold', 'expired', 'withdrawn', 'suspended',
            ])->default('draft');

            /*
             * Holds `domain_id` while this listing is open and NULL once it
             * closes, so the unique index permits a domain to be listed,
             * withdrawn and relisted any number of times while never having two
             * open listings at once. Same NULL-tolerant unique-index technique
             * as domains.verified_name.
             */
            $table->unsignedBigInteger('open_domain_id')->nullable()->unique();

            // Optional marketing headline. Falls back to the domain name.
            $table->string('title')->nullable();
            $table->text('description')->nullable();

            /*
             * Minor units, and nullable because not every listing type carries a
             * price: a make-offer listing deliberately publishes none, and
             * inventing a placeholder would misprice it in every filter.
             */
            $table->unsignedBigInteger('asking_price_minor')->nullable();

            /*
             * The floor below which an offer is refused outright. Kept private
             * to the seller -- publishing it would simply become the price
             * everybody offers.
             */
            $table->unsignedBigInteger('min_offer_minor')->nullable();

            $table->char('currency', 3)->default('USD');

            $table->boolean('is_featured')->default(false);

            /*
             * Featured placement is bought or granted by plan entitlement, so it
             * expires. Stored rather than derived from the subscription, because
             * a credit already spent is not refunded when a plan changes.
             */
            $table->timestamp('featured_until')->nullable();

            /*
             * Counters, denormalised because every one of them is read on a
             * browse grid and none is worth an aggregate per card. They are
             * maintained by the application, not trusted as the source of truth
             * for anything financial.
             */
            $table->unsignedInteger('views_count')->default(0);
            $table->unsignedInteger('offers_count')->default(0);
            $table->unsignedInteger('watchers_count')->default(0);

            $table->timestamp('published_at')->nullable();
            $table->timestamp('expires_at')->nullable();
            $table->timestamp('sold_at')->nullable();

            /*
             * Staff enforcement, held separately from the seller's own
             * withdrawal so that a suspension cannot be made to look like the
             * seller's decision. Never exposed on a public resource.
             */
            $table->timestamp('suspended_at')->nullable();
            $table->string('suspension_reason')->nullable();

            $table->json('metadata')->nullable();

            $table->timestamps();
            $table->softDeletes(precision: 6);

            /*
             * The public browse query: visible listings, newest published
             * first. Featured placement sorts ahead of it, hence the second
             * index rather than one composite that serves neither well.
             */
            $table->index(['status', 'published_at']);
            $table->index(['status', 'is_featured', 'published_at']);

            // Category and price filters, both applied on top of status.
            $table->index(['domain_category_id', 'status']);
            $table->index(['status', 'asking_price_minor']);

            // The seller's own listing management view.
            $table->index(['seller_profile_id', 'status']);

            // The scheduled sweep that expires listings past their term.
            $table->index(['status', 'expires_at']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('listings');
    }
};
