<template>
	<el-scrollbar class="w-full box-border p-12px">
		<div class="schedule-setter">
			<div class="w-full flex items-center justify-between beautify">
				<label class="text-14px font-bold text-gray-700">{{ text.title }}</label>
				<el-button link @click="toggleMode">
					<Icon :icon="formData.mode === 'visual' ? 'lucide:asterisk' : 'lucide:calendar-range'" />
					<span>{{ formData.mode === 'visual' ? text.modeCron : text.modeVisual }}</span>
				</el-button>
			</div>

			<template v-if="formData.mode === 'visual'">
				<div class="config-grid">
					<div class="field-block">
						<div class="field-label">{{ text.frequency }}</div>
						<el-select v-model="formData.frequency" class="w-full">
							<el-option
								v-for="item in frequencyOptions"
								:key="item.value"
								:label="item.label"
								:value="item.value"
							/>
						</el-select>
					</div>

					<div class="field-block field-block--wide">
						<div class="field-label">
							{{ formData.frequency === 'hourly' ? text.minute : text.time }}
						</div>

						<div v-if="formData.frequency === 'hourly'" class="slider-panel">
							<el-slider v-model="formData.visual_config.minute" :min="0" :max="59" :step="1" />
							<span>{{ formData.visual_config.minute }}</span>
						</div>

						<el-time-picker
							v-else
							v-model="visualTimeModel"
							format="HH:mm A"
							value-format="HH:mm"
							style="width: 100%"
							:clearable="false"
							:suffix="timezoneLabel"
						/>
					</div>
				</div>

				<div v-if="formData.frequency === 'weekly'" class="field-block">
					<div class="field-label">{{ text.weekday }}</div>
					<div class="tag-grid tag-grid--week">
						<el-check-tag
							v-for="item in WEEKDAY_OPTIONS"
							:key="item.value"
							:checked="selectedWeekdays.has(item.value)"
							class="schedule-tag"
							@change="onWeekdayTagChange(item.value, $event)"
						>
							{{ item.label }}
						</el-check-tag>
					</div>
				</div>

				<div v-if="formData.frequency === 'monthly'" class="field-block">
					<div class="field-label">{{ text.monthDay }}</div>
					<div class="tag-grid tag-grid--month">
						<el-check-tag
							v-for="day in MONTH_DAYS"
							:key="day"
							:checked="selectedMonthlyDays.has(day)"
							class="schedule-tag"
							@change="onMonthDayTagChange(day, $event)"
						>
							{{ day }}
						</el-check-tag>
						<el-check-tag
							:checked="selectedMonthlyDays.has('last')"
							class="schedule-tag schedule-tag--last"
							@change="onMonthDayTagChange('last', $event)"
						>
							<span>{{ text.lastDay }}</span>
							<el-tooltip :content="text.lastDayTip" placement="top">
								<el-icon class="hint-icon"><QuestionFilled /></el-icon>
							</el-tooltip>
						</el-check-tag>
					</div>
				</div>

				<el-divider />

				<div class="field-block">
					<div class="field-label">{{ text.nextRuns }}</div>
					<div class="preview-panel">
						<div v-if="nextRuns.length" class="preview-list">
							<div v-for="(item, index) in nextRuns" :key="item.timestamp" class="preview-item">
								<span class="preview-index">{{ padIndex(index + 1) }}</span>
								<span class="preview-text">{{ item.label }}</span>
							</div>
						</div>
						<div v-else class="preview-empty">{{ text.previewEmpty }}</div>
					</div>
				</div>
			</template>

			<template v-else>
				<div class="field-block">
					<div class="field-label">{{ text.cronLabel }}</div>
					<el-input
						v-model="formData.cron_expression"
						class="cron-input"
						:placeholder="text.cronPlaceholder"
					/>
					<div class="cron-hint" :class="{ 'cron-hint--error': !isCronExpressionValid }">
						{{ cronHint }}
					</div>
				</div>
			</template>
			<NodeRuntimeConfig v-model="formData" />
		</div>
	</el-scrollbar>
</template>

<script setup lang="ts">
import { QuestionFilled } from '@element-plus/icons-vue'
import { computed, watch } from 'vue'
import { Icon } from '@repo/ui'

import NodeRuntimeConfig from '@/nodes/_base/NodeRuntimeConfig.vue'
import { useSetterModel } from '../_shared/useSetterModel'
import { useI18n } from '@/composables/useI18n'

import type { ScheduleData, ScheduleFrequency, ScheduleWeekday } from './index'

interface Emits {
	(e: 'update', value: ScheduleData): void
}

type ScheduleMonthDay = number | 'last'
type PreviewItem = {
	timestamp: string
	label: string
}

const WEEKDAY_OPTIONS: Array<{ label: string; value: ScheduleWeekday; day: number }> = [
	{ label: 'Sun', value: 'sun', day: 0 },
	{ label: 'Mon', value: 'mon', day: 1 },
	{ label: 'Tue', value: 'tue', day: 2 },
	{ label: 'Wed', value: 'wed', day: 3 },
	{ label: 'Thu', value: 'thu', day: 4 },
	{ label: 'Fri', value: 'fri', day: 5 },
	{ label: 'Sat', value: 'sat', day: 6 }
]

const WEEKDAY_TO_CRON: Record<ScheduleWeekday, string> = {
	sun: 'SUN',
	mon: 'MON',
	tue: 'TUE',
	wed: 'WED',
	thu: 'THU',
	fri: 'FRI',
	sat: 'SAT'
}

const MONTH_DAYS = Array.from({ length: 31 }, (_, index) => index + 1)
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'

const props = defineProps<{
	data: ScheduleData
}>()

const emit = defineEmits<Emits>()
const { t } = useI18n()
const formData = useSetterModel<ScheduleData>(props, emit)

const text = computed(() => ({
	title: t('pages.scheduleSetter.title'),
	modeCron: t('pages.scheduleSetter.modeCron'),
	modeVisual: t('pages.scheduleSetter.modeVisual'),
	frequency: t('pages.scheduleSetter.frequency'),
	minute: t('pages.scheduleSetter.minute'),
	time: t('pages.scheduleSetter.time'),
	weekday: t('pages.scheduleSetter.weekday'),
	monthDay: t('pages.scheduleSetter.monthDay'),
	lastDay: t('pages.scheduleSetter.lastDay'),
	lastDayTip: t('pages.scheduleSetter.lastDayTip'),
	nextRuns: t('pages.scheduleSetter.nextRuns'),
	previewEmpty: t('pages.scheduleSetter.previewEmpty'),
	cronLabel: t('pages.scheduleSetter.cronLabel'),
	cronPlaceholder: t('pages.scheduleSetter.cronPlaceholder'),
	cronSupport: t('pages.scheduleSetter.cronSupport'),
	cronUsing: t('pages.scheduleSetter.cronUsing'),
	cronInvalid: t('pages.scheduleSetter.cronInvalid')
}))

const frequencyOptions = computed<Array<{ label: string; value: ScheduleFrequency }>>(() => [
	{ label: t('pages.scheduleSetter.hourly'), value: 'hourly' },
	{ label: t('pages.scheduleSetter.daily'), value: 'daily' },
	{ label: t('pages.scheduleSetter.weekly'), value: 'weekly' },
	{ label: t('pages.scheduleSetter.monthly'), value: 'monthly' }
])

const normalizeTime = (value?: string) => {
	return /^([01]\d|2[0-3]):([0-5]\d)$/.test(value || '') ? value! : '00:00'
}

const clampMinute = (value: unknown) => {
	const minute = Number(value)
	if (Number.isNaN(minute)) {
		return 0
	}

	return Math.min(59, Math.max(0, Math.trunc(minute)))
}

const sortWeekdays = (weekdays: ScheduleWeekday[]) => {
	const orderMap = new Map(WEEKDAY_OPTIONS.map((item) => [item.value, item.day]))
	return [...new Set(weekdays)].sort((left, right) => {
		return (orderMap.get(left) || 0) - (orderMap.get(right) || 0)
	})
}

const normalizeMonthlyDays = (values: Array<ScheduleMonthDay | string>): ScheduleMonthDay[] => {
	const parsedValues = values
		.map((item) => {
			if (item === 'last') {
				return 'last' as const
			}

			const day = Number(item)
			if (Number.isNaN(day)) {
				return null
			}

			return Math.min(31, Math.max(1, Math.trunc(day)))
		})
		.filter((item): item is ScheduleMonthDay => item !== null)

	if (!parsedValues.length) {
		return [1]
	}

	const uniqueDays = Array.from(new Set(parsedValues))

	return uniqueDays.sort((left, right) => {
		if (left === 'last') return 1
		if (right === 'last') return -1
		return left - right
	})
}

const ensureScheduleState = () => {
	if (!formData.value.title) {
		formData.value.title = text.value.title
	}
	if (!formData.value.type) {
		formData.value.type = 'trigger-schedule'
	}
	if (!formData.value.variables) {
		formData.value.variables = []
	}
	if (!formData.value.outputs) {
		formData.value.outputs = []
	}
	if (!formData.value.default_value) {
		formData.value.default_value = []
	}
	if (!formData.value.retry_config) {
		formData.value.retry_config = {
			max_retries: 3,
			retry_enabled: false,
			retry_interval: 100
		}
	}
	if (!formData.value.error_strategy) {
		formData.value.error_strategy = 'none'
	}
	if (!['visual', 'cron'].includes(formData.value.mode)) {
		formData.value.mode = 'visual'
	}
	if (!['hourly', 'daily', 'weekly', 'monthly'].includes(formData.value.frequency)) {
		formData.value.frequency = 'daily'
	}
	if (!formData.value.timezone) {
		formData.value.timezone = browserTimeZone
	}
	if (!formData.value.visual_config || typeof formData.value.visual_config !== 'object') {
		formData.value.visual_config = {
			minute: 0,
			time: '00:00',
			weekdays: ['sun'],
			monthly_days: [1]
		}
	}

	formData.value.visual_config.minute = clampMinute(formData.value.visual_config.minute)
	formData.value.visual_config.time = normalizeTime(formData.value.visual_config.time)
	formData.value.visual_config.weekdays = sortWeekdays(
		(formData.value.visual_config.weekdays || ['sun']) as ScheduleWeekday[]
	)
	formData.value.visual_config.monthly_days = normalizeMonthlyDays(
		formData.value.visual_config.monthly_days || [1]
	)
}

ensureScheduleState()

watch(text, () => {
	if (
		!formData.value.title ||
		formData.value.title === t('nodes.meta.trigger-schedule.displayName')
	) {
		formData.value.title = text.value.title
	}
})

const parseTime = (value: string) => {
	const [hourText = '0', minuteText = '0'] = normalizeTime(value).split(':')
	return {
		hour: Math.min(23, Math.max(0, Number(hourText))),
		minute: Math.min(59, Math.max(0, Number(minuteText)))
	}
}

const buildQuartzCronExpression = (data: ScheduleData) => {
	const minute = clampMinute(data.visual_config.minute)
	const { hour, minute: timeMinute } = parseTime(data.visual_config.time)

	switch (data.frequency) {
		case 'hourly':
			return `0 ${minute} * * * ? *`
		case 'weekly':
			return `0 ${timeMinute} ${hour} ? * ${sortWeekdays(data.visual_config.weekdays)
				.map((item) => WEEKDAY_TO_CRON[item])
				.join(',')} *`
		case 'monthly':
			return `0 ${timeMinute} ${hour} ${normalizeMonthlyDays(data.visual_config.monthly_days)
				.map((item) => (item === 'last' ? 'L' : item))
				.join(',')} * ? *`
		case 'daily':
		default:
			return `0 ${timeMinute} ${hour} * * ? *`
	}
}

const selectedWeekdays = computed<Set<ScheduleWeekday>>(() => {
	return new Set(sortWeekdays(formData.value.visual_config.weekdays))
})

const selectedMonthlyDays = computed<Set<ScheduleMonthDay>>(() => {
	return new Set(normalizeMonthlyDays(formData.value.visual_config.monthly_days))
})

const visualTimeModel = computed<string>({
	get: () => normalizeTime(formData.value.visual_config.time),
	set: (value: string) => {
		formData.value.visual_config.time = normalizeTime(value)
	}
})

const timezoneLabel = computed(() => {
	const offset = -new Date().getTimezoneOffset()
	const sign = offset >= 0 ? '+' : '-'
	const absolute = Math.abs(offset)
	const hours = Math.floor(absolute / 60)
	const minutes = absolute % 60

	if (minutes === 0) {
		return `UTC${sign}${hours}`
	}

	return `UTC${sign}${hours}:${String(minutes).padStart(2, '0')}`
})

const toggleMode = () => {
	formData.value.cron_expression = buildQuartzCronExpression(formData.value)
	formData.value.mode = formData.value.mode === 'visual' ? 'cron' : 'visual'
}

const handleWeekdayChange = (weekday: ScheduleWeekday, checked: boolean) => {
	const weekdays = sortWeekdays(formData.value.visual_config.weekdays)
	const nextWeekdays = checked
		? [...weekdays, weekday]
		: weekdays.filter((item) => item !== weekday)

	formData.value.visual_config.weekdays = sortWeekdays(
		nextWeekdays.length ? nextWeekdays : weekdays
	)
}

const handleMonthDayChange = (day: ScheduleMonthDay, checked: boolean) => {
	const monthlyDays = normalizeMonthlyDays(formData.value.visual_config.monthly_days)
	const nextMonthlyDays = checked
		? [...monthlyDays, day]
		: monthlyDays.filter((item) => item !== day)

	formData.value.visual_config.monthly_days = normalizeMonthlyDays(
		nextMonthlyDays.length ? nextMonthlyDays : monthlyDays
	)
}

const onWeekdayTagChange = (weekday: ScheduleWeekday, checked: unknown) => {
	handleWeekdayChange(weekday, Boolean(checked))
}

const onMonthDayTagChange = (day: ScheduleMonthDay, checked: unknown) => {
	handleMonthDayChange(day, Boolean(checked))
}

const createCandidate = (base: Date, hour: number, minute: number) => {
	const candidate = new Date(base)
	candidate.setHours(hour, minute, 0, 0)
	return candidate
}

const getLastDayOfMonth = (date: Date) => {
	return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
}

const generateHourlyRuns = (count: number) => {
	const nextRuns: Date[] = []
	const now = new Date()
	const scheduleMinute = clampMinute(formData.value.visual_config.minute)
	let cursor = new Date(now)
	cursor.setSeconds(0, 0)
	cursor.setMinutes(scheduleMinute, 0, 0)

	if (cursor <= now) {
		cursor.setHours(cursor.getHours() + 1)
		cursor.setMinutes(scheduleMinute, 0, 0)
	}

	while (nextRuns.length < count) {
		nextRuns.push(new Date(cursor))
		cursor.setHours(cursor.getHours() + 1)
	}

	return nextRuns
}

const generateDailyRuns = (count: number) => {
	const nextRuns: Date[] = []
	const now = new Date()
	const { hour, minute } = parseTime(formData.value.visual_config.time)
	let cursor = createCandidate(now, hour, minute)

	if (cursor <= now) {
		cursor.setDate(cursor.getDate() + 1)
	}

	while (nextRuns.length < count) {
		nextRuns.push(new Date(cursor))
		cursor.setDate(cursor.getDate() + 1)
	}

	return nextRuns
}

const generateWeeklyRuns = (count: number) => {
	const nextRuns: Date[] = []
	const now = new Date()
	const { hour, minute } = parseTime(formData.value.visual_config.time)
	const weekdays = new Set(
		WEEKDAY_OPTIONS.filter((item) =>
			formData.value.visual_config.weekdays.includes(item.value)
		).map((item) => item.day)
	)
	let cursor = new Date(now)
	cursor.setHours(0, 0, 0, 0)

	for (let dayOffset = 0; dayOffset < 370 && nextRuns.length < count; dayOffset += 1) {
		const candidate = createCandidate(cursor, hour, minute)
		if (weekdays.has(candidate.getDay()) && candidate > now) {
			nextRuns.push(candidate)
		}
		cursor.setDate(cursor.getDate() + 1)
	}

	return nextRuns
}

const generateMonthlyRuns = (count: number) => {
	const nextRuns: Date[] = []
	const now = new Date()
	const { hour, minute } = parseTime(formData.value.visual_config.time)
	const monthlyDays = normalizeMonthlyDays(formData.value.visual_config.monthly_days)
	let cursor = new Date(now)
	cursor.setHours(0, 0, 0, 0)

	for (let dayOffset = 0; dayOffset < 740 && nextRuns.length < count; dayOffset += 1) {
		const candidate = createCandidate(cursor, hour, minute)
		const currentDate = candidate.getDate()
		const isLastDay = currentDate === getLastDayOfMonth(candidate)
		const matched = monthlyDays.some((item) => (item === 'last' ? isLastDay : item === currentDate))

		if (matched && candidate > now) {
			nextRuns.push(candidate)
		}

		cursor.setDate(cursor.getDate() + 1)
	}

	return nextRuns
}

const formatPreviewDate = (date: Date) => {
	const formatter = new Intl.DateTimeFormat('en-US', {
		month: 'long',
		day: 'numeric',
		year: 'numeric',
		hour: 'numeric',
		minute: '2-digit',
		hour12: true
	})

	return `${formatter.format(date)} (${timezoneLabel.value})`
}

const nextRuns = computed<PreviewItem[]>(() => {
	let dates: Date[] = []
	switch (formData.value.frequency) {
		case 'hourly':
			dates = generateHourlyRuns(5)
			break
		case 'weekly':
			dates = generateWeeklyRuns(5)
			break
		case 'monthly':
			dates = generateMonthlyRuns(5)
			break
		case 'daily':
		default:
			dates = generateDailyRuns(5)
			break
	}

	return dates.map((date) => ({
		timestamp: `${date.getTime()}`,
		label: formatPreviewDate(date)
	}))
})

const isCronExpressionValid = computed(() => {
	const value = formData.value.cron_expression?.trim()
	if (!value) {
		return false
	}

	return /^(?:\S+\s+){4,6}\S+$/.test(value)
})

const cronHint = computed(() => {
	if (!formData.value.cron_expression?.trim()) {
		return text.value.cronSupport
	}

	return isCronExpressionValid.value ? text.value.cronUsing : text.value.cronInvalid
})

const padIndex = (value: number) => String(value).padStart(2, '0')

watch(
	() => [
		formData.value.mode,
		formData.value.frequency,
		formData.value.visual_config?.minute,
		formData.value.visual_config?.time,
		(formData.value.visual_config?.weekdays || []).join(','),
		(formData.value.visual_config?.monthly_days || []).join(',')
	],
	() => {
		if (formData.value.mode === 'visual' || !formData.value.cron_expression?.trim()) {
			formData.value.cron_expression = buildQuartzCronExpression(formData.value)
		}
	},
	{ immediate: true }
)
</script>

<style scoped lang="less">
.schedule-setter {
	display: flex;
	flex-direction: column;
	gap: 18px;
}

.schedule-header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
}

.schedule-title {
	font-size: 16px;
	font-weight: 700;
	color: #344054;
}

.mode-toggle {
	display: inline-flex;
	align-items: center;
	gap: 8px;
	padding: 0;
	border: 0;
	background: transparent;
	font-size: 13px;
	color: #344054;
	cursor: pointer;
}

.config-grid {
	display: grid;
	grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
	gap: 16px;
}

.field-block {
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.field-block--wide {
	min-width: 0;
}

.field-label {
	font-size: 13px;
	color: #667085;
}

.slider-panel,
.time-panel,
.preview-panel {
	border-radius: 8px;
	background: #f2f5f9;
}

.slider-panel {
	padding: 0px 14px 0px 8px;
	display: flex;
	align-items: center;
	gap: 12px;
}

.time-panel {
	position: relative;
}

.tag-grid {
	display: grid;
	gap: 10px;
}

.tag-grid--week {
	grid-template-columns: repeat(7, minmax(0, 1fr));
}

.tag-grid--month {
	grid-template-columns: repeat(7, minmax(0, 1fr));
}

.schedule-tag {
	display: flex;
	align-items: center;
	justify-content: center;
	border-radius: 12px;
	font-size: 14px;
	color: #475467;
	background: #f8fafc;
	border: 1px solid #e5e7eb;
}

.schedule-tag--last {
	grid-column: span 3;
	gap: 6px;
}

.hint-icon {
	color: #98a2b3;
}

.preview-panel {
	padding: 14px 16px;
}

.preview-list {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.preview-item {
	display: flex;
	align-items: baseline;
	gap: 10px;
	font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
	font-size: 13px;
	color: #475467;
}

.preview-index {
	color: #98a2b3;
}

.preview-text {
	word-break: break-word;
}

.preview-empty,
.cron-hint {
	font-size: 12px;
	color: #667085;
}

.cron-hint--error {
	color: #f04438;
}
</style>
