<?php

declare(strict_types=1);

namespace Database\Seeders;

use App\Enums\Gender;
use App\Enums\HouseholdRole;
use App\Enums\MembershipStatus;
use App\Models\Branch;
use App\Models\Group;
use App\Models\Member;
use App\Models\Region;
use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * Makes the 300k performance targets in docs/05-performance-and-scale.md §10
 * measurable. Never touches Eloquent per-row for the members table (events +
 * individual INSERTs would take hours) — everything is plain arrays and
 * chunked query-builder bulk operations.
 *
 * Run manually: php artisan db:seed --class=BigSeeder
 */
class BigSeeder extends Seeder
{
    private const TOTAL_MEMBERS = 300_000;

    private const TOTAL_REGIONS = 10;

    private const TOTAL_BRANCHES = 150;

    private const MEMBER_CHUNK = 1000;

    private const TARGET_HOUSEHOLDS = 30_000;

    private const TARGET_GROUPS = 500;

    private const AUDIT_SAMPLE_TOTAL = 10_000;

    /** @var list<string> */
    private array $surnames;

    /** @var list<string> */
    private array $maleFirstNames;

    /** @var list<string> */
    private array $femaleFirstNames;

    /** @var list<string> */
    private array $cities;

    /** @var list<string> */
    private array $states;

    private int $phoneCounter = 2_000_000_000;

    private Carbon $now;

    public function run(): void
    {
        $existing = DB::table('members')->count();

        if ($existing > 10_000 && env('BIG_SEED_FORCE') !== '1') {
            $this->command?->error(
                "members already has {$existing} rows. Refusing to run BigSeeder ".
                '(set BIG_SEED_FORCE=1 to override).'
            );

            return;
        }

        $this->now = now();
        $this->buildNamePools();

        $startedAt = microtime(true);

        $branches = $this->ensureRegionsAndBranches();
        $this->command?->info('Regions/branches ready: '.$branches->count().' branches.');

        $branchRanges = $this->seedMembers($branches);
        $this->command?->info('Members seeded.');

        $this->seedHouseholds($branchRanges);
        $this->command?->info('Households seeded.');

        $this->seedGroups($branches, $branchRanges);
        $this->command?->info('Groups seeded.');

        $elapsed = round(microtime(true) - $startedAt, 1);
        $this->command?->info("BigSeeder completed in {$elapsed}s.");
    }

    private function buildNamePools(): void
    {
        $faker = fake();

        $this->surnames = collect(range(1, 2000))->map(fn () => $faker->lastName())->all();
        $this->maleFirstNames = collect(range(1, 300))->map(fn () => $faker->firstNameMale())->all();
        $this->femaleFirstNames = collect(range(1, 300))->map(fn () => $faker->firstNameFemale())->all();
        $this->cities = collect(range(1, 200))->map(fn () => $faker->city())->all();
        $this->states = collect(range(1, 50))->map(fn () => $faker->state())->all();
    }

    /**
     * @return Collection<int, Branch>
     */
    private function ensureRegionsAndBranches(): Collection
    {
        $regions = Region::query()->get();

        if ($regions->count() < self::TOTAL_REGIONS) {
            $regions = $regions->merge(
                Region::factory()->count(self::TOTAL_REGIONS - $regions->count())->create()
            );
        }
        $regions = $regions->take(self::TOTAL_REGIONS)->values();

        $branches = Branch::query()->get();

        if ($branches->count() < self::TOTAL_BRANCHES) {
            $needed = self::TOTAL_BRANCHES - $branches->count();

            for ($i = 0; $i < $needed; $i++) {
                $region = $regions[$i % $regions->count()];
                $branches->push(Branch::factory()->create(['region_id' => $region->id]));
            }
        }

        return $branches->take(self::TOTAL_BRANCHES)->values();
    }

    /**
     * @param  Collection<int, Branch>  $branches
     * @return array<int, array{first: int, last: int}>
     */
    private function seedMembers(Collection $branches): array
    {
        $perBranch = intdiv(self::TOTAL_MEMBERS, $branches->count());
        $ranges = [];
        $chunksSoFar = 0;
        $auditPerChunk = (int) round(self::AUDIT_SAMPLE_TOTAL / (self::TOTAL_MEMBERS / self::MEMBER_CHUNK));

        foreach ($branches as $branch) {
            $branchFirstId = null;
            $branchLastId = null;

            for ($offset = 0; $offset < $perBranch; $offset += self::MEMBER_CHUNK) {
                $size = min(self::MEMBER_CHUNK, $perBranch - $offset);
                $rows = [];
                $statuses = [];

                for ($i = 0; $i < $size; $i++) {
                    [$row, $status] = $this->buildMemberRow($branch->id);
                    $rows[] = $row;
                    $statuses[] = $status;
                }

                DB::table('members')->insert($rows);
                $firstId = (int) DB::getPdo()->lastInsertId();
                $lastId = $firstId + $size - 1;

                $branchFirstId ??= $firstId;
                $branchLastId = $lastId;

                $this->insertStatusHistories($firstId, $lastId, $statuses);
                $this->insertAuditSample($firstId, $lastId, $auditPerChunk);

                $chunksSoFar++;
                if ($chunksSoFar % 10 === 0) {
                    $this->command?->info("  ...{$chunksSoFar} member chunks inserted");
                }
            }

            DB::table('members')
                ->whereBetween('id', [$branchFirstId, $branchLastId])
                ->whereNull('member_number')
                ->update(['member_number' => DB::raw("CONCAT('M', LPAD(id, 8, '0'))")]);

            $ranges[$branch->id] = ['first' => $branchFirstId, 'last' => $branchLastId];
        }

        return $ranges;
    }

    /**
     * @return array{0: array<string, mixed>, 1: MembershipStatus}
     */
    private function buildMemberRow(int $branchId): array
    {
        $gender = fake()->boolean() ? Gender::Male : Gender::Female;
        $firstName = $gender === Gender::Male
            ? $this->maleFirstNames[array_rand($this->maleFirstNames)]
            : $this->femaleFirstNames[array_rand($this->femaleFirstNames)];
        $lastName = $this->surnames[array_rand($this->surnames)];
        $status = $this->weightedMembershipStatus();
        $dob = Carbon::createFromTimestamp(random_int(
            Carbon::create(1935, 1, 1)->timestamp,
            Carbon::create(2015, 12, 31)->timestamp,
        ))->toDateString();
        $dateJoined = Carbon::createFromTimestamp(random_int(
            $this->now->clone()->subYears(20)->timestamp,
            $this->now->timestamp,
        ))->toDateString();

        $row = [
            'member_number' => null,
            'branch_id' => $branchId,
            'household_id' => null,
            'household_role' => null,
            'title' => null,
            'first_name' => $firstName,
            'middle_name' => null,
            'last_name' => $lastName,
            'preferred_name' => null,
            'gender' => $gender->value,
            'date_of_birth' => $dob,
            'marital_status' => null,
            'email' => null,
            'phone_primary' => (string) $this->phoneCounter++,
            'phone_secondary' => null,
            'address_line1' => null,
            'address_line2' => null,
            'city' => $this->cities[array_rand($this->cities)],
            'state_province' => $this->states[array_rand($this->states)],
            'postal_code' => null,
            'country' => 'US',
            'photo_path' => null,
            'membership_status' => $status->value,
            'date_joined' => $dateJoined,
            'date_baptized' => null,
            'baptism_type' => null,
            'occupation' => null,
            'employer' => null,
            'emergency_contact_name' => null,
            'emergency_contact_phone' => null,
            'emergency_contact_relationship' => null,
            'custom_fields' => null,
            'created_by' => null,
            'updated_by' => null,
            'created_at' => $this->now,
            'updated_at' => $this->now,
            'deleted_at' => null,
        ];

        return [$row, $status];
    }

    private function weightedMembershipStatus(): MembershipStatus
    {
        $roll = random_int(1, 100);

        return match (true) {
            $roll <= 80 => MembershipStatus::Member,
            $roll <= 88 => MembershipStatus::Visitor,
            $roll <= 93 => MembershipStatus::NewConvert,
            $roll <= 97 => MembershipStatus::Inactive,
            $roll <= 99 => MembershipStatus::TransferredOut,
            default => MembershipStatus::Deceased,
        };
    }

    /**
     * @param  list<MembershipStatus>  $statuses
     */
    private function insertStatusHistories(int $firstId, int $lastId, array $statuses): void
    {
        $rows = [];

        for ($id = $firstId; $id <= $lastId; $id++) {
            $rows[] = [
                'member_id' => $id,
                'from_status' => null,
                'to_status' => $statuses[$id - $firstId]->value,
                'reason' => null,
                'effective_date' => $this->now->toDateString(),
                'changed_by' => null,
                'created_at' => $this->now,
            ];
        }

        DB::table('member_status_histories')->insert($rows);
    }

    private function insertAuditSample(int $firstId, int $lastId, int $sampleSize): void
    {
        $ids = range($firstId, $lastId);
        shuffle($ids);
        $sample = array_slice($ids, 0, min($sampleSize, count($ids)));

        if ($sample === []) {
            return;
        }

        $rows = array_map(fn (int $id) => [
            'user_id' => null,
            'action' => 'imported',
            'auditable_type' => Member::class,
            'auditable_id' => $id,
            'old_values' => null,
            'new_values' => null,
            'ip_address' => null,
            'created_at' => $this->now,
        ], $sample);

        DB::table('audit_logs')->insert($rows);
    }

    /**
     * @param  array<int, array{first: int, last: int}>  $branchRanges
     */
    private function seedHouseholds(array $branchRanges): void
    {
        $householdsPerBranch = intdiv(self::TARGET_HOUSEHOLDS, count($branchRanges));

        foreach ($branchRanges as $branchId => $range) {
            $ids = range($range['first'], $range['last']);
            shuffle($ids);
            $cursor = 0;

            $householdRows = [];
            $groups = [];

            for ($i = 0; $i < $householdsPerBranch; $i++) {
                $size = random_int(2, 6);
                $group = array_slice($ids, $cursor, $size);
                $cursor += $size;

                if (count($group) < 2) {
                    break;
                }

                $groups[] = $group;
                $householdRows[] = [
                    'branch_id' => $branchId,
                    'head_member_id' => $group[0],
                    'name' => $this->surnames[array_rand($this->surnames)].' Family',
                    'address_line1' => null,
                    'address_line2' => null,
                    'city' => $this->cities[array_rand($this->cities)],
                    'state_province' => $this->states[array_rand($this->states)],
                    'postal_code' => null,
                    'country' => 'US',
                    'deleted_at' => null,
                    'created_at' => $this->now,
                    'updated_at' => $this->now,
                ];
            }

            if ($householdRows === []) {
                continue;
            }

            DB::table('households')->insert($householdRows);
            $firstHouseholdId = (int) DB::getPdo()->lastInsertId();

            $this->applyHouseholdAssignments($firstHouseholdId, $groups);
        }
    }

    /**
     * @param  list<list<int>>  $groups
     */
    private function applyHouseholdAssignments(int $firstHouseholdId, array $groups): void
    {
        $roles = [HouseholdRole::Spouse, HouseholdRole::Child, HouseholdRole::Other];

        $householdIdCase = 'CASE id ';
        $roleCase = 'CASE id ';
        $allIds = [];

        foreach ($groups as $index => $memberIds) {
            $householdId = $firstHouseholdId + $index;

            foreach ($memberIds as $position => $memberId) {
                $role = $position === 0 ? HouseholdRole::Head : $roles[array_rand($roles)];
                $householdIdCase .= "WHEN {$memberId} THEN {$householdId} ";
                $roleCase .= "WHEN {$memberId} THEN '{$role->value}' ";
                $allIds[] = $memberId;
            }
        }

        $householdIdCase .= 'END';
        $roleCase .= 'END';

        $idList = implode(',', $allIds);

        DB::statement(
            "UPDATE members SET household_id = {$householdIdCase}, household_role = {$roleCase} WHERE id IN ({$idList})"
        );
    }

    /**
     * @param  Collection<int, Branch>  $branches
     * @param  array<int, array{first: int, last: int}>  $branchRanges
     */
    private function seedGroups(Collection $branches, array $branchRanges): void
    {
        $orgWideCount = 20;
        $perBranchCount = intdiv(self::TARGET_GROUPS - $orgWideCount, $branches->count());

        Group::factory()->organizationWide()->count($orgWideCount)->create()->each(
            fn (Group $group) => $this->attachRandomMembers($group, $branchRanges, sampleAcrossAllBranches: true)
        );

        if ($perBranchCount > 0) {
            foreach ($branches as $branch) {
                Group::factory()->count($perBranchCount)->create(['branch_id' => $branch->id])->each(
                    fn (Group $group) => $this->attachRandomMembers($group, $branchRanges, branchId: $branch->id)
                );
            }
        }
    }

    /**
     * @param  array<int, array{first: int, last: int}>  $branchRanges
     */
    private function attachRandomMembers(
        Group $group,
        array $branchRanges,
        bool $sampleAcrossAllBranches = false,
        ?int $branchId = null,
    ): void {
        $size = random_int(10, 50);

        if ($sampleAcrossAllBranches) {
            $branchIds = array_keys($branchRanges);
            $memberIds = collect(range(1, $size))->map(function () use ($branchRanges, $branchIds) {
                $range = $branchRanges[$branchIds[array_rand($branchIds)]];

                return random_int($range['first'], $range['last']);
            })->unique()->values();
        } else {
            $range = $branchRanges[$branchId];
            $memberIds = collect(range(1, $size))
                ->map(fn () => random_int($range['first'], $range['last']))
                ->unique()
                ->values();
        }

        $now = $this->now;
        $rows = $memberIds->map(fn (int $memberId, int $index) => [
            'group_id' => $group->id,
            'member_id' => $memberId,
            'role_in_group' => $index === 0 ? 'leader' : 'member',
            'joined_at' => $now->toDateString(),
            'created_at' => $now,
            'updated_at' => $now,
        ])->all();

        DB::table('group_member')->insert($rows);
    }
}
