<?php

namespace App\Services;

use App\Models\MeetupBooth;
use App\Models\MeetupBoothVote;
use App\Models\MeetupEvent;
use App\Models\MeetupParticipant;
use App\Models\MeetupRole;
use App\Models\MeetupSession;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;

class MeetupService
{
    public const MAX_VOTES_PER_PARTICIPANT = 3;

    public function slugify(string $email): string
    {
        return strtolower(preg_replace('/[^a-z0-9]/', '_', trim(strtolower($email))));
    }

    public function digits(?string $value): string
    {
        return preg_replace('/\D/', '', (string) $value) ?? '';
    }

    public function listEvents(): Collection
    {
        return MeetupEvent::query()->orderBy('name')->get();
    }

    public function listEventsForApi(bool $publicOnly = false): array
    {
        $query = MeetupEvent::query()->orderBy('name');

        if ($publicOnly) {
            $query->where(function ($q) {
                $q->whereNull('ends_at')
                    ->orWhere('ends_at', '>=', now());
            });
        }

        return $query->get()
            ->map(fn (MeetupEvent $event) => $this->eventPublicPayload($event))
            ->values()
            ->all();
    }

    public function findEventByOrganiserLogin(string $email, string $password): ?MeetupEvent
    {
        $result = app(OrganiserAuthService::class)->authenticate($email, $password);

        if ($result['type'] === 'failed' || $result['events']->isEmpty()) {
            return null;
        }

        if ($result['events']->count() === 1) {
            return $result['events']->first();
        }

        return null;
    }

    public function authenticateOrganiser(string $email, string $password): array
    {
        return app(OrganiserAuthService::class)->authenticate($email, $password);
    }

    public function eventPublicPayload(MeetupEvent $event): array
    {
        return [
            'id' => $event->id,
            'name' => $event->name,
            'eventName' => $event->fullName(),
            'shortName' => $event->shortName(),
            'venue' => $event->venue ?? '',
            'dates' => $event->dates ?? '',
            'isVirtual' => (bool) $event->is_virtual,
            'startsAt' => $event->starts_at?->toIso8601String(),
            'endsAt' => $event->ends_at?->toIso8601String(),
            'availability' => $this->eventAvailability($event),
            'durationLabel' => $this->eventDurationLabel($event),
            'bannerUrl' => $event->bannerUrl(),
        ];
    }

    public function eventAvailability(MeetupEvent $event): string
    {
        $now = now();

        if ($event->starts_at && $now->lt($event->starts_at)) {
            return 'upcoming';
        }

        if ($event->ends_at && $now->gt($event->ends_at)) {
            return 'ended';
        }

        if ($event->starts_at || $event->ends_at) {
            return 'live';
        }

        return 'open';
    }

    public function eventDurationLabel(MeetupEvent $event): string
    {
        if ($event->starts_at && $event->ends_at) {
            return $event->starts_at->format('d M Y, H:i').' – '.$event->ends_at->format('d M Y, H:i');
        }

        if ($event->starts_at) {
            return 'From '.$event->starts_at->format('d M Y, H:i');
        }

        if ($event->ends_at) {
            return 'Until '.$event->ends_at->format('d M Y, H:i');
        }

        return $event->dates ?: 'Always available';
    }

    public function findAccount(string $email, ?string $icPassword): array
    {
        $email = strtolower(trim($email));
        $slug = $this->slugify($email);
        $matches = [];

        $participants = MeetupParticipant::query()
            ->where(function ($q) use ($slug, $email) {
                $q->where('slug', $slug)
                    ->orWhereRaw('LOWER(email) = ?', [$email]);
            })
            ->with('event')
            ->get();

        foreach ($participants as $p) {
            $event = $p->event;
            $matches[] = [
                'eventId' => $p->event_id,
                'eventName' => $event?->fullName() ?? $event?->name ?? 'Event',
                'shortName' => $event?->shortName() ?? $event?->name ?? '',
                'name' => $p->name,
                'org' => $p->org ?? '',
                'ic' => $p->ic ?? '',
                'phone' => $p->phone ?? '',
                'role' => strtoupper($p->role ?? 'VISITOR'),
                'is_committee' => (bool) $p->is_committee,
            ];
        }

        if (empty($matches)) {
            return ['ok' => false, 'error' => 'No account found. Please contact the organiser.'];
        }

        $icInput = $this->digits($icPassword);
        if (strlen($icInput) < 6) {
            return ['ok' => false, 'error' => 'Enter your IC number to sign in.'];
        }

        $knownIcs = collect($matches)
            ->pluck('ic')
            ->map(fn ($ic) => $this->digits($ic))
            ->filter()
            ->unique()
            ->values();

        if ($knownIcs->isEmpty()) {
            return ['ok' => false, 'error' => 'Your account has no IC on record. Please contact the organiser.'];
        }

        if (! $knownIcs->contains(fn ($stored) => $this->icMatches($stored, $icInput))) {
            return ['ok' => false, 'error' => 'Invalid email or IC number.'];
        }

        return [
            'ok' => true,
            'email' => $email,
            'slug' => $participants->first()->slug,
            'name' => $matches[0]['name'],
            'ic' => $matches[0]['ic'],
            'matches' => $matches,
        ];
    }

    /**
     * Roles allowed to use MeXCo Attend (committee check-in).
     *
     * @return list<string>
     */
    public function attendRoles(): array
    {
        return ['COMMITTEE', 'ORGANISER'];
    }

    public function isAttendRole(?string $role): bool
    {
        return in_array(strtoupper(trim((string) $role)), $this->attendRoles(), true);
    }

    public function hasCommitteeAccess(array|MeetupParticipant $participant): bool
    {
        if ($participant instanceof MeetupParticipant) {
            return (bool) $participant->is_committee;
        }

        return ! empty($participant['is_committee']);
    }

    /**
     * Ensure COMMITTEE / ORGANISER roles exist so organisers can register Attend accounts.
     */
    public function ensureAttendRoles(string $eventId): void
    {
        $defaults = [
            'COMMITTEE' => ['sessions' => null, 'stamps' => 0, 'note' => 'Attendance check-in access'],
            'ORGANISER' => ['sessions' => null, 'stamps' => 0, 'note' => 'Organiser / committee access'],
        ];

        foreach ($defaults as $name => $cfg) {
            MeetupRole::query()->firstOrCreate(
                ['event_id' => $eventId, 'name' => $name],
                [
                    'sessions_limit' => $cfg['sessions'],
                    'stamps_required' => $cfg['stamps'],
                    'note' => $cfg['note'],
                ]
            );
        }
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listCommitteeAccounts(string $eventId): array
    {
        return array_values(array_filter(
            $this->listPeople($eventId),
            fn (array $p) => $this->hasCommitteeAccess($p)
        ));
    }

    /**
     * Login for MeXCo Attend using organiser-registered participant credentials.
     * Only events where the account has committee access are returned.
     */
    public function findAttendAccount(string $email, ?string $icPassword): array
    {
        $result = $this->findAccount($email, $icPassword);

        if (! ($result['ok'] ?? false)) {
            return $result;
        }

        $matches = array_values(array_filter(
            $result['matches'] ?? [],
            fn (array $match) => $this->hasCommitteeAccess($match)
        ));

        if ($matches === []) {
            return [
                'ok' => false,
                'error' => 'You are not registered as committee for any event. Ask the organiser to add you under Committee Accounts.',
            ];
        }

        $result['matches'] = $matches;
        $result['name'] = $matches[0]['name'] ?? $result['name'];
        $result['ic'] = $matches[0]['ic'] ?? $result['ic'];

        return $result;
    }

    protected function icMatches(string $storedDigits, string $inputDigits): bool
    {
        if ($storedDigits === '' || $inputDigits === '') {
            return false;
        }

        if ($storedDigits === $inputDigits) {
            return true;
        }

        return strlen($inputDigits) === 6 && substr($storedDigits, -6) === $inputDigits;
    }

    public function getEventConfig(string $eventId): array
    {
        $event = MeetupEvent::query()
            ->with(['roles', 'booths' => fn ($q) => $q->withCount('votes'), 'sessions', 'participants'])
            ->findOrFail($eventId);

        $roles = [];
        foreach ($event->roles as $role) {
            $roles[$role->name] = [
                'sessions' => $role->sessions_limit,
                'stamps' => $role->stamps_required,
                'note' => $role->note,
            ];
        }

        $entitlements = app(EntitlementService::class);
        $allowsVoting = $entitlements->allows($eventId, 'booth_voting');
        $allowsFloorPlan = $entitlements->allows($eventId, 'floor_plan');

        $booths = $event->booths->map(fn ($b) => [
            'code' => $b->code,
            'name' => $b->name,
            'mark' => $b->mark,
            'location' => $b->location ?? '',
            'status' => $b->status ?? 'active',
            'booth_type' => $b->booth_type ?? '',
            'description' => $b->description ?? '',
            'voting_enabled' => (bool) ($allowsVoting && $b->voting_enabled),
            'votes' => $allowsVoting ? ($b->votes_count ?? 0) : 0,
            'allow_checkin' => (bool) ($b->allow_checkin ?? true),
            'logo_url' => $b->logo_path ? asset('storage/'.$b->logo_path) : null,
        ])->values()->all();

        $sessions = $event->sessions
            ->filter(fn ($s) => $s->is_published)
            ->map(fn ($s) => [
                'id' => $s->session_key,
                'day' => $s->day->format('Y-m-d'),
                'time' => $s->time,
                'title' => $s->title,
                'speaker' => $s->speaker ?? '',
                'venue' => $s->venue ?? '',
                'roles' => $s->allowed_roles ?? [],
            ])->values()->all();

        $registry = [];
        foreach ($event->participants as $p) {
            $registry[$p->slug] = [
                'email' => $p->email,
                'name' => $p->name,
                'org' => $p->org ?? '',
                'ic' => $p->ic ?? '',
                'phone' => $p->phone ?? '',
                'role' => strtoupper($p->role ?? 'VISITOR'),
            ];
        }

        return [
            'config' => [
                'eventName' => $event->fullName(),
                'shortName' => $event->shortName(),
                'bannerUrl' => $event->bannerUrl(),
                'floorPlanUrl' => $allowsFloorPlan ? $event->floorPlanUrl() : null,
                'allowFloorPlan' => $allowsFloorPlan,
                'signer' => $event->signer,
                'signerTitle' => $event->signer_title ?? '',
                'mailEndpoint' => $event->mail_endpoint ?? '',
                'roles' => $roles,
                'booths' => $booths,
                'sessions' => $sessions,
                'certificateTemplates' => $this->certificateTemplatesForEvent($event),
                'allowVoting' => $allowsVoting,
                'maxVotesPerParticipant' => self::MAX_VOTES_PER_PARTICIPANT,
                'entitlements' => $entitlements->entitlementsForApi($eventId),
                'limits' => $entitlements->limits($eventId),
            ],
            'registry' => $registry,
        ];
    }

    public function defaultCertificateTemplates(): array
    {
        $body = 'in attendance of';

        return [
            'attendance' => [
                'key' => 'attendance',
                'name' => 'Certificate of Attendance',
                'description' => 'For visitors and general participants',
                'roles' => ['VISITOR', 'STUDENT'],
                'title' => 'Certificate of Participation',
                'lead' => 'This certificate is awarded to',
                'body' => $body,
            ],
            'participation' => [
                'key' => 'participation',
                'name' => 'Certificate of Participation',
                'description' => 'For authors and contributors',
                'roles' => ['AUTHOR'],
                'title' => 'Certificate of Participation',
                'lead' => 'This certificate is awarded to',
                'body' => $body,
            ],
            'presentation' => [
                'key' => 'presentation',
                'name' => 'Certificate of Presentation',
                'description' => 'For keynote speakers and presenters',
                'roles' => ['PRESENTER', 'KEYNOTE SPEAKER'],
                'title' => 'Certificate of Presentation',
                'lead' => 'This certificate is awarded to',
                'body' => $body,
            ],
        ];
    }

    public function certificateTemplatesForEvent(MeetupEvent $event): array
    {
        $defaults = $this->defaultCertificateTemplates();
        $stored = is_array($event->certificate_templates) ? $event->certificate_templates : [];

        $merged = [];
        foreach ($defaults as $key => $default) {
            $custom = is_array($stored[$key] ?? null) ? $stored[$key] : [];
            $backgroundPath = $custom['background_path'] ?? null;
            $merged[$key] = [
                'key' => $key,
                'name' => $custom['name'] ?? $default['name'],
                'description' => $custom['description'] ?? $default['description'],
                'roles' => array_values(array_unique(array_map(
                    fn ($r) => strtoupper(trim((string) $r)),
                    $custom['roles'] ?? $default['roles']
                ))),
                'title' => $custom['title'] ?? $default['title'],
                'lead' => $custom['lead'] ?? $default['lead'],
                'body' => $custom['body'] ?? $default['body'],
                'background_path' => $backgroundPath,
                'background_url' => $backgroundPath ? asset('storage/'.$backgroundPath) : null,
            ];
        }

        return $merged;
    }

    public function certificateTemplate(MeetupEvent $event, string $key): ?array
    {
        $templates = $this->certificateTemplatesForEvent($event);

        return $templates[$key] ?? null;
    }

    public function resolveCertificateTemplate(MeetupEvent $event, string $role): array
    {
        $role = strtoupper(trim($role));
        $templates = $this->certificateTemplatesForEvent($event);

        foreach ($templates as $template) {
            if (in_array($role, $template['roles'], true)) {
                return $template;
            }
        }

        return $templates['attendance']
            ?? reset($templates)
            ?? [
                'key' => 'attendance',
                'name' => 'Certificate of Attendance',
                'description' => '',
                'roles' => [$role],
                'title' => 'Certificate of Participation',
                'lead' => 'This certificate is awarded to',
                'body' => 'in attendance of',
                'background_path' => null,
                'background_url' => null,
            ];
    }

    protected function persistCertificateTemplates(MeetupEvent $event, array $templates): void
    {
        $payload = collect($templates)->map(function (array $template) {
            unset($template['background_url']);

            return $template;
        })->all();

        $event->update(['certificate_templates' => $payload]);
    }

    public function updateCertificateTemplate(string $eventId, string $key, array $data): array
    {
        $event = MeetupEvent::query()->findOrFail($eventId);
        $defaults = $this->defaultCertificateTemplates();

        if (! isset($defaults[$key])) {
            throw ValidationException::withMessages(['key' => 'Unknown certificate template.']);
        }

        $templates = $this->certificateTemplatesForEvent($event);

        if (! isset($templates[$key])) {
            throw ValidationException::withMessages(['key' => 'Unknown certificate template.']);
        }

        $name = trim((string) $data['name']);
        $nameKey = strtolower($name);
        foreach ($templates as $otherKey => $other) {
            if ($otherKey === $key) {
                continue;
            }
            if (strtolower(trim((string) ($other['name'] ?? ''))) === $nameKey) {
                throw ValidationException::withMessages([
                    'name' => 'Only one certificate template is allowed for each certificate name.',
                ]);
            }
        }

        $roles = array_values(array_unique(array_map(
            fn ($r) => strtoupper(trim((string) $r)),
            $data['roles'] ?? $templates[$key]['roles']
        )));

        foreach ($templates as $otherKey => $other) {
            if ($otherKey === $key) {
                continue;
            }
            $overlap = array_values(array_intersect($roles, $other['roles'] ?? []));
            if ($overlap) {
                throw ValidationException::withMessages([
                    'roles' => 'Role checkbox already used by "'.$other['name'].'": '.implode(', ', $overlap).'. Each role can only belong to one template.',
                ]);
            }
        }

        $backgroundPath = array_key_exists('background_path', $data)
            ? $data['background_path']
            : ($templates[$key]['background_path'] ?? null);

        $templates[$key] = [
            'key' => $key,
            'name' => $name,
            'description' => $data['description'] ?? $templates[$key]['description'],
            'roles' => $roles,
            'title' => $data['title'],
            'lead' => $data['lead'] ?? 'This is to certify that',
            'body' => $data['body'],
            'background_path' => $backgroundPath ?: null,
        ];

        $this->persistCertificateTemplates($event, $templates);

        if (array_key_exists('signer', $data) || array_key_exists('signer_title', $data)) {
            $event->update([
                'signer' => $data['signer'] ?? $event->signer,
                'signer_title' => $data['signer_title'] ?? $event->signer_title,
            ]);
        }

        return $this->certificateTemplate($event->fresh(), $key);
    }

    public function getParticipant(string $eventId, string $slug): ?array
    {
        $p = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where('slug', $slug)
            ->first();

        if (! $p) {
            return null;
        }

        return $this->participantToArray($p);
    }

    public function saveParticipant(string $eventId, string $slug, array $data): array
    {
        $p = MeetupParticipant::query()->firstOrNew([
            'event_id' => $eventId,
            'slug' => $slug,
        ]);

        $p->fill([
            'email' => strtolower($data['email'] ?? $p->email ?? ''),
            'name' => $data['name'] ?? $p->name ?? 'Guest',
            'org' => $data['org'] ?? $p->org,
            'ic' => $data['ic'] ?? $p->ic,
            'phone' => $data['phone'] ?? $p->phone,
            'role' => strtoupper($data['role'] ?? $p->role ?? 'VISITOR'),
            'booth_stamps' => $data['stamps'] ?? $p->booth_stamps ?? [],
            'session_attendance' => $data['sessions'] ?? $p->session_attendance ?? [],
            'verified_at' => isset($data['verified'])
                ? ($data['verified'] ? now() : null)
                : ($p->verified_at ?? null),
        ]);

        if (is_string($data['verified'] ?? null)) {
            $p->verified_at = $data['verified'];
        }

        $p->save();

        return $this->participantToArray($p);
    }

    public function applyStamp(string $eventId, string $slug, string $code): array
    {
        $booth = MeetupBooth::query()
            ->with('event')
            ->where('event_id', $eventId)
            ->where('code', strtoupper($code))
            ->first();

        if (! $booth) {
            return ['ok' => false, 'msg' => "That code isn't part of this event."];
        }

        if ($booth->status !== 'active') {
            return ['ok' => false, 'msg' => $booth->name.' is currently inactive.'];
        }

        $p = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where('slug', $slug)
            ->firstOrFail();

        $stamps = $p->booth_stamps ?? [];
        $alreadyStamped = isset($stamps[$booth->code]);

        if ($alreadyStamped) {
            return ['ok' => false, 'msg' => $booth->name.' is already stamped.'];
        }

        $now = now()->toIso8601String();
        $stamps[$booth->code] = $now;
        $p->booth_stamps = $stamps;
        $p->save();

        $allowsVoting = app(EntitlementService::class)->allows($eventId, 'booth_voting');

        return [
            'ok' => true,
            'booth' => [
                'code' => $booth->code,
                'name' => $booth->name,
                'mark' => $booth->mark,
                'voting_enabled' => (bool) ($allowsVoting && $booth->voting_enabled),
            ],
            'at' => $now,
            'msg' => $booth->name.' stamped',
            'participant' => $this->participantToArray($p),
        ];
    }

    public function castBoothVote(string $eventId, string $slug, string $code): array
    {
        $booth = MeetupBooth::query()
            ->with('event')
            ->where('event_id', $eventId)
            ->where('code', strtoupper($code))
            ->first();

        if (! $booth) {
            return ['ok' => false, 'msg' => "That code isn't part of this event."];
        }

        if (! (app(EntitlementService::class)->allows($eventId, 'booth_voting') && $booth->voting_enabled)) {
            return ['ok' => false, 'msg' => 'Voting is not open for this booth.'];
        }

        if ($booth->status !== 'active') {
            return ['ok' => false, 'msg' => $booth->name.' is currently inactive.'];
        }

        $p = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where('slug', $slug)
            ->firstOrFail();

        $existing = MeetupBoothVote::query()
            ->where('event_id', $eventId)
            ->where('participant_slug', $slug)
            ->where('booth_id', $booth->id)
            ->exists();

        if ($existing) {
            return ['ok' => false, 'msg' => 'You already voted for '.$booth->name.'.'];
        }

        $voteCount = MeetupBoothVote::query()
            ->where('event_id', $eventId)
            ->where('participant_slug', $slug)
            ->count();

        if ($voteCount >= self::MAX_VOTES_PER_PARTICIPANT) {
            return [
                'ok' => false,
                'msg' => 'You can vote for up to '.self::MAX_VOTES_PER_PARTICIPANT.' booths only.',
                'max_votes' => self::MAX_VOTES_PER_PARTICIPANT,
                'votes_used' => $voteCount,
            ];
        }

        MeetupBoothVote::query()->create([
            'booth_id' => $booth->id,
            'participant_slug' => $slug,
            'event_id' => $eventId,
            'voted_at' => now(),
        ]);

        return [
            'ok' => true,
            'booth' => [
                'code' => $booth->code,
                'name' => $booth->name,
            ],
            'votes' => $booth->votes()->count(),
            'votes_used' => $voteCount + 1,
            'max_votes' => self::MAX_VOTES_PER_PARTICIPANT,
            'msg' => 'Vote recorded for '.$booth->name.'.',
            'participant' => $this->participantToArray($p),
        ];
    }

    public function joinSession(string $eventId, string $slug, string $sessionKey): array
    {
        $session = MeetupSession::query()
            ->where('event_id', $eventId)
            ->where('session_key', $sessionKey)
            ->first();

        if (! $session) {
            return ['ok' => false, 'msg' => 'Session not found.'];
        }

        if (! $session->is_published) {
            return ['ok' => false, 'msg' => 'This session is not published.'];
        }

        $p = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where('slug', $slug)
            ->firstOrFail();

        $attendance = $p->session_attendance ?? [];
        if (isset($attendance[$sessionKey])) {
            return ['ok' => false, 'msg' => 'Attendance already recorded for this session.'];
        }

        $event = MeetupEvent::query()->findOrFail($eventId);
        $roleName = strtoupper($p->role ?? 'VISITOR');
        $role = $event->roles->firstWhere('name', $roleName);

        $allowed = $session->allowed_roles ?? [];
        if (! empty($allowed) && ! in_array($roleName, array_map('strtoupper', $allowed), true)) {
            return ['ok' => false, 'msg' => "This session isn't open to your role."];
        }

        if ($role && $role->sessions_limit !== null) {
            $count = count($attendance);
            if ($count >= $role->sessions_limit) {
                return ['ok' => false, 'msg' => 'Limit reached', 'why' => 'Limit reached'];
            }
        }

        $now = now()->toIso8601String();
        $attendance[$sessionKey] = $now;
        $p->session_attendance = $attendance;
        $p->save();

        return [
            'ok' => true,
            'session' => $session->title,
            'participant' => $this->participantToArray($p),
        ];
    }

    public function resolveParticipantToken(string $eventId, string $token): ?MeetupParticipant
    {
        $token = trim($token);
        if ($token === '') {
            return null;
        }

        $token = preg_replace('/^(PASS|UID|USERID|USER|MEXCO):/i', '', $token) ?? $token;
        $token = trim($token);
        if ($token === '') {
            return null;
        }

        $upper = strtoupper($token);

        return MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where(function ($q) use ($token, $upper) {
                $q->where('user_id', $upper)
                    ->orWhere('user_id', $token)
                    ->orWhere('slug', $token)
                    ->orWhere('slug', strtolower($token));
            })
            ->first();
    }

    public function verifyParticipant(string $eventId, string $slug): array
    {
        $p = $this->resolveParticipantToken($eventId, $slug);

        if (! $p) {
            return ['ok' => false, 'msg' => "This person isn't registered."];
        }

        $already = (bool) $p->verified_at;
        if (! $already) {
            $p->verified_at = now();
            $p->save();
        }

        return [
            'ok' => true,
            'already' => $already,
            'rec' => $this->participantToArray($p),
            'msg' => $already
                ? $p->name.' was already verified.'
                : $p->name.' verified - '.($p->org ?? ''),
        ];
    }

    public function listPeople(string $eventId): array
    {
        return MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->orderBy('name')
            ->get()
            ->map(fn ($p) => $this->participantToArray($p))
            ->all();
    }

    public function saveSettings(string $eventId, array $data): void
    {
        $event = MeetupEvent::query()->findOrFail($eventId);
        $payload = [
            'signer' => $data['signer'] ?? $event->signer,
            'signer_title' => $data['signerTitle'] ?? $event->signer_title,
            'mail_endpoint' => $data['mailEndpoint'] ?? $event->mail_endpoint,
        ];

        if (array_key_exists('eventName', $data)) {
            $payload['event_name'] = trim((string) $data['eventName']) ?: $event->name;
        }

        if (array_key_exists('shortName', $data)) {
            $payload['short_name'] = trim((string) $data['shortName']) ?: $event->name;
        }

        if (array_key_exists('banner_path', $data)) {
            $payload['banner_path'] = $data['banner_path'];
        }

        $event->update($payload);

        if (! empty($data['organiserPassword'])) {
            $event->update(['organiser_password' => $data['organiserPassword']]);
        }
    }

    public function updateEventBanner(string $eventId, ?\Illuminate\Http\UploadedFile $file = null, bool $remove = false): ?string
    {
        $event = MeetupEvent::query()->findOrFail($eventId);
        $disk = \Illuminate\Support\Facades\Storage::disk('public');

        if ($remove || $file) {
            if ($event->banner_path && $disk->exists($event->banner_path)) {
                $disk->delete($event->banner_path);
            }
        }

        if ($remove && ! $file) {
            $event->update(['banner_path' => null]);

            return null;
        }

        if ($file) {
            $path = $file->store('event-banners', 'public');
            $event->update(['banner_path' => $path]);

            return $event->bannerUrl();
        }

        return $event->bannerUrl();
    }

    public function updateEventFloorPlan(string $eventId, ?\Illuminate\Http\UploadedFile $file = null, bool $remove = false): ?string
    {
        $event = MeetupEvent::query()->findOrFail($eventId);

        if (! app(EntitlementService::class)->allows($eventId, 'floor_plan')) {
            abort(403, 'Floor plan is not allowed for this event. Ask a super admin to enable it.');
        }

        $disk = \Illuminate\Support\Facades\Storage::disk('public');

        if ($remove || $file) {
            if ($event->floor_plan_path && $disk->exists($event->floor_plan_path)) {
                $disk->delete($event->floor_plan_path);
            }
        }

        if ($remove && ! $file) {
            $event->update(['floor_plan_path' => null]);

            return null;
        }

        if ($file) {
            $path = $file->store('event-floor-plans', 'public');
            $event->update(['floor_plan_path' => $path]);

            return $event->fresh()->floorPlanUrl();
        }

        return $event->floorPlanUrl();
    }

    public function saveRoles(string $eventId, array $roles): void
    {
        $event = MeetupEvent::query()->findOrFail($eventId);

        foreach ($roles as $name => $cfg) {
            MeetupRole::query()->updateOrCreate(
                ['event_id' => $eventId, 'name' => $name],
                [
                    'sessions_limit' => $cfg['sessions'] ?? null,
                    'stamps_required' => $cfg['stamps'] ?? 0,
                    'note' => $cfg['note'] ?? null,
                ]
            );
        }
    }

    public function saveBooths(string $eventId, array $booths): void
    {
        MeetupBooth::query()->where('event_id', $eventId)->delete();

        foreach ($booths as $b) {
            MeetupBooth::query()->create([
                'event_id' => $eventId,
                'code' => strtoupper($b['code']),
                'name' => $b['name'],
                'mark' => strtoupper(substr($b['mark'] ?? $b['name'], 0, 3)),
            ]);
        }
    }

    public function saveSessions(string $eventId, array $sessions): void
    {
        MeetupSession::query()->where('event_id', $eventId)->delete();

        foreach ($sessions as $i => $s) {
            MeetupSession::query()->create([
                'event_id' => $eventId,
                'session_key' => $s['id'] ?? ('S'.($i + 1)),
                'day' => $s['day'],
                'time' => $s['time'],
                'title' => $s['title'],
                'speaker' => $s['speaker'] ?? '',
                'venue' => $s['venue'] ?? '',
                'allowed_roles' => $s['roles'] ?? [],
            ]);
        }
    }

    public function nextSessionKey(string $eventId): string
    {
        $max = 0;
        foreach (MeetupSession::query()->where('event_id', $eventId)->pluck('session_key') as $key) {
            if (preg_match('/^S(\d+)$/i', $key, $matches)) {
                $max = max($max, (int) $matches[1]);
            }
        }

        return 'S'.($max + 1);
    }

    public function parseSessionTimeRange(string $time): ?array
    {
        if (! preg_match('/^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/', trim($time), $matches)) {
            return null;
        }

        $start = ((int) $matches[1] * 60) + (int) $matches[2];
        $end = ((int) $matches[3] * 60) + (int) $matches[4];

        if ($end <= $start) {
            return null;
        }

        return ['start' => $start, 'end' => $end];
    }

    public function sessionTimeToParts(?string $time = null): array
    {
        $defaults = [
            'start_hour' => '9',
            'start_minute' => '00',
            'start_ampm' => 'AM',
            'end_hour' => '10',
            'end_minute' => '00',
            'end_ampm' => 'AM',
        ];

        $parsed = $time ? $this->parseSessionTimeRange($time) : null;
        if (! $parsed) {
            return $defaults;
        }

        $start = $this->minutesTo12hParts($parsed['start']);
        $end = $this->minutesTo12hParts($parsed['end']);

        return [
            'start_hour' => (string) $start['hour'],
            'start_minute' => $start['minute'],
            'start_ampm' => $start['ampm'],
            'end_hour' => (string) $end['hour'],
            'end_minute' => $end['minute'],
            'end_ampm' => $end['ampm'],
        ];
    }

    public function buildSessionTimeFromParts(
        int $startHour,
        int $startMinute,
        string $startAmpm,
        int $endHour,
        int $endMinute,
        string $endAmpm
    ): ?string {
        $start = $this->to24hMinutes($startHour, $startMinute, $startAmpm);
        $end = $this->to24hMinutes($endHour, $endMinute, $endAmpm);

        if ($end <= $start) {
            return null;
        }

        return sprintf(
            '%02d:%02d-%02d:%02d',
            intdiv($start, 60),
            $start % 60,
            intdiv($end, 60),
            $end % 60
        );
    }

    protected function minutesTo12hParts(int $minutes): array
    {
        $hour24 = intdiv($minutes, 60);
        $minute = $minutes % 60;
        $roundedMinute = min(45, (int) (round($minute / 15) * 15));
        $ampm = $hour24 >= 12 ? 'PM' : 'AM';
        $hour12 = $hour24 % 12;

        if ($hour12 === 0) {
            $hour12 = 12;
        }

        return [
            'hour' => $hour12,
            'minute' => sprintf('%02d', $roundedMinute),
            'ampm' => $ampm,
        ];
    }

    protected function to24hMinutes(int $hour12, int $minute, string $ampm): int
    {
        $hour12 = max(1, min(12, $hour12));
        $ampm = strtoupper($ampm);

        if ($ampm === 'AM') {
            $hour24 = $hour12 === 12 ? 0 : $hour12;
        } else {
            $hour24 = $hour12 === 12 ? 12 : $hour12 + 12;
        }

        return ($hour24 * 60) + max(0, min(59, $minute));
    }

    public function sessionTimesOverlap(string $timeA, string $timeB): bool
    {
        $a = $this->parseSessionTimeRange($timeA);
        $b = $this->parseSessionTimeRange($timeB);

        if (! $a || ! $b) {
            return false;
        }

        return $a['start'] < $b['end'] && $b['start'] < $a['end'];
    }

    public function overlappingSessionOnDay(string $eventId, string $day, string $time, ?int $excludeId = null): ?MeetupSession
    {
        $query = MeetupSession::query()
            ->where('event_id', $eventId)
            ->whereDate('day', $day);

        if ($excludeId) {
            $query->where('id', '!=', $excludeId);
        }

        foreach ($query->get() as $session) {
            if ($this->sessionTimesOverlap($time, $session->time)) {
                return $session;
            }
        }

        return null;
    }

    public function createSession(string $eventId, array $data): MeetupSession
    {
        app(UsageService::class)->assertCanAdd($eventId, 'sessions');

        $session = MeetupSession::query()->create([
            'event_id' => $eventId,
            'session_key' => strtoupper($data['session_key']),
            'day' => $data['day'],
            'time' => $data['time'],
            'title' => $data['title'],
            'speaker' => $data['speaker'] ?? '',
            'venue' => $data['venue'] ?? '',
            'allowed_roles' => $data['allowed_roles'] ?? [],
            'is_parallel' => (bool) ($data['is_parallel'] ?? false),
            'is_published' => (bool) ($data['is_published'] ?? true),
        ]);

        app(UsageService::class)->recalculate($eventId);

        return $session;
    }

    public function updateSession(MeetupSession $session, array $data): MeetupSession
    {
        $session->update([
            'session_key' => strtoupper($data['session_key']),
            'day' => $data['day'],
            'time' => $data['time'],
            'title' => $data['title'],
            'speaker' => $data['speaker'] ?? '',
            'venue' => $data['venue'] ?? '',
            'allowed_roles' => $data['allowed_roles'] ?? [],
            'is_parallel' => (bool) ($data['is_parallel'] ?? false),
            'is_published' => array_key_exists('is_published', $data)
                ? (bool) $data['is_published']
                : $session->is_published,
        ]);

        return $session->fresh();
    }

    public function deleteSession(MeetupSession $session): void
    {
        $eventId = $session->event_id;
        $session->delete();
        app(UsageService::class)->recalculate($eventId);
    }

    public function saveRegistry(string $eventId, array $registry): void
    {
        $existing = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->get()
            ->keyBy('slug');

        $seen = [];
        foreach ($registry as $slug => $entry) {
            $seen[] = $slug;
            $p = $existing->get($slug) ?? new MeetupParticipant(['event_id' => $eventId, 'slug' => $slug]);
            $p->fill([
                'email' => strtolower($entry['email']),
                'name' => $entry['name'] ?? 'Guest',
                'org' => $entry['org'] ?? '',
                'ic' => $entry['ic'] ?? '',
                'phone' => $entry['phone'] ?? '',
                'role' => strtoupper($entry['role'] ?? 'VISITOR'),
            ]);
            $p->booth_stamps = $p->booth_stamps ?? [];
            $p->session_attendance = $p->session_attendance ?? [];
            $p->save();
        }

        MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->whereNotIn('slug', $seen)
            ->delete();
    }

    /**
     * Find a participant on an event by email (or slug derived from email).
     */
    public function findParticipantByEmail(string $eventId, string $email): ?array
    {
        $email = strtolower(trim($email));
        if ($email === '') {
            return null;
        }

        $slug = $this->slugify($email);
        $p = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where(function ($q) use ($slug, $email) {
                $q->where('slug', $slug)
                    ->orWhereRaw('LOWER(email) = ?', [$email]);
            })
            ->first();

        return $p ? $this->participantToArray($p) : null;
    }

    /**
     * Find an existing participant account by email on any event.
     * Used so organisers can add the same person to another event with one UserID.
     *
     * @return array{email:string,slug:string,user_id:?string,name:string,org:string,ic:string,phone:string,role:string,event_id:string,event_name:string,other_events:list<array{id:string,name:string}},on_this_event:bool}|null
     */
    public function findAccountProfileByEmail(string $email, ?string $currentEventId = null): ?array
    {
        $email = strtolower(trim($email));
        if ($email === '') {
            return null;
        }

        $slug = $this->slugify($email);
        $rows = MeetupParticipant::query()
            ->with('event')
            ->where(function ($q) use ($slug, $email) {
                $q->where('slug', $slug)
                    ->orWhereRaw('LOWER(email) = ?', [$email]);
            })
            ->orderByDesc('updated_at')
            ->get();

        if ($rows->isEmpty()) {
            return null;
        }

        $primary = $currentEventId
            ? ($rows->firstWhere('event_id', $currentEventId) ?: $rows->first())
            : $rows->first();

        $otherEvents = $rows
            ->filter(fn (MeetupParticipant $p) => $p->event_id !== ($currentEventId ?? ''))
            ->map(fn (MeetupParticipant $p) => [
                'id' => $p->event_id,
                'name' => $p->event?->shortName() ?? $p->event?->name ?? $p->event_id,
            ])
            ->unique('id')
            ->values()
            ->all();

        return [
            'email' => strtolower((string) $primary->email),
            'slug' => $primary->slug,
            'user_id' => $primary->user_id,
            'name' => $primary->name,
            'org' => $primary->org ?? '',
            'ic' => $primary->ic ?? '',
            'phone' => $primary->phone ?? '',
            'role' => strtoupper($primary->role ?? 'VISITOR'),
            'event_id' => $primary->event_id,
            'event_name' => $primary->event?->shortName() ?? $primary->event?->name ?? $primary->event_id,
            'other_events' => $otherEvents,
            'on_this_event' => $currentEventId
                ? $rows->contains(fn (MeetupParticipant $p) => $p->event_id === $currentEventId)
                : false,
        ];
    }

    /**
     * Resolve the shared UserID for an email (reuse across events when possible).
     */
    public function resolveSharedUserId(string $email, ?string $preferredUserId = null): string
    {
        if ($preferredUserId) {
            return strtoupper(trim($preferredUserId));
        }

        $profile = $this->findAccountProfileByEmail($email);
        if ($profile && ! empty($profile['user_id'])) {
            return strtoupper((string) $profile['user_id']);
        }

        return MeetupParticipant::generateUniqueUserId();
    }

    /**
     * Register Attend access for a committee account.
     * Keeps the existing participant role and adds committee access separately.
     *
     * @return array{ok: bool, created?: bool, promoted?: bool, already?: bool, participant?: array, error?: string}
     */
    public function registerCommitteeAccount(string $eventId, array $entry): array
    {
        $this->ensureAttendRoles($eventId);

        $email = strtolower(trim((string) ($entry['email'] ?? '')));
        $existing = $this->findParticipantByEmail($eventId, $email);

        if ($existing) {
            $ic = $this->digits($entry['ic'] ?? '') ?: $this->digits($existing['ic'] ?? '');
            if (strlen($ic) < 6) {
                return [
                    'ok' => false,
                    'error' => 'This person has no IC on file. Enter an IC / ID with at least 6 digits so they can sign in to Attend.',
                ];
            }

            $already = $this->hasCommitteeAccess($existing);
            $payload = [
                'email' => $email,
                'name' => trim((string) ($entry['name'] ?? '')) ?: ($existing['name'] ?? 'Committee'),
                'org' => array_key_exists('org', $entry) ? ($entry['org'] ?? '') : ($existing['org'] ?? ''),
                'ic' => $ic,
                'phone' => array_key_exists('phone', $entry) ? ($entry['phone'] ?? '') : ($existing['phone'] ?? ''),
                'role' => $existing['role'] ?? 'VISITOR',
                'is_committee' => true,
            ];

            $result = $this->upsertParticipant($eventId, $existing['slug'], $payload);
            if (! ($result['ok'] ?? false)) {
                return $result;
            }

            return [
                'ok' => true,
                'created' => false,
                'promoted' => ! $already,
                'already' => $already,
                'participant' => $result['participant'],
            ];
        }

        $ic = $this->digits($entry['ic'] ?? '');
        $elsewhere = $this->findAccountProfileByEmail($email, $eventId);
        if ($elsewhere) {
            $ic = $ic ?: $this->digits($elsewhere['ic'] ?? '');
            if (trim((string) ($entry['name'] ?? '')) === '') {
                $entry['name'] = $elsewhere['name'] ?? '';
            }
            if (! array_key_exists('org', $entry) || $entry['org'] === null || $entry['org'] === '') {
                $entry['org'] = $elsewhere['org'] ?? '';
            }
            if (! array_key_exists('phone', $entry) || $entry['phone'] === null || $entry['phone'] === '') {
                $entry['phone'] = $elsewhere['phone'] ?? '';
            }
        }

        if (trim((string) ($entry['name'] ?? '')) === '') {
            return ['ok' => false, 'error' => 'Enter a full name for the new committee account.'];
        }
        if (strlen($ic) < 6) {
            return ['ok' => false, 'error' => 'IC / ID must contain at least 6 digits for Attend login.'];
        }

        $result = $this->upsertParticipant($eventId, null, [
            'email' => $email,
            'name' => $entry['name'],
            'org' => $entry['org'] ?? '',
            'ic' => $ic,
            'phone' => $entry['phone'] ?? '',
            'role' => 'VISITOR',
            'is_committee' => true,
            'user_id' => $elsewhere['user_id'] ?? null,
        ]);

        if (! ($result['ok'] ?? false)) {
            return $result;
        }

        return [
            'ok' => true,
            'created' => true,
            'promoted' => false,
            'already' => false,
            'linked' => ! empty($result['linked']) || $elsewhere !== null,
            'participant' => $result['participant'],
        ];
    }

    public function revokeCommitteeAccess(string $eventId, string $slug): ?array
    {
        $p = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where('slug', $slug)
            ->first();

        if (! $p || ! $this->hasCommitteeAccess($p)) {
            return null;
        }

        $p->is_committee = false;
        $p->save();

        return $this->participantToArray($p);
    }

    public function upsertParticipant(string $eventId, ?string $editingSlug, array $entry): array
    {
        $email = strtolower(trim((string) ($entry['email'] ?? '')));
        $slug = $editingSlug ?: $this->slugify($email);

        if (! $editingSlug) {
            app(UsageService::class)->assertCanAdd($eventId, 'participants');
            $exists = MeetupParticipant::query()
                ->where('event_id', $eventId)
                ->where('slug', $slug)
                ->exists();
            if ($exists) {
                return ['ok' => false, 'error' => 'That email is already registered for this event.'];
            }
        }

        $p = MeetupParticipant::query()->firstOrNew([
            'event_id' => $eventId,
            'slug' => $slug,
        ]);

        $isNew = ! $p->exists;
        $existingProfile = $isNew ? $this->findAccountProfileByEmail($email, $eventId) : null;

        $fill = [
            'email' => $email,
            'name' => trim((string) ($entry['name'] ?? '')) ?: ($existingProfile['name'] ?? 'Guest'),
            'org' => array_key_exists('org', $entry)
                ? ($entry['org'] ?? '')
                : ($p->org ?? ($existingProfile['org'] ?? '')),
            'ic' => $this->digits($entry['ic'] ?? '')
                ?: $this->digits($p->ic ?? '')
                ?: $this->digits($existingProfile['ic'] ?? ''),
            'phone' => preg_replace('/\s/', '', (string) ($entry['phone'] ?? ''))
                ?: ($p->phone ?? ($existingProfile['phone'] ?? '')),
            'role' => strtoupper($entry['role'] ?? ($p->role ?? 'VISITOR')),
        ];

        if (array_key_exists('is_committee', $entry)) {
            $fill['is_committee'] = (bool) $entry['is_committee'];
        }

        $p->fill($fill);

        if ($isNew || empty($p->user_id)) {
            $p->user_id = $this->resolveSharedUserId(
                $email,
                $entry['user_id'] ?? ($existingProfile['user_id'] ?? null)
            );
        }

        $p->booth_stamps = $p->booth_stamps ?? [];
        $p->session_attendance = $p->session_attendance ?? [];
        $p->save();

        // Keep UserID aligned for the same email across events.
        if ($p->user_id) {
            MeetupParticipant::query()
                ->whereRaw('LOWER(email) = ?', [$email])
                ->where(function ($q) use ($p) {
                    $q->whereNull('user_id')->orWhere('user_id', '!=', $p->user_id);
                })
                ->update(['user_id' => $p->user_id]);
        }

        app(UsageService::class)->recalculate($eventId);

        return [
            'ok' => true,
            'created' => $isNew,
            'linked' => $isNew && $existingProfile !== null,
            'participant' => $this->participantToArray($p),
        ];
    }

    public function deleteParticipant(string $eventId, string $slug): void
    {
        MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->where('slug', $slug)
            ->delete();

        app(UsageService::class)->recalculate($eventId);
    }

    public function saveEvents(array $eventsList): void
    {
        $ids = collect($eventsList)->pluck('id')->all();
        MeetupEvent::query()->whereNotIn('id', $ids)->delete();

        foreach ($eventsList as $ev) {
            MeetupEvent::query()->updateOrCreate(
                ['id' => $ev['id']],
                [
                    'name' => $ev['name'],
                    'venue' => $ev['venue'] ?? '',
                    'dates' => $ev['dates'] ?? '',
                    'event_name' => $ev['name'],
                ]
            );
        }
    }

    public function verifyPasscode(string $eventId, string $passcode): bool
    {
        $event = MeetupEvent::query()->findOrFail($eventId);

        return $event->verifyOrganiserPassword($passcode);
    }

    public function verifyOrganiserPassword(string $eventId, string $password): bool
    {
        $event = MeetupEvent::query()->findOrFail($eventId);

        return $event->verifyOrganiserPassword($password);
    }

    public function getEvent(string $eventId): MeetupEvent
    {
        return MeetupEvent::query()
            ->with(['roles', 'sessions', 'booths' => fn ($q) => $q->withCount('votes')])
            ->findOrFail($eventId);
    }

    public function roleColors(): array
    {
        return [
            'VISITOR' => '#2563EB',
            'AUTHOR' => '#12855C',
            'PRESENTER' => '#4B23D4',
            'KEYNOTE SPEAKER' => '#D97706',
            'STUDENT' => '#0E7490',
            'EXHIBITOR' => '#EA580C',
            'VIP' => '#171A24',
            'ORGANISER' => '#7C3AED',
            'COMMITTEE' => '#5B21B6',
        ];
    }

    public function roleColor(string $role): string
    {
        return $this->roleColors()[strtoupper($role)] ?? '#4B23D4';
    }

    public function dashboardStats(string $eventId): array
    {
        $event = $this->getEvent($eventId);
        $people = $this->listPeople($eventId);
        $roles = $event->roles->keyBy('name');

        $byRole = [];
        foreach ($people as $p) {
            $r = strtoupper($p['role'] ?? 'VISITOR');
            $byRole[$r] = ($byRole[$r] ?? 0) + 1;
        }

        $eligible = count(array_filter($people, fn ($p) => $this->isEligible($p, $roles)));

        return [
            'total' => count($people),
            'verified' => count(array_filter($people, fn ($p) => ! empty($p['verified']))),
            'certificates_ready' => $eligible,
            'sessions' => $event->sessions->count(),
            'booths' => $event->booths->count(),
            'by_role' => $byRole,
            'role_names' => $event->roles->pluck('name')->all(),
        ];
    }

    public function recentActivity(string $eventId, int $limit = 5): array
    {
        $items = [];

        foreach (MeetupParticipant::query()->where('event_id', $eventId)->orderByDesc('updated_at')->limit(4)->get() as $p) {
            $isNew = $p->created_at->equalTo($p->updated_at) || $p->created_at->diffInMinutes($p->updated_at) < 2;
            $items[] = [
                'type' => 'participant',
                'icon' => 'user',
                'color' => '#12855C',
                'bg' => '#DCF6E9',
                'title' => $isNew ? 'New participant registered' : 'Participant updated',
                'desc' => strtoupper($p->role ?? 'VISITOR').' · '.$p->name,
                'at' => $p->updated_at,
            ];
        }

        foreach (MeetupSession::query()->where('event_id', $eventId)->orderByDesc('updated_at')->limit(3)->get() as $s) {
            $items[] = [
                'type' => 'session',
                'icon' => 'calendar',
                'color' => '#2563EB',
                'bg' => '#EEF2FF',
                'title' => 'Session updated',
                'desc' => 'Session "'.$s->title.'" was updated',
                'at' => $s->updated_at,
            ];
        }

        foreach (MeetupBooth::query()->where('event_id', $eventId)->orderByDesc('updated_at')->limit(3)->get() as $b) {
            $isNew = $b->created_at->equalTo($b->updated_at) || $b->created_at->diffInMinutes($b->updated_at) < 2;
            $items[] = [
                'type' => 'booth',
                'icon' => 'booth',
                'color' => '#EA580C',
                'bg' => '#FFF4ED',
                'title' => $isNew ? 'Booth added' : 'Booth updated',
                'desc' => ($isNew ? 'New booth "' : 'Booth "').$b->name.'"'.($isNew ? ' added' : ' was updated'),
                'at' => $b->updated_at,
            ];
        }

        return collect($items)
            ->sortByDesc(fn ($i) => $i['at']->timestamp)
            ->take($limit)
            ->values()
            ->all();
    }

    public function isEligible(array $participant, $roles = null): bool
    {
        if (empty($participant['verified'])) {
            return false;
        }

        $roleName = strtoupper($participant['role'] ?? 'VISITOR');
        $need = 0;

        if ($roles instanceof Collection) {
            $role = $roles->get($roleName) ?? $roles->firstWhere('name', $roleName);
            $need = $role?->stamps_required ?? 0;
        } elseif (is_array($roles) && isset($roles[$roleName])) {
            $need = $roles[$roleName]['stamps'] ?? $roles[$roleName]['stamps_required'] ?? 0;
        }

        $stampCount = count($participant['stamps'] ?? []);

        return $stampCount >= $need;
    }

    public function parseBoothsText(string $text): array
    {
        $booths = [];
        foreach (preg_split('/\r\n|\r|\n/', $text) as $line) {
            $line = trim($line);
            if ($line === '') {
                continue;
            }
            $parts = array_map('trim', explode('|', $line));
            if (empty($parts[0])) {
                continue;
            }
            $name = $parts[1] ?? $parts[0];
            $booths[] = [
                'code' => strtoupper($parts[0]),
                'name' => $name,
                'mark' => strtoupper(substr($parts[2] ?? $name, 0, 3)),
            ];
        }

        return $booths;
    }

    public function parseSessionsText(string $text): array
    {
        $sessions = [];
        $i = 0;
        foreach (preg_split('/\r\n|\r|\n/', $text) as $line) {
            $line = trim($line);
            if ($line === '') {
                continue;
            }
            $parts = array_map('trim', explode('|', $line));
            $i++;
            $roles = [];
            if (! empty($parts[5])) {
                $roles = array_values(array_filter(array_map(
                    fn ($r) => strtoupper(trim($r)),
                    explode(',', $parts[5])
                )));
            }
            $sessions[] = [
                'id' => 'S'.$i,
                'day' => $parts[0] ?? '',
                'time' => $parts[1] ?? '',
                'title' => $parts[2] ?? 'Session',
                'speaker' => $parts[3] ?? '',
                'venue' => $parts[4] ?? '',
                'roles' => $roles,
            ];
        }

        return $sessions;
    }

    public function boothsToText(Collection $booths): string
    {
        return $booths->map(fn ($b) => "{$b->code} | {$b->name} | {$b->mark}")->join("\n");
    }

    public function sessionsToText(Collection $sessions): string
    {
        return $sessions->map(fn ($s) => implode(' | ', [
            $s->day->format('Y-m-d'),
            $s->time,
            $s->title,
            $s->speaker ?? '',
            $s->venue ?? '',
            implode(',', $s->allowed_roles ?? []),
        ]))->join("\n");
    }

    public function importParticipantsFromCsv(string $eventId, string $csvContent, bool $append = true): array
    {
        $event = $this->getEvent($eventId);
        $roleNames = $event->roles->pluck('name')->map(fn ($n) => strtoupper($n))->all();

        $csvContent = preg_replace('/^\xEF\xBB\xBF/', '', $csvContent) ?? $csvContent;
        $lines = preg_split('/\r\n|\r|\n/', trim($csvContent)) ?: [];

        $added = 0;
        $skipped = 0;
        $duplicates = [];
        $invalid = [];

        $failFile = static function (string $reason) use (&$invalid): array {
            return [
                'added' => 0,
                'skipped' => 0,
                'duplicates' => [],
                'invalid' => [[
                    'row' => 1,
                    'email' => '-',
                    'ic' => '',
                    'name' => '-',
                    'reason' => $reason,
                ]],
            ];
        };

        if ($lines === [] || (count($lines) === 1 && trim($lines[0]) === '')) {
            return $failFile('CSV file is empty');
        }

        $header = false;
        $map = null;
        $first = str_getcsv($lines[0]);
        $map = $this->mapCsvHeaders($first);
        if ($map) {
            $header = true;
        }

        $startRow = $header ? 1 : 0;
        if (! $map) {
            $map = ['name' => 0, 'email' => 1, 'org' => 2, 'ic' => 3, 'phone' => 4, 'role' => 5];
            $firstJoined = strtolower(implode(',', array_map('strval', $first)));
            if (str_contains($firstJoined, 'email') || str_contains($firstJoined, 'name')) {
                return $failFile('Could not read CSV headers. Use columns: name, email, organization, ic, phone, role');
            }
        }

        if (! isset($map['email'])) {
            return $failFile('CSV must include an email column');
        }

        $existing = MeetupParticipant::query()
            ->where('event_id', $eventId)
            ->get(['email', 'ic', 'name', 'slug']);

        $existingByEmail = [];
        $existingByCombo = [];
        foreach ($existing as $p) {
            $emailKey = strtolower(trim((string) $p->email));
            $icKey = $this->digits($p->ic);
            if ($emailKey !== '') {
                $existingByEmail[$emailKey] = $p;
            }
            if ($emailKey !== '' && $icKey !== '') {
                $existingByCombo[$emailKey.'|'.$icKey] = $p;
            }
        }

        $parsedRows = [];
        $seenInFile = [];

        for ($i = $startRow; $i < count($lines); $i++) {
            $lineNo = $i + 1;
            if (trim($lines[$i]) === '') {
                continue;
            }

            $row = str_getcsv($lines[$i]);
            $nonEmpty = false;
            foreach ($row as $cell) {
                if (trim((string) $cell) !== '') {
                    $nonEmpty = true;
                    break;
                }
            }
            if (! $nonEmpty) {
                continue;
            }

            $email = strtolower(trim((string) ($row[$map['email']] ?? '')));
            $name = trim((string) ($row[$map['name'] ?? -1] ?? ''));
            $ic = $this->digits((string) ($row[$map['ic'] ?? -1] ?? ''));
            $org = trim((string) ($row[$map['org'] ?? -1] ?? ''));
            $phone = preg_replace('/\s/', '', (string) ($row[$map['phone'] ?? -1] ?? '')) ?? '';
            $roleRaw = strtoupper(trim((string) ($row[$map['role'] ?? -1] ?? 'VISITOR')));

            $pushInvalid = function (string $reason) use (&$invalid, &$skipped, $lineNo, $email, $ic, $name): void {
                $skipped++;
                $invalid[] = [
                    'row' => $lineNo,
                    'email' => $email !== '' ? $email : '(blank)',
                    'ic' => $ic,
                    'name' => $name !== '' ? $name : '-',
                    'reason' => $reason,
                ];
            };

            if ($email === '') {
                $pushInvalid('Missing email address');
                continue;
            }
            if (! filter_var($email, FILTER_VALIDATE_EMAIL)) {
                $pushInvalid('Invalid email address');
                continue;
            }
            if ($name === '') {
                $pushInvalid('Missing full name');
                continue;
            }
            if ($ic === '') {
                $pushInvalid('Missing IC number (required with email to detect duplicates)');
                continue;
            }
            if (strlen($ic) < 6) {
                $pushInvalid('IC number must contain at least 6 digits');
                continue;
            }

            $fileKey = $email.'|'.$ic;
            if (isset($seenInFile[$fileKey]) || isset($seenInFile[$email])) {
                $skipped++;
                $duplicates[] = [
                    'row' => $lineNo,
                    'email' => $email,
                    'ic' => $ic,
                    'name' => $name,
                    'reason' => isset($seenInFile[$email]) && ! isset($seenInFile[$fileKey])
                        ? 'Duplicate email earlier in this CSV'
                        : 'Duplicate email + IC earlier in this CSV',
                ];
                continue;
            }
            $seenInFile[$fileKey] = true;
            $seenInFile[$email] = true;

            $comboKey = $email.'|'.$ic;
            if (isset($existingByCombo[$comboKey])) {
                $skipped++;
                $duplicates[] = [
                    'row' => $lineNo,
                    'email' => $email,
                    'ic' => $ic,
                    'name' => $existingByCombo[$comboKey]->name ?: $name,
                    'reason' => 'Already in database (same email + IC) — row ignored',
                ];
                continue;
            }

            if (isset($existingByEmail[$email])) {
                $existingIc = $this->digits($existingByEmail[$email]->ic);
                $skipped++;
                $duplicates[] = [
                    'row' => $lineNo,
                    'email' => $email,
                    'ic' => $ic,
                    'name' => $existingByEmail[$email]->name ?: $name,
                    'reason' => ($existingIc !== '' && $existingIc === $ic)
                        ? 'Already in database (same email + IC) — row ignored'
                        : 'Email already registered on this event — CSV row ignored (existing data kept)',
                ];
                continue;
            }

            $role = in_array($roleRaw, $roleNames, true) ? $roleRaw : 'VISITOR';

            $parsedRows[] = [
                'row' => $lineNo,
                'email' => $email,
                'name' => $name,
                'org' => $org,
                'ic' => $ic,
                'phone' => $phone,
                'role' => $role,
            ];
        }

        if (count($parsedRows) > 0) {
            app(UsageService::class)->assertCanAdd($eventId, 'participants', count($parsedRows));
        }

        foreach ($parsedRows as $entry) {
            try {
                $result = $this->upsertParticipant($eventId, null, $entry);
            } catch (ValidationException $e) {
                $skipped++;
                $invalid[] = [
                    'row' => $entry['row'],
                    'email' => $entry['email'],
                    'ic' => $entry['ic'],
                    'name' => $entry['name'],
                    'reason' => collect($e->errors())->flatten()->first() ?: 'Validation failed',
                ];
                continue;
            } catch (\Throwable $e) {
                report($e);
                $skipped++;
                $invalid[] = [
                    'row' => $entry['row'],
                    'email' => $entry['email'],
                    'ic' => $entry['ic'],
                    'name' => $entry['name'],
                    'reason' => 'Could not save this row. Please check the data and try again.',
                ];
                continue;
            }

            if ($result['ok'] ?? false) {
                $added++;
                $emailKey = $entry['email'];
                $icKey = $this->digits($entry['ic']);
                $existingByEmail[$emailKey] = (object) [
                    'email' => $emailKey,
                    'ic' => $icKey,
                    'name' => $entry['name'],
                ];
                if ($icKey !== '') {
                    $existingByCombo[$emailKey.'|'.$icKey] = $existingByEmail[$emailKey];
                }
            } else {
                $skipped++;
                $invalid[] = [
                    'row' => $entry['row'],
                    'email' => $entry['email'],
                    'ic' => $entry['ic'],
                    'name' => $entry['name'],
                    'reason' => $result['error'] ?? 'Could not import this row',
                ];
            }
        }

        app(UsageService::class)->recalculate($eventId);

        return [
            'added' => $added,
            'skipped' => $skipped,
            'duplicates' => $duplicates,
            'invalid' => $invalid,
        ];
    }
    protected function mapCsvHeaders(array $cells): ?array
    {
        $heads = [
            'email' => ['email', 'e-mail', 'emel', 'email address'],
            'name' => ['name', 'full name', 'fullname', 'nama', 'participant', 'participant name'],
            'org' => ['organization', 'organisation', 'org', 'company', 'institution'],
            'ic' => ['ic', 'ic number', 'ic no', 'nric', 'mykad'],
            'phone' => ['phone', 'phone number', 'whatsapp', 'mobile', 'handphone', 'hp', 'tel'],
            'role' => ['role', 'category', 'kategori', 'access', 'type'],
        ];

        $map = [];
        $found = 0;
        foreach ($cells as $i => $cell) {
            $key = strtolower(preg_replace('/[._-]/', ' ', trim($cell)));
            $key = preg_replace('/\s+/', ' ', $key);
            foreach ($heads as $field => $aliases) {
                if (isset($map[$field])) {
                    continue;
                }
                if (in_array($key, $aliases, true)) {
                    $map[$field] = $i;
                    $found++;
                    break;
                }
            }
        }

        return $found >= 2 ? $map : null;
    }

    public function generateEventId(string $name): string
    {
        $base = preg_replace('/[^a-z0-9]/', '', strtolower($name)) ?: 'event';
        $id = $base;
        $n = 1;
        while (MeetupEvent::query()->where('id', $id)->exists()) {
            $id = $base.$n;
            $n++;
        }

        return $id;
    }

    public function createEvent(array $data): MeetupEvent
    {
        $id = $data['id'] ?? $this->generateEventId($data['name']);

        $event = MeetupEvent::query()->create([
            'id' => $id,
            'organisation_id' => $data['organisation_id'] ?? null,
            'name' => $data['name'],
            'description' => $data['description'] ?? null,
            'event_type' => $data['event_type'] ?? null,
            'expected_participants' => $data['expected_participants'] ?? null,
            'contact_person' => $data['contact_person'] ?? null,
            'contact_email' => isset($data['contact_email']) ? strtolower(trim($data['contact_email'])) : null,
            'venue' => $data['venue'] ?? '',
            'dates' => $data['dates'] ?? '',
            'event_name' => $data['event_name'] ?? $data['name'],
            'short_name' => $data['short_name'] ?? $data['name'],
            'signer' => $data['signer'] ?? 'Organising Chairperson',
            'signer_title' => $data['signer_title'] ?? '',
            'mail_endpoint' => $data['mail_endpoint'] ?? '',
            'passcode' => $data['passcode'] ?? '1234',
            'organiser_username' => strtolower(trim($data['organiser_username'] ?? ($id.'@meetup.local'))),
            'organiser_password' => $data['organiser_password'] ?? '1234',
            'is_virtual' => (bool) ($data['is_virtual'] ?? false),
            'allow_voting' => (bool) ($data['allow_voting'] ?? false),
            'allow_floor_plan' => (bool) ($data['allow_floor_plan'] ?? false),
            'starts_at' => $data['starts_at'] ?? null,
            'ends_at' => $data['ends_at'] ?? null,
        ]);

        $this->bootstrapEventDefaults($event->id);

        return $event;
    }

    public function updateEventRecord(string $eventId, array $data): MeetupEvent
    {
        $event = MeetupEvent::query()->findOrFail($eventId);

        $updates = [
            'name' => $data['name'],
            'venue' => $data['venue'] ?? '',
            'dates' => $data['dates'] ?? '',
            'event_name' => $data['event_name'] ?? $data['name'],
            'short_name' => $data['short_name'] ?? ($event->short_name ?: $data['name']),
            'signer' => $data['signer'] ?? $event->signer,
            'signer_title' => $data['signer_title'] ?? $event->signer_title,
            'mail_endpoint' => $data['mail_endpoint'] ?? $event->mail_endpoint,
            'organiser_username' => strtolower(trim($data['organiser_username'] ?? $event->organiser_username)),
            'is_virtual' => array_key_exists('is_virtual', $data)
                ? (bool) $data['is_virtual']
                : $event->is_virtual,
            'allow_voting' => array_key_exists('allow_voting', $data)
                ? (bool) $data['allow_voting']
                : (bool) $event->allow_voting,
            'allow_floor_plan' => array_key_exists('allow_floor_plan', $data)
                ? (bool) $data['allow_floor_plan']
                : (bool) $event->allow_floor_plan,
            'starts_at' => array_key_exists('starts_at', $data) ? $data['starts_at'] : $event->starts_at,
            'ends_at' => array_key_exists('ends_at', $data) ? $data['ends_at'] : $event->ends_at,
        ];

        if (array_key_exists('organisation_id', $data)) {
            $updates['organisation_id'] = $data['organisation_id'];
        }

        if (! empty($data['organiser_password'])) {
            $updates['organiser_password'] = $data['organiser_password'];
        }

        $event->update($updates);

        if (array_key_exists('allow_voting', $data) && ! $updates['allow_voting']) {
            MeetupBooth::query()
                ->where('event_id', $eventId)
                ->where('voting_enabled', true)
                ->update(['voting_enabled' => false]);
        }

        return $event->fresh();
    }

    public function deleteEvent(string $eventId): void
    {
        MeetupEvent::query()->findOrFail($eventId)->delete();
    }

    public function bootstrapEventDefaults(string $eventId): void
    {
        $roles = [
            'VISITOR' => ['sessions' => 5, 'stamps' => 5, 'note' => 'Up to 5 sessions'],
            'AUTHOR' => ['sessions' => 10, 'stamps' => 8, 'note' => 'Up to 10 sessions'],
            'PRESENTER' => ['sessions' => null, 'stamps' => 8, 'note' => 'Unlimited sessions'],
            'KEYNOTE SPEAKER' => ['sessions' => null, 'stamps' => 0, 'note' => 'Unlimited sessions'],
            'STUDENT' => ['sessions' => 5, 'stamps' => 5, 'note' => 'Up to 5 sessions'],
            'EXHIBITOR' => ['sessions' => null, 'stamps' => 0, 'note' => 'Selected sessions only'],
            'VIP' => ['sessions' => null, 'stamps' => 0, 'note' => 'Unlimited sessions'],
            'COMMITTEE' => ['sessions' => null, 'stamps' => 0, 'note' => 'Attendance check-in access'],
            'ORGANISER' => ['sessions' => null, 'stamps' => 0, 'note' => 'Organiser / committee access'],
        ];

        foreach ($roles as $name => $cfg) {
            MeetupRole::query()->updateOrCreate(
                ['event_id' => $eventId, 'name' => $name],
                [
                    'sessions_limit' => $cfg['sessions'],
                    'stamps_required' => $cfg['stamps'],
                    'note' => $cfg['note'],
                ]
            );
        }

        MeetupBooth::query()->where('event_id', $eventId)->delete();
        foreach (range(1, 8) as $i) {
            $code = 'B'.str_pad((string) $i, 2, '0', STR_PAD_LEFT);
            MeetupBooth::query()->create([
                'event_id' => $eventId,
                'code' => $code,
                'name' => 'Booth '.str_pad((string) $i, 2, '0', STR_PAD_LEFT),
                'mark' => str_pad((string) $i, 2, '0', STR_PAD_LEFT),
            ]);
        }
    }

    public function eventSummary(MeetupEvent $event): array
    {
        return [
            'participants' => $event->participants()->count(),
            'sessions' => $event->sessions()->count(),
            'booths' => $event->booths()->count(),
        ];
    }

    public function createBooth(string $eventId, array $data): MeetupBooth
    {
        app(UsageService::class)->assertCanAdd($eventId, 'booths');

        $booth = MeetupBooth::query()->create($this->boothPayload($eventId, $data));
        app(UsageService::class)->recalculate($eventId);

        return $booth;
    }

    public function updateBoothRecord(MeetupBooth $booth, array $data): MeetupBooth
    {
        $booth->update($this->boothPayload($booth->event_id, $data, $booth));

        return $booth->fresh();
    }

    public function deleteBoothRecord(MeetupBooth $booth): void
    {
        $eventId = $booth->event_id;
        $booth->delete();
        app(UsageService::class)->recalculate($eventId);
    }

    public function boothPayload(string $eventId, array $data, ?MeetupBooth $existing = null): array
    {
        $mark = strtoupper(substr($data['mark'] ?? $data['name'] ?? 'XX', 0, 2));

        return [
            'event_id' => $eventId,
            'code' => strtoupper($data['code']),
            'name' => $data['name'],
            'mark' => $mark,
            'location' => $data['location'] ?? null,
            'status' => $data['status'] ?? 'active',
            'voting_enabled' => (bool) ($data['voting_enabled'] ?? false),
            'booth_type' => $data['booth_type'] ?? null,
            'description' => $data['description'] ?? null,
            'notes' => $data['notes'] ?? null,
            'show_in_map' => (bool) ($data['show_in_map'] ?? true),
            'allow_checkin' => (bool) ($data['allow_checkin'] ?? false),
            'require_approval' => (bool) ($data['require_approval'] ?? false),
            'visible_public' => (bool) ($data['visible_public'] ?? true),
            'logo_path' => $data['logo_path'] ?? $existing?->logo_path,
        ];
    }

    public function boothTypeLabel(?string $type): string
    {
        return match ($type) {
            'exhibitor' => 'Exhibitor',
            'sponsor' => 'Sponsor',
            'info_desk' => 'Info Desk',
            default => '-',
        };
    }

    protected function participantToArray(MeetupParticipant $p): array
    {
        if (empty($p->user_id)) {
            $p->user_id = MeetupParticipant::generateUniqueUserId();
            $p->save();
        }

        $votedCodes = MeetupBoothVote::query()
            ->where('event_id', $p->event_id)
            ->where('participant_slug', $p->slug)
            ->whereHas('booth', fn ($q) => $q->where('event_id', $p->event_id))
            ->with(['booth:id,code'])
            ->get()
            ->pluck('booth.code')
            ->filter()
            ->values()
            ->all();

        return [
            'slug' => $p->slug,
            'user_id' => $p->user_id,
            'email' => $p->email,
            'name' => $p->name,
            'org' => $p->org ?? '',
            'ic' => $p->ic ?? '',
            'phone' => $p->phone ?? '',
            'role' => strtoupper($p->role ?? 'VISITOR'),
            'is_committee' => (bool) $p->is_committee,
            'stamps' => $p->booth_stamps ?? [],
            'votes' => $votedCodes,
            'sessions' => $p->session_attendance ?? [],
            'verified' => $p->verified_at?->toIso8601String(),
            'registered_at' => $p->created_at,
        ];
    }
}
