import { ITime } from '@core/types'
import { TZDate } from '@date-fns/tz'
import { set } from 'date-fns'
import { clone } from 'es-toolkit/compat'

/**
 * Converts a time string into {@link ITime}.
 * Throws an error if invalid time string is provided
 * @param time The time string to parse in hh:mm format
 * @returns Instance of {@link ITime}
 * @throws {@link Error}
 * If Invalid time string is provided
 */
function parseFromString(time: string): ITime {
	const split = time.split(':')
	const hours = Number.parseInt(split[0])
	const minutes = Number.parseInt(split[1])

	if (hours >= 0 && hours < 24 && minutes >= 0 && minutes < 60) {
		return { hours, minutes }
	}

	throw new Error('Invalid time string')
}

/**
 * Formates a {@link ITime} object into string of hh:mm format
 * @param time The {@link time} to format
 * @returns Formatted string
 */
function formatAsString(time: ITime): string {
	return `${time.hours < 10 ? `0${time.hours}` : time.hours}:${time.minutes < 10 ? `0${time.minutes}` : time.minutes}`
}

/**
 * Extract time information as {@link ITime} from a {@link Date} object
 * @param date The {@link Date} from which time is extracted
 * @returns An instance of {@link ITime}
 */
function extractTimeFromDate(date: Date): ITime {
	return { hours: date.getHours(), minutes: date.getMinutes() }
}

/**
 * Checks if two instances of {@link ITime} have same time information
 * @param leftTime
 * @param rightTime
 * @returns True if both times are same and vice versa
 */
function isEqual(leftTime: ITime, rightTime: ITime): boolean {
	if (leftTime.hours !== rightTime.hours) return false
	if (leftTime.minutes !== rightTime.minutes) return false
	return true
}

/**
 * Is the first time before the second one?
 * @param time the time that should be before the other one to return true
 * @param timeToCompare	the time to compare with
 * @returns True if the first time is before the second time
 */
function isBefore(time: ITime, timeToCompare: ITime): boolean {
	if (time.hours < timeToCompare.hours) return true
	if (time.hours > timeToCompare.hours) return false
	return time.minutes < timeToCompare.minutes
}

/**
 * Is the first time after the second one?
 * @param time the time that should be after the other one to return true
 * @param timeToCompare	the time to compare with
 * @returns True if the first time is after the second time
 */
function isAfter(time: ITime, timeToCompare: ITime): boolean {
	if (time.hours > timeToCompare.hours) return true
	if (time.hours < timeToCompare.hours) return false
	return time.minutes > timeToCompare.minutes
}

/**
 * Add the specified number of minutes to the given time
 * @param time the time to be changed
 * @param minutes the amount of minutes to be added
 * @param options Optional list of arguments
 * - Prevent Overflow: After adding minutes if time exceeds 23:59 (end of day),
 * setting this value to true will return 23:59
 * @returns the new time with the minutes added
 */
function addMinutes(time: ITime, minutes: number, options?: { preventOverflow: boolean }): ITime {
	let timeClone = clone(time)
	timeClone.minutes += minutes

	if (timeClone.minutes > 59) {
		const totalDifference = timeClone.minutes - 60

		const mintesDifference = totalDifference % 60
		timeClone.minutes = mintesDifference

		const hoursDifference = Number.parseInt((totalDifference / 60).toString()) + 1
		timeClone = addHours(timeClone, hoursDifference, options)
	}

	return timeClone
}

/**
 * Add the specified number of hours to the given time
 * @param time the time to be changed
 * @param hours the amount of hours to be added
 * @param options Optional list of arguments
 * - Prevent Overflow: After adding hours if time exceeds 23:59 (end of day),
 * setting this value to true will return 23:59
 * @returns the new time with the hours added
 */
function addHours(time: ITime, hours: number, options?: { preventOverflow: boolean }): ITime {
	const timeClone = clone(time)
	timeClone.hours += hours

	// If hours exceed 23
	if (timeClone.hours > 23) {
		if (options?.preventOverflow) {
			// Freeze at 23:59
			timeClone.hours = 23
			timeClone.minutes = 59
		} else {
			// Reset hour count from 00
			const totalDifference = timeClone.hours - 24
			const hoursDifference = totalDifference % 60
			timeClone.hours = hoursDifference
		}
	}

	return timeClone
}

function localToUtc(time: ITime, timezone?: string): ITime {
	const local = set(TZDate.tz(timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone), { ...time })

	const utcDate = local.withTimeZone('UTC')

	return {
		hours: utcDate.getHours(),
		minutes: utcDate.getMinutes(),
	}
}

function utcToLocal(time: ITime, timezone?: string): ITime {
	const utcDate = set(TZDate.tz('UTC'), { ...time })

	const localDate = utcDate.withTimeZone(timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone)

	return {
		hours: localDate.getHours(),
		minutes: localDate.getMinutes(),
	}
}

export const TimeUtils = {
	parseFromString,
	formatAsString,
	extractTimeFromDate,
	isEqual,
	isBefore,
	isAfter,
	addMinutes,
	addHours,
	localToUtc,
	utcToLocal,
}
