// 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, EventEmitter, HostListener,
	Input, OnChanges, Output, SimpleChanges, ViewChild,
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { BookingResource } 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 Muro { id: string; points: { x: number; y: number }[]; }

export 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;
}

/**
 * Mappa interattiva delle risorse — componente condiviso form-prenotazione (cliente)
 * e gestione-prenotazioni tab Mappa (manager).
 *
 * Codice copiato 1:1 da form-prenotazione (logica mappa). Niente business logic.
 *   - resources:         tutte le risorse incluse _PASSERELLA_/_STABILIMENTO_/_CANVAS_CONFIG_
 *   - bookedResourceIds: Set degli ID risorse "occupate" (logica esterna)
 *   - selectedResource:  risorsa attualmente selezionata (per highlight)
 *   - mode:              'beach' | 'standard' | 'auto' (default 'auto')
 *   - getFilaColors:     callback opzionale per colorare le bande di fila (usato dal cliente per prezzi)
 */
@Component({
	selector: 'app-resource-map',
	templateUrl: './resource-map.component.html',
	styleUrls: ['./resource-map.component.scss'],
	standalone: false,
})
export class ResourceMapComponent implements AfterViewInit, OnChanges {
	@Input() resources: BookingResource[] = [];
	@Input() bookedResourceIds: Set<number> = new Set();
	@Input() selectedResource: BookingResource | null = null;
	@Input() mode: 'standard' | 'beach' | 'auto' = 'auto';
	/** Callback opzionale per colorare le bande di fila (cliente: per prezzo) */
	@Input() getFilaColors?: (filaIndex: number, yPercent: number) => { border: string; bg: string } | null;
	/** Callback opzionale: etichetta sotto la risorsa (manager: nome cliente) */
	@Input() getResourceLabel?: (r: BookingResource) => string | null;
	/**
	 * Callback opzionale per il rendering "gruppo prenotazione" RESTAURANT.
	 * Per ogni risorsa ritorna le info del booking associato (se presente):
	 *   - groupColor: colore del bordo del gruppo
	 *   - customerLabel: etichetta da mostrare (es. "Mario Rossi · 4 posti")
	 *   - isPrimary: true se questa risorsa è il "tavolo primario" del booking
	 *                (la label viene mostrata solo sul primario)
	 *   - resourceAlias?: alias logico del cluster (es. "Tavolo 2"); se valorizzato,
	 *                il primario lo mostra al posto del nome reale del tavolo.
	 *   - isClusterMember?: true se la prenotazione raggruppa ≥2 tavoli; i tavoli
	 *                secondari del cluster nascondono l'etichetta nome.
	 */
	@Input() getBookingGroupInfo?: (r: BookingResource) => { groupColor: string; customerLabel: string; isPrimary: boolean; resourceAlias?: string | null; isClusterMember?: boolean } | null;
	/**
	 * Override visivi per le posizioni dei tavoli (es. cluster ristorante).
	 * Coordinate in PIXEL nel sistema REF_WIDTH × REF_HEIGHT (es. {x: 360, y: 120}).
	 * Quando presente per un bkr_id, sovrascrive la posizione parsata da bkr_position.
	 */
	@Input() displayPositions?: Map<number, { x: number; y: number }>;

	@Output() resourceClick = new EventEmitter<BookingResource>();
	@Output() zoneSelect    = new EventEmitter<BeachL1Zone | null>();

	@ViewChild('mappaWrapperRef') mappaWrapperRef?: ElementRef<HTMLDivElement>;

	// Costanti
	readonly REF_WIDTH  = 900;
	readonly REF_HEIGHT = 540;
	readonly GRID_STEP  = 36;

	// State derivato dal parsing delle resources
	passerelle: Passerella[] = [];
	stabilimento: Stabilimento | null = null;
	walls: Muro[] = [];
	canvasHeight = 540;

	// Spessore del muro (semi-spessore offset) — copiato dal web
	private readonly D_WALL = 4;

	// Scaling
	mapScale = 1;
	mapTranslateX = 0;
	mapTranslateY = 0;
	l2CenterOffsetPx = 0;

	// Zoom utente (moltiplicativo rispetto a mapScale)
	userZoom = 1;
	readonly minZoom = 0.5;
	readonly maxZoom = 4;
	readonly zoomStep = 0.25;

	get effectiveScale(): number { return this.mapScale * this.userZoom; }

	// Zone L1 (BEACH)
	selectedZone: BeachL1Zone | null = null;
	_autoZone = false;

	private _containerW = 0;
	private _availH = 0;
	private _canvasTop = 0;

	constructor(private sanitizer: DomSanitizer) {}

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

	ngOnChanges(changes: SimpleChanges): void {
		if (changes['resources']) {
			this._parseSpecialResources();
			this.selectedZone = null;
			this._autoZone = false;
			setTimeout(() => this._computeMapScale(), 50);
		}
	}

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

	get isBeach(): boolean {
		if (this.mode === 'beach')    return true;
		if (this.mode === 'standard') return false;
		return this.positionedResources.some(r => {
			try {
				const n = JSON.parse(r.bkr_notes || '{}');
				return n.subtype === 'OMBRELLONE' || n.subtype === 'PALMA';
			} catch { return false; }
		});
	}

	/** Vero se ci sono risorse di tipo ristorante (QUADRATO/TONDO) o muri */
	get isRestaurant(): boolean {
		if (this.isBeach) return false;
		if (this.walls.length > 0) return true;
		return this.positionedResources.some(r => {
			try {
				const n = JSON.parse(r.bkr_notes || '{}');
				return n.subtype === 'QUADRATO' || n.subtype === 'TONDO';
			} catch { return false; }
		});
	}

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

	private _parseSpecialResources(): void {
		const list = this.resources || [];
		this.passerelle = list
			.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 };
			});
		// Muri RESTAURANT: codice _MURO_* — bkr_notes.points = array { x, y } in percentuale
		this.walls = list
			.filter((r) => (r.bkr_code || '').startsWith('_MURO_'))
			.map((r) => {
				let notes: any = {};
				try { notes = JSON.parse(r.bkr_notes || '{}'); } catch {}
				const pts: { x: number; y: number }[] = Array.isArray(notes.points) ? notes.points : [];
				return { id: String(r.bkr_id), points: pts };
			});
		const stab = list.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 = list.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;
		}
	}

	get positionedResources(): BookingResource[] {
		return (this.resources || []).filter(r => !!r.bkr_position && !r.bkr_code?.startsWith('_'));
	}

	// ── Helpers parsing JSON notes / 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' }; }
	}

	/** RESTAURANT: forma del tavolo ('QUADRATO' default | 'TONDO') */
	getRestaurantSubtype(r: BookingResource): 'QUADRATO' | 'TONDO' {
		try {
			const sub = JSON.parse(r.bkr_notes || '{}').subtype as string;
			return sub === 'TONDO' ? 'TONDO' : 'QUADRATO';
		} catch { return 'QUADRATO'; }
	}

	/** RESTAURANT: colore tovaglia (scacchiera) */
	getRestaurantColor(r: BookingResource): string {
		try {
			const n = JSON.parse(r.bkr_notes || '{}');
			return n.color1 || '#e53935';
		} catch { return '#e53935'; }
	}

	/** Wrapper per getBookingGroupInfo input */
	bookingGroupInfo(r: BookingResource): { groupColor: string; customerLabel: string; isPrimary: boolean; resourceAlias?: string | null; isClusterMember?: boolean } | null {
		return this.getBookingGroupInfo ? this.getBookingGroupInfo(r) : null;
	}

	/**
	 * Nome da visualizzare per una risorsa nella mappa:
	 *   - primario di cluster con alias  → alias logico
	 *   - secondario di cluster (non primario) → '' (etichetta nascosta)
	 *   - altrimenti → bkr_name
	 */
	resourceDisplayName(r: BookingResource): string {
		const bg = this.bookingGroupInfo(r);
		if (bg?.isClusterMember) {
			if (!bg.isPrimary) return '';
			if (bg.resourceAlias) return bg.resourceAlias;
		}
		return r.bkr_name;
	}

	// ── Muri (path SVG con offset bilaterale) — copiato da web ────────
	private _offsetPolylinePts(points: { x: number; y: number }[], d: number, closed: boolean): { x: number; y: number }[] {
		if (points.length < 2) return [];
		const px = points.map(p => ({ x: p.x / 100 * this.REF_WIDTH, y: p.y / 100 * this.REF_HEIGHT }));
		const N = px.length;
		const numSegs = closed ? N : N - 1;
		const norms: { nx: number; ny: number }[] = [];
		for (let i = 0; i < numSegs; i++) {
			const next = closed ? (i + 1) % N : i + 1;
			const dx = px[next].x - px[i].x;
			const dy = px[next].y - px[i].y;
			const len = Math.sqrt(dx * dx + dy * dy) || 1;
			norms.push({ nx: -dy / len, ny: dx / len });
		}
		const calcMiter = (vx: number, vy: number, np: { nx: number; ny: number }, nn: { nx: number; ny: number }) => {
			const bx = np.nx + nn.nx, by = np.ny + nn.ny;
			const blen2 = bx * bx + by * by;
			if (blen2 < 0.0001) return { x: vx + d * np.nx, y: vy + d * np.ny };
			const factor = (2 * d) / blen2;
			return { x: vx + factor * bx, y: vy + factor * by };
		};
		const out: { x: number; y: number }[] = [];
		if (closed) {
			for (let i = 0; i < N; i++) {
				out.push(calcMiter(px[i].x, px[i].y, norms[(i - 1 + numSegs) % numSegs], norms[i % numSegs]));
			}
		} else {
			out.push({ x: px[0].x + d * norms[0].nx, y: px[0].y + d * norms[0].ny });
			for (let i = 1; i < N - 1; i++) out.push(calcMiter(px[i].x, px[i].y, norms[i - 1], norms[i]));
			const last = norms[norms.length - 1];
			out.push({ x: px[N - 1].x + d * last.nx, y: px[N - 1].y + d * last.ny });
		}
		return out;
	}

	/** Path SVG chiuso del muro (cap agli estremi se polilinea aperta, due anelli se chiusa) */
	getWallShapePath(m: Muro): string {
		if (m.points.length < 2) return '';
		const first = m.points[0];
		const last = m.points[m.points.length - 1];
		const isClosed = first.x === last.x && first.y === last.y && m.points.length >= 4;
		const fmt = (p: { x: number; y: number }) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
		if (isClosed) {
			const ptsNoDup = m.points.slice(0, -1);
			const outer = this._offsetPolylinePts(ptsNoDup, this.D_WALL, true);
			const inner = this._offsetPolylinePts(ptsNoDup, -this.D_WALL, true);
			let d = '';
			if (outer.length) d += `M ${outer.map(fmt).join(' L ')} Z `;
			if (inner.length) d += `M ${inner.map(fmt).join(' L ')} Z`;
			return d;
		}
		const outer = this._offsetPolylinePts(m.points, this.D_WALL, false);
		const inner = this._offsetPolylinePts(m.points, -this.D_WALL, false);
		const innerRev = inner.slice().reverse();
		return `M ${[...outer, ...innerRev].map(fmt).join(' L ')} Z`;
	}

	// ── Stili ─────────────────────────────────────────────────────────
	getResourceStyle(r: BookingResource): { [k: string]: string } {
		// Override visivo (es. cluster ristorante affiancato): coordinate in px
		const override = this.displayPositions?.get(r.bkr_id);
		if (override) {
			return {
				left:   override.x + 'px',
				top:    override.y + 'px',
				width:  (this.getResourceCols(r) * this.GRID_STEP) + 'px',
				height: (this.getResourceRows(r) * this.GRID_STEP) + 'px',
			};
		}
		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 1:1 da form-prenotazione) */
	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);
	}

	// ── Dimensioni canvas effettive ──────────────────────────────────
	get canvasUsedW(): number {
		let maxX = 0;
		for (const r of this.positionedResources) {
			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.positionedResources) {
			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);
	}

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

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

	// ── Controlli zoom utente ────────────────────────────────────────
	zoomIn(): void {
		this.userZoom = Math.min(this.maxZoom, +(this.userZoom + this.zoomStep).toFixed(2));
	}

	zoomOut(): void {
		this.userZoom = Math.max(this.minZoom, +(this.userZoom - this.zoomStep).toFixed(2));
	}

	resetZoom(): void {
		this.userZoom = 1;
	}

	// ── Zone L1 spiaggia — griglia nx×ny dinamica (copiato 1:1 da form) ──
	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.isBooked(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.isBooked(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;
		setTimeout(() => this._computeMapScale(), 20);
		this.zoneSelect.emit(zone);
	}

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

	// ── Bande orizzontali per fila — colore via callback (cliente: prezzi) ─
	get filaZones(): { fila: number; topPx: number; heightPx: number; bgColor: string; borderColor: string }[] {
		if (!this.isBeach || !this.getFilaColors) return [];
		const ySet = new Set<number>();
		for (const r of this.positionedResources) {
			try { ySet.add(JSON.parse(r.bkr_position).y); } catch {}
		}
		const ySorted = Array.from(ySet).sort((a, b) => a - b);
		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 colors = this.getFilaColors!(index, yPercent) || { border: '#9e9e9e', bg: 'rgba(158,158,158,0.15)' };
			return {
				fila:        index + 1,
				topPx:       Math.max(0, yPercent / 100 * this.REF_HEIGHT - padPx),
				heightPx:    maxRows * this.GRID_STEP + padPx * 2,
				bgColor:     colors.bg,
				borderColor: colors.border,
			};
		});
	}

	// ── Scaling mappa (identico a form-prenotazione) ─────────────────
	_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;
		}
	}

	resourceLabel(r: BookingResource): string | null {
		return this.getResourceLabel ? this.getResourceLabel(r) : null;
	}

	// ── Click handler (identico a form-prenotazione.onResourceSelected) ──
	onResourceSelected(r: BookingResource): void {
		// In L1 BEACH: click su risorsa = seleziona la zona corrispondente
		if (this.isBeach && !this.selectedZone) {
			try {
				const xPx = JSON.parse(r.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(r.bkr_id)) {
			// L'esterno decide se reagire (es. manager apre dettaglio prenotazione)
			this.resourceClick.emit(r);
			return;
		}
		this.resourceClick.emit(r);
	}
}
