<?php

namespace App\Livewire;

use App\Livewire\Concerns\ResolvesCustomer;
use App\Models\Customer;
use App\Models\Seat;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Attributes\Validate;
use Livewire\Component;

#[Layout('layouts.portal-app')]
class Users extends Component
{
    use ResolvesCustomer;

    #[Validate('required|email|max:255')]
    public string $inviteEmail = '';

    #[Validate('nullable|string|max:255')]
    public string $inviteName = '';

    #[Validate('required|in:admin,member,readonly')]
    public string $inviteRole = 'member';


    public function mount(): void
    {
        // Every customer starts with themselves as the owner seat. firstOrCreate
        // keyed on (customer_id, email) is idempotent; the catch covers the
        // concurrent-first-visit race against the unique index.
        $customer = $this->customer();
        if ($customer !== null && $customer->seats()->count() === 0) {
            try {
                $customer->seats()->firstOrCreate(
                    ['email' => $customer->email],
                    ['name' => $customer->name, 'role' => 'owner', 'status' => 'active', 'invited_at' => now()],
                );
            } catch (UniqueConstraintViolationException) {
                // Another concurrent first visit created it — fine.
            }
        }
    }

    public function invite(): void
    {
        $customer = $this->requireCustomer();
        if ($customer === null) {
            return;
        }

        $data = $this->validate();

        // Serialize the limit check with the insert so two concurrent invites for
        // the last free seat can't both pass (lock the customer row).
        $result = DB::transaction(function () use ($customer, $data) {
            $locked = Customer::query()->whereKey($customer->id)->lockForUpdate()->first();

            if ($locked->seats()->where('email', $data['inviteEmail'])->exists()) {
                return 'duplicate';
            }
            if ($this->usedSeats($locked) >= $this->seatLimit($locked)) {
                return 'limit';
            }

            $locked->seats()->create([
                'email' => $data['inviteEmail'],
                'name' => $data['inviteName'] ?: null,
                'role' => $data['inviteRole'],
                'status' => 'invited',
                'invited_at' => now(),
            ]);

            return 'ok';
        });

        if ($result === 'limit') {
            $this->addError('inviteEmail', __('users.limit_reached'));

            return;
        }
        if ($result === 'duplicate') {
            $this->addError('inviteEmail', __('users.duplicate'));

            return;
        }

        $this->reset('inviteEmail', 'inviteName', 'inviteRole');
        $this->inviteRole = 'member';
        $this->dispatch('notify', message: __('users.invited'));
    }

    public function setRole(string $uuid, string $role): void
    {
        if (! in_array($role, Seat::ROLES, true)) {
            return;
        }
        $customer = $this->requireCustomer();
        if ($customer === null) {
            return;
        }

        // Lock the customer so a concurrent owner change can't race past the guard.
        $ok = DB::transaction(function () use ($customer, $uuid, $role) {
            Customer::query()->whereKey($customer->id)->lockForUpdate()->first();
            $seat = $customer->seats()->where('uuid', $uuid)->first();
            if ($seat === null) {
                return true;
            }
            if ($seat->role === 'owner' && $role !== 'owner' && $customer->seats()->where('role', 'owner')->count() <= 1) {
                return false; // would remove the last owner
            }
            $seat->update(['role' => $role]);

            return true;
        });

        if (! $ok) {
            $this->dispatch('notify', message: __('users.last_owner'));
        }
    }

    /**
     * Pause a seat without destroying it.
     *
     * The action an owner actually needs when someone leaves: access stops now,
     * and the record of who held it survives — which is the half a deletion
     * throws away, on a product sold on being able to show who had access to
     * what.
     */
    public function suspend(string $uuid): void
    {
        $customer = $this->requireCustomer();

        if ($customer === null) {
            return;
        }

        $seat = $customer->seats()->where('uuid', $uuid)->first();

        if ($seat === null || $seat->role === 'owner') {
            // The owner cannot lock themselves out of their own cloud.
            $this->dispatch('notify', message: __('users.owner_locked'));

            return;
        }

        $seat->update(['status' => $seat->status === 'suspended' ? 'active' : 'suspended']);

        $this->dispatch('notify', message: __(
            $seat->status === 'suspended' ? 'users.suspended' : 'users.reactivated',
        ));
    }

    public function revoke(string $uuid): void
    {
        $customer = $this->requireCustomer();
        if ($customer === null) {
            return;
        }

        $result = DB::transaction(function () use ($customer, $uuid) {
            Customer::query()->whereKey($customer->id)->lockForUpdate()->first();
            $seat = $customer->seats()->where('uuid', $uuid)->first();
            if ($seat === null) {
                return 'gone';
            }
            if ($seat->role === 'owner' && $customer->seats()->where('role', 'owner')->count() <= 1) {
                return 'last_owner';
            }
            $seat->delete();

            return 'ok';
        });

        if ($result === 'last_owner') {
            $this->dispatch('notify', message: __('users.last_owner'));
        } elseif ($result === 'ok') {
            $this->dispatch('notify', message: __('users.revoked'));
        }
    }

    /**
     * The revoke button opens ConfirmRevokeSeat instead of calling revoke()
     * directly (R23); its confirm button dispatches back here.
     */
    #[On('seat-revoke-confirmed')]
    public function onRevokeConfirmed(string $uuid): void
    {
        $this->revoke($uuid);
    }

    public function resend(string $uuid): void
    {
        // Invite delivery is mocked for now.
        if ($this->seat($uuid) !== null) {
            $this->dispatch('notify', message: __('users.resent'));
        }
    }


    private function ownerCount(): int
    {
        $customer = $this->customer();

        return $customer ? $customer->seats()->where('role', 'owner')->count() : 0;
    }

    private function usedSeats(Customer $customer): int
    {
        return $customer->seats()->where('status', '!=', 'revoked')->count();
    }

    private function seatLimit(Customer $customer): int
    {
        // The entitlement follows the active (or cancelling) package, not a newer
        // failed/deprovisioned record.
        $instance = $customer->instances()->whereIn('status', ['active', 'cancellation_scheduled'])->latest('id')->first()
            ?? $customer->instances()->latest('id')->first();
        // From the contract: how many people a customer may invite is part of
        // what they bought. Cutting a plan's seats in the catalogue must not
        // lock users out of an existing customer's cloud.
        return (int) ($instance?->subscription?->seats ?? 5);
    }

    private function seat(string $uuid): ?Seat
    {
        $customer = $this->customer();

        return $customer?->seats()->where('uuid', $uuid)->first();
    }

    public function render()
    {
        $customer = $this->customer();
        $seats = $customer ? $customer->seats()->orderByRaw("role = 'owner' desc")->orderBy('email')->get() : collect();

        // The actions column is ALWAYS drawn. It used to be hidden when the
        // only seat was the owner, on the reasoning that there was nothing to
        // act on — but every seat can be renamed, and a column that disappears
        // does not read as "nothing applies here", it reads as "this product
        // cannot do that". Which is exactly how it was reported.
        return view('livewire.users', [
            'seats' => $seats,
            'used' => $customer ? $this->usedSeats($customer) : 0,
            'limit' => $customer ? $this->seatLimit($customer) : 0,
            'roles' => Seat::ROLES,
        ]);
    }
}
