<?php

namespace App\Services\Care;

use App\Models\CareUnit;
use App\Models\Member;
use App\Models\Organization;
use App\Models\RosterEntry;
use App\Models\Shift;
use App\Models\User;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;

class RosterService
{
    /**
     * Default org shifts (seeded once per organization).
     *
     * @var list<array{code: string, label: string, start_time: string, end_time: string, color: string, sort_order: int}>
     */
    protected array $defaults = [
        ['code' => 'day', 'label' => 'Day shift', 'start_time' => '07:00:00', 'end_time' => '15:00:00', 'color' => '#0ea5e9', 'sort_order' => 10],
        ['code' => 'evening', 'label' => 'Evening shift', 'start_time' => '15:00:00', 'end_time' => '23:00:00', 'color' => '#f59e0b', 'sort_order' => 20],
        ['code' => 'night', 'label' => 'Night shift', 'start_time' => '23:00:00', 'end_time' => '07:00:00', 'color' => '#6366f1', 'sort_order' => 30],
    ];

    /**
     * @return Collection<int, Shift>
     */
    public function ensureDefaultShifts(Organization $organization, string $ownerRef): Collection
    {
        foreach ($this->defaults as $row) {
            Shift::query()->firstOrCreate(
                [
                    'organization_id' => $organization->id,
                    'code' => $row['code'],
                ],
                [
                    'owner_ref' => $ownerRef,
                    'label' => $row['label'],
                    'start_time' => $row['start_time'],
                    'end_time' => $row['end_time'],
                    'color' => $row['color'],
                    'sort_order' => $row['sort_order'],
                    'is_active' => true,
                ],
            );
        }

        return Shift::owned($ownerRef)
            ->where('organization_id', $organization->id)
            ->where('is_active', true)
            ->orderBy('sort_order')
            ->orderBy('label')
            ->get();
    }

    /**
     * Week grid: days × shifts → list of roster entries.
     *
     * @return array{week_start: Carbon, days: list<Carbon>, shifts: Collection, cells: array<string, list<RosterEntry>>, members: Collection}
     */
    public function unitWeekGrid(
        CareUnit $unit,
        string $ownerRef,
        ?CarbonInterface $weekStart = null,
    ): array {
        $organization = $unit->organization ?? Organization::query()->findOrFail($unit->organization_id);
        $shifts = $this->ensureDefaultShifts($organization, $ownerRef);

        $start = Carbon::parse($weekStart ?? now())->startOfWeek(Carbon::MONDAY)->startOfDay();
        $days = [];
        for ($i = 0; $i < 7; $i++) {
            $days[] = $start->copy()->addDays($i);
        }

        $entries = RosterEntry::owned($ownerRef)
            ->where('care_unit_id', $unit->id)
            ->whereBetween('duty_date', [$start->toDateString(), $start->copy()->addDays(6)->toDateString()])
            ->where('status', '!=', RosterEntry::STATUS_CANCELLED)
            ->with(['member', 'shift'])
            ->orderBy('id')
            ->get();

        $cells = [];
        foreach ($entries as $entry) {
            $key = $entry->duty_date->toDateString().'|'.$entry->shift_id;
            $cells[$key][] = $entry;
        }

        $members = Member::owned($ownerRef)
            ->where('organization_id', $unit->organization_id)
            ->orderBy('role')
            ->get();

        return [
            'week_start' => $start,
            'days' => $days,
            'shifts' => $shifts,
            'cells' => $cells,
            'members' => $members,
            'member_labels' => $this->memberLabels($members),
        ];
    }

    public function assign(
        CareUnit $unit,
        Shift $shift,
        Member $member,
        CarbonInterface|string $dutyDate,
        string $ownerRef,
        ?string $actorRef = null,
        ?string $notes = null,
    ): RosterEntry {
        if ((int) $shift->organization_id !== (int) $unit->organization_id
            || (int) $member->organization_id !== (int) $unit->organization_id) {
            throw ValidationException::withMessages([
                'member_id' => 'Staff and shift must belong to the same organization as the care unit.',
            ]);
        }

        $date = Carbon::parse($dutyDate)->toDateString();

        $existing = RosterEntry::withTrashed()
            ->where('care_unit_id', $unit->id)
            ->where('shift_id', $shift->id)
            ->where('member_id', $member->id)
            ->whereDate('duty_date', $date)
            ->first();

        if ($existing && ! $existing->trashed() && $existing->status !== RosterEntry::STATUS_CANCELLED) {
            throw ValidationException::withMessages([
                'member_id' => 'This staff member is already rostered on this shift for that date.',
            ]);
        }

        if ($existing) {
            if ($existing->trashed()) {
                $existing->restore();
            }
            $existing->update([
                'status' => RosterEntry::STATUS_SCHEDULED,
                'staff_assignment_id' => null,
                'notes' => $notes,
                'created_by' => $actorRef,
            ]);
            $entry = $existing;
        } else {
            $entry = RosterEntry::create([
                'owner_ref' => $ownerRef,
                'organization_id' => $unit->organization_id,
                'care_unit_id' => $unit->id,
                'shift_id' => $shift->id,
                'member_id' => $member->id,
                'duty_date' => $date,
                'status' => RosterEntry::STATUS_SCHEDULED,
                'staff_assignment_id' => null,
                'notes' => $notes,
                'created_by' => $actorRef,
            ]);
        }

        AuditLogger::record(
            $ownerRef,
            'roster.assigned',
            $unit->organization_id,
            $actorRef,
            RosterEntry::class,
            $entry->id,
        );

        return $entry->load(['member', 'shift']);
    }

    public function cancel(RosterEntry $entry, string $ownerRef, ?string $actorRef = null): void
    {
        $entryId = $entry->id;
        $organizationId = $entry->organization_id;

        $entry->update(['status' => RosterEntry::STATUS_CANCELLED]);

        // Soft-delete so the unique (unit, shift, member, date) can be reused.
        $entry->delete();

        AuditLogger::record(
            $ownerRef,
            'roster.cancelled',
            $organizationId,
            $actorRef,
            RosterEntry::class,
            $entryId,
        );
    }

    /**
     * Today's allocation across units (or one unit).
     *
     * @return Collection<int, RosterEntry>
     */
    public function todayAllocation(Organization $organization, string $ownerRef, ?int $careUnitId = null): Collection
    {
        return RosterEntry::owned($ownerRef)
            ->where('organization_id', $organization->id)
            ->whereDate('duty_date', now()->toDateString())
            ->where('status', '!=', RosterEntry::STATUS_CANCELLED)
            ->when($careUnitId, fn ($q) => $q->where('care_unit_id', $careUnitId))
            ->with(['member', 'shift', 'careUnit.department'])
            ->orderBy('care_unit_id')
            ->orderBy('shift_id')
            ->get();
    }

    /**
     * Roster entries for one staff member over a week (or open-ended upcoming window).
     *
     * @return array{week_start: Carbon, days: list<Carbon>, entries: Collection<int, RosterEntry>, today: Collection<int, RosterEntry>}
     */
    public function memberWeek(
        Member $member,
        string $ownerRef,
        ?CarbonInterface $weekStart = null,
    ): array {
        $start = Carbon::parse($weekStart ?? now())->startOfWeek(Carbon::MONDAY)->startOfDay();
        $end = $start->copy()->addDays(6);
        $days = [];
        for ($i = 0; $i < 7; $i++) {
            $days[] = $start->copy()->addDays($i);
        }

        $entries = RosterEntry::owned($ownerRef)
            ->where('member_id', $member->id)
            ->whereBetween('duty_date', [$start->toDateString(), $end->toDateString()])
            ->where('status', '!=', RosterEntry::STATUS_CANCELLED)
            ->with(['shift', 'careUnit.department'])
            ->orderBy('duty_date')
            ->orderBy('shift_id')
            ->get();

        $today = $entries->filter(
            fn (RosterEntry $entry) => $entry->duty_date->isSameDay(now())
        )->values();

        return [
            'week_start' => $start,
            'days' => $days,
            'entries' => $entries,
            'today' => $today,
        ];
    }

    /**
     * @param  iterable<Member>  $members
     * @return array<int, string>
     */
    public function memberLabels(iterable $members): array
    {
        $list = collect($members)->filter()->unique('id')->values();
        $refs = $list->pluck('user_ref')->all();
        $users = User::query()->whereIn('public_id', $refs)->get()->keyBy('public_id');
        $roles = config('care.roles');

        $labels = [];
        foreach ($list as $member) {
            $user = $users->get($member->user_ref);
            $display = $user?->name
                ?? (str_contains((string) $member->user_ref, '@') ? $member->user_ref : ($user?->email ?? $member->user_ref));
            $labels[(int) $member->id] = $display.' · '.($roles[$member->role] ?? $member->role);
        }

        return $labels;
    }
}
