// 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 { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { LoadingController, AlertController, ActionSheetController } from '@ionic/angular';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';

import { ConfigService } from 'src/app/providers/config.service';
import { BizAppointmentsService } from 'src/app/providers/biz-appointments.service';
import { MessagingService } from 'src/app/providers/messaging.service';
import { CompaniesService } from 'src/app/providers/companies.service';
import { Appointment, AppointmentStatusColors, AppointmentStatusOptions, AppointmentStatus, StatusTransitionRequest } from 'src/models/biz-appointments/appointment.model';
import { AppointmentAttachment } from 'src/models/biz-appointments/appointment-attachment.model';
import { FileDownloadService } from 'src/app/providers/fileDownload.service';
import { FunctionService } from 'src/app/providers/function.service';
import { BizAvailabilityService } from 'src/app/providers/biz-availability.service';
import { OperatorSimple, FreeSlot } from 'src/models/biz-appointments/availability.model';

interface SlotGroup {
	date: string;
	dateLabel: string;
	morningSlots: FreeSlot[];
	afternoonSlots: FreeSlot[];
}

@Component({
	standalone: false,
	selector: 'app-biz-appointment-detail',
	templateUrl: './biz-appointment-detail.page.html',
	styleUrls: ['./biz-appointment-detail.page.scss'],
})
export class BizAppointmentDetailPage implements OnInit {
	appointment: Appointment | null = null;
	attachments: AppointmentAttachment[] = [];
	loading: boolean = false;
	isGestore: boolean = false;
	currentUserId: number = 0;
	statusColors = AppointmentStatusColors;
	statusOptions = AppointmentStatusOptions;

	// Chat azienda-cliente disponibile solo se l'azienda e' in modalita' COMPANY
	chatEnabled: boolean = false;

	// Route params
	prt_id: number = 0;
	cmp_id: number = 0;

	// Modal proponi data - selettore slot (come wizard richiesta)
	showProposeModal: boolean = false;
	proposeNotes: string = '';
	proposeOperatorEmId: number | null = null;
	operators: OperatorSimple[] = [];
	freeSlots: FreeSlot[] = [];
	slotGroups: SlotGroup[] = [];
	selectedSlot: FreeSlot | null = null;
	selectedDateGroup: SlotGroup | null = null;
	selectedPeriod: 'morning' | 'afternoon' = 'morning';
	slotsLoading: boolean = false;

	// Modal rifiuta/annulla
	showRejectModal: boolean = false;
	rejectReason: string = '';
	rejectAction: 'RIFIUTATO' | 'ANNULLATO' | 'PREVENTIVO_RIFIUTATO' = 'RIFIUTATO';

	// Modal preventivo (modalità SU_PREVENTIVO)
	showQuoteModal: boolean = false;
	quoteAmount: number | null = null;
	quoteDescription: string = '';
	quoteValidUntil: string = '';

	// Evento programmatico (scadenza prossima esecuzione, yyyy-MM-dd)
	programmaticDueDate: string = '';
	canManageProgrammatic: boolean = false;
	private readonly programmaticModuleName = 'Appuntamenti';

	get isQuoteMode(): boolean {
		return this.appointment?.service_type_mode === 'SU_PREVENTIVO';
	}

	// Manutenzione ricorrente
	programmaticRecurrenceMonths: number | null = null;
	programmaticLeadDays: number = 30;
	recurrenceOptions = [
		{ label: 'Nessuna (singola)', value: null },
		{ label: 'Annuale', value: 12 },
		{ label: 'Semestrale', value: 6 },
		{ label: 'Trimestrale', value: 3 },
		{ label: 'Mensile', value: 1 },
	];

	constructor(
		private route: ActivatedRoute,
		private router: Router,
		private loadingController: LoadingController,
		private alertController: AlertController,
		private actionSheetController: ActionSheetController,
		private configService: ConfigService,
		private appointmentsService: BizAppointmentsService,
		private fileDownload: FileDownloadService,
		private funService: FunctionService,
		private availabilityService: BizAvailabilityService,
		private messagingService: MessagingService,
		private companiesService: CompaniesService,
	) {}

	ngOnInit(): void {
		this.currentUserId = this.configService.getUserID();
		this.prt_id = Number(this.route.snapshot.paramMap.get('prt_id') || this.route.parent?.snapshot.paramMap.get('prt_id') || 0);
		this.cmp_id = Number(this.route.snapshot.paramMap.get('cmp_id') || this.route.parent?.snapshot.paramMap.get('cmp_id') || 0);
		this.loadProgrammaticPermission();

		const id = Number(this.route.snapshot.paramMap.get('id'));
		if (id) {
			this.loadAppointment(id);
		}
	}

	async loadAppointment(id: number): Promise<void> {
		const loader = await this.loadingController.create({ message: 'Caricamento...' });
		await loader.present();

		this.appointmentsService.getAppointment(id).subscribe({
			next: (data: any) => {
				this.appointment = data;
				// ion-datetime richiede un ISO completo: il backend ritorna 'yyyy-MM-dd', aggiungo l'orario locale a mezzanotte
				this.programmaticDueDate = data?.apt_programmatic_due_date ? `${data.apt_programmatic_due_date}T00:00:00` : '';
				this.programmaticRecurrenceMonths = data?.apt_recurrence_months ?? null;
				this.programmaticLeadDays = data?.apt_reminder_lead_days ?? 30;
				this.loadAttachments(id);
				if (data?.apt_cmp_id) {
					this.companiesService.getChatMode(data.apt_cmp_id).subscribe((mode) => {
						this.chatEnabled = mode === 'COMPANY';
					});
				}
			},
			error: () => {
				this.configService.presentToastError('Errore caricamento appuntamento');
				loader.dismiss();
			},
			complete: () => {
				loader.dismiss();
			},
		});
	}

	loadAttachments(apt_id: number): void {
		this.appointmentsService.getAttachments(apt_id).subscribe({
			next: (data: any) => {
				this.attachments = data || [];
			},
		});
	}

	// --- Evento programmatico ---

	loadProgrammaticPermission(): void {
		this.funService.funzionibyuser(this.programmaticModuleName).subscribe({
			next: (res: any[]) => {
				const funcs = (res || []).map((f) => f.mdfu_function);
				this.canManageProgrammatic = funcs.includes('eventi_programmatici');
				// operatore/gestore via module function (sostituisce isUserAuthorizedAs('ADMIN'))
				this.isGestore = funcs.includes('gestione');
			},
			error: () => {},
		});
	}

	/** Il campo scadenza si imposta PRIMA che l'appuntamento sia confermato (quando l'operatore fissa la data). */
	get canSetProgrammatic(): boolean {
		if (!this.canManageProgrammatic || !this.appointment) return false;
		const s = this.appointment.apt_status;
		const postConfirm = s === 'CONFERMATO' || s === 'IN_CORSO' || s === 'COMPLETATO';
		if (this.isQuoteMode) return s === 'PREVENTIVO_ACCETTATO' || s === 'ATTENDE_CONFERMA' || postConfirm;
		return s === 'IN_ATTESA' || s === 'ATTENDE_CONFERMA' || postConfirm;
	}

	// --- Azioni stato ---

	canProposeDate(): boolean {
		// In modalità preventivo la data si propone DOPO l'accettazione del preventivo
		return this.isGestore && ((this.appointment?.apt_status === 'IN_ATTESA' && !this.isQuoteMode) || this.appointment?.apt_status === 'PREVENTIVO_ACCETTATO');
	}

	canConfirmDirect(): boolean {
		return this.isGestore && this.appointment?.apt_status === 'IN_ATTESA' && !this.isQuoteMode;
	}

	canAcceptProposal(): boolean {
		return !this.isGestore && this.appointment?.apt_status === 'ATTENDE_CONFERMA';
	}

	canReject(): boolean {
		return this.isGestore && this.appointment?.apt_status === 'IN_ATTESA';
	}

	// --- Preventivo (SU_PREVENTIVO) ---
	canSendQuote(): boolean {
		return this.isGestore && this.appointment?.apt_status === 'IN_ATTESA' && this.isQuoteMode;
	}

	canAcceptQuote(): boolean {
		return !this.isGestore && this.appointment?.apt_status === 'PREVENTIVO_INVIATO';
	}

	canRejectQuote(): boolean {
		return !this.isGestore && this.appointment?.apt_status === 'PREVENTIVO_INVIATO';
	}

	canCancel(): boolean {
		if (!this.appointment) return false;
		const status = this.appointment.apt_status;
		if (this.isGestore) {
			return status === 'CONFERMATO' || status === 'PREVENTIVO_INVIATO' || status === 'PREVENTIVO_ACCETTATO';
		}
		return status === 'ATTENDE_CONFERMA' || status === 'CONFERMATO';
	}

	openProposeModal(): void {
		this.proposeNotes = '';
		this.proposeOperatorEmId = this.appointment?.apt_operator_em_id ?? null;
		// reset stato slot
		this.freeSlots = [];
		this.slotGroups = [];
		this.selectedSlot = null;
		this.selectedDateGroup = null;
		this.selectedPeriod = 'morning';
		this.slotsLoading = false;
		this.loadOperators();
		this.loadFreeSlots();
		this.showProposeModal = true;
	}

	loadOperators(): void {
		const cmpId = this.appointment?.apt_cmp_id || this.cmp_id;
		if (!cmpId) return;
		this.availabilityService.getOperators(cmpId).subscribe({
			next: (data: OperatorSimple[]) => {
				this.operators = data || [];
			},
			error: () => {},
		});
	}

	/** Cambio del filtro operatore: ricarica gli slot per quell'operatore (o tutti se pulito). */
	onProposeOperatorChange(): void {
		this.selectedSlot = null;
		this.loadFreeSlots();
	}

	loadFreeSlots(): void {
		if (!this.appointment) return;

		this.slotsLoading = true;
		this.selectedSlot = null;
		const today = new Date();
		const twoWeeks = new Date();
		twoWeeks.setDate(today.getDate() + 14);

		const dateFrom = this.formatDateISO(today);
		const dateTo = this.formatDateISO(twoWeeks);
		const duration = this.appointment.apt_duration_minutes || 30;
		const operatorEmId = this.proposeOperatorEmId ?? undefined;

		this.appointmentsService.getFreeSlots(this.appointment.apt_cmp_id, this.appointment.apt_service_type_id, dateFrom, dateTo, operatorEmId, duration).subscribe({
			next: (data: any) => {
				this.freeSlots = (data || []).filter((s: FreeSlot) => !s.reserved);
				this.groupSlotsByDate();
			},
			error: () => {
				this.configService.presentToastError('Errore caricamento slot');
			},
			complete: () => {
				this.slotsLoading = false;
			},
		});
	}

	groupSlotsByDate(): void {
		const groups: Map<string, SlotGroup> = new Map();
		const seen: Set<string> = new Set();
		const now = new Date();
		const todayStr = this.formatDateISO(now);
		const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;

		for (const slot of this.freeSlots) {
			if (slot.date === todayStr && slot.start_time <= currentTime) continue;

			const key = `${slot.date}_${slot.start_time}`;
			if (seen.has(key)) continue;
			seen.add(key);

			if (!groups.has(slot.date)) {
				const d = new Date(slot.date);
				groups.set(slot.date, {
					date: slot.date,
					dateLabel: d.toLocaleDateString('it-IT', { weekday: 'long', day: 'numeric', month: 'long' }),
					morningSlots: [],
					afternoonSlots: [],
				});
			}
			const group = groups.get(slot.date)!;
			const hour = parseInt(slot.start_time.split(':')[0], 10);
			if (hour < 13) {
				group.morningSlots.push(slot);
			} else {
				group.afternoonSlots.push(slot);
			}
		}

		this.slotGroups = Array.from(groups.values());
		if (this.slotGroups.length > 0) {
			this.selectDateGroup(this.slotGroups[0]);
		} else {
			this.selectedDateGroup = null;
		}
	}

	selectDateGroup(group: SlotGroup): void {
		this.selectedDateGroup = group;
		this.selectedSlot = null;
		this.selectedPeriod = group.morningSlots.length > 0 ? 'morning' : 'afternoon';
	}

	selectPeriod(period: 'morning' | 'afternoon'): void {
		this.selectedPeriod = period;
	}

	getDayName(group: SlotGroup): string {
		const d = new Date(group.date);
		const today = new Date();
		today.setHours(0, 0, 0, 0);
		const dateObj = new Date(group.date);
		dateObj.setHours(0, 0, 0, 0);
		if (dateObj.getTime() === today.getTime()) {
			return 'Oggi';
		}
		return d.toLocaleDateString('it-IT', { weekday: 'short' });
	}

	getDayDate(group: SlotGroup): string {
		const d = new Date(group.date);
		return d.toLocaleDateString('it-IT', { day: 'numeric', month: 'short' });
	}

	selectSlot(slot: FreeSlot): void {
		this.selectedSlot = slot;
	}

	submitProposal(): void {
		if (!this.appointment || !this.selectedSlot) return;

		const dateStr = this.selectedSlot.date;
		const timeStr = this.selectedSlot.start_time.substring(0, 5);

		const request: StatusTransitionRequest = {
			new_status: 'ATTENDE_CONFERMA',
			proposed_date: dateStr,
			proposed_time: timeStr,
			notes: this.proposeNotes,
			operator_em_id: this.proposeOperatorEmId ?? this.selectedSlot.operator_em_id,
		};

		this.doTransition(request, 'Proposta inviata');
		this.showProposeModal = false;
	}

	private formatDateISO(date: Date): string {
		const y = date.getFullYear();
		const m = String(date.getMonth() + 1).padStart(2, '0');
		const d = String(date.getDate()).padStart(2, '0');
		return `${y}-${m}-${d}`;
	}

	async confirmDirect(): Promise<void> {
		const alert = await this.alertController.create({
			header: 'Conferma',
			message: 'Confermare direttamente questo appuntamento?',
			buttons: [
				{ text: 'Annulla', role: 'cancel' },
				{
					text: 'Conferma',
					handler: () => {
						const request: StatusTransitionRequest = { new_status: 'CONFERMATO' };
						this.doTransition(request, 'Appuntamento confermato');
					},
				},
			],
		});
		await alert.present();
	}

	async acceptProposal(): Promise<void> {
		const alert = await this.alertController.create({
			header: 'Accetta proposta',
			message: 'Accettare la data/ora proposta?',
			buttons: [
				{ text: 'Annulla', role: 'cancel' },
				{
					text: 'Accetta',
					handler: () => {
						const request: StatusTransitionRequest = { new_status: 'CONFERMATO' };
						this.doTransition(request, 'Proposta accettata');
					},
				},
			],
		});
		await alert.present();
	}

	// --- Preventivo ---
	openQuoteModal(): void {
		this.quoteAmount = this.appointment?.apt_quote_amount ?? null;
		this.quoteDescription = this.appointment?.apt_quote_description ?? '';
		this.quoteValidUntil = this.appointment?.apt_quote_valid_until ?? '';
		this.showQuoteModal = true;
	}

	submitQuote(): void {
		if (!this.appointment || this.quoteAmount == null || this.quoteAmount <= 0) {
			this.configService.presentToastWarning("Inserisci l'importo del preventivo");
			return;
		}
		let validUntil: string | undefined = undefined;
		if (this.quoteValidUntil) {
			const d = new Date(this.quoteValidUntil);
			validUntil = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
		}
		const request: StatusTransitionRequest = {
			new_status: 'PREVENTIVO_INVIATO',
			quote_amount: this.quoteAmount,
			quote_description: this.quoteDescription,
			quote_valid_until: validUntil,
		};
		this.doTransition(request, 'Preventivo inviato');
		this.showQuoteModal = false;
	}

	async acceptQuote(): Promise<void> {
		const alert = await this.alertController.create({
			header: 'Accetta preventivo',
			message: 'Accettare il preventivo ricevuto?',
			buttons: [
				{ text: 'Annulla', role: 'cancel' },
				{
					text: 'Accetta',
					handler: () => {
						this.doTransition({ new_status: 'PREVENTIVO_ACCETTATO' }, 'Preventivo accettato');
					},
				},
			],
		});
		await alert.present();
	}

	openRejectQuoteModal(): void {
		this.rejectReason = '';
		this.rejectAction = 'PREVENTIVO_RIFIUTATO';
		this.showRejectModal = true;
	}

	openRejectModal(): void {
		this.rejectReason = '';
		this.rejectAction = 'RIFIUTATO';
		this.showRejectModal = true;
	}

	openCancelModal(): void {
		this.rejectReason = '';
		this.rejectAction = 'ANNULLATO';
		this.showRejectModal = true;
	}

	submitReject(): void {
		if (!this.appointment) return;
		const request: StatusTransitionRequest = {
			new_status: this.rejectAction,
			rejection_reason: this.rejectAction === 'ANNULLATO' ? undefined : this.rejectReason,
			cancellation_reason: this.rejectAction === 'ANNULLATO' ? this.rejectReason : undefined,
		};
		const msg = this.rejectAction === 'ANNULLATO' ? 'Appuntamento annullato' : this.rejectAction === 'PREVENTIVO_RIFIUTATO' ? 'Preventivo rifiutato' : 'Appuntamento rifiutato';
		this.doTransition(request, msg);
		this.showRejectModal = false;
	}

	private doTransition(request: StatusTransitionRequest, successMsg: string): void {
		if (!this.appointment) return;
		this.appointmentsService.transitionStatus(this.appointment.apt_id, request).subscribe({
			next: (updated: any) => {
				this.appointment = updated;
				this.configService.presentToastSuccess(successMsg);
			},
			error: (err: any) => {
				const msg = err?.error?.message || err?.message || 'Errore transizione stato';
				this.configService.presentToastError(msg);
			},
		});
	}

	// --- Manutenzione ricorrente ---

	/** Estrae la data calendario locale (yyyy-MM-dd) dal valore ion-datetime, gestendo sia l'ISO locale sia quello con offset/Z. */
	private toDateOnly(value: string | null): string | null {
		if (!value) return null;
		// ISO senza fuso (default ion-datetime presentation="date"): il prefisso e' gia' il giorno scelto
		if (!/[zZ]|[+-]\d{2}:?\d{2}$/.test(value)) return value.substring(0, 10);
		// ISO con offset/Z: converto al giorno calendario locale
		const d = new Date(value);
		const y = d.getFullYear();
		const m = String(d.getMonth() + 1).padStart(2, '0');
		const day = String(d.getDate()).padStart(2, '0');
		return `${y}-${m}-${day}`;
	}

	saveProgrammatic(): void {
		if (!this.appointment) return;
		const due = this.toDateOnly(this.programmaticDueDate);
		const recurrence = due ? this.programmaticRecurrenceMonths : null;
		const lead = this.programmaticLeadDays != null ? this.programmaticLeadDays : 30;
		this.appointmentsService.setProgrammaticDeadline(this.appointment.apt_id, due, recurrence, lead).subscribe({
			next: () => {
				this.configService.presentToastSuccess('Manutenzione aggiornata');
				this.loadAppointment(this.appointment!.apt_id);
			},
			error: () => this.configService.presentToastError('Errore salvataggio manutenzione'),
		});
	}

	clearProgrammatic(): void {
		this.programmaticDueDate = '';
		this.programmaticRecurrenceMonths = null;
		this.programmaticLeadDays = 30;
		this.saveProgrammatic();
	}

	// --- Allegati ---

	async addAttachment(): Promise<void> {
		const actionSheet = await this.actionSheetController.create({
			header: 'Aggiungi allegato',
			buttons: [
				{
					text: 'Fotocamera',
					icon: 'camera-outline',
					handler: () => this.capturePhoto(),
				},
				{
					text: 'File',
					icon: 'document-outline',
					handler: () => this.pickFile(),
				},
				{ text: 'Annulla', icon: 'close-outline', role: 'cancel' },
			],
		});
		await actionSheet.present();
	}

	async capturePhoto(): Promise<void> {
		try {
			const photo = await Camera.getPhoto({
				resultType: CameraResultType.Uri,
				source: CameraSource.Camera,
				quality: 80,
			});
			if (photo.webPath) {
				const response = await fetch(photo.webPath);
				const blob = await response.blob();
				const file = new File([blob], `foto_${Date.now()}.jpg`, { type: 'image/jpeg' });
				this.uploadFile(file);
			}
		} catch (e) {
			console.error('Camera error:', e);
		}
	}

	pickFile(): void {
		const input = document.createElement('input');
		input.type = 'file';
		input.accept = 'image/*,.pdf,.doc,.docx';
		input.onchange = (event: any) => {
			const file = event.target.files[0];
			if (file) {
				this.uploadFile(file);
			}
		};
		input.click();
	}

	uploadFile(file: File): void {
		if (!this.appointment) return;
		this.appointmentsService.uploadAttachment(this.appointment.apt_id, file).subscribe({
			next: () => {
				this.configService.presentToastSuccess('Allegato caricato');
				this.loadAttachments(this.appointment!.apt_id);
			},
			error: () => {
				this.configService.presentToastError('Errore caricamento allegato');
			},
		});
	}

	downloadAttachment(att: AppointmentAttachment): void {
		this.appointmentsService.downloadAttachment(att.att_id).subscribe({
			next: (res: any) => {
				if (res && res.res) {
					const url = this.configService.imageBaseURL + '/temp/' + res.res;
					this.fileDownload.downloadAndOpenFile(url, att.att_filename, att.att_mime_type);
				}
			},
			error: () => {
				this.configService.presentToastError('Errore download');
			},
		});
	}

	async deleteAttachment(att: AppointmentAttachment): Promise<void> {
		const alert = await this.alertController.create({
			header: 'Elimina allegato',
			message: `Eliminare "${att.att_filename}"?`,
			buttons: [
				{ text: 'Annulla', role: 'cancel' },
				{
					text: 'Elimina',
					role: 'destructive',
					handler: () => {
						this.appointmentsService.deleteAttachment(att.att_id).subscribe({
							next: () => {
								this.configService.presentToastSuccess('Allegato eliminato');
								this.loadAttachments(this.appointment!.apt_id);
							},
							error: () => {
								this.configService.presentToastError('Errore eliminazione');
							},
						});
					},
				},
			],
		});
		await alert.present();
	}

	// --- Navigazione ---

	goToMessages(): void {
		if (!this.appointment) return;
		// Bug B: i messaggi dell'appuntamento confluiscono nella chat centralizzata
		// azienda<->cliente, invece del canale separato per-appuntamento.
		const isClient = this.currentUserId === this.appointment.apt_client_usr_id;
		this.messagingService.findOrCreateCompanyConversationForClient(this.appointment.apt_cmp_id, this.appointment.apt_client_usr_id).subscribe({
			next: (conv) => {
				this.router.navigate(['/messaging/chat', conv.conv_id], {
					state: {
						// Il cliente vede l'azienda (titolo risolto dai messaggi), l'operatore vede il cliente
						chatWith: isClient ? 'Chat' : [this.appointment?.client_firstname, this.appointment?.client_lastname].filter(Boolean).join(' ') || 'Chat',
						isCompany: isClient,
					},
				});
			},
			error: () => this.configService.presentToastError('Impossibile aprire la chat'),
		});
	}

	// --- Helpers ---

	getStatusLabel(status: AppointmentStatus): string {
		const opt = this.statusOptions.find((o) => o.value === status);
		return opt ? opt.label : status;
	}

	getStatusColor(status: AppointmentStatus): string {
		return this.statusColors[status] || '#999';
	}

	formatDate(date: string | null): string {
		if (!date) return '-';
		return new Date(date).toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' });
	}

	formatTime(time: string | null): string {
		if (!time) return '-';
		return time.substring(0, 5);
	}

	formatDateTime(dateStr: string): string {
		if (!dateStr) return '-';
		const d = new Date(dateStr);
		return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
	}

	formatFileSize(bytes: number): string {
		if (bytes < 1024) return bytes + ' B';
		if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
		return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
	}
}
