<?php

declare(strict_types=1);

namespace Database\Seeders;

use App\Enums\ListingStatus;
use App\Enums\ListingType;
use App\Enums\UserStatus;
use App\Enums\VerificationStatus;
use App\Models\Domain;
use App\Models\DomainCategory;
use App\Models\Listing;
use App\Models\SellerProfile;
use App\Models\Tld;
use App\Models\User;
use App\Services\Marketplace\ListingCounters;
use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use InvalidArgumentException;

/**
 * Demonstration inventory for a development environment.
 *
 * Deliberately NOT called from DatabaseSeeder. The reference seeders -- TLDs,
 * categories, plans, fees -- are configuration that every environment needs,
 * including production. This is fabricated marketplace inventory, and the one
 * thing worse than an empty marketplace is a live one showing invented prices
 * against names nobody owns. Run it explicitly:
 *
 *     php artisan db:seed --class=DemoListingSeeder
 *
 * It refuses to run in production for the same reason.
 *
 * Everything it writes goes through the real models, so the demo data is
 * subject to every rule genuine data is: names are parsed and split against the
 * TLD table, a listing cannot be published on an unverified domain, and the
 * browse-grid counters are rebuilt from what is actually visible. Data that
 * bypassed those rules would make the seeded marketplace a worse test of the
 * application than no data at all.
 */
class DemoListingSeeder extends Seeder
{
    /**
     * Demo sellers. Two, so that a seller filter and the trust badges on a card
     * have something to distinguish.
     *
     * @var list<array{name: string, email: string, handle: string, display: string, about: string, identity: VerificationStatus, sales: int, rating: float, ratings: int}>
     */
    private const SELLERS = [
        [
            'name' => 'Anisha Shrestha',
            'email' => 'anisha@demo.domainsansar.test',
            'handle' => 'himalaya-names',
            'display' => 'Himalaya Names',
            'about' => 'Curated brandable names for startups, held since 2016.',
            'identity' => VerificationStatus::Verified,
            'sales' => 24,
            'rating' => 4.8,
            'ratings' => 19,
        ],
        [
            'name' => 'Bikash Thapa',
            'email' => 'bikash@demo.domainsansar.test',
            'handle' => 'kathmandu-domains',
            'display' => 'Kathmandu Domains',
            'about' => 'Short, memorable domains. Transfers handled same day.',
            'identity' => VerificationStatus::Unverified,
            'sales' => 3,
            'rating' => 4.2,
            'ratings' => 4,
        ],
    ];

    /**
     * The demo inventory.
     *
     * Prices are in minor units of the base currency. `category` is a slug from
     * DomainCategorySeeder; a slug that does not exist is skipped with a warning
     * rather than silently filed as uncategorised.
     *
     * @var list<array{name: string, seller: int, category: string, type: ListingType, price: ?int, min_offer: ?int, title: ?string, description: ?string, status: ListingStatus, featured: bool}>
     */
    private const LISTINGS = [
        [
            'name' => 'brightledger.com', 'seller' => 0, 'category' => 'finance',
            'type' => ListingType::FixedPrice, 'price' => 1_250_000, 'min_offer' => null,
            'title' => 'BrightLedger', 'description' => 'A clean two-word name for accounting or bookkeeping software. Pronounceable, spells easily over the phone.',
            'status' => ListingStatus::Active, 'featured' => true,
        ],
        [
            'name' => 'nimbusdesk.com', 'seller' => 0, 'category' => 'technology',
            'type' => ListingType::FixedPrice, 'price' => 890_000, 'min_offer' => null,
            'title' => 'NimbusDesk', 'description' => 'Reads as a cloud helpdesk product without being literal about it.',
            'status' => ListingStatus::Active, 'featured' => true,
        ],
        [
            'name' => 'quietfox.io', 'seller' => 0, 'category' => 'technology',
            'type' => ListingType::MakeOffer, 'price' => null, 'min_offer' => 350_000,
            'title' => 'QuietFox', 'description' => 'Short, two syllables, no hyphens. Offers considered from serious buyers.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],
        [
            'name' => 'ledgerloop.com', 'seller' => 0, 'category' => 'finance',
            'type' => ListingType::FixedPrice, 'price' => 640_000, 'min_offer' => null,
            'title' => null, 'description' => 'Alliterative fintech name, unregistered trademark search came back clean.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],
        [
            'name' => 'trailmarker.co', 'seller' => 0, 'category' => 'travel',
            'type' => ListingType::FixedPrice, 'price' => 275_000, 'min_offer' => null,
            'title' => 'TrailMarker', 'description' => 'Outdoor and hiking brand name. Also works for onboarding software.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],
        [
            'name' => 'sherpa.ai', 'seller' => 0, 'category' => 'ai',
            'type' => ListingType::MakeOffer, 'price' => null, 'min_offer' => 2_500_000,
            'title' => 'Sherpa', 'description' => 'One word, high recognition, on the TLD every AI company wants.',
            'status' => ListingStatus::UnderOffer, 'featured' => true,
        ],
        [
            'name' => 'plotwise.io', 'seller' => 1, 'category' => 'ai',
            'type' => ListingType::FixedPrice, 'price' => 410_000, 'min_offer' => null,
            'title' => 'PlotWise', 'description' => 'Analytics or charting product name.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],
        [
            'name' => 'hirelane.com', 'seller' => 1, 'category' => 'business',
            'type' => ListingType::FixedPrice, 'price' => 520_000, 'min_offer' => null,
            'title' => 'HireLane', 'description' => 'Recruitment and applicant tracking. Two syllables, no ambiguity in spelling.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],
        [
            'name' => 'coffee-club.com', 'seller' => 1, 'category' => 'business',
            'type' => ListingType::FixedPrice, 'price' => 95_000, 'min_offer' => null,
            'title' => null, 'description' => 'Hyphenated, priced accordingly. Included so the hyphen filter has something to exclude.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],
        [
            'name' => 'studio24.io', 'seller' => 1, 'category' => 'technology',
            'type' => ListingType::FixedPrice, 'price' => 130_000, 'min_offer' => null,
            'title' => null, 'description' => 'Contains a numeral, which some buyers filter out and others look for.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],
        [
            'name' => 'verdantly.com', 'seller' => 1, 'category' => 'healthcare',
            'type' => ListingType::MakeOffer, 'price' => null, 'min_offer' => 180_000,
            'title' => 'Verdantly', 'description' => 'Wellness or sustainability brand. Adverbial form reads as a product name.',
            'status' => ListingStatus::Active, 'featured' => false,
        ],

        /*
         * Deliberate negative cases. A demo marketplace where every row is
         * visible and for sale tests nothing: these exist so the browse query,
         * the counters and the public resource are exercised against rows that
         * must NOT appear.
         */
        [
            'name' => 'soldalready.com', 'seller' => 0, 'category' => 'business',
            'type' => ListingType::FixedPrice, 'price' => 300_000, 'min_offer' => null,
            'title' => null, 'description' => 'Completed sale. Must not appear in search results.',
            'status' => ListingStatus::Sold, 'featured' => false,
        ],
        [
            'name' => 'draftonly.com', 'seller' => 0, 'category' => 'business',
            'type' => ListingType::FixedPrice, 'price' => 200_000, 'min_offer' => null,
            'title' => null, 'description' => 'Never published. Must not appear in search results.',
            'status' => ListingStatus::Draft, 'featured' => false,
        ],
        [
            'name' => 'withdrawnname.com', 'seller' => 1, 'category' => 'business',
            'type' => ListingType::FixedPrice, 'price' => 150_000, 'min_offer' => null,
            'title' => null, 'description' => 'Taken off the market by the seller. Must not appear in search results.',
            'status' => ListingStatus::Withdrawn, 'featured' => false,
        ],
    ];

    public function run(): void
    {
        if (app()->isProduction()) {
            $this->command?->error('DemoListingSeeder refuses to run in production.');

            return;
        }

        /*
         * The reference data has to exist first: a domain name cannot be parsed
         * without the TLD it sits on, and every demo listing names a category
         * slug the category seeder creates.
         */
        if (Tld::query()->count() === 0) {
            $this->call(TldSeeder::class);
        }

        if (DomainCategory::query()->count() === 0) {
            $this->call(DomainCategorySeeder::class);
        }

        $sellers = $this->seedSellers();
        $categories = DomainCategory::query()->pluck('id', 'slug');

        $created = 0;
        $skipped = [];

        foreach (self::LISTINGS as $spec) {
            $seller = $sellers[$spec['seller']];

            $categoryId = $categories[$spec['category']] ?? null;

            if ($categoryId === null) {
                $skipped[] = sprintf('%s (no category [%s])', $spec['name'], $spec['category']);

                continue;
            }

            $domain = $this->seedDomain($spec['name'], $seller);

            if ($domain === null) {
                $skipped[] = sprintf('%s (TLD not in the catalogue)', $spec['name']);

                continue;
            }

            $this->seedListing($spec, $domain, $seller, (int) $categoryId);
            $created++;
        }

        /*
         * Rebuilt once at the end. Every listing write already recounts through
         * the observer, so this is belt-and-braces -- but it is also the call a
         * developer wants after editing this seeder, and having it here means the
         * counters are right even if a future bulk path skips model events.
         */
        app(ListingCounters::class)->resyncAll();

        $this->command?->info(sprintf(
            'Seeded %d demo listings across %d sellers.',
            $created,
            count($sellers),
        ));

        foreach ($skipped as $skip) {
            $this->command?->warn('Skipped '.$skip);
        }

        $this->command?->line('  Sign in as any demo seller with: '.self::demoPassword());
    }

    /**
     * The password every demo account shares.
     *
     * Fixed and printed rather than random, because a demo account nobody can
     * sign in to is half a demo. It satisfies the application's own password
     * rules so these accounts are not a special case.
     */
    public static function demoPassword(): string
    {
        return 'demo-password-2026';
    }

    /**
     * @return list<SellerProfile>
     */
    private function seedSellers(): array
    {
        $profiles = [];

        foreach (self::SELLERS as $spec) {
            $user = User::query()->firstOrCreate(
                ['email' => $spec['email']],
                [
                    'name' => $spec['name'],

                    // The model casts `password` as hashed, so the plain value
                    // is passed here -- hashing it first would hash it twice.
                    'password' => self::demoPassword(),
                    'status' => UserStatus::Active,

                    /*
                     * Verified, because an unverified account cannot transact --
                     * and a demo seller who cannot publish a listing would make
                     * this seeder produce an empty marketplace.
                     */
                    'email_verified_at' => Carbon::now()->subMonths(6),
                ],
            );

            $profile = SellerProfile::query()->firstOrCreate(
                ['user_id' => $user->getKey()],
                [
                    'handle' => $spec['handle'],
                    'display_name' => $spec['display'],
                    'about' => $spec['about'],
                    'identity_status' => $spec['identity'],
                    'payment_status' => VerificationStatus::Verified,
                    'completed_sales_count' => $spec['sales'],
                    'rating_average' => $spec['rating'],
                    'rating_count' => $spec['ratings'],
                    'onboarded_at' => Carbon::now()->subMonths(6),
                ],
            );

            $profiles[] = $profile;
        }

        return $profiles;
    }

    /**
     * Creates the inventory row, verified, so it is genuinely listable.
     *
     * Verification is set through the model's own markVerified() rather than by
     * writing the columns, so the demo data carries a real verified_at and a
     * transfer lock derived from the TLD -- the same values a seller who proved
     * ownership this morning would have.
     */
    private function seedDomain(string $name, SellerProfile $seller): ?Domain
    {
        $domain = Domain::query()
            ->with('tld')
            ->where('user_id', $seller->user_id)
            ->where('name', $name)
            ->first();

        if ($domain === null) {
            $domain = new Domain(['name' => $name]);
            $domain->user_id = $seller->user_id;

            try {
                // Populates name, label, length, character flags and tld_id.
                $domain->save();
            } catch (InvalidArgumentException) {
                return null;
            }
        }

        /*
         * Re-applied on an existing row rather than skipped. Editing this seeder
         * and re-running it should change the demo data; a find-and-return would
         * silently keep whatever the first run produced and make the seeder look
         * broken.
         */
        $domain->load('tld');
        $domain->markVerified();
        $domain->registrar = 'Namecheap';
        $domain->expires_at = Carbon::now()->addMonths(14);

        /*
         * Back-date the proof, spread by the name so a reseed is stable.
         *
         * Verifying everything in one run gives every domain the same
         * verified_at and the same transfer lock, which puts an identical
         * "transfer from <date>" line on every card -- and leaves no domain past
         * its lock, so the case where the notice should NOT appear is never
         * exercised. Spreading it means some names are clear to transfer today.
         */
        $age = Str::length($name) * 13 % 120;
        $domain->verified_at = Carbon::now()->subDays($age);
        $domain->transfer_lock_until = $domain->tld->supports_transfer_lock
            ? Carbon::now()->subDays($age)->addDays((int) $domain->tld->transfer_lock_days)
            : null;

        $domain->save();

        return $domain;
    }

    /**
     * @param  array{name: string, seller: int, category: string, type: ListingType, price: ?int, min_offer: ?int, title: ?string, description: ?string, status: ListingStatus, featured: bool}  $spec
     */
    private function seedListing(array $spec, Domain $domain, SellerProfile $seller, int $categoryId): void
    {
        $listing = Listing::query()->where('domain_id', $domain->getKey())->first() ?? new Listing;

        $listing->domain_id = $domain->getKey();
        $listing->user_id = $seller->user_id;
        $listing->seller_profile_id = $seller->getKey();
        $listing->domain_category_id = $categoryId;
        $listing->type = $spec['type'];
        $listing->title = $spec['title'];
        $listing->description = $spec['description'];
        $listing->asking_price_minor = $spec['price'];
        $listing->min_offer_minor = $spec['min_offer'];
        $listing->currency = 'USD';
        $listing->is_featured = $spec['featured'];
        $listing->featured_until = $spec['featured'] ? Carbon::now()->addDays(30) : null;

        /*
         * Spread over the last few months so "recently listed" has a real
         * ordering to show rather than a dozen identical timestamps.
         */
        $publishedAt = Carbon::now()->subDays(Str::length($spec['name']) * 3 % 90 + 1);

        $listing->status = $spec['status'];

        // A draft has never been published, and giving it a published_at would
        // make the browse query's two conditions disagree.
        $listing->published_at = $spec['status'] === ListingStatus::Draft ? null : $publishedAt;
        $listing->sold_at = $spec['status'] === ListingStatus::Sold ? $publishedAt->copy()->addDays(20) : null;

        // Plausible engagement, derived from the name so a reseed is stable.
        $listing->views_count = Str::length($spec['name']) * 17;
        $listing->offers_count = $spec['status'] === ListingStatus::UnderOffer ? 2 : 0;
        $listing->watchers_count = Str::length($spec['name']) % 7;

        $listing->save();
    }
}
