import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, forwardRef, input } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
import { DateUtils, getDaySchedule } from '@core/utils'
import { ScheduleSettings } from '@api-client/angular'
import { UntilDestroy } from '@ngneat/until-destroy'
import { addDays, differenceInCalendarWeeks, isSameDay, isSunday, startOfWeek } from 'date-fns'
import { TimeFormatPipe } from '../../pipes'

interface CalendarDay {
	date: Date
	disabled: boolean
}

@UntilDestroy({ checkProperties: true })
@Component({
	selector: 'kadi-weekly-date-picker',
	templateUrl: './weekly-date-picker.component.html',
	styleUrls: ['./weekly-date-picker.component.scss'],
	changeDetection: ChangeDetectionStrategy.OnPush,
	imports: [CommonModule, TimeFormatPipe],
	providers: [
		{
			provide: NG_VALUE_ACCESSOR,
			useExisting: forwardRef(() => WeeklyDatePickerComponent),
			multi: true,
		},
	],
})
export class WeeklyDatePickerComponent implements ControlValueAccessor {
	readonly min = input<Date>()
	readonly max = input<Date>()
	readonly schedule = input.required<ScheduleSettings>()
	readonly showHeader = input(true)
	readonly todayHightlight = input(false)

	currentWeek: CalendarDay[] = []
	weekIndex = 0
	currentDate = DateUtils.normalizeDay(new Date())

	selectedDate: Date | undefined

	/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */
	onChange = (_date: Date | undefined) => {}
	onTouched = () => {}
	onValidatorChange = () => {}
	/* eslint-enable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */

	//#region ControlValueAccessor method implementation
	writeValue(date: Date | null | undefined): void {
		if (date === undefined || date === null) this.selectToday()
		else this.selectDay({ date, disabled: false })
	}

	registerOnChange(onChange: (_date: Date | undefined) => void): void {
		this.onChange = onChange
	}

	registerOnTouched(onTouched: () => void): void {
		this.onTouched = onTouched
	}
	//#endregion

	changeWeek(operation: 'Increment' | 'Decrement'): void {
		const weekIndex = this.weekIndex + (operation === 'Increment' ? 1 : -1)
		this.setWeek(weekIndex)
	}

	selectDay(day: CalendarDay): void {
		if (day.disabled) return

		const weekIndex = differenceInCalendarWeeks(day.date, this.currentDate, { weekStartsOn: 1 })
		if (weekIndex !== this.weekIndex) {
			this.weekIndex = weekIndex
			this.setWeek(weekIndex)
		}

		this.onTouched()
		this.selectedDate = day.date
		this.onChange(this.selectedDate)
	}

	selectToday(): void {
		this.setWeek(0)

		const today = this.currentWeek.find((calendarDay) => isSameDay(calendarDay.date, this.currentDate))
		if (today === undefined) return

		this.selectNextAvailableDay(today.date)
	}

	isDatesEqual(leftDate: Date | undefined, rightDate: Date | undefined): boolean {
		if (leftDate === undefined || rightDate === undefined) return false
		return DateUtils.isEqual(leftDate, rightDate)
	}

	private selectNextAvailableDay(date: Date) {
		const day = this.currentWeek.find((calendarDay) => isSameDay(calendarDay.date, date))
		if (day === undefined) return

		if (day.disabled) {
			// If it is last day of week, change to next week
			if (isSunday(day.date)) {
				this.changeWeek('Increment')
			}

			const nextDate = addDays(day.date, 1)
			this.selectNextAvailableDay(nextDate)
		} else {
			this.selectDay(day)
		}
	}

	private setWeek(weekIndex: number): void {
		const weekStartDate = DateUtils.normalizeDayEnd(addDays(this.currentDate, weekIndex * 7))

		this.currentWeek = this.createWeek(weekStartDate)
		this.weekIndex = weekIndex
	}

	private createWeek(startDate: Date): CalendarDay[] {
		// 1 represents Monday, https://date-fns.org/v2.29.3/docs/startOfWeek
		const weekStartDate = DateUtils.normalizeDayEnd(startOfWeek(startDate, { weekStartsOn: 1 }))

		const week = []
		for (let index = 0; index < 7; index++) {
			const date = addDays(weekStartDate, index)
			const disabled = !this.isDateAvailable(date)
			week.push({ date, disabled })
		}

		return week
	}

	private isDateAvailable(date: Date): boolean {
		const min = this.min()
		const max = this.max()

		if (min !== undefined && DateUtils.isBefore(date, min)) return false
		else if (max !== undefined && DateUtils.isAfter(date, max)) return false
		else if (!this.isDayAvailable(date)) return false
		return true
	}

	private isDayAvailable(day: Date): boolean {
		const daySchedule = getDaySchedule(day, this.schedule())
		return !daySchedule.closed
	}
}
