<?php

namespace Modules\Bms\Http\Controllers\Api;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Modules\Bms\Entities\BmsAddon;
use Modules\Bms\Entities\BmsAsset;
use Modules\Bms\Entities\BmsBooking;
use Modules\Bms\Entities\BmsPackageItem;
use Modules\Bms\Entities\BmsHall;
use Modules\Bms\Entities\BmsLead;
use Modules\Bms\Entities\BmsMaintenanceRequest;
use Modules\Bms\Entities\BmsMaintenanceUpdate;
use Modules\Bms\Entities\BmsOfficeVisit;
use Modules\Bms\Entities\BmsVenue;
use Modules\Bms\Entities\BmsTask;
use Modules\Bms\Entities\BmsTransferRequest;
use Modules\Bms\Services\Booking\BookingService;
use Modules\Bms\Services\Booking\CancellationService;
use Modules\Bms\Services\Booking\HoldConflictException;
use Modules\Bms\Services\Booking\HoldService;
use Modules\Bms\Services\Finance\PaymentPlanService;
use Modules\Bms\Services\Contracts\AssetServiceContract;
use Modules\Bms\Services\Maintenance\MaintenanceService;
use Modules\Bms\Services\Contracts\InvoiceServiceContract;
use Modules\Bms\Services\Contracts\PaymentServiceContract;
use Modules\Bms\Services\Matching\MatchingEngine;
use Modules\Bms\Services\Tasks\TaskService;
use Modules\Bms\Services\Transfers\TransferService;

/**
 * Employee (P2) endpoints. Guarded by bms.api:employee.
 */
class EmployeeController extends ApiController
{
    /** Lead pipeline / follow-up queue (incl. abandoned wizard sessions). */
    public function leads(Request $request)
    {
        $businessId = $this->authBusinessId($request);

        $leads = BmsLead::where('business_id', $businessId)
            ->when($request->filled('stage'), fn ($q) => $q->where('stage', $request->input('stage')))
            ->with('contact:id,name,mobile')
            ->latest()
            ->paginate(min((int) $request->input('per_page', 25), 100));

        $leads->getCollection()->transform(fn ($l) => [
            'id'         => $l->id,
            'stage'      => $l->stage,
            'contact'    => $l->contact ? ['name' => $l->contact->name, 'mobile' => $l->contact->mobile] : null,
            'event_type' => $l->event_type,
            'attendees'  => $l->attendees,
            'budget_max' => $l->budget_max,
            'created_at' => optional($l->created_at)->toIso8601String(),
        ]);

        return $this->ok(['leads' => $leads]);
    }

    /** Dashboard KPIs for the sales/ops workspace. */
    public function dashboard(Request $request)
    {
        $businessId = $this->authBusinessId($request);

        $byStage = BmsLead::where('business_id', $businessId)
            ->select('stage', DB::raw('count(*) as c'))
            ->groupBy('stage')->pluck('c', 'stage');

        // Revenue health from the ERP invoices/payments linked to bookings.
        $invoiceIds = BmsBooking::where('business_id', $businessId)->whereNotNull('erp_invoice_id')->pluck('erp_invoice_id');
        $invoiced = (float) DB::table('transactions')->whereIn('id', $invoiceIds)->sum('final_total');
        $collected = (float) DB::table('transaction_payments')->whereIn('transaction_id', $invoiceIds)->sum('amount');

        return $this->ok([
            'revenue' => [
                'invoiced'      => $invoiced,
                'collected'     => $collected,
                'due'           => max(0, $invoiced - $collected),
                'collected_pct' => $invoiced > 0 ? (int) round($collected / $invoiced * 100) : 0,
            ],
            'leads_total'     => (int) $byStage->sum(),
            'leads_by_stage'  => $byStage,
            'bookings_total'  => BmsBooking::where('business_id', $businessId)->count(),
            'upcoming_visits' => BmsOfficeVisit::where('business_id', $businessId)
                ->whereDate('visit_date', '>=', now()->toDateString())
                ->whereIn('status', ['requested', 'scheduled'])->count(),
            'halls_total'     => BmsHall::where('business_id', $businessId)->where('is_active', true)->count(),
            'venues_total'    => BmsVenue::where('business_id', $businessId)->count(),
            'tasks_open'      => BmsTask::where('business_id', $businessId)->where('status', '!=', 'done')->count(),
            'tasks_overdue'   => BmsTask::where('business_id', $businessId)->where('status', '!=', 'done')
                ->whereNotNull('due_at')->where('due_at', '<', now())->count(),
            'assets_total'      => BmsAsset::where('business_id', $businessId)->count(),
            'assets_maintenance'=> BmsAsset::where('business_id', $businessId)->where('status', 'maintenance')->count(),
            'upcoming_bookings' => BmsBooking::where('bms_bookings.business_id', $businessId)
                ->leftJoin('contacts', 'contacts.id', '=', 'bms_bookings.contact_id')
                ->whereDate('bms_bookings.event_date', '>=', now()->toDateString())
                ->whereNotIn('bms_bookings.status', ['cancelled'])
                ->orderBy('bms_bookings.event_date')->limit(6)
                ->get([
                    'bms_bookings.id', 'bms_bookings.booking_no', 'bms_bookings.event_date',
                    'bms_bookings.event_type', 'bms_bookings.status', 'bms_bookings.attendees',
                    'contacts.name as contact_name',
                ])
                ->map(fn ($b) => [
                    'id'         => $b->id,
                    'booking_no' => $b->booking_no,
                    'event_date' => $b->event_date ? substr((string) $b->event_date, 0, 10) : null,
                    'event_type' => $b->event_type,
                    'status'     => $b->status,
                    'attendees'  => $b->attendees,
                    'contact'    => $b->contact_name,
                ])->values(),
        ]);
    }

    /** Lead detail with selected halls + visits (sales context). */
    public function leadDetail(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);

        $lead = BmsLead::where('business_id', $businessId)
            ->with(['contact:id,name,mobile,email', 'selectedHalls.hall:id,name', 'selectedHalls.package:id,name,price'])
            ->find($id);
        if (! $lead) {
            return $this->fail('Not found', 404);
        }

        $visits = BmsOfficeVisit::where('business_id', $businessId)->where('lead_id', $lead->id)
            ->orderBy('visit_date')->get(['id', 'visit_date', 'visit_time', 'status', 'location_id']);

        return $this->ok([
            'lead' => [
                'id'         => $lead->id,
                'stage'      => $lead->stage,
                'source'     => $lead->source,
                'event_type' => $lead->event_type,
                'attendees'  => $lead->attendees,
                'fnb_level'  => $lead->fnb_level,
                'venue_preference' => $lead->venue_preference,
                'budget_max' => $lead->budget_max,
                'candidate_dates'  => $lead->candidate_dates,
                'contact'    => $lead->contact ? ['name' => $lead->contact->name, 'mobile' => $lead->contact->mobile, 'email' => $lead->contact->email] : null,
                'halls'      => $lead->selectedHalls->map(fn ($lh) => $this->leadHallBreakdown($lh))->values(),
                'grand_total'=> (float) $lead->selectedHalls->sum(fn ($lh) => app(BookingService::class)->estimateHall($lh)),
                'visits'     => $visits->map(fn ($v) => [
                    'date' => optional($v->visit_date)->toDateString(), 'time' => $v->visit_time, 'status' => $v->status,
                ])->values(),
            ],
        ]);
    }

    /** Full configured breakdown for one selected hall (package, add-ons, swaps, total). */
    protected function leadHallBreakdown($lh): array
    {
        $pkg = $lh->package;

        $addons = collect((array) $lh->addons)->map(function ($a) {
            $addon = BmsAddon::find($a['addon_id'] ?? null);
            return $addon ? ['name' => $addon->name, 'qty' => (float) ($a['qty'] ?? 1), 'price' => (float) $addon->price] : null;
        })->filter()->values();

        $extras = [];
        $swaps = [];
        foreach ((array) $lh->package_items as $pi) {
            $item = BmsPackageItem::find($pi['item_id'] ?? null);
            if (! $item) {
                continue;
            }
            $included = ! empty($pi['included']);
            if ($item->is_addon && $included) {
                $extras[] = ['name' => $item->name, 'qty' => (float) ($pi['qty'] ?? $item->quantity), 'price' => (float) ($item->addon_price ?? 0)];
            }
            $vi = (int) ($pi['variant_index'] ?? 0);
            if ($vi > 0 && is_array($item->options) && isset($item->options[$vi - 1])) {
                $swaps[] = ['item' => $item->name, 'label' => $item->options[$vi - 1]['label'] ?? '', 'delta' => (float) ($item->options[$vi - 1]['price_delta'] ?? 0)];
            }
        }

        return [
            'hall'          => optional($lh->hall)->name,
            'package'       => optional($pkg)->name,
            'package_price' => (float) optional($pkg)->price,
            'addons'        => $addons,
            'extras'        => $extras,
            'swaps'         => $swaps,
            'total'         => app(BookingService::class)->estimateHall($lh),
            'score'         => $lh->score,
        ];
    }

    /** Ad-hoc matching for walk-in / phone inquiries (scenario 2.1). */
    public function match(Request $request, MatchingEngine $engine)
    {
        $businessId = $this->authBusinessId($request);
        $criteria = $request->validate([
            'attendees'        => 'nullable|integer|min:0',
            'fnb_level'        => 'nullable|in:high_tea,drinks_only,set_menu,open_buffet,custom',
            'venue_preference' => 'nullable|in:open,closed,both',
            'area_id'          => 'nullable|integer',
            'budget'           => 'nullable|numeric|min:0',
            'dates'            => 'nullable|array',
        ]);

        $outcome = $engine->run($businessId, $criteria);
        $owner = $request->attributes->get('bms_owner');
        $engine->logRun($businessId, $outcome, ['source' => 'employee', 'created_by' => $owner->id ?? null]);

        return $this->ok([
            'threshold'     => $outcome['threshold'],
            'matched_count' => $outcome['matched_count'],
            'results'       => $outcome['results'],
        ]);
    }

    /** Convert a lead's hall selection into a (tentative) booking. */
    public function book(Request $request, int $leadId, BookingService $bookings)
    {
        $businessId = $this->authBusinessId($request);
        $data = $request->validate(['event_date' => 'nullable|date']);

        $lead = BmsLead::where('business_id', $businessId)->findOrFail($leadId);
        $owner = $request->attributes->get('bms_owner');

        try {
            $booking = $bookings->createFromLead($lead, $data['event_date'] ?? null, $owner->id ?? null);
        } catch (HoldConflictException $e) {
            return $this->fail($e->getMessage(), 409);
        } catch (\RuntimeException $e) {
            return $this->fail($e->getMessage(), 422);
        }

        return $this->ok($this->bookingPayload($booking));
    }

    /** Confirm a booking → raises the ERP invoice. */
    public function confirmBooking(Request $request, int $id, BookingService $bookings)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);

        $invoiceId = $bookings->confirm($booking);

        return $this->ok(array_merge($this->bookingPayload($booking->fresh()), ['erp_invoice_id' => $invoiceId]));
    }

    /** Record a payment against a booking's ERP invoice. */
    public function recordPayment(Request $request, int $id, PaymentServiceContract $payments)
    {
        $businessId = $this->authBusinessId($request);
        $data = $request->validate([
            'amount'     => 'required|numeric|min:0.01',
            'method'     => 'nullable|string|max:30',
            'account_id' => 'nullable|integer',
            'note'       => 'nullable|string|max:255',
        ]);
        $owner = $request->attributes->get('bms_owner');

        try {
            $payments->recordPayment($businessId, $id, array_merge($data, ['user_id' => $owner->id ?? null]));
        } catch (\RuntimeException $e) {
            return $this->fail($e->getMessage(), 422);
        }

        $booking = BmsBooking::find($id);
        app(BookingService::class)->notifyContact(
            $booking, 'payment_received', 'Payment received',
            'We have received your payment of ' . number_format((float) $data['amount']) . ' for booking ' . $booking->booking_no . '.'
        );

        return $this->ok($this->bookingPayload($booking));
    }

    /** Booking + invoice/payment status. */
    public function bookingStatus(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);

        return $this->ok($this->bookingPayload($booking));
    }

    /** Move a confirmed booking to the contracted state (deposit-gated). */
    public function contractBooking(Request $request, int $id, BookingService $bookings)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);
        $owner = $request->attributes->get('bms_owner');

        try {
            $bookings->markContracted($booking, $owner->id ?? null);
        } catch (\RuntimeException $e) {
            return $this->fail($e->getMessage(), 422);
        }

        return $this->ok($this->bookingPayload($booking->fresh()));
    }

    /** Close out a booking after the event. */
    public function completeBooking(Request $request, int $id, BookingService $bookings)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);
        $owner = $request->attributes->get('bms_owner');

        $bookings->markCompleted($booking, $owner->id ?? null);

        return $this->ok($this->bookingPayload($booking->fresh()));
    }

    /** Preview the cancellation penalty/refund (live quote, no side effects). */
    public function cancelQuote(Request $request, int $id, CancellationService $cancellations)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);

        $override = $this->resolveOverride($request);

        return $this->ok([
            'quote'  => $cancellations->quote($booking, $override),
            'policy' => $cancellations->policy($businessId),
            'reasons' => $cancellations->reasons($businessId),
            'can_override' => $this->canOverridePenalty($request),
        ]);
    }

    /** Cancel a booking per the policy (refund + free the date). */
    public function cancelBooking(Request $request, int $id, CancellationService $cancellations)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);
        $owner = $request->attributes->get('bms_owner');

        $request->validate([
            'reason'                  => 'nullable|string|max:255',
            'reason_code'             => 'nullable|string|max:60',
            'override_penalty_percent' => 'nullable|numeric|min:0|max:100',
        ]);
        $override = $this->resolveOverride($request);

        try {
            $result = $cancellations->cancel($booking, [
                'reason'                   => $request->input('reason'),
                'reason_code'              => $request->input('reason_code'),
                'override_penalty_percent' => $override,
                'user_id'                  => $owner->id ?? null,
            ]);
        } catch (\RuntimeException $e) {
            return $this->fail($e->getMessage(), 422);
        }

        return $this->ok(array_merge($this->bookingPayload($booking->fresh()), ['cancellation' => $result]));
    }

    /** Preview the reschedule fee for a new date. */
    public function rescheduleQuote(Request $request, int $id, CancellationService $cancellations)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);
        $override = $this->resolveOverride($request);

        return $this->ok([
            'quote'        => $cancellations->quote($booking, $override),
            'policy'       => $cancellations->policy($businessId),
            'can_override' => $this->canOverridePenalty($request),
        ]);
    }

    /** Move a booking to a new event date (charges the policy rebooking fee). */
    public function rescheduleBooking(Request $request, int $id, CancellationService $cancellations)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);
        $owner = $request->attributes->get('bms_owner');

        $request->validate([
            'event_date'               => 'required|date',
            'override_penalty_percent' => 'nullable|numeric|min:0|max:100',
        ]);
        $override = $this->resolveOverride($request);

        try {
            $result = $cancellations->reschedule($booking, $request->input('event_date'), [
                'override_penalty_percent' => $override,
                'user_id'                  => $owner->id ?? null,
            ]);
        } catch (HoldConflictException $e) {
            return $this->fail($e->getMessage(), 409);
        } catch (\RuntimeException $e) {
            return $this->fail($e->getMessage(), 422);
        }

        return $this->ok(array_merge($this->bookingPayload($booking->fresh()), ['reschedule' => $result]));
    }

    /** Booking audit trail (lifecycle + money events). */
    public function bookingEvents(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        BmsBooking::where('business_id', $businessId)->findOrFail($id);

        $events = \Modules\Bms\Entities\BmsBookingEvent::where('booking_id', $id)
            ->latest()->get(['id', 'type', 'payload', 'user_id', 'created_at']);

        return $this->ok(['events' => $events->map(fn ($e) => [
            'id'      => $e->id,
            'type'    => $e->type,
            'payload' => $e->payload,
            'at'      => optional($e->created_at)->toIso8601String(),
        ])->values()]);
    }

    /**
     * Only honour an override percentage when the employee actually holds the
     * override permission — otherwise the policy value is enforced.
     */
    protected function resolveOverride(Request $request): ?float
    {
        if (! $request->filled('override_penalty_percent')) {
            return null;
        }
        return $this->canOverridePenalty($request)
            ? (float) $request->input('override_penalty_percent')
            : null;
    }

    protected function canOverridePenalty(Request $request): bool
    {
        $owner = $request->attributes->get('bms_owner');
        return $owner && method_exists($owner, 'can') && $owner->can('bms.bookings.override_penalty');
    }

    /** Tasks auto-generated for a booking. */
    public function bookingTasks(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        BmsBooking::where('business_id', $businessId)->findOrFail($id);

        $tasks = BmsTask::where('business_id', $businessId)->where('booking_id', $id)
            ->orderBy('due_at')
            ->get(['id', 'title', 'department', 'due_at', 'assignee_role', 'status', 'completed_at']);

        return $this->ok(['tasks' => $tasks->map(fn ($t) => [
            'id' => $t->id, 'title' => $t->title, 'department' => $t->department,
            'due_at' => optional($t->due_at)->toIso8601String(), 'role' => $t->assignee_role,
            'status' => $t->status,
        ])->values()]);
    }

    /**
     * Consolidated operations board: every task across bookings with its
     * booking context, filterable by status (open|done|all, default open) and
     * department, ordered by due date. Lets staff work the day without opening
     * each booking.
     */
    public function tasks(Request $request)
    {
        $businessId = $this->authBusinessId($request);
        $status = $request->input('status', 'open');
        $department = $request->input('department');

        $rows = BmsTask::where('bms_tasks.business_id', $businessId)
            ->join('bms_bookings', 'bms_bookings.id', '=', 'bms_tasks.booking_id')
            ->leftJoin('contacts', 'contacts.id', '=', 'bms_bookings.contact_id')
            ->when($status === 'open', fn ($q) => $q->where('bms_tasks.status', '!=', 'done'))
            ->when($status === 'done', fn ($q) => $q->where('bms_tasks.status', 'done'))
            ->when($department, fn ($q) => $q->where('bms_tasks.department', $department))
            ->orderByRaw('bms_tasks.status = "done"')
            ->orderByRaw('bms_tasks.due_at is null, bms_tasks.due_at')
            ->get([
                'bms_tasks.id', 'bms_tasks.title', 'bms_tasks.department', 'bms_tasks.due_at',
                'bms_tasks.assignee_role', 'bms_tasks.status', 'bms_tasks.booking_id',
                'bms_tasks.read_at', 'bms_tasks.accepted_at',
                'bms_bookings.booking_no', 'bms_bookings.event_date', 'contacts.name as contact_name',
            ]);

        $now = now();
        return $this->ok([
            'departments' => BmsTask::where('business_id', $businessId)->distinct()->pluck('department')->filter()->values(),
            'tasks' => $rows->map(fn ($t) => [
                'id'         => $t->id,
                'title'      => $t->title,
                'department' => $t->department,
                'due_at'     => optional($t->due_at)->toIso8601String(),
                'overdue'    => $t->status !== 'done' && $t->due_at && $t->due_at->lt($now),
                'role'       => $t->assignee_role,
                'status'     => $t->status,
                'booking_id' => $t->booking_id,
                'booking_no' => $t->booking_no,
                'event_date' => $t->event_date ? substr((string) $t->event_date, 0, 10) : null,
                'contact'    => $t->contact_name,
                'read'       => $t->read_at !== null,
                'accepted'   => $t->accepted_at !== null,
            ])->values(),
        ]);
    }

    /** Task detail + activity trail; opening it records a 'read' acknowledgment. */
    public function taskDetail(Request $request, int $id, TaskService $tasks)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $task = BmsTask::where('business_id', $businessId)->findOrFail($id);
        $tasks->markRead($task, $owner->id ?? null);
        $task = $task->fresh();

        return $this->ok(['task' => $this->taskPayload($task)]);
    }

    /** The assignee accepts (acknowledges ownership of) a task. */
    public function acceptTask(Request $request, int $id, TaskService $tasks)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $task = BmsTask::where('business_id', $businessId)->findOrFail($id);
        $tasks->accept($task, $owner->id ?? null);

        return $this->ok(['task' => $this->taskPayload($task->fresh())]);
    }

    protected function taskPayload(BmsTask $t): array
    {
        $users = \App\User::whereIn('id', $t->events->pluck('user_id')->filter())->get()->keyBy('id');
        return [
            'id' => $t->id, 'title' => $t->title, 'department' => $t->department,
            'status' => $t->status, 'due_at' => optional($t->due_at)->toIso8601String(),
            'received_at' => optional($t->received_at)->toIso8601String(),
            'read_at'     => optional($t->read_at)->toIso8601String(),
            'accepted_at' => optional($t->accepted_at)->toIso8601String(),
            'events' => $t->events->map(fn ($e) => [
                'type' => $e->type, 'note' => $e->note,
                'user' => optional($users->get($e->user_id))->first_name,
                'at' => optional($e->created_at)->toIso8601String(),
            ])->values(),
        ];
    }

    /** Mark a task complete. */
    public function completeTask(Request $request, int $id, TaskService $tasks)
    {
        $businessId = $this->authBusinessId($request);
        $task = BmsTask::where('business_id', $businessId)->findOrFail($id);
        $owner = $request->attributes->get('bms_owner');

        $tasks->complete($task, $owner->id ?? null);

        return $this->ok(['id' => $task->id, 'status' => $task->status]);
    }

    protected function bookingPayload(BmsBooking $booking): array
    {
        $invoice = app(InvoiceServiceContract::class)->statusForBooking($booking);
        $plan = app(PaymentPlanService::class);

        return [
            'booking' => [
                'id'              => $booking->id,
                'booking_no'      => $booking->booking_no,
                'status'          => $booking->status,
                'event_date'      => optional($booking->event_date)->toDateString(),
                'total'           => (float) $booking->total_amount,
                'penalty_amount'  => $booking->penalty_amount !== null ? (float) $booking->penalty_amount : null,
                'refund_amount'   => $booking->refund_amount !== null ? (float) $booking->refund_amount : null,
                'rescheduled_from' => optional($booking->rescheduled_from)->toDateString(),
                'reschedule_count' => (int) $booking->reschedule_count,
                'cancelled_at'    => optional($booking->cancelled_at)->toIso8601String(),
                'contracted_at'   => optional($booking->contracted_at)->toIso8601String(),
                'completed_at'    => optional($booking->completed_at)->toIso8601String(),
            ],
            'invoice'  => $invoice,
            'schedule' => $plan->schedule($booking),
            'deposit_met' => $plan->depositMet($booking),
        ];
    }

    // ---- Maintenance (P2 staff: raise + work the queue) ------------------

    /** Maintenance queue, filterable by status / mine / overdue. */
    public function maintenance(Request $request)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $status = $request->input('status'); // open|<status>|all
        $scope = $request->input('scope');   // mine

        $rows = BmsMaintenanceRequest::where('bms_maintenance_requests.business_id', $businessId)
            ->leftJoin('bms_halls', 'bms_halls.id', '=', 'bms_maintenance_requests.hall_id')
            ->when($status === 'open' || $status === null, fn ($q) => $q->whereNotIn('bms_maintenance_requests.status', ['verified', 'cancelled']))
            ->when($status && ! in_array($status, ['open', 'all'], true), fn ($q) => $q->where('bms_maintenance_requests.status', $status))
            ->when($scope === 'mine' && $owner, fn ($q) => $q->where('bms_maintenance_requests.assigned_to', $owner->id))
            ->orderByRaw("FIELD(bms_maintenance_requests.priority,'urgent','high','medium','low')")
            ->orderBy('bms_maintenance_requests.due_at')
            ->get([
                'bms_maintenance_requests.id', 'bms_maintenance_requests.code', 'bms_maintenance_requests.title',
                'bms_maintenance_requests.category', 'bms_maintenance_requests.priority', 'bms_maintenance_requests.status',
                'bms_maintenance_requests.due_at', 'bms_maintenance_requests.assigned_to', 'bms_halls.name as hall_name',
            ]);

        $now = now();
        return $this->ok(['requests' => $rows->map(fn ($r) => [
            'id'       => $r->id, 'code' => $r->code, 'title' => $r->title,
            'category' => $r->category, 'priority' => $r->priority, 'status' => $r->status,
            'hall'     => $r->hall_name,
            'due_at'   => optional($r->due_at)->toIso8601String(),
            'overdue'  => ! in_array($r->status, ['verified', 'cancelled'], true) && $r->due_at && $r->due_at->lt($now),
            'mine'     => $owner && (int) $r->assigned_to === (int) $owner->id,
        ])->values()]);
    }

    /** One maintenance request with its update trail. */
    public function maintenanceDetail(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        $r = BmsMaintenanceRequest::where('business_id', $businessId)->findOrFail($id);
        $updates = BmsMaintenanceUpdate::where('request_id', $id)->latest()->get();

        return $this->ok(['request' => [
            'id' => $r->id, 'code' => $r->code, 'title' => $r->title, 'description' => $r->description,
            'category' => $r->category, 'priority' => $r->priority, 'status' => $r->status,
            'hall_id' => $r->hall_id, 'asset_id' => $r->asset_id, 'assigned_to' => $r->assigned_to,
            'due_at' => optional($r->due_at)->toIso8601String(),
            'resolved_at' => optional($r->resolved_at)->toIso8601String(),
            'verified_at' => optional($r->verified_at)->toIso8601String(),
            'flow' => MaintenanceService::FLOW[$r->status] ?? [],
            'updates' => $updates->map(fn ($u) => [
                'id' => $u->id, 'from' => $u->from_status, 'to' => $u->to_status,
                'comment' => $u->comment, 'at' => optional($u->created_at)->toIso8601String(),
            ])->values(),
        ]]);
    }

    /** Raise a maintenance request from the field. */
    public function createMaintenance(Request $request, MaintenanceService $svc)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $data = $request->validate([
            'title'       => 'required|string|max:150',
            'description' => 'nullable|string|max:1000',
            'hall_id'     => 'nullable|integer',
            'location_id' => 'nullable|integer',
            'asset_id'    => 'nullable|integer',
            'category'    => 'nullable|string|max:30',
            'priority'    => 'nullable|in:low,medium,high,urgent',
        ]);

        $r = $svc->create($businessId, $data, $owner->id ?? null);

        return $this->ok(['id' => $r->id, 'code' => $r->code]);
    }

    /** Advance a maintenance request through its workflow. */
    public function transitionMaintenance(Request $request, int $id, MaintenanceService $svc)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $r = BmsMaintenanceRequest::where('business_id', $businessId)->findOrFail($id);
        $data = $request->validate([
            'to'        => 'required|string|max:20',
            'comment'   => 'nullable|string|max:500',
            'assign_to' => 'nullable|integer',
        ]);

        try {
            $svc->transition($r, $data['to'], $owner->id ?? null, $data['comment'] ?? null, $data['assign_to'] ?? null);
        } catch (\RuntimeException $e) {
            return $this->fail($e->getMessage(), 422);
        }

        return $this->maintenanceDetail($request, $id);
    }

    // ---- AI assistant (grounded on whitelisted BMS data via AiCore) ------

    public function aiChat(Request $request, \Modules\Bms\Services\Ai\BmsAssistant $assistant)
    {
        $businessId = $this->authBusinessId($request);
        $data = $request->validate(['message' => 'required|string|max:500', 'lang' => 'nullable|string|max:5']);

        $res = $assistant->answer($businessId, $data['message'], $data['lang'] ?? app()->getLocale());

        if (! $res['ok']) {
            $reason = $res['error'] === 'ai_disabled' || $res['error'] === 'ai_core_absent'
                ? __('bms::lang.ai_unavailable') : ($res['error'] ?: 'error');
            return $this->fail($reason, 422, ['reason' => $res['error']]);
        }

        return $this->ok(['reply' => $res['text']]);
    }

    // ---- Client change requests (approve / reject) -----------------------

    public function changeRequests(Request $request)
    {
        $businessId = $this->authBusinessId($request);
        $status = $request->input('status', 'pending');

        $rows = \Modules\Bms\Entities\BmsChangeRequest::where('bms_change_requests.business_id', $businessId)
            ->leftJoin('contacts', 'contacts.id', '=', 'bms_change_requests.contact_id')
            ->when($status !== 'all', fn ($q) => $q->where('bms_change_requests.status', $status))
            ->orderByRaw("bms_change_requests.status = 'pending' desc")
            ->latest('bms_change_requests.id')
            ->get([
                'bms_change_requests.id', 'bms_change_requests.type', 'bms_change_requests.message',
                'bms_change_requests.status', 'bms_change_requests.staff_note', 'bms_change_requests.booking_id',
                'bms_change_requests.lead_id', 'bms_change_requests.created_at', 'contacts.name as contact_name',
            ]);

        return $this->ok(['requests' => $rows->map(fn ($c) => [
            'id' => $c->id, 'type' => $c->type, 'message' => $c->message, 'status' => $c->status,
            'staff_note' => $c->staff_note, 'booking_id' => $c->booking_id, 'lead_id' => $c->lead_id,
            'contact' => $c->contact_name, 'at' => optional($c->created_at)->toIso8601String(),
        ])->values()]);
    }

    public function decideChangeRequest(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $cr = \Modules\Bms\Entities\BmsChangeRequest::where('business_id', $businessId)->findOrFail($id);
        $data = $request->validate([
            'status' => 'required|in:approved,rejected',
            'note'   => 'nullable|string|max:500',
        ]);

        $cr->update([
            'status'     => $data['status'],
            'staff_note' => $data['note'] ?? null,
            'decided_by' => $owner->id ?? null,
            'decided_at' => now(),
        ]);

        // Tell the client the outcome (best effort).
        $contact = \App\Contact::find($cr->contact_id);
        if ($contact) {
            $to = array_values(array_filter([$contact->mobile, $contact->email]));
            try {
                app(\Modules\Bms\Services\Notifications\NotificationRouter::class)->notify(
                    $businessId, 'change_decided', $to,
                    'Change request ' . $data['status'],
                    'Your change request was ' . $data['status'] . ($data['note'] ?? '' ? (': ' . $data['note']) : '') . '.',
                    ['change_request_id' => $cr->id], 'change_decided:' . $cr->id
                );
            } catch (\Throwable $e) {
                \Log::warning('[BMS] change-decided notify failed: ' . $e->getMessage());
            }
        }

        return $this->ok(['id' => $cr->id, 'status' => $cr->status]);
    }

    // ---- Quality control -------------------------------------------------

    /** Get (or open) the QC inspection for a booking + stage. */
    public function qcInspection(Request $request, int $id, \Modules\Bms\Services\Qc\QcService $qc)
    {
        $businessId = $this->authBusinessId($request);
        $booking = BmsBooking::where('business_id', $businessId)->findOrFail($id);
        $stage = $request->input('stage', 'pre_event');

        $insp = $qc->inspectionFor($booking, $stage);

        return $this->ok([
            'inspection' => $this->qcPayload($insp),
            'reasons'    => $qc->reasons($businessId),
        ]);
    }

    /** Attach a recorded voice note to a QC inspection. */
    public function qcVoice(Request $request, int $id, \Modules\Bms\Services\Qc\QcService $qc)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $insp = \Modules\Bms\Entities\BmsQcInspection::where('business_id', $businessId)->findOrFail($id);
        $request->validate(['file' => 'required|file|max:20480']); // 20 MB audio

        $stored = app(\Modules\Bms\Services\Contracts\MediaServiceContract::class)
            ->store($businessId, "qc/{$insp->id}", $request->file('file'));
        $qc->addVoice($insp, $stored['url'] ?: $stored['path'], $owner->id ?? null);

        return $this->ok(['inspection' => $this->qcPayload($insp->fresh())]);
    }

    /** Submit QC results (pass/fail/na + notes per item). */
    public function qcSubmit(Request $request, int $id, \Modules\Bms\Services\Qc\QcService $qc)
    {
        $businessId = $this->authBusinessId($request);
        $owner = $request->attributes->get('bms_owner');
        $insp = \Modules\Bms\Entities\BmsQcInspection::where('business_id', $businessId)->findOrFail($id);
        $data = $request->validate(['items' => 'required|array']);

        $insp = $qc->record($insp, $data['items'], $owner->id ?? null);

        return $this->ok(['inspection' => $this->qcPayload($insp)]);
    }

    protected function qcPayload(\Modules\Bms\Entities\BmsQcInspection $i): array
    {
        $events = \Modules\Bms\Entities\BmsQcEvent::where('inspection_id', $i->id)->latest()->get();
        $users = \App\User::whereIn('id', $events->pluck('user_id')->filter())->get()->keyBy('id');

        return [
            'id' => $i->id, 'booking_id' => $i->booking_id, 'stage' => $i->stage,
            'status' => $i->status, 'score' => $i->score,
            'items' => $i->items ?? [],
            'completed_at' => optional($i->completed_at)->toIso8601String(),
            'events' => $events->map(fn ($e) => [
                'type' => $e->type, 'detail' => $e->detail,
                'user' => optional($users->get($e->user_id))->first_name,
                'at' => optional($e->created_at)->toIso8601String(),
            ])->values(),
        ];
    }

    // ---- Transfers + acceptance audit (scenarios 2.5–2.7) ----------------

    /** List transfer requests (optionally by status). */
    public function transfers(Request $request)
    {
        $businessId = $this->authBusinessId($request);

        $rows = BmsTransferRequest::where('bms_transfer_requests.business_id', $businessId)
            ->leftJoin('bms_halls', 'bms_halls.id', '=', 'bms_transfer_requests.to_hall_id')
            ->when($request->filled('status'), fn ($q) => $q->where('bms_transfer_requests.status', $request->input('status')))
            ->withCount('items')
            ->orderByDesc('bms_transfer_requests.id')
            ->select(['bms_transfer_requests.id', 'bms_transfer_requests.type', 'bms_transfer_requests.status',
                'bms_transfer_requests.booking_id', 'bms_transfer_requests.accepted_at', 'bms_halls.name as to_hall'])
            ->paginate(min((int) $request->input('per_page', 25), 100));

        return $this->ok(['transfers' => $rows]);
    }

    /** Registered assets for the transfer picker (search by code/name). */
    public function assets(Request $request)
    {
        $businessId = $this->authBusinessId($request);
        $status = $request->input('status');
        $category = $request->input('category');

        $rows = BmsAsset::where('bms_assets.business_id', $businessId)
            ->leftJoin('business_locations', 'business_locations.id', '=', 'bms_assets.location_id')
            ->leftJoin('bms_halls', 'bms_halls.id', '=', 'bms_assets.hall_id')
            ->when($request->filled('q'), function ($query) use ($request) {
                $term = '%' . $request->input('q') . '%';
                $query->where(fn ($w) => $w->where('bms_assets.code', 'like', $term)->orWhere('bms_assets.name', 'like', $term));
            })
            ->when($request->boolean('available_only'), fn ($q) => $q->whereNotIn('bms_assets.status', ['retired']))
            ->when($status, fn ($q) => $q->where('bms_assets.status', $status))
            ->when($category, fn ($q) => $q->where('bms_assets.category', $category))
            ->orderBy('bms_assets.code')
            ->limit(500)
            ->get([
                'bms_assets.id', 'bms_assets.code', 'bms_assets.name', 'bms_assets.category',
                'bms_assets.condition', 'bms_assets.status', 'bms_assets.quantity',
                'bms_assets.location_id', 'bms_assets.hall_id',
                'business_locations.name as location_name', 'bms_halls.name as hall_name',
            ]);

        return $this->ok([
            'assets'     => $rows,
            'categories' => BmsAsset::where('business_id', $businessId)->distinct()->pluck('category')->filter()->values(),
            'statuses'   => BmsAsset::where('business_id', $businessId)->distinct()->pluck('status')->filter()->values(),
        ]);
    }

    /** Relocate an asset to a hall (or location), writing a movement audit. */
    public function relocateAsset(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        $data = $request->validate([
            'hall_id'     => 'nullable|integer',
            'location_id' => 'nullable|integer',
            'condition'   => 'nullable|string|max:30',
            'notes'       => 'nullable|string|max:255',
        ]);

        $asset = BmsAsset::where('business_id', $businessId)->findOrFail($id);
        $hallId = $data['hall_id'] ?? null;
        $locationId = $hallId
            ? optional(BmsHall::where('business_id', $businessId)->find($hallId))->location_id
            : ($data['location_id'] ?? null);

        if (! $locationId) {
            return $this->fail('A target hall or location is required.', 422);
        }

        $owner = $request->attributes->get('bms_owner');
        app(AssetServiceContract::class)->transferLocation($businessId, (int) $asset->id, (int) $locationId, [
            'to_hall_id' => $hallId,
            'condition'  => $data['condition'] ?? $asset->condition,
            'reason'     => 'manual',
            'moved_by'   => $owner->id ?? null,
            'notes'      => $data['notes'] ?? null,
        ]);

        return $this->ok(['id' => $asset->id]);
    }

    /** An asset's movement / chain-of-custody history (newest first). */
    public function assetHistory(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        $asset = BmsAsset::where('business_id', $businessId)->findOrFail($id);

        $rows = \Modules\Bms\Entities\BmsAssetMovement::where('bms_asset_movements.business_id', $businessId)
            ->where('asset_id', $id)
            ->leftJoin('business_locations as fl', 'fl.id', '=', 'bms_asset_movements.from_location_id')
            ->leftJoin('business_locations as tl', 'tl.id', '=', 'bms_asset_movements.to_location_id')
            ->leftJoin('bms_halls as fh', 'fh.id', '=', 'bms_asset_movements.from_hall_id')
            ->leftJoin('bms_halls as th', 'th.id', '=', 'bms_asset_movements.to_hall_id')
            ->leftJoin('users as u', 'u.id', '=', 'bms_asset_movements.moved_by')
            ->orderByDesc('bms_asset_movements.created_at')
            ->get([
                'bms_asset_movements.id', 'bms_asset_movements.condition', 'bms_asset_movements.reason',
                'bms_asset_movements.notes', 'bms_asset_movements.created_at', 'bms_asset_movements.transfer_id',
                'fl.name as from_location', 'tl.name as to_location',
                'fh.name as from_hall', 'th.name as to_hall',
                'u.first_name as u_first', 'u.surname as u_surname',
            ]);

        return $this->ok([
            'asset' => ['id' => $asset->id, 'code' => $asset->code, 'name' => $asset->name],
            'movements' => $rows->map(fn ($m) => [
                'id'        => $m->id,
                'from'      => $m->from_hall ?: $m->from_location,
                'to'        => $m->to_hall ?: $m->to_location,
                'condition' => $m->condition,
                'reason'    => $m->reason,
                'notes'     => $m->notes,
                'by'        => trim(($m->u_first ?? '') . ' ' . ($m->u_surname ?? '')) ?: null,
                'at'        => optional($m->created_at)->toIso8601String(),
            ])->values(),
        ]);
    }

    /**
     * Office visits list for staff: upcoming by default (today onward, not
     * done/cancelled), or filtered by a specific status; with contact context.
     */
    public function visits(Request $request)
    {
        $businessId = $this->authBusinessId($request);
        $status = $request->input('status', 'upcoming');

        $rows = BmsOfficeVisit::where('bms_office_visits.business_id', $businessId)
            ->leftJoin('contacts', 'contacts.id', '=', 'bms_office_visits.contact_id')
            ->leftJoin('business_locations', 'business_locations.id', '=', 'bms_office_visits.location_id')
            ->when($status === 'upcoming', fn ($q) => $q->whereDate('visit_date', '>=', now()->toDateString())
                ->whereNotIn('bms_office_visits.status', ['done', 'cancelled']))
            ->when(in_array($status, ['requested', 'confirmed', 'done', 'cancelled'], true),
                fn ($q) => $q->where('bms_office_visits.status', $status))
            ->orderBy('bms_office_visits.visit_date')->orderBy('bms_office_visits.visit_time')
            ->limit(300)
            ->get([
                'bms_office_visits.id', 'bms_office_visits.visit_date', 'bms_office_visits.visit_time',
                'bms_office_visits.status', 'bms_office_visits.lead_id',
                'contacts.name as contact_name', 'contacts.mobile as contact_mobile',
                'business_locations.name as location_name',
            ]);

        $today = now()->toDateString();
        return $this->ok(['visits' => $rows->map(fn ($v) => [
            'id'       => $v->id,
            'date'     => $v->visit_date ? substr((string) $v->visit_date, 0, 10) : null,
            'time'     => $v->visit_time,
            'status'   => $v->status,
            'overdue'  => $v->status !== 'done' && $v->status !== 'cancelled' && $v->visit_date && substr((string) $v->visit_date, 0, 10) < $today,
            'lead_id'  => $v->lead_id,
            'contact'  => $v->contact_name,
            'mobile'   => $v->contact_mobile,
            'location' => $v->location_name,
        ])->values()]);
    }

    /** Raise a transfer request (warehouse→hall / hall→hall) with items. */
    public function createTransfer(Request $request, TransferService $transfers)
    {
        $businessId = $this->authBusinessId($request);
        $data = $request->validate([
            'type'             => 'nullable|in:warehouse_to_hall,hall_to_hall',
            'from_location_id' => 'nullable|integer',
            'from_hall_id'     => 'nullable|integer',
            'to_hall_id'       => 'required|integer',
            'booking_id'       => 'nullable|integer',
            'notes'            => 'nullable|string|max:255',
            'items'            => 'required|array|min:1',
            'items.*.name'     => 'required|string|max:191',
            'items.*.kind'     => 'nullable|in:consumable,asset',
            'items.*.quantity' => 'nullable|numeric|min:0',
            'items.*.product_id'   => 'nullable|integer',
            'items.*.variation_id' => 'nullable|integer',
            'items.*.asset_id'     => 'nullable|integer',
        ]);
        $owner = $request->attributes->get('bms_owner');

        $transfer = $transfers->createRequest($businessId, $data, $owner->id ?? null);

        return $this->ok($this->transferPayload($transfer));
    }

    /** A single transfer with its items (for the accept screen). */
    public function transferDetail(Request $request, int $id)
    {
        $businessId = $this->authBusinessId($request);
        $transfer = BmsTransferRequest::where('business_id', $businessId)->with('items')->findOrFail($id);

        return $this->ok($this->transferPayload($transfer));
    }

    /** Accept a transfer — records the acceptance audit + posts stock. */
    public function acceptTransfer(Request $request, int $id, TransferService $transfers)
    {
        $businessId = $this->authBusinessId($request);
        $request->validate([
            'items'                      => 'nullable|array',
            'items.*.accepted_quantity'  => 'nullable|numeric|min:0',
            'items.*.condition'          => 'nullable|string|max:60',
        ]);

        $transfer = BmsTransferRequest::where('business_id', $businessId)->with('items')->findOrFail($id);
        $owner = $request->attributes->get('bms_owner');

        // Re-key the acceptance map by item id.
        $acceptance = [];
        foreach ((array) $request->input('items', []) as $itemId => $vals) {
            $acceptance[(int) $itemId] = $vals;
        }

        try {
            $transfer = $transfers->accept($transfer, $owner->id ?? 0, $acceptance);
        } catch (\RuntimeException $e) {
            return $this->fail($e->getMessage(), 422);
        }

        return $this->ok($this->transferPayload($transfer));
    }

    protected function transferPayload(BmsTransferRequest $transfer): array
    {
        return [
            'transfer' => [
                'id'          => $transfer->id,
                'type'        => $transfer->type,
                'status'      => $transfer->status,
                'to_hall_id'  => $transfer->to_hall_id,
                'booking_id'  => $transfer->booking_id,
                'accepted_by' => $transfer->accepted_by,
                'accepted_at' => optional($transfer->accepted_at)->toIso8601String(),
                'stock_refs'  => $transfer->stock_refs,
                'items'       => $transfer->items->map(fn ($i) => [
                    'id' => $i->id, 'name' => $i->name, 'kind' => $i->kind,
                    'asset_id' => $i->asset_id,
                    'quantity' => (float) $i->quantity,
                    'accepted_quantity' => $i->accepted_quantity !== null ? (float) $i->accepted_quantity : null,
                    'condition' => $i->condition,
                ])->values(),
            ],
        ];
    }

    /** Place a tentative hold on a hall-date (row-locked, double-booking safe). */
    public function placeHold(Request $request, HoldService $holds)
    {
        $data = $request->validate([
            'hall_id' => 'required|integer',
            'date'    => 'required|date',
            'slot'    => 'nullable|in:full_day,morning,evening',
        ]);

        $owner = $request->attributes->get('bms_owner');

        try {
            $hold = $holds->placeHold(
                $this->authBusinessId($request),
                $data['hall_id'],
                $data['date'],
                $data['slot'] ?? 'full_day',
                null,
                $owner->id ?? null
            );
        } catch (HoldConflictException $e) {
            return $this->fail($e->getMessage(), 409);
        }

        return $this->ok([
            'hold_token' => $hold->hold_token,
            'expires_at' => optional($hold->hold_expires_at)->toIso8601String(),
        ]);
    }

    /** Release a previously placed hold. */
    public function releaseHold(Request $request, HoldService $holds)
    {
        $data = $request->validate(['hold_token' => 'required|string|max:64']);
        $released = $holds->releaseHold($this->authBusinessId($request), $data['hold_token']);

        return $this->ok(['released' => $released]);
    }
}
