// Questo file fa parte di MyTown.
// MyTown è software libero: puoi ridistribuirlo e/o modificarlo secondo
// i termini della GNU Affero General Public License come pubblicata dalla
// Free Software Foundation, versione 3.
// Distribuito senza ALCUNA GARANZIA; vedi la GNU AGPL v3 per i dettagli.
// Copyright (C) Kondor Consulting & Services S.r.l.
// SPDX-License-Identifier: AGPL-3.0-or-later

import { AfterViewInit, Component, ElementRef, HostListener, ViewChild } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ConfigService } from '../../providers/config.service';
import { BookingService } from '../../providers/booking.service';
import { CustomerRegistrationService } from '../../providers/customer-registration.service';
import { LayoutService } from '../../providers/layout.service';
import {
	Booking,
	BookingBeachPricing,
	BookingResource,
	BookingResourceType,
	BookingTimeSlot,
} from '../../models/booking.model';

interface Passerella { id: number; x: number; y: number; w: number; h: number; }
interface Stabilimento { x: number; y: number; w: number; h: number; }

interface BeachL1Zone {
	label: string;
	xStartPx: number;
	widthPx: number;
	yStartPx: number;
	heightPx: number;
	col: number;
	row: number;
	isLastCol: boolean;
	isLastRow: boolean;
	ombrLiberi: number;
	ombrTotali: number;
	palmeLibere: number;
	palmeTotali: number;
	bgColor: string;
	borderColor: string;
}

@Component({
	selector: 'app-form-prenotazione',
	templateUrl: 'form-prenotazione.page.html',
	styleUrls: ['form-prenotazione.page.scss'],
	standalone: false,
})
export class FormPrenotazionePage implements AfterViewInit {
	cmp_id = 0;
	prt_id = 0;
	bkb_id = 0;

	currentStep = 1;
	resourceViewMode: 'mappa' | 'lista' = 'mappa';
	soloLiberi = false;
	selectedGroupY: number | null = null;
	prezziExpanded = true;

	// ── Mappa L1/L2 (identico al portale Angular) ───────────────
	@ViewChild('mappaWrapperRef') mappaWrapperRef?: ElementRef<HTMLDivElement>;
	mapScale = 1;
	mapTranslateX = 0;
	mapTranslateY = 0;
	selectedZone: BeachL1Zone | null = null;
	/** Offset px applicato a canvas-scaled-outer per centrare le risorse della zona */
	l2CenterOffsetPx = 0;
	/** true quando la mappa intera entra nello schermo e si salta il livello L1 */
	_autoZone = false;
	private _containerW = 0;
	private _availH = 0;

	/** Lista completa risorse (incluse _PASSERELLA_/_STABILIMENTO_/_CANVAS_CONFIG_) passata al componente <app-resource-map> */
	resourcesAll: BookingResource[] = [];

	resourceTypes: BookingResourceType[] = [];
	get activeResourceTypes(): BookingResourceType[] {
		return this.resourceTypes.filter((t) => t.bkrt_is_active);
	}
	timeSlots: BookingTimeSlot[] = [];
	resources: BookingResource[] = [];
	passerelle: Passerella[] = [];
	stabilimento: Stabilimento | null = null;
	canvasHeight = 540;
	beachPricing: BookingBeachPricing[] = [];
	bookedResourceIds = new Set<number>();

	// ── Stato disponibilità ristorante ─────────────────────────────
	totalCapacity = 0;
	monthBookings: Booking[] = [];
	/** YYYY-MM-DD → { occupied, total } */
	dayCapacityMap = new Map<string, { occupied: number; total: number }>();
	/** 'YYYY-MM-DD|slotId' → posti occupati */
	slotOccupancyMap = new Map<string, number>();
	calendarMonth = '';
	/** Array calcolato per ion-datetime [highlightedDates] */
	highlightedDatesArray: { date: string; textColor: string; backgroundColor: string }[] = [];
	/** Risorsa "_DA_ASSEGNARE_" usata come placeholder per le prenotazioni PENDING */
	placeholderRestaurantResource: BookingResource | null = null;

	selectedType: BookingResourceType | null = null;
	selectedDate = '';
	selectedDateTo = '';
	selectedSlot: BookingTimeSlot | null = null;
	selectedResource: BookingResource | null = null;

	prezzoBase = 0;
	prezzoDettaglio: { label: string; subtotal: number }[] = [];

	booking: Partial<Booking> = {
		bkb_guests_count: 1,
		bkb_lettini: 1,
		bkb_sedie: 0,
		bkb_sdraio: 1,
	};

	// ── Canvas (identico al portale Angular) ─────────────────────
	readonly REF_WIDTH  = 900;
	readonly REF_HEIGHT = 540;
	readonly GRID_STEP  = 36;   // 4% di 900px

	private _canvasTop = 0;
	private paramsSubscription!: Subscription;

	constructor(
		private activatedRoute: ActivatedRoute,
		private router: Router,
		public configService: ConfigService,
		private bookingService: BookingService,
		private sanitizer: DomSanitizer,
		private layoutService: LayoutService,
		private customerRegistration: CustomerRegistrationService,
	) {}

	ngOnInit() {
		this.paramsSubscription = this.activatedRoute.params.subscribe((params: Record<string, string>) => {
			if (params['cmp_id']) this.cmp_id = +params['cmp_id'];
			if (params['prt_id']) this.prt_id = +params['prt_id'];
			if (params['bkb_id']) this.bkb_id = +params['bkb_id'];
			this.layoutService.setPageConfig({
				showBack: true,
				backButtonHref: `/portale/${this.prt_id}/company/${this.cmp_id}/service/prenotazioni`,
			});
		});
	}

	ngOnDestroy() {
		this.paramsSubscription?.unsubscribe();
	}

	ngAfterViewInit() {
		setTimeout(() => this._computeMapScale(), 80);
	}

	@HostListener('window:resize')
	onWindowResize() { this._computeMapScale(); }

	ionViewWillEnter() {
		this.loadResourceTypes();
		if (this.bkb_id) {
			this.loadExistingBooking();
		} else {
			const u = this.configService.getUser();
			if (u) {
				this.booking.bkb_user_id        = u.usr_id || null;
				this.booking.bkb_customer_name  = u.usr_displayname || '';
				this.booking.bkb_customer_phone = u.usr_phone || '';
				this.booking.bkb_customer_email = u.usr_mail || '';
			}
		}
	}

	loadResourceTypes() {
		this.bookingService.GetResourceTypes(this.cmp_id).subscribe({
			next: (res) => {
				this.resourceTypes = res || [];
				if (this.activeResourceTypes.length === 1) {
					this.onTypeSelected(this.activeResourceTypes[0]);
					this.currentStep = 2;
				}
			},
			error: (err) => this.configService.presentToastError(err),
		});
	}

	loadExistingBooking() {
		this.bookingService.GetBookingById(this.bkb_id).subscribe({
			next: (res) => {
				this.booking = { ...res };
				this.selectedDate = res.bkb_date;
				this.selectedDateTo = res.bkb_date_to || res.bkb_date;
				const type = this.resourceTypes.find((t) => t.bkrt_id === res.bkb_resource_id);
				if (type) this.onTypeSelected(type);
			},
			error: (err) => this.configService.presentToastError(err),
		});
	}

	onTypeSelected(type: BookingResourceType, autoAdvance = false) {
		this.selectedType = type;
		this.selectedZone = null;
		if (autoAdvance) this.currentStep = 2;
		this.bookingService.GetTimeSlots(this.cmp_id, type.bkrt_id).subscribe({
			next: (res) => {
				this.timeSlots = (res || []).filter((s) => s.bkts_is_active);
				if (this.timeSlots.length === 1) this.selectedSlot = this.timeSlots[0];
				if (this.isRestaurant) this.recomputeAvailability();
			},
			error: (err) => this.configService.presentToastError(err),
		});
		if (type.bkrt_type === 'BEACH') {
			this.bookingService.GetBeachPricing(this.cmp_id, type.bkrt_id).subscribe({
				next: (res) => { this.beachPricing = res || []; },
				error: () => {},
			});
		}
		if (type.bkrt_type === 'RESTAURANT') {
			this.loadRestaurantCapacity();
			const today = new Date();
			this.calendarMonth = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}`;
			this.loadMonthAvailability();
		}
	}

	// ── RISTORANTE ─────────────────────────────────────────────────
	get isRestaurant(): boolean { return this.selectedType?.bkrt_type === 'RESTAURANT'; }

	loadRestaurantCapacity() {
		if (!this.selectedType) return;
		this.bookingService.GetResources(this.cmp_id, this.selectedType.bkrt_id).subscribe({
			next: (all) => {
				const list = all || [];
				this.placeholderRestaurantResource = list.find((r) => r.bkr_code === '_DA_ASSEGNARE_') || null;
				const tavoli = list.filter((r) => r.bkr_is_active && !r.bkr_code?.startsWith('_'));
				this.totalCapacity = tavoli.reduce((sum, r) => sum + (r.bkr_capacity || 0), 0);
				this.recomputeAvailability();
			},
			error: (err) => this.configService.presentToastError(err),
		});
	}

	loadMonthAvailability() {
		if (!this.selectedType || !this.calendarMonth) return;
		const [y, m] = this.calendarMonth.split('-').map(Number);
		const dateFrom = `${y}-${String(m).padStart(2, '0')}-01`;
		const lastDay = new Date(y, m, 0).getDate();
		const dateTo = `${y}-${String(m).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
		this.bookingService.GetBookingsByPeriod(this.cmp_id, dateFrom, dateTo, this.selectedType.bkrt_id).subscribe({
			next: (bookings) => {
				this.monthBookings = (bookings || []).filter((b) => b.bkb_status === 'CONFIRMED');
				this.recomputeAvailability();
			},
			error: (err) => console.error('[ristorante] errore disponibilità mese:', err),
		});
	}

	private recomputeAvailability() {
		this.dayCapacityMap.clear();
		this.slotOccupancyMap.clear();
		if (this.totalCapacity <= 0) {
			this.highlightedDatesArray = [];
			return;
		}
		const slotsTotalCapacity = this.totalCapacity * Math.max(1, this.timeSlots.length);
		const dayOccupied = new Map<string, number>();
		for (const b of this.monthBookings) {
			const date = this._normDate(b.bkb_date);
			if (!date) continue;
			const guests = b.bkb_guests_count || 0;
			dayOccupied.set(date, (dayOccupied.get(date) || 0) + guests);
			if (b.bkb_time_slot_id) {
				const key = `${date}|${b.bkb_time_slot_id}`;
				this.slotOccupancyMap.set(key, (this.slotOccupancyMap.get(key) || 0) + guests);
			}
		}
		for (const [date, occupied] of dayOccupied.entries()) {
			this.dayCapacityMap.set(date, { occupied, total: slotsTotalCapacity });
		}
		this._rebuildHighlightedDates();
	}

	private _rebuildHighlightedDates() {
		if (!this.calendarMonth || this.totalCapacity <= 0) {
			this.highlightedDatesArray = [];
			return;
		}
		const [y, m] = this.calendarMonth.split('-').map(Number);
		const lastDay = new Date(y, m, 0).getDate();
		const todayStr = this.minBookingDate;
		const slotsTotal = this.totalCapacity * Math.max(1, this.timeSlots.length);
		const out: { date: string; textColor: string; backgroundColor: string }[] = [];
		for (let d = 1; d <= lastDay; d++) {
			const dateStr = `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
			if (dateStr < todayStr) continue;
			const info = this.dayCapacityMap.get(dateStr);
			const pct = info && slotsTotal > 0 ? info.occupied / slotsTotal : 0;
			if (pct >= 0.85)      out.push({ date: dateStr, textColor: '#fff', backgroundColor: '#e74c3c' });
			else if (pct >= 0.5)  out.push({ date: dateStr, textColor: '#333', backgroundColor: '#f1c40f' });
			else                  out.push({ date: dateStr, textColor: '#fff', backgroundColor: '#27ae60' });
		}
		this.highlightedDatesArray = out;
	}

	onCalendarMonthChange(ev: any) {
		const value = ev?.detail?.value;
		if (!value) return;
		const newMonth = String(value).substring(0, 7);
		if (newMonth !== this.calendarMonth) {
			this.calendarMonth = newMonth;
			this.loadMonthAvailability();
		}
	}

	onRestaurantDateChange(ev: any) {
		const value = ev?.detail?.value;
		if (!value) return;
		this.selectedDate = String(value).substring(0, 10);
		this.selectedDateTo = this.selectedDate;
	}

	getSlotOccupied(slot: BookingTimeSlot): number {
		return this.slotOccupancyMap.get(`${this.selectedDate}|${slot.bkts_id}`) || 0;
	}

	getSlotFree(slot: BookingTimeSlot): number {
		return Math.max(0, this.totalCapacity - this.getSlotOccupied(slot));
	}

	getSlotOccupancyPercent(slot: BookingTimeSlot): number {
		if (this.totalCapacity <= 0) return 0;
		return Math.min(100, Math.round(this.getSlotOccupied(slot) / this.totalCapacity * 100));
	}

	canFitInSlot(slot: BookingTimeSlot): boolean {
		const guests = this.booking.bkb_guests_count || 1;
		return this.getSlotFree(slot) >= guests;
	}

	get minBookingDate(): string {
		return new Date().toISOString().substring(0, 10);
	}

	incrementGuests() {
		this.booking.bkb_guests_count = (this.booking.bkb_guests_count || 1) + 1;
	}

	decrementGuests() {
		this.booking.bkb_guests_count = Math.max(1, (this.booking.bkb_guests_count || 1) - 1);
	}

	onDateFromChange() {
		if (!this.selectedDateTo || this.selectedDateTo < this.selectedDate) {
			this.selectedDateTo = this.selectedDate;
		}
		this.loadResourcesIfReady();
	}

	onDateToChange() {
		this.loadResourcesIfReady();
	}

	loadResourcesIfReady() {
		if (this.selectedDate && this.selectedDateTo && this.selectedType) {
			this.selectedZone = null;
			this.bookingService.GetBookingsByPeriod(this.cmp_id, this.selectedDate, this.selectedDateTo, this.selectedType.bkrt_id).subscribe({
				next: (bookings) => {
					const all = bookings || [];
					this.bookedResourceIds = new Set(
						all
							.filter(b => b.bkb_status !== 'CANCELLED')
							.map(b => b.bkb_resource_id)
							.filter((id): id is number => id != null)
					);
				},
				error: (err) => console.error('[mappa] errore GetBookingsByPeriod:', err),
			});
			this.bookingService.GetResources(this.cmp_id, this.selectedType.bkrt_id).subscribe({
				next: (all) => {
					const lista = all || [];
					this.resourcesAll = lista;  // tutte (con _PASSERELLA_/_STABILIMENTO_) per <app-resource-map>
					this.resources = lista
						.filter((r) => !r.bkr_code?.startsWith('_'))
						.sort((a, b) => a.bkr_name.localeCompare(b.bkr_name, undefined, { numeric: true, sensitivity: 'base' }));
					setTimeout(() => this._computeMapScale(), 80);
					this.passerelle = lista
						.filter((r) => r.bkr_code?.startsWith('_PASSERELLA_'))
						.map((r) => {
							let pos: any = {}; let notes: any = {};
							try { pos   = JSON.parse(r.bkr_position || '{}'); } catch {}
							try { notes = JSON.parse(r.bkr_notes    || '{}'); } catch {}
							return { id: r.bkr_id, x: pos.x || 0, y: pos.y || 0, w: notes.w || 0, h: notes.h || 0 };
						});
					const stab = lista.find((r) => r.bkr_code === '_STABILIMENTO_');
					if (stab) {
						let pos: any = {}; let notes: any = {};
						try { pos   = JSON.parse(stab.bkr_position || '{}'); } catch {}
						try { notes = JSON.parse(stab.bkr_notes    || '{}'); } catch {}
						this.stabilimento = { x: pos.x || 0, y: pos.y || 0, w: notes.w || 0, h: notes.h || 0 };
					} else {
						this.stabilimento = null;
					}
					const canvasCfg = lista.find((r) => r.bkr_code === '_CANVAS_CONFIG_');
					if (canvasCfg) {
						try { const notes = JSON.parse(canvasCfg.bkr_notes || '{}'); this.canvasHeight = Math.max(540, notes.canvasHeight || 540); } catch {}
					} else {
						this.canvasHeight = 540;
					}
				},
				error: (err) => this.configService.presentToastError(err),
			});
		}
	}

	onSlotSelected(slot: BookingTimeSlot) {
		this.selectedSlot = slot;
	}

	isBooked(resourceId: number): boolean {
		return this.bookedResourceIds.has(resourceId);
	}

	onResourceSelected(resource: BookingResource) {
		// In L1 BEACH: il click sulla risorsa seleziona la zona corrispondente
		if (this.isBeach && !this.selectedZone) {
			try {
				const xPx = JSON.parse(resource.bkr_position || '{}').x / 100 * this.REF_WIDTH;
				const zone = this.beachL1Zones.find(z => xPx >= z.xStartPx && xPx < z.xStartPx + z.widthPx);
				if (zone) this.selectZone(zone);
			} catch {}
			return;
		}
		if (this.isBooked(resource.bkr_id)) return;
		this.selectedResource = resource;
		this.currentStep = 3;
		this.calcolaPrezzo();
	}

	calcolaPrezzo(): void {
		this.prezzoBase = 0;
		this.prezzoDettaglio = [];
		if (!this.isBeach || !this.beachPricing.length || !this.selectedResource || !this.selectedDate || !this.selectedDateTo) return;

		const fila = this._getSelectedFila();
		const fromDate  = new Date(this.selectedDate);
		const toDate    = new Date(this.selectedDateTo);
		const totalDays = this.giorni;
		const isPalma   = this.getResourceSubtype(this.selectedResource) === 'PALMA';

		const mainElement = isPalma ? 'PALMA' : 'OMBRELLONE';
		const quantities: { element: string; qty: number; label: string }[] = [
			{ element: mainElement, qty: 1,                               label: isPalma ? 'Palma' : 'Ombrellone' },
			{ element: 'LETTINO',   qty: this.booking.bkb_lettini  || 0,  label: 'Lettini'  },
			{ element: 'SEDIA',     qty: this.booking.bkb_sedie    || 0,  label: 'Sedie'    },
			{ element: 'SDRAIO',    qty: this.booking.bkb_sdraio   || 0,  label: 'Sdraio'   },
		];

		for (const { element, qty, label } of quantities) {
			if (qty <= 0) continue;
			const byPeriod = new Map<number, { pricing: BookingBeachPricing; days: number }>();
			const cur = new Date(fromDate);
			while (cur <= toDate) {
				const p = this._findFilaPricing(element, fila, cur.toISOString().split('T')[0]);
				if (p) {
					const e = byPeriod.get(p.bbp_id);
					if (e) e.days++; else byPeriod.set(p.bbp_id, { pricing: p, days: 1 });
				}
				cur.setDate(cur.getDate() + 1);
			}
			let subtotal = 0;
			for (const { pricing, days } of byPeriod.values())
				subtotal += qty * this._computeBeachCost(pricing, days, totalDays);
			if (subtotal > 0) {
				this.prezzoDettaglio.push({ label, subtotal: +subtotal.toFixed(2) });
				this.prezzoBase += subtotal;
			}
		}
		this.prezzoBase = +this.prezzoBase.toFixed(2);
	}

	private _getSelectedFila(): number {
		if (!this.selectedResource?.bkr_position) return 1;
		try {
			const y = JSON.parse(this.selectedResource.bkr_position).y;
			const ySet = new Set<number>();
			for (const r of this.positionedResources) {
				try { ySet.add(JSON.parse(r.bkr_position).y); } catch {}
			}
			return [...ySet].sort((a, b) => a - b).indexOf(y) + 1;
		} catch { return 1; }
	}

	// ── Helpers risorse ──────────────────────────────────────────
	get filteredResources(): BookingResource[] {
		return this.soloLiberi ? this.resources.filter(r => !this.bookedResourceIds.has(r.bkr_id)) : this.resources;
	}

	get selectedGroupResources(): BookingResource[] {
		if (this.selectedGroupY === null) return [];
		const list = this.resources.filter(r => {
			try { return JSON.parse(r.bkr_position).y === this.selectedGroupY; } catch { return false; }
		});
		return this.soloLiberi ? list.filter(r => !this.bookedResourceIds.has(r.bkr_id)) : list;
	}

	get resourceGroups(): { fila: number; y: number; borderColor: string; bgColor: string; prezzo: number; tipologia: string; total: number; freeCount: number }[] {
		if (!this.isBeach) return [];
		const ySet = new Set<number>();
		for (const r of this.positionedResources) {
			try { ySet.add(JSON.parse(r.bkr_position).y); } catch {}
		}
		const ySorted = [...ySet].sort((a, b) => a - b);
		const checkDate = this.selectedDate;
		const colorMap = this._buildFilaColorMap(ySorted, checkDate);
		return ySorted.map((y, index) => {
			const fila = index + 1;
			const { border, bg } = colorMap[index];
			const rowRes = this.resources.filter(r => {
				try { return JSON.parse(r.bkr_position).y === y; } catch { return false; }
			});
			const hasPalma = rowRes.some(r => this.getResourceSubtype(r) === 'PALMA');
			const hasOmbr  = rowRes.some(r => this.getResourceSubtype(r) !== 'PALMA');
			const tipologia = hasPalma && !hasOmbr ? 'Palma' : hasOmbr && !hasPalma ? 'Ombrellone' : 'Ombrellone / Palma';
			const sample = rowRes[0];
			return {
				fila, y,
				borderColor: border,
				bgColor:     bg,
				prezzo:      sample ? this.getPrezzoRisorsa(sample) : 0,
				tipologia,
				total:       rowRes.length,
				freeCount:   rowRes.filter(r => !this.bookedResourceIds.has(r.bkr_id)).length,
			};
		});
	}

	getPrezzoDettaglioRisorsa(res: BookingResource): { label: string; subtotal: number }[] {
		if (!this.isBeach || !this.beachPricing.length || !this.selectedDate || !this.selectedDateTo) return [];
		const ySet = new Set<number>();
		for (const r of this.positionedResources) {
			try { ySet.add(JSON.parse(r.bkr_position).y); } catch {}
		}
		let fila = 1;
		try {
			const y = JSON.parse(res.bkr_position || '{}').y;
			fila = [...ySet].sort((a, b) => a - b).indexOf(y) + 1;
		} catch {}
		const isPalma = this.getResourceSubtype(res) === 'PALMA';
		const mainElement = isPalma ? 'PALMA' : 'OMBRELLONE';
		const quantities: { element: string; qty: number; label: string }[] = [
			{ element: mainElement,  qty: 1,                               label: isPalma ? 'Palma' : 'Ombrellone' },
			{ element: 'LETTINO',    qty: this.booking.bkb_lettini ?? 1,   label: 'Lettino' },
			{ element: 'SEDIA',      qty: this.booking.bkb_sedie   ?? 0,   label: 'Sedia'   },
			{ element: 'SDRAIO',     qty: this.booking.bkb_sdraio  ?? 1,   label: 'Sdraio'  },
		];
		const fromDate  = new Date(this.selectedDate);
		const toDate    = new Date(this.selectedDateTo);
		const totalDays = this.giorni;
		const result: { label: string; subtotal: number }[] = [];
		for (const { element, qty, label } of quantities) {
			if (qty <= 0) continue;
			const byPeriod = new Map<number, { pricing: BookingBeachPricing; days: number }>();
			const cur = new Date(fromDate);
			while (cur <= toDate) {
				const p = this._findFilaPricing(element, fila, cur.toISOString().split('T')[0]);
				if (p) {
					const e = byPeriod.get(p.bbp_id);
					if (e) e.days++; else byPeriod.set(p.bbp_id, { pricing: p, days: 1 });
				}
				cur.setDate(cur.getDate() + 1);
			}
			let subtotal = 0;
			for (const { pricing, days } of byPeriod.values())
				subtotal += qty * this._computeBeachCost(pricing, days, totalDays);
			if (subtotal > 0) result.push({ label: qty > 1 ? `${label} ×${qty}` : label, subtotal: +subtotal.toFixed(2) });
		}
		return result;
	}

	getPrezzoRisorsa(res: BookingResource): number {
		if (!this.isBeach || !this.beachPricing.length || !this.selectedDate || !this.selectedDateTo) return 0;
		const ySet = new Set<number>();
		for (const r of this.positionedResources) {
			try { ySet.add(JSON.parse(r.bkr_position).y); } catch {}
		}
		let fila = 1;
		try {
			const y = JSON.parse(res.bkr_position || '{}').y;
			fila = [...ySet].sort((a, b) => a - b).indexOf(y) + 1;
		} catch {}
		const isPalma = this.getResourceSubtype(res) === 'PALMA';
		const mainElement = isPalma ? 'PALMA' : 'OMBRELLONE';
		const quantities: { element: string; qty: number }[] = [
			{ element: mainElement, qty: 1 },
			{ element: 'LETTINO',   qty: this.booking.bkb_lettini ?? 1 },
			{ element: 'SEDIA',     qty: this.booking.bkb_sedie   ?? 0 },
			{ element: 'SDRAIO',    qty: this.booking.bkb_sdraio  ?? 1 },
		];
		const fromDate  = new Date(this.selectedDate);
		const toDate    = new Date(this.selectedDateTo);
		const totalDays = this.giorni;
		let total = 0;
		for (const { element, qty } of quantities) {
			if (qty <= 0) continue;
			const byPeriod = new Map<number, { pricing: BookingBeachPricing; days: number }>();
			const cur = new Date(fromDate);
			while (cur <= toDate) {
				const p = this._findFilaPricing(element, fila, cur.toISOString().split('T')[0]);
				if (p) {
					const e = byPeriod.get(p.bbp_id);
					if (e) e.days++; else byPeriod.set(p.bbp_id, { pricing: p, days: 1 });
				}
				cur.setDate(cur.getDate() + 1);
			}
			for (const { pricing, days } of byPeriod.values())
				total += qty * this._computeBeachCost(pricing, days, totalDays);
		}
		return +total.toFixed(2);
	}

	get isBeach(): boolean { return this.selectedType?.bkrt_type === 'BEACH'; }
	get dateReady(): boolean {
		return !!(this.selectedDate && this.selectedDateTo && (this.isBeach || !!this.selectedSlot));
	}
	get positionedResources(): BookingResource[] {
		return this.resources.filter((r) => r.bkr_position);
	}

	getResourceCols(r: BookingResource): number {
		try { return JSON.parse(r.bkr_notes || '{}').cols || 1; } catch { return 1; }
	}
	getResourceRows(r: BookingResource): number {
		try { return JSON.parse(r.bkr_notes || '{}').rows || 1; } catch { return 1; }
	}
	getResourceSubtype(r: BookingResource): string {
		try { return JSON.parse(r.bkr_notes || '{}').subtype || 'OMBRELLONE'; } catch { return 'OMBRELLONE'; }
	}
	getResourceColors(r: BookingResource): { c1: string; c2: string } {
		try {
			const n = JSON.parse(r.bkr_notes || '{}');
			return { c1: n.color1 || '#f5a623', c2: n.color2 || '#e84040' };
		} catch { return { c1: '#f5a623', c2: '#e84040' }; }
	}

	// ── Stili posizionamento landscape (identici al portale Angular) ──
	getResourceStyle(r: BookingResource): { [k: string]: string } {
		try {
			const pos = JSON.parse(r.bkr_position);
			return {
				left:   (pos.x / 100 * this.REF_WIDTH)  + 'px',
				top:    (pos.y / 100 * this.REF_HEIGHT) + 'px',
				width:  (this.getResourceCols(r) * this.GRID_STEP) + 'px',
				height: (this.getResourceRows(r) * this.GRID_STEP) + 'px',
			};
		} catch { return {}; }
	}

	getPasserellaStyle(p: Passerella): { [k: string]: string } {
		return {
			left:   (p.x / 100 * this.REF_WIDTH)  + 'px',
			top:    (p.y / 100 * this.REF_HEIGHT) + 'px',
			width:  (p.w / 100 * this.REF_WIDTH)  + 'px',
			height: (p.h / 100 * this.REF_HEIGHT) + 'px',
		};
	}

	getStabilimentoStyle(): { [k: string]: string } {
		if (!this.stabilimento) return {};
		return {
			left:   (this.stabilimento.x / 100 * this.REF_WIDTH)  + 'px',
			top:    (this.stabilimento.y / 100 * this.REF_HEIGHT) + 'px',
			width:  (this.stabilimento.w / 100 * this.REF_WIDTH)  + 'px',
			height: (this.stabilimento.h / 100 * this.REF_HEIGHT) + 'px',
		};
	}

	/** SVG generato dinamicamente per lo stabilimento (copiato dal portale Angular) */
	get stabilimentoSvg(): SafeHtml {
		if (!this.stabilimento) return '';
		const W  = Math.round(this.stabilimento.w / 100 * this.REF_WIDTH);
		const H  = Math.round(this.stabilimento.h / 100 * this.REF_HEIGHT);
		const sp = Math.round(Math.min(W, H) * 0.08);
		const vw = Math.round(W * 0.13);
		const pw = Math.round(W * 0.12);
		const cx = W / 2, cy = H / 2;
		const rx1 = sp + vw + pw, ry1 = sp;
		const rx2 = W - sp - vw - pw, ry2 = H - sp;
		const fs  = Math.max(7, Math.min(12, Math.round(W / 14)));

		const bushStrip = (stripX: number, stripW: number): string => {
			const r  = Math.round(stripW * 0.55);
			const step = Math.round(r * 1.4);
			let out = '';
			for (let y = sp + r; y < H - sp; y += step) {
				const cx2 = Math.round(stripX + stripW / 2);
				out +=
					`<circle cx="${cx2}" cy="${y}" r="${r}" fill="#3d7a2e" opacity="0.9"/>` +
					`<circle cx="${cx2}" cy="${y}" r="${Math.round(r*0.65)}" fill="#4d9c3a" opacity="0.85"/>` +
					`<circle cx="${cx2 - Math.round(r*0.35)}" cy="${y - Math.round(r*0.3)}" r="${Math.round(r*0.4)}" fill="#5cb84a" opacity="0.6"/>`;
			}
			return out;
		};

		const ew = Math.round((rx2 - rx1) * 0.55);
		const ex = cx - ew / 2;
		const eh = Math.round(sp * 1.2);

		const svg =
			`<svg width="100%" height="100%" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg">` +
			`<rect width="${W}" height="${H}" rx="3" fill="#4a8c3a"/>` +
			bushStrip(sp, vw) +
			bushStrip(W - sp - vw, vw) +
			`<rect x="${sp + vw}" y="${ry1}" width="${pw}" height="${ry2 - ry1}" fill="#d4cbb8" stroke="#bfb49e" stroke-width="0.5"/>` +
			`<rect x="${rx2}" y="${ry1}" width="${pw}" height="${ry2 - ry1}" fill="#d4cbb8" stroke="#bfb49e" stroke-width="0.5"/>` +
			`<polygon points="${rx1},${ry1} ${rx2},${ry1} ${cx},${cy}" fill="#c97868"/>` +
			`<polygon points="${rx2},${ry1} ${rx2},${ry2} ${cx},${cy}" fill="#a85c4c"/>` +
			`<polygon points="${rx1},${ry2} ${rx2},${ry2} ${cx},${cy}" fill="#db9080"/>` +
			`<polygon points="${rx1},${ry1} ${rx1},${ry2} ${cx},${cy}" fill="#b86858"/>` +
			`<line x1="${rx1}" y1="${ry1}" x2="${cx}" y2="${cy}" stroke="#7a3828" stroke-width="0.7" opacity="0.5"/>` +
			`<line x1="${rx2}" y1="${ry1}" x2="${cx}" y2="${cy}" stroke="#7a3828" stroke-width="0.7" opacity="0.5"/>` +
			`<line x1="${rx2}" y1="${ry2}" x2="${cx}" y2="${cy}" stroke="#7a3828" stroke-width="0.7" opacity="0.5"/>` +
			`<line x1="${rx1}" y1="${ry2}" x2="${cx}" y2="${cy}" stroke="#7a3828" stroke-width="0.7" opacity="0.5"/>` +
			`<rect x="${ex}" y="${ry2 - 2}" width="${ew}" height="${eh + 2}" rx="1" fill="#ddd8ce" stroke="#b0a898" stroke-width="0.7"/>` +
			`<text x="${cx}" y="${cy + Math.round(fs * 0.4)}" text-anchor="middle" font-size="${fs}" font-weight="700" ` +
			`fill="rgba(255,255,255,0.85)" font-family="sans-serif" letter-spacing="0.5">STABILIMENTO</text>` +
			`</svg>`;

		return this.sanitizer.bypassSecurityTrustHtml(svg);
	}

	// ── Larghezza/altezza effettiva canvas (come Angular canvasUsedW/H) ──
	get canvasUsedW(): number {
		let maxX = 0;
		for (const r of this.resources) {
			try { const p = JSON.parse(r.bkr_position); if (p.x > maxX) maxX = p.x; } catch {}
		}
		return Math.max((maxX / 100) * this.REF_WIDTH + 2 * this.GRID_STEP, this.REF_WIDTH);
	}

	get canvasUsedH(): number {
		let maxY = 0;
		for (const r of this.resources) {
			try { const p = JSON.parse(r.bkr_position); if (p.y > maxY) maxY = p.y; } catch {}
		}
		return Math.max((maxY / 100) * this.REF_HEIGHT + 2 * this.GRID_STEP, this.canvasHeight);
	}

	// ── Zone L1 spiaggia — griglia nx×ny dinamica (identico al portale Angular) ──
	private _computeZoneCount(): { nx: number; ny: number } {
		if (!this._containerW || !this._availH) return { nx: 1, ny: 1 };
		const nx = Math.max(1, Math.ceil(this.canvasUsedW / (this._containerW * 1.5)));
		return { nx, ny: 1 };
	}

	private _makeFullZone(): BeachL1Zone {
		let oL = 0, oT = 0, pL = 0, pT = 0;
		for (const r of this.positionedResources) {
			const booked = this.bookedResourceIds.has(r.bkr_id);
			if (this.getResourceSubtype(r) === 'PALMA') { pT++; if (!booked) pL++; }
			else { oT++; if (!booked) oL++; }
		}
		return {
			label: 'Vista completa',
			xStartPx: 0, widthPx:  this.canvasUsedW,
			yStartPx: 0, heightPx: this.canvasUsedH,
			col: 0, row: 0, isLastCol: true, isLastRow: true,
			ombrLiberi: oL, ombrTotali: oT, palmeLibere: pL, palmeTotali: pT,
			bgColor: 'rgba(30,136,229,0.15)', borderColor: 'rgba(20,110,200,1.00)',
		};
	}

	get beachL1Zones(): BeachL1Zone[] {
		if (!this.isBeach) return [];

		const { nx, ny } = this._computeZoneCount();
		const stepX  = this.GRID_STEP;
		const canvasW = this.canvasUsedW;
		const canvasH = this.canvasUsedH;

		const xSet = new Set<number>(), ySet = new Set<number>();
		for (const r of this.positionedResources) {
			try {
				const p = JSON.parse(r.bkr_position);
				xSet.add(p.x / 100 * this.REF_WIDTH);
				ySet.add(p.y / 100 * this.REF_HEIGHT);
			} catch {}
		}
		const xCols = Array.from(xSet).sort((a, b) => a - b);
		const yRows = Array.from(ySet).sort((a, b) => a - b);

		const xBounds: number[] = [];
		if (xCols.length >= nx && nx > 1) {
			for (let k = 1; k < nx; k++) {
				const i = Math.round(k * xCols.length / nx);
				xBounds.push((xCols[i - 1] + stepX + xCols[i]) / 2);
			}
		} else if (nx > 1) {
			for (let k = 1; k < nx; k++) xBounds.push(canvasW * k / nx);
		}

		const yBounds: number[] = [];
		if (yRows.length >= ny && ny > 1) {
			for (let k = 1; k < ny; k++) {
				const i = Math.round(k * yRows.length / ny);
				yBounds.push((yRows[i - 1] + stepX + yRows[i]) / 2);
			}
		} else if (ny > 1) {
			for (let k = 1; k < ny; k++) yBounds.push(canvasH * k / ny);
		}

		const xRanges: [number, number][] = [];
		let px = 0;
		for (const b of xBounds) { xRanges.push([px, b]); px = b; }
		xRanges.push([px, canvasW]);

		const yRanges: [number, number][] = [];
		let py = 0;
		for (const b of yBounds) { yRanges.push([py, b]); py = b; }
		yRanges.push([py, canvasH]);

		const ALL_LABELS = ['Sinistra', 'Centro', 'Destra', 'Zona 4', 'Zona 5', 'Zona 6'];
		const ALL_BG     = [
			'rgba(30,136,229,0.32)', 'rgba(67,160,71,0.32)',  'rgba(251,140,0,0.32)',
			'rgba(142,36,170,0.32)', 'rgba(0,172,193,0.32)',  'rgba(244,81,30,0.32)',
		];
		const ALL_BORDER = [
			'rgba(20,110,200,1.00)', 'rgba(50,150,60,1.00)',  'rgba(230,120,0,1.00)',
			'rgba(120,30,150,1.00)', 'rgba(0,140,160,1.00)',  'rgba(210,60,10,1.00)',
		];

		const zones: BeachL1Zone[] = [];
		for (let row = 0; row < ny; row++) {
			const [ysPx, yePx] = yRanges[row];
			for (let col = 0; col < nx; col++) {
				const [xsPx, xePx] = xRanges[col];
				let oL = 0, oT = 0, pL = 0, pT = 0;
				for (const r of this.positionedResources) {
					try {
						const pos = JSON.parse(r.bkr_position);
						const xPx = pos.x / 100 * this.REF_WIDTH;
						const yPx = pos.y / 100 * this.REF_HEIGHT;
						if (xPx < xsPx || xPx >= xePx || yPx < ysPx || yPx >= yePx) continue;
						const booked = this.bookedResourceIds.has(r.bkr_id);
						if (this.getResourceSubtype(r) === 'PALMA') { pT++; if (!booked) pL++; }
						else                                         { oT++; if (!booked) oL++; }
					} catch {}
				}
				const idx = row * nx + col;
				let label: string;
				if (ny === 1)      { label = idx < ALL_LABELS.length ? ALL_LABELS[idx] : `Zona ${idx + 1}`; }
				else if (nx === 1) { label = `Fila ${row + 1}`; }
				else               { label = `${col < ALL_LABELS.length ? ALL_LABELS[col] : 'Z' + (col + 1)} F${row + 1}`; }
				zones.push({
					label,
					xStartPx: xsPx, widthPx:  xePx - xsPx,
					yStartPx: ysPx, heightPx: yePx - ysPx,
					col, row, isLastCol: col === nx - 1, isLastRow: row === ny - 1,
					ombrLiberi: oL, ombrTotali: oT, palmeLibere: pL, palmeTotali: pT,
					bgColor: ALL_BG[idx % ALL_BG.length], borderColor: ALL_BORDER[idx % ALL_BORDER.length],
				});
			}
		}
		return zones;
	}

	selectZone(zone: BeachL1Zone): void {
		this._autoZone = false;
		this.selectedZone = zone;
		this.prezziExpanded = false;
		setTimeout(() => this._computeMapScale(), 20);
	}

	deselectZone(): void {
		this._autoZone = false;
		this.selectedZone = null;
		this.l2CenterOffsetPx = 0;
		setTimeout(() => this._computeMapScale(), 20);
	}

	// ── Numero di giorni selezionati ─────────────────────────────
	get giorni(): number {
		if (!this.selectedDate || !this.selectedDateTo) return 1;
		const from = new Date(this.selectedDate);
		const to   = new Date(this.selectedDateTo);
		return Math.max(1, Math.round((to.getTime() - from.getTime()) / 86400000) + 1);
	}

	// ── Fasce di prezzo orizzontali (bande per ogni fila) ────────
	get filaZones(): { fila: number; topPx: number; heightPx: number; bgColor: string; borderColor: string }[] {
		if (!this.isBeach || this.positionedResources.length === 0) return [];
		const ySorted = this._getSortedFilaYValues();
		if (ySorted.length === 0) return [];
		const checkDate = this.selectedDate;
		const colorMap = this._buildFilaColorMap(ySorted, checkDate);
		return ySorted.map((yPercent, index) => {
			const rowRes = this.positionedResources.filter(r => {
				try { return JSON.parse(r.bkr_position).y === yPercent; } catch { return false; }
			});
			const maxRows = Math.max(1, ...rowRes.map(r => this.getResourceRows(r)));
			const padPx = this.GRID_STEP * 0.3;
			const { border, bg } = colorMap[index];
			return {
				fila:      index + 1,
				topPx:     Math.max(0, yPercent / 100 * this.REF_HEIGHT - padPx),
				heightPx:  maxRows * this.GRID_STEP + padPx * 2,
				bgColor:   bg,
				borderColor: border,
			};
		});
	}

	private _getSortedFilaYValues(): number[] {
		const ySet = new Set<number>();
		for (const r of this.positionedResources) {
			try { ySet.add(JSON.parse(r.bkr_position).y); } catch {}
		}
		return Array.from(ySet).sort((a, b) => a - b);
	}

	/** Callback passata al componente <app-resource-map> per colorare le bande fila in base al prezzo */
	getFilaColorsCallback = (idx: number, _yPercent: number): { border: string; bg: string } | null => {
		if (!this.isBeach) return null;
		const ySorted = this._getSortedFilaYValues();
		if (!ySorted.length) return null;
		const map = this._buildFilaColorMap(ySorted, this.selectedDate);
		return map[idx] || null;
	};

	/** Colori per fila: prezzi uguali → stesso colore (come Angular) */
	private _buildFilaColorMap(ySorted: number[], checkDate: string): { border: string; bg: string }[] {
		const PALETTE = [
			{ border: '#1976d2', bg: 'rgba(25,118,210,0.20)' },
			{ border: '#ef6c00', bg: 'rgba(239,108,0,0.20)' },
			{ border: '#7b1fa2', bg: 'rgba(123,31,162,0.20)' },
			{ border: '#00838f', bg: 'rgba(0,131,143,0.20)' },
			{ border: '#f9a825', bg: 'rgba(249,168,37,0.20)' },
			{ border: '#3949ab', bg: 'rgba(57,73,171,0.20)' },
			{ border: '#ad1457', bg: 'rgba(173,20,87,0.20)' },
			{ border: '#6d4c41', bg: 'rgba(109,76,65,0.20)' },
		];
		const NO_PRICE = { border: '#9e9e9e', bg: 'rgba(158,158,158,0.15)' };
		const priceToIdx = new Map<number, number>();
		let nextIdx = 0;
		return ySorted.map((_y, i) => {
			const fila = i + 1;
			const p = this._findFilaPricing('OMBRELLONE', fila, checkDate);
			const price = p?.bbp_daily_cost ?? null;
			if (price === null || price === 0) return NO_PRICE;
			if (!priceToIdx.has(price)) priceToIdx.set(price, nextIdx++);
			return PALETTE[priceToIdx.get(price)! % PALETTE.length];
		});
	}

	// ── Legenda prezzi ────────────────────────────────────────────
	get filaLegend(): { fila: number; bgColor: string; borderColor: string; ombrTotal: number | null; palmaTotal: number | null; periodName: string; hasOmbrelloni: boolean; hasPalme: boolean }[] {
		if (!this.selectedDate || !this.selectedDateTo) return this.filaZones.map(z => ({ ...z, ombrTotal: null, palmaTotal: null, periodName: '', hasOmbrelloni: false, hasPalme: false }));
		const fromDate  = new Date(this.selectedDate);
		const toDate    = new Date(this.selectedDateTo);
		const days      = this.giorni;
		const checkDate = this.selectedDate;
		const ySorted   = this._getSortedFilaYValues();

		return this.filaZones.map((zone, index) => {
			const yPercent = ySorted[index];
			const { hasOmbrelloni, hasPalme } = this._getFilaSubtypes(yPercent);
			const ombrTotal  = hasOmbrelloni ? this._calcFilaTotal('OMBRELLONE', zone.fila, fromDate, toDate, days) : null;
			const palmaTotal = hasPalme      ? this._calcFilaTotal('PALMA',      zone.fila, fromDate, toDate, days) : null;
			const pRef = this._findFilaPricing(hasPalme && !hasOmbrelloni ? 'PALMA' : 'OMBRELLONE', zone.fila, checkDate);
			return {
				fila:        zone.fila,
				bgColor:     zone.bgColor,
				borderColor: zone.borderColor,
				ombrTotal,
				palmaTotal,
				periodName:  pRef?.bbp_period_name ?? '',
				hasOmbrelloni,
				hasPalme,
			};
		});
	}

	get filaLegendGrouped(): { filaFrom: number; filaTo: number; bgColor: string; borderColor: string; ombrTotal: number | null; palmaTotal: number | null }[] {
		const legend = this.filaLegend;
		if (!legend.length) return [];
		const groups: { filaFrom: number; filaTo: number; bgColor: string; borderColor: string; ombrTotal: number | null; palmaTotal: number | null }[] = [];
		let cur = { filaFrom: legend[0].fila, filaTo: legend[0].fila, bgColor: legend[0].bgColor, borderColor: legend[0].borderColor, ombrTotal: legend[0].ombrTotal, palmaTotal: legend[0].palmaTotal };
		for (let i = 1; i < legend.length; i++) {
			const f = legend[i];
			if (f.ombrTotal === cur.ombrTotal && f.palmaTotal === cur.palmaTotal && f.bgColor === cur.bgColor && f.borderColor === cur.borderColor) {
				cur.filaTo = f.fila;
			} else {
				groups.push(cur);
				cur = { filaFrom: f.fila, filaTo: f.fila, bgColor: f.bgColor, borderColor: f.borderColor, ombrTotal: f.ombrTotal, palmaTotal: f.palmaTotal };
			}
		}
		groups.push(cur);
		return groups;
	}

	private _getFilaSubtypes(yPercent: number): { hasOmbrelloni: boolean; hasPalme: boolean } {
		const rowRes = this.positionedResources.filter(r => {
			try { return JSON.parse(r.bkr_position).y === yPercent; } catch { return false; }
		});
		return {
			hasOmbrelloni: rowRes.some(r => this.getResourceSubtype(r) !== 'PALMA'),
			hasPalme:      rowRes.some(r => this.getResourceSubtype(r) === 'PALMA'),
		};
	}

	private _calcFilaTotal(mainElement: 'OMBRELLONE' | 'PALMA', fila: number, fromDate: Date, toDate: Date, totalDays: number): number | null {
		const elements: Array<BookingBeachPricing['bbp_element']> =
			mainElement === 'OMBRELLONE' ? ['OMBRELLONE', 'LETTINO', 'SDRAIO'] : ['PALMA', 'LETTINO', 'SDRAIO'];
		let total = 0; let hasPricing = false;
		for (const element of elements) {
			const byPeriod = new Map<number, { pricing: BookingBeachPricing; days: number }>();
			const cur = new Date(fromDate);
			while (cur <= toDate) {
				const dateStr = cur.toISOString().split('T')[0];
				const p = this._findFilaPricing(element, fila, dateStr);
				if (p) {
					hasPricing = true;
					const e = byPeriod.get(p.bbp_id);
					if (e) e.days++; else byPeriod.set(p.bbp_id, { pricing: p, days: 1 });
				}
				cur.setDate(cur.getDate() + 1);
			}
			for (const { pricing, days } of byPeriod.values())
				total += this._computeBeachCost(pricing, days, totalDays);
		}
		return hasPricing ? +total.toFixed(2) : null;
	}

	private _computeBeachCost(p: BookingBeachPricing, daysInPeriod: number, totalDays: number): number {
		if (totalDays >= 90 && p.bbp_seasonal_cost > 0)
			return p.bbp_seasonal_cost * (daysInPeriod / totalDays);
		if (totalDays >= 30 && p.bbp_monthly_cost > 0) {
			const d = p.bbp_monthly_cost / 30;
			return Math.floor(daysInPeriod / 30) * p.bbp_monthly_cost + (daysInPeriod % 30) * d;
		}
		if (totalDays >= 7 && p.bbp_weekly_cost > 0) {
			const d = p.bbp_weekly_cost / 7;
			return Math.floor(daysInPeriod / 7) * p.bbp_weekly_cost + (daysInPeriod % 7) * d;
		}
		return daysInPeriod * (p.bbp_daily_cost || 0);
	}

	private _findFilaPricing(element: string, fila: number, checkDate?: string): BookingBeachPricing | null {
		const date = checkDate ?? this.selectedDate;
		const hasPrice = (p: BookingBeachPricing) =>
			(p.bbp_daily_cost || 0) > 0 || (p.bbp_hourly_cost || 0) > 0 ||
			(p.bbp_weekly_cost || 0) > 0 || (p.bbp_monthly_cost || 0) > 0 ||
			(p.bbp_seasonal_cost || 0) > 0;
		const matchElem = (p: BookingBeachPricing) =>
			p.bbp_element === element &&
			p.bbp_is_active !== false &&
			(p.bbp_fila === fila || p.bbp_fila === null);
		const best = (pool: BookingBeachPricing[]): BookingBeachPricing | null =>
			pool.find(p => p.bbp_fila === fila && hasPrice(p))
			?? pool.find(p => p.bbp_fila === null && hasPrice(p))
			?? pool[0] ?? null;

		const inDate = this.beachPricing.filter(p =>
			matchElem(p) &&
			this._normDate(p.bbp_date_from) <= date &&
			this._normDate(p.bbp_date_to)   >= date
		);
		const hit = best(inDate);
		if (hit) return hit;
		const future = this.beachPricing
			.filter(p => matchElem(p) && this._normDate(p.bbp_date_from) > date)
			.sort((a, b) => this._normDate(a.bbp_date_from).localeCompare(this._normDate(b.bbp_date_from)));
		return best(future);
	}

	private _normDate(d: string | number[] | number | any): string {
		if (!d) return '';
		if (Array.isArray(d)) {
			const [y, m, day] = d as number[];
			return `${y}-${String(m).padStart(2,'0')}-${String(day).padStart(2,'0')}`;
		}
		if (typeof d === 'number') {
			const dt = new Date(d);
			return `${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`;
		}
		return typeof d === 'string' ? d.substring(0, 10) : '';
	}

	togglePrezzi(): void {
		this.prezziExpanded = !this.prezziExpanded;
		setTimeout(() => this._computeMapScale(), 50);
	}

	// ── Scaling mappa (identico al portale Angular _computeMapScale) ──
	_computeMapScale(): void {
		const el = this.mappaWrapperRef?.nativeElement;
		if (!el) return;
		const rawTop = el.getBoundingClientRect().top;
		if (rawTop > 100) this._canvasTop = rawTop;
		if (this._canvasTop < 100) { setTimeout(() => this._computeMapScale(), 50); return; }
		const containerW = el.clientWidth || this.canvasUsedW;
		const winH = typeof window !== 'undefined' ? window.innerHeight : 700;
		const footerEl = typeof document !== 'undefined'
			? document.querySelector('ion-footer') as HTMLElement | null : null;
		const footerH = footerEl?.clientHeight ?? 0;
		const availH = Math.max(200, winH - this._canvasTop - footerH - 16);
		this._containerW = containerW;
		this._availH = availH;
		if (this.isBeach) {
			const { nx, ny } = this._computeZoneCount();
			if (nx <= 1 && ny <= 1) {
				if (!this._autoZone) {
					this._autoZone = true;
					this.selectedZone = this._makeFullZone();
				} else if (this.selectedZone) {
					this.selectedZone = { ...this.selectedZone, widthPx: this.canvasUsedW, heightPx: this.canvasUsedH };
				}
			} else if (this._autoZone) {
				this._autoZone = false;
				this.selectedZone = null;
			}
		}
		if (this.selectedZone) {
			const s = Math.min(containerW / this.selectedZone.widthPx, availH * 0.88 / this.selectedZone.heightPx);
			this.mapScale = s;
			this.mapTranslateX = -this.selectedZone.xStartPx * s;
			this.mapTranslateY = -this.selectedZone.yStartPx * s;
			this.l2CenterOffsetPx = 0;
		} else {
			this.mapScale = Math.min(containerW / this.canvasUsedW, availH / this.canvasUsedH, 1);
			this.mapTranslateX = 0;
			this.mapTranslateY = 0;
		}
	}

	get canvasScaledOuterW(): number {
		if (this.selectedZone) {
			return this.selectedZone.widthPx * this.mapScale;
		}
		return this.canvasUsedW * this.mapScale;
	}

	get canvasScaledOuterH(): number {
		if (this.selectedZone) return this.selectedZone.heightPx * this.mapScale;
		return this.canvasUsedH * this.mapScale;
	}

	// ── Navigation ───────────────────────────────────────────────
	canGoNext(): boolean {
		switch (this.currentStep) {
			case 1: return !!this.selectedType;
			case 2:
				if (this.isRestaurant) {
					const guests = this.booking.bkb_guests_count || 0;
					return !!this.selectedDate && !!this.selectedSlot && guests > 0 && this.canFitInSlot(this.selectedSlot);
				}
				return this.dateReady && !!this.selectedResource;
			case 3: return !!(this.booking.bkb_customer_name && this.booking.bkb_customer_phone);
			default: return false;
		}
	}

	nextStep() { if (this.canGoNext()) this.currentStep++; }
	prevStep()  { if (this.currentStep > 1) this.currentStep--; }
	goToStep(step: number) { this.currentStep = step; }

	conferma() {
		if (!this.selectedType || !this.selectedDate) {
			this.configService.presentToastError('Completare tutti i campi richiesti');
			return;
		}
		if (this.isRestaurant) {
			if (!this.selectedSlot || !this.booking.bkb_guests_count) {
				this.configService.presentToastError('Seleziona fascia oraria e numero ospiti');
				return;
			}
			if (!this.placeholderRestaurantResource) {
				this.configService.presentToastError('Configurazione mancante: contatta l\'amministratore (risorsa placeholder ristorante non trovata)');
				return;
			}
		} else if (!this.selectedResource) {
			this.configService.presentToastError('Completare tutti i campi richiesti');
			return;
		}
		if (!this.bkb_id) {
			const today = new Date().toISOString().split('T')[0];
			const endDate = this.selectedDateTo || this.selectedDate;
			if (endDate < today) {
				this.configService.presentToastError('Non è possibile creare prenotazioni nel passato');
				return;
			}
		}
		const payload: Partial<Booking> = {
			...this.booking,
			bkb_resource_id:  this.isRestaurant ? this.placeholderRestaurantResource!.bkr_id : (this.selectedResource?.bkr_id ?? null),
			bkb_time_slot_id: this.selectedSlot?.bkts_id ?? null,
			bkb_date:         this.selectedDate,
			bkb_date_to:      this.selectedDateTo || this.selectedDate,
			bkb_total_amount: this.isRestaurant ? 0 : this.prezzoBase,
			...(this.selectedSlot ? { bkb_start_time: this.selectedSlot.bkts_start_time, bkb_end_time: this.selectedSlot.bkts_end_time } : {}),
		};
		if (this.bkb_id) {
			this.bookingService.UpdateBooking(payload as Booking).subscribe({
				next: () => { this.configService.presentToastSuccess('Prenotazione aggiornata'); this.goBack(); },
				error: (err) => this.configService.presentToastError(err),
			});
		} else {
			this.bookingService.InsertBooking(this.cmp_id, payload).subscribe({
				next: (res) => {
					// Registra l'utente come cliente dell'azienda (+ dipendente) se non lo è già.
					// Non bloccante: non deve compromettere l'esito della prenotazione.
					this.customerRegistration.ensureCustomerForCurrentUser(this.cmp_id).subscribe();
					this.configService.presentToastSuccess('Prenotazione confermata');
					this.router.navigate(['/portale', this.prt_id, 'company', this.cmp_id, 'service', 'prenotazioni', 'dettaglio', res.bkb_id]);
				},
				error: (err) => this.configService.presentToastError(err),
			});
		}
	}

	goBack() {
		this.router.navigate(['/portale', this.prt_id, 'company', this.cmp_id, 'service', 'prenotazioni']);
	}
}
