<?php

namespace Modules\Bms\Services\Matching;

use Modules\Bms\Entities\BmsHall;
use Modules\Bms\Entities\BmsHallAvailability;
use Modules\Bms\Entities\BmsMatchRun;
use Modules\Bms\Entities\BmsMatchingSetting;

/**
 * Weighted hall-matching engine (settings-driven from P1).
 *
 * score = Σ(weight × criterionMatch) / Σ(weight) × 100, computed only over the
 * criteria the requester actually specified (an unspecified criterion neither
 * helps nor penalises). Halls scoring >= threshold are matches, sorted by
 * score; the rest follow, each failed criterion carried as an explicit reason.
 *
 * Built once here and consumed by both the client PWA (P3) and the employee
 * workspace (P2 scenario 2.1) through the REST API.
 */
class MatchingEngine
{
    /**
     * Run a match for a business against a set of criteria.
     *
     * @param  array  $criteria  [
     *     'attendees'        => int|null,
     *     'fnb_level'        => string|null,
     *     'venue_preference' => 'open'|'closed'|'both'|null,
     *     'area_id'          => int|null,
     *     'budget'           => numeric|null,   // the client's budget ceiling
     *     'dates'            => array<string>|null,  // candidate Y-m-d dates
     *  ]
     * @return array  ['threshold','weights','criteria','results','matched_count','top_score']
     */
    public function run(int $businessId, array $criteria): array
    {
        $criteria = $this->normalizeCriteria($criteria);
        $setting = BmsMatchingSetting::resolve($businessId);
        $weights = $setting->weights ?? [];
        $threshold = (int) $setting->threshold;

        $halls = BmsHall::where('business_id', $businessId)
            ->where('is_active', true)
            ->with('venue:id,name')
            ->get();

        $occupied = $this->occupiedDatesByHall($businessId, $criteria['dates']);

        $results = [];
        foreach ($halls as $hall) {
            $results[] = $this->scoreHall($hall, $criteria, $weights, $threshold, $occupied[$hall->id] ?? []);
        }

        // Matches first (score desc), then non-matches (score desc).
        usort($results, function ($a, $b) {
            if ($a['matched'] !== $b['matched']) {
                return $a['matched'] ? -1 : 1;
            }
            return $b['score'] <=> $a['score'];
        });

        return [
            'threshold'     => $threshold,
            'weights'       => $weights,
            'criteria'      => $criteria,
            'results'       => $results,
            'matched_count' => count(array_filter($results, fn ($r) => $r['matched'])),
            'top_score'     => $results ? max(array_column($results, 'score')) : 0,
        ];
    }

    /**
     * Score a single hall, returning its score, matched flag, per-criterion
     * breakdown and the reasons for any failed criteria.
     */
    protected function scoreHall(BmsHall $hall, array $criteria, array $weights, int $threshold, array $occupiedDates): array
    {
        $checks = $this->criterionChecks($hall, $criteria, $occupiedDates);

        $num = 0.0;
        $den = 0.0;
        $breakdown = [];
        $reasons = [];

        foreach ($checks as $key => $check) {
            if (! $check['applicable']) {
                continue; // criterion not specified → neither helps nor penalises
            }
            $weight = (float) ($weights[$key] ?? 0);
            if ($weight <= 0) {
                continue;
            }
            $den += $weight;
            if ($check['match']) {
                $num += $weight;
            } else {
                $reasons[] = $check['reason'];
            }
            $breakdown[$key] = ['weight' => $weight, 'match' => $check['match'] ? 1 : 0];
        }

        $score = $den > 0 ? (int) round(($num / $den) * 100) : 100;

        return [
            'hall_id'    => $hall->id,
            'hall_name'  => $hall->name,
            'venue_name' => optional($hall->venue)->name,
            'score'      => $score,
            'matched'    => $score >= $threshold,
            'breakdown'  => $breakdown,
            'reasons'    => $reasons,
        ];
    }

    /**
     * The per-criterion match decisions for a hall. Each returns:
     *   applicable (was the criterion specified?), match (bool), reason (string).
     */
    protected function criterionChecks(BmsHall $hall, array $criteria, array $occupiedDates): array
    {
        return [
            'capacity'     => $this->checkCapacity($hall, $criteria['attendees']),
            'fnb_level'    => $this->checkFnb($hall, $criteria['fnb_level']),
            'venue_type'   => $this->checkVenueType($hall, $criteria['venue_preference']),
            'area'         => $this->checkArea($hall, $criteria['area_id']),
            'budget'       => $this->checkBudget($hall, $criteria['budget']),
            'availability' => $this->checkAvailability($criteria['dates'], $occupiedDates),
        ];
    }

    protected function checkCapacity(BmsHall $hall, ?int $attendees): array
    {
        if ($attendees === null) {
            return ['applicable' => false, 'match' => true, 'reason' => ''];
        }
        $ok = $attendees >= $hall->capacity_min && ($hall->capacity_max == 0 || $attendees <= $hall->capacity_max);
        $reason = $attendees < $hall->capacity_min
            ? __('bms::lang.mr_min_attendees', ['n' => $hall->capacity_min])
            : __('bms::lang.mr_max_attendees', ['n' => $hall->capacity_max]);
        return ['applicable' => true, 'match' => $ok, 'reason' => $ok ? '' : $reason];
    }

    protected function checkFnb(BmsHall $hall, ?string $level): array
    {
        if (empty($level)) {
            return ['applicable' => false, 'match' => true, 'reason' => ''];
        }
        $ok = $hall->supportsFnbLevel($level);
        $label = __('bms::lang.' . $level);
        return ['applicable' => true, 'match' => $ok, 'reason' => $ok ? '' : __('bms::lang.mr_fnb_na', ['item' => $label])];
    }

    protected function checkVenueType(BmsHall $hall, ?string $preference): array
    {
        if (empty($preference)) {
            return ['applicable' => false, 'match' => true, 'reason' => ''];
        }
        $ok = $hall->matchesVenuePreference($preference);
        $typeLabel = __('bms::lang.' . ($hall->venue_type === 'open' ? 'open_air' : $hall->venue_type));
        return ['applicable' => true, 'match' => $ok, 'reason' => $ok ? '' : __('bms::lang.mr_venue_only', ['type' => $typeLabel])];
    }

    protected function checkArea(BmsHall $hall, ?int $areaId): array
    {
        if (empty($areaId)) {
            return ['applicable' => false, 'match' => true, 'reason' => ''];
        }
        $ok = (int) $hall->area_id === (int) $areaId;
        return ['applicable' => true, 'match' => $ok, 'reason' => $ok ? '' : __('bms::lang.mr_diff_area')];
    }

    protected function checkBudget(BmsHall $hall, $budget): array
    {
        if ($budget === null || $budget === '') {
            return ['applicable' => false, 'match' => true, 'reason' => ''];
        }
        $ok = (float) $budget >= (float) $hall->min_budget;
        $min = number_format((float) $hall->min_budget);
        return ['applicable' => true, 'match' => $ok, 'reason' => $ok ? '' : __('bms::lang.mr_min_budget', ['amount' => $min])];
    }

    protected function checkAvailability(array $dates, array $occupiedDates): array
    {
        if (empty($dates)) {
            return ['applicable' => false, 'match' => true, 'reason' => ''];
        }
        $free = array_diff($dates, $occupiedDates);
        $ok = ! empty($free);
        return ['applicable' => true, 'match' => $ok, 'reason' => $ok ? '' : __('bms::lang.mr_unavail_dates')];
    }

    /**
     * Preload occupied (booked/tentative/hold/blocked) dates per hall for the
     * requested dates, in one query.
     *
     * @return array<int, array<string>>  hall_id => [Y-m-d, ...]
     */
    protected function occupiedDatesByHall(int $businessId, array $dates): array
    {
        if (empty($dates)) {
            return [];
        }

        $map = [];
        BmsHallAvailability::where('business_id', $businessId)
            ->whereIn('date', $dates)
            ->whereIn('status', ['hold', 'tentative', 'booked', 'blocked'])
            ->get(['hall_id', 'date'])
            ->each(function ($row) use (&$map) {
                $map[$row->hall_id][] = $row->date->toDateString();
            });

        return $map;
    }

    protected function normalizeCriteria(array $c): array
    {
        $dates = $c['dates'] ?? [];
        if (is_string($dates)) {
            $dates = array_filter(array_map('trim', explode(',', $dates)));
        }

        return [
            'attendees'        => isset($c['attendees']) && $c['attendees'] !== '' ? (int) $c['attendees'] : null,
            'fnb_level'        => $c['fnb_level'] ?? null,
            'venue_preference' => $c['venue_preference'] ?? null,
            'area_id'          => isset($c['area_id']) && $c['area_id'] !== '' ? (int) $c['area_id'] : null,
            'budget'           => isset($c['budget']) && $c['budget'] !== '' ? (float) $c['budget'] : null,
            'dates'            => array_values($dates),
        ];
    }

    /**
     * Persist a match run against the lead/wizard session for sales context.
     */
    public function logRun(int $businessId, array $outcome, array $context = []): BmsMatchRun
    {
        return BmsMatchRun::create([
            'business_id'       => $businessId,
            'source'            => $context['source'] ?? 'employee',
            'wizard_session_id' => $context['wizard_session_id'] ?? null,
            'lead_id'           => $context['lead_id'] ?? null,
            'criteria'          => $outcome['criteria'] ?? [],
            'results'           => $outcome['results'] ?? [],
            'top_score'         => $outcome['top_score'] ?? 0,
            'matched_count'     => $outcome['matched_count'] ?? 0,
            'created_by'        => $context['created_by'] ?? auth()->id(),
        ]);
    }
}
