import { ConnectionPositionPair, FlexibleConnectedPositionStrategy, Overlay } from '@angular/cdk/overlay'
import { ComponentPortal } from '@angular/cdk/portal'
import { AsyncPipe, NgClass, NgStyle } from '@angular/common'
import { AfterViewInit, Component, Injector, inject } from '@angular/core'
import { DateUtils, getDaySchedule } from '@core/utils'
import { ScheduleSettings, TableResponse } from '@api-client/angular'
import { KadiOverlayModule } from '@frontend/shared'
import { TranslocoDirective } from '@jsverse/transloco'
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'
import { addMinutes, differenceInHours, differenceInMinutes, isBefore, isEqual, isSameDay } from 'date-fns'
import { range } from 'es-toolkit'
import { flatMap, uniq } from 'es-toolkit/compat'
import { Observable, map, tap, timer } from 'rxjs'
import { AddReservationService } from '../../../../shared'
import { StateOptionsPipe } from '../../../../shared/pipes/state-options.pipe'
import { ReservationSAM } from '../../reservation.sam'
import { ReservationStore } from '../../reservation.store'
import { ReservationView } from '../../reservation.types'
import { ReservationDetailsPopupComponent } from './reservation-details-popup.component'
import { RESERVATION_DETAILS_POPUP_DATA } from './token'
import { CalendarColumnLeftPositionPipe, CalendarReservationWidthPipe } from './util.pipe'
import { LucideEllipsisVertical, LucideUser } from '@lucide/angular'

@UntilDestroy({ checkProperties: true })
@Component({
	selector: 'kadi-reservations-calender-view',
	templateUrl: './reservations-calender-view.component.html',
	styleUrls: ['./reservations-calender-view.component.scss'],
	imports: [
		TranslocoDirective,
		NgStyle,
		NgClass,
		KadiOverlayModule,
		AsyncPipe,
		StateOptionsPipe,
		CalendarColumnLeftPositionPipe,
		CalendarReservationWidthPipe,
		LucideUser,
		LucideEllipsisVertical,
	],
})
export class ReservationsCalenderViewComponent implements AfterViewInit {
	private readonly _overlay = inject(Overlay)
	private readonly _store = inject(ReservationStore)
	private readonly _addReservationService = inject(AddReservationService)

	readonly vm$: Observable<{
		tables: Array<TableResponse & { reservations: ReservationView[] }>
	}>
	readonly currentTime$: Observable<Date> = timer(0, 1000 * 60).pipe(map(() => new Date()))
	hours: number[] = []
	showCurrentTime = false

	readonly headerRowWidth = 175
	readonly rowWidth = 125
	readonly rowHeight = 56
	hoursDifference = differenceInHours
	minutesDifference = differenceInMinutes

	unassignedTableId = 'default_unassigned_table'

	constructor() {
		this.vm$ = this._store
			.select({
				tables: this._store.tables$,
				filterDate: this._store.filterDate$,
				schedule: this._store.schedule$,
				reservations: this._store.reservations$,
			})
			.pipe(
				tap(({ reservations, filterDate, schedule }) => {
					const hours = this.generateHours(filterDate, schedule)
					hours.push(
						...flatMap(reservations, (reservation) => [
							reservation.ref.startDate.getHours(),
							reservation.ref.endDate.getHours(),
						]),
					)
					this.hours = uniq(hours).sort((a, b) => a - b)

					const currentDate = new Date()
					if (!isSameDay(filterDate, currentDate)) this.showCurrentTime = false
					else if (this.hours.includes(currentDate.getHours())) this.showCurrentTime = true
				}),
				map(({ reservations, tables }) => {
					const tablesWithReservations = tables.map((table) => ({
						...table,
						reservations: reservations.filter((reservation) => reservation.ref.tableIds.includes(table.id)),
					}))
					const reservationsWithoutTable = reservations.filter((reservation) => reservation.tables.length === 0)
					if (reservationsWithoutTable.length > 0) {
						tablesWithReservations.unshift({
							id: this.unassignedTableId,
							name: 'Unassigned',
							reservations: reservationsWithoutTable,
							capacity: 0,
							columnStart: 0,
							rowStart: 0,
							height: 0,
							width: 0,
							shape: TableResponse.ShapeEnum.rectangle,
							priority: TableResponse.PriorityEnum.low,
						})
					}
					return { tables: tablesWithReservations }
				}),
			)
	}

	ngAfterViewInit(): void {
		this._store.filterDate$.pipe(untilDestroyed(this)).subscribe({
			next: (filterDate) => {
				// Only scroll when selected date is today
				if (DateUtils.isEqual(filterDate, new Date())) this.scrollViewToTime()
			},
		})
	}

	edit(reservation: ReservationView): void {
		this._addReservationService.edit(reservation.ref, {})
	}

	updateState(reservation: ReservationView, state: ReservationSAM.State): void {
		reservation.stateService.send(state)
	}

	openDetails(reservation: ReservationView, origin: HTMLElement): void {
		const position = this.createConnectedPositionStrategy(origin)
		const overlayRef = this._overlay.create({
			positionStrategy: position,
			hasBackdrop: true,
			minWidth: 400,
			width: origin.clientWidth,
		})
		const injector = Injector.create({
			providers: [
				{ provide: RESERVATION_DETAILS_POPUP_DATA, useValue: { reservation, store: this._store, ref: overlayRef } },
			],
		})
		const portal = new ComponentPortal(ReservationDetailsPopupComponent, null, injector)
		overlayRef.attach(portal)
		overlayRef
			.backdropClick()
			.pipe(untilDestroyed(this))
			.subscribe(() => {
				overlayRef.detach()
				overlayRef.dispose()
			})
	}

	private createConnectedPositionStrategy(origin: HTMLElement): FlexibleConnectedPositionStrategy {
		return this._overlay
			.position()
			.flexibleConnectedTo(origin)
			.withPositions([
				new ConnectionPositionPair({ originX: 'start', originY: 'top' }, { overlayX: 'start', overlayY: 'top' }),
			])
	}

	/** Scrolls table until time divider is in center */
	private scrollViewToTime(): void {
		// Wait for element to be rendered, required beacuse we are using <ng-container> with ngIf
		const observer = new MutationObserver(() => {
			const element = document.querySelector('.time-divider')
			if (element && document.contains(element)) {
				element.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'center' })
				observer.disconnect()
			}
		})

		observer.observe(document, {
			attributes: false,
			childList: true,
			characterData: false,
			subtree: true,
		})
	}

	private generateHours(date: Date, schedule?: ScheduleSettings): number[] {
		if (!schedule) return range(24)

		const hours: number[] = []
		const daySchedule = getDaySchedule(date, schedule)
		if (!daySchedule.rules) return range(24)

		for (const rule of daySchedule.rules) {
			let startingTime = new Date(0, 0, 0, rule.openingTime.hours, rule.openingTime.minutes)
			const endingTime = new Date(0, 0, 0, rule.closingTime.hours, rule.closingTime.minutes)

			while (isEqual(startingTime, endingTime) || isBefore(startingTime, endingTime)) {
				hours.push(startingTime.getHours())
				startingTime = addMinutes(startingTime, 30)
			}
		}
		return uniq(hours)
	}
}
