<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Icon } from '@repo/ui'
import { cloneDeep } from 'lodash-es'
import { agent } from '@repo/api-service'

import InputTab from './components/InputTab.vue'
import TriggerTab from './components/TriggerTab.vue'
import ResultTab from './components/ResultTab.vue'
import DetailTab from './components/DetailTab.vue'
import TraceTab from './components/TraceTab.vue'
import {
	useRunnerStore,
	type NodeStatus,
	type RunnerExecution,
	type RunnerStatus
} from '@/store/modules/runner.store'
import { useI18n } from '@/composables/useI18n'

import type { IWorkflow, IWorkflowNode } from '@repo/workflow'
import type { StartVariable } from '@/nodes/src/start'

interface Props {
	workflow: IWorkflow
	visible: boolean
	closeOnRun?: boolean
	inputOnly?: boolean
	activeNodeId?: string
	initialTab?: 'input' | 'trigger' | 'result' | 'detail' | 'trace'
}

const props = withDefaults(defineProps<Props>(), {
	visible: false,
	closeOnRun: false,
	inputOnly: false,
	activeNodeId: '',
	initialTab: 'input'
})

const emit = defineEmits<{
	'update:visible': [visible: boolean]
	'run-started': [nodeId: string]
}>()

const { t, locale } = useI18n()
const runnerStore = useRunnerStore()
const activeTab = ref('input')
const executing = ref(false)
const inputValues = reactive<Record<string, any>>({})
const jsonDrafts = reactive<Record<string, string>>({})
const validationErrors = reactive<Record<string, string>>({})
const submittedParams = ref<Record<string, any> | null>(null)

const startNode = computed<IWorkflowNode | null>(() => {
	const nodes = props.workflow?.nodes || []
	return (
		nodes.find((node) => {
			const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
			return nodeType === 'start'
		}) || null
	)
})

const activeNode = computed<IWorkflowNode | null>(() => {
	if (props.activeNodeId) {
		return props.workflow?.nodes?.find((node) => node.id === props.activeNodeId) || null
	}

	return startNode.value
})

const activeNodeType = computed(() => {
	return ((activeNode.value as any)?.nodeType ||
		(activeNode.value as any)?.data?.nodeType ||
		'') as string
})

const showInputTab = computed(() => activeNodeType.value === 'start')
const showTriggerTab = computed(() =>
	['trigger-schedule', 'trigger-webhook'].includes(activeNodeType.value)
)

const startVariables = computed<StartVariable[]>(() => {
	const variables = (startNode.value as any)?.data?.variables
	return Array.isArray(variables) ? variables : []
})

const visibleVariables = computed(() => startVariables.value.filter((item) => !item.is_hide))

const currentExecution = computed<RunnerExecution | null>(() => {
	if (runnerStore.currentRunnerKey) {
		return (
			runnerStore.executions.find((item) => item.runnerKey === runnerStore.currentRunnerKey) || null
		)
	}
	return runnerStore.executions[0] || null
})

const traceNodes = computed(() => currentExecution.value?.nodes || runnerStore.nodes || [])

const hasExecutionData = computed(
	() => !!currentExecution.value || !!submittedParams.value || !!runnerStore.agentResult
)

const isRunning = computed(
	() => executing.value || runnerStore.status === 'connecting' || runnerStore.status === 'running'
)

const executionStatusText = computed(() =>
	statusText(currentExecution.value?.status || runnerStore.status)
)

const executionDurationText = computed(() =>
	formatExecutionDuration(
		currentExecution.value,
		runnerStore.agentResult || currentExecution.value?.result
	)
)

const totalTokenText = computed(
	() => `${extractTokenCount(runnerStore.agentResult || currentExecution.value?.result)} Tokens`
)

const detailMeta = computed(() => [
	{ label: t('pages.runWorkflow.metaStatus'), value: executionStatusText.value || '-' },
	{ label: t('pages.runWorkflow.metaRunId'), value: currentExecution.value?.runnerKey || '-' },
	{ label: t('pages.runWorkflow.metaStartedAt'), value: currentExecution.value?.startedAt || '-' },
	{ label: t('pages.runWorkflow.metaFinishedAt'), value: currentExecution.value?.finishedAt || '-' },
	{ label: t('pages.runWorkflow.metaDuration'), value: executionDurationText.value || '-' },
	{ label: t('pages.runWorkflow.metaSteps'), value: `${traceNodes.value.length || 0}` }
])

const finalResultValue = computed(() =>
	extractFinalResult(runnerStore.agentResult?.data?.result || currentExecution.value?.result)
)

const finalResultText = computed(() => formatDisplayValue(finalResultValue.value))
const detailInputText = computed(() => formatDisplayValue(submittedParams.value))
const detailOutputText = computed(() => formatDisplayValue(finalResultValue.value))
const detailStatusClassName = computed(() =>
	statusClass(currentExecution.value?.status || runnerStore.status)
)
const triggerWebhookDebugUrl = computed(() => {
	return `${(activeNode.value as any)?.data?.webhook_debug_url || ''}`.trim()
})
const triggerScheduleNextRunText = computed(() => {
	if (activeNodeType.value !== 'trigger-schedule') return ''

	const data = (activeNode.value as any)?.data || {}
	const visualConfig = data.visual_config || {}
	const frequency = `${data.frequency || 'daily'}`
	if (data.mode === 'cron' && `${data.cron_expression || ''}`.trim()) {
		return `${data.cron_expression || ''}`.trim()
	}
	const now = new Date()
	const createCandidate = (base: Date, hour: number, minute: number) => {
		const candidate = new Date(base)
		candidate.setHours(hour, minute, 0, 0)
		return candidate
	}
	const parseTime = (value?: string) => {
		const [hourText = '0', minuteText = '0'] = `${value || '00:00'}`.split(':')
		return {
			hour: Math.min(23, Math.max(0, Number(hourText))),
			minute: Math.min(59, Math.max(0, Number(minuteText)))
		}
	}
	const getLastDayOfMonth = (date: Date) => {
		return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
	}

	let nextRun: Date | null = null
	if (frequency === 'hourly') {
		const minute = Math.min(59, Math.max(0, Number(visualConfig.minute ?? 0)))
		const cursor = new Date(now)
		cursor.setSeconds(0, 0)
		cursor.setMinutes(minute, 0, 0)
		if (cursor <= now) {
			cursor.setHours(cursor.getHours() + 1)
			cursor.setMinutes(minute, 0, 0)
		}
		nextRun = cursor
	} else if (frequency === 'weekly') {
		const { hour, minute } = parseTime(visualConfig.time)
		const weekdayMap: Record<string, number> = {
			sun: 0,
			mon: 1,
			tue: 2,
			wed: 3,
			thu: 4,
			fri: 5,
			sat: 6
		}
		const weekdays = new Set(
			(Array.isArray(visualConfig.weekdays) ? visualConfig.weekdays : ['sun'])
				.map((item: string) => weekdayMap[item])
				.filter((item: number) => Number.isInteger(item))
		)
		const cursor = new Date(now)
		cursor.setHours(0, 0, 0, 0)
		for (let dayOffset = 0; dayOffset < 370; dayOffset += 1) {
			const candidate = createCandidate(cursor, hour, minute)
			if (weekdays.has(candidate.getDay()) && candidate > now) {
				nextRun = candidate
				break
			}
			cursor.setDate(cursor.getDate() + 1)
		}
	} else if (frequency === 'monthly') {
		const { hour, minute } = parseTime(visualConfig.time)
		const monthlyDays = Array.isArray(visualConfig.monthly_days) ? visualConfig.monthly_days : [1]
		const cursor = new Date(now)
		cursor.setHours(0, 0, 0, 0)
		for (let dayOffset = 0; dayOffset < 740; dayOffset += 1) {
			const candidate = createCandidate(cursor, hour, minute)
			const currentDate = candidate.getDate()
			const isLastDay = currentDate === getLastDayOfMonth(candidate)
			const matched = monthlyDays.some((item: number | 'last') =>
				item === 'last' ? isLastDay : Number(item) === currentDate
			)
			if (matched && candidate > now) {
				nextRun = candidate
				break
			}
			cursor.setDate(cursor.getDate() + 1)
		}
	} else {
		const { hour, minute } = parseTime(visualConfig.time)
		const candidate = createCandidate(now, hour, minute)
		if (candidate <= now) {
			candidate.setDate(candidate.getDate() + 1)
		}
		nextRun = candidate
	}

	if (!nextRun) return `${data.cron_expression || ''}`.trim()

	return new Intl.DateTimeFormat(locale.value === 'zh-cn' ? 'zh-CN' : 'en-US', {
		weekday: 'long',
		month: 'long',
		day: 'numeric',
		year: 'numeric',
		hour: 'numeric',
		minute: '2-digit',
		hour12: true
	}).format(nextRun)
})

const initializeInputValues = () => {
	startVariables.value.forEach((variable) => {
		const initialValue = cloneDeep(
			variable.default_value !== undefined
				? variable.default_value
				: createEmptyValue(variable.formType)
		)

		if (variable.formType === 'json_object') {
			inputValues[variable.name] =
				initialValue && typeof initialValue === 'object' && !Array.isArray(initialValue)
					? initialValue
					: {}
			jsonDrafts[variable.name] = formatJsonDraft(inputValues[variable.name], '{}')
			return
		}

		inputValues[variable.name] = initialValue
		delete jsonDrafts[variable.name]
	})
}

const resetValidation = () => {
	Object.keys(validationErrors).forEach((key) => {
		delete validationErrors[key]
	})
}

watch(
	() => [props.visible, props.initialTab, showInputTab.value, showTriggerTab.value],
	([visible]) => {
		if (visible) {
			if (props.initialTab === 'trigger' && showTriggerTab.value) {
				activeTab.value = 'trigger'
			} else if (props.initialTab === 'input' && showInputTab.value) {
				activeTab.value = 'input'
			} else if (showTriggerTab.value) {
				activeTab.value = 'trigger'
			} else if (showInputTab.value) {
				activeTab.value = 'input'
			} else {
				activeTab.value = 'detail'
			}
			resetValidation()
			initializeInputValues()
			return
		}

		resetValidation()
	},
	{ immediate: true }
)

const closeDrawer = () => {
	emit('update:visible', false)
}

const handleStopTriggerListening = () => {
	runnerStore.stopRunner()
}

const buildExecuteParams = () => {
	resetValidation()
	const params: Record<string, any> = {}
	let hasError = false

	startVariables.value.forEach((variable) => {
		const fieldName = variable.name
		let value = cloneDeep(inputValues[fieldName])

		if (variable.formType === 'json_object') {
			const draft = `${jsonDrafts[fieldName] || ''}`.trim()
			if (draft) {
				try {
					value = JSON.parse(draft)
				} catch {
					if (!variable.is_hide) {
						validationErrors[fieldName] = t('pages.runWorkflow.invalidJson')
					}
					hasError = true
					return
				}
			} else {
				value = {}
			}

			if (!value || typeof value !== 'object' || Array.isArray(value)) {
				if (!variable.is_hide) {
					validationErrors[fieldName] = t('pages.runWorkflow.invalidJson')
				}
				hasError = true
				return
			}
		}

		if (!variable.is_hide && variable.is_require && isEmptyValue(value, variable.formType)) {
			validationErrors[fieldName] = t('pages.runWorkflow.fieldRequired', {
				name: variable.label || fieldName
			})
			hasError = true
			return
		}

		if (
			!variable.is_hide &&
			['text-input', 'text-area'].includes(variable.formType) &&
			variable.max_length &&
			typeof value === 'string' &&
			value.length > variable.max_length
		) {
			validationErrors[fieldName] = t('pages.runWorkflow.fieldTooLong', {
				name: variable.label || fieldName,
				max: `${variable.max_length}`
			})
			hasError = true
			return
		}

		params[fieldName] = value
	})

	return hasError ? null : params
}

const handleRunWorkflow = async () => {
	if (!props.workflow?.id || !startNode.value?.id) {
		ElMessage.warning(t('pages.runWorkflow.missingStartNode'))
		return
	}

	const params = buildExecuteParams()
	if (!params) {
		activeTab.value = 'input'
		return
	}

	submittedParams.value = params
	activeTab.value = props.inputOnly ? 'input' : 'result'
	executing.value = true
	if (props.closeOnRun) {
		closeDrawer()
	}

	try {
		const response = await agent.postAgentDoExecute({
			appAgentId: props.workflow.id,
			start_node_id: startNode.value.id,
			is_debugger: true,
			responseType: 'ws',
			params
		})

		const agentRunnerKey = response?.result
		if (agentRunnerKey) {
			runnerStore.startRunner(agentRunnerKey, startNode.value.id)
			emit('run-started', startNode.value.id)
			return
		}

		ElMessage.error(t('pages.runWorkflow.runStartFailed'))
	} catch (error) {
		console.error('postAgentDoExecute error', error)
		ElMessage.error(t('pages.runWorkflow.runFailed'))
	} finally {
		executing.value = false
	}
}

function createEmptyValue(formType: string) {
	switch (formType) {
		case 'number':
			return 0
		case 'checkbox':
			return false
		case 'file':
			return {}
		case 'file-list':
			return []
		case 'json_object':
			return {}
		default:
			return ''
	}
}

function isEmptyValue(value: unknown, formType: string) {
	if (formType === 'checkbox') return false
	if (formType === 'number') {
		return value === null || value === undefined || Number.isNaN(Number(value))
	}
	if (formType === 'file-list') return !Array.isArray(value) || value.length === 0
	if (formType === 'file') {
		if (!value || Array.isArray(value) || typeof value !== 'object') return true
		return !Object.keys(value as Record<string, unknown>).length
	}
	if (formType === 'json_object') {
		return (
			!value ||
			typeof value !== 'object' ||
			Array.isArray(value) ||
			!Object.keys(value as Record<string, unknown>).length
		)
	}
	return !`${value ?? ''}`.trim()
}

function formatJsonDraft(value: unknown, fallback = '{}') {
	if (value === undefined || value === null || value === '') return fallback
	try {
		return JSON.stringify(value, null, 2)
	} catch {
		return fallback
	}
}

function formatDisplayValue(value: unknown) {
	if (value === undefined || value === null || value === '') return ''
	if (typeof value === 'string') return value
	try {
		return JSON.stringify(value, null, 2)
	} catch {
		return String(value)
	}
}

function extractFinalResult(value: any) {
	if (!value) return null
	if (value.output_variable !== undefined) return value.output_variable
	if (value.result !== undefined && typeof value.result !== 'string') return value.result
	return value
}

function pickNumber(value: any, paths: string[]) {
	for (const path of paths) {
		const result = path.split('.').reduce((acc, key) => acc?.[key], value)
		const num = Number(result)
		if (Number.isFinite(num) && num >= 0) {
			return num
		}
	}
	return 0
}

function extractTokenCount(value: any) {
	return pickNumber(value, [
		'total_tokens',
		'totalToken',
		'token_count',
		'usage.total_tokens',
		'usage.totalToken'
	])
}

function formatExecutionDuration(execution: RunnerExecution | null, result: any) {
	const rawDuration = pickNumber(result, [
		'use_time',
		'useTime',
		'elapsed',
		'elapsed_ms',
		'duration'
	])
	if (rawDuration > 0) {
		return rawDuration > 10 ? `${(rawDuration / 1000).toFixed(3)}s` : `${rawDuration.toFixed(3)}s`
	}

	const startedAt = execution?.startedAt ? new Date(execution.startedAt).getTime() : 0
	const finishedAt = execution?.finishedAt ? new Date(execution.finishedAt).getTime() : 0
	if (startedAt && finishedAt && finishedAt >= startedAt) {
		return `${((finishedAt - startedAt) / 1000).toFixed(3)}s`
	}

	return isRunning.value ? t('pages.runWorkflow.running') : '-'
}

function statusText(status?: NodeStatus | RunnerStatus | null) {
	if (status === 'running') return t('pages.runWorkflow.running')
	if (status === 'success' || status === 'finished') return t('pages.runWorkflow.success')
	if (status === 'failed') return t('pages.runWorkflow.failed')
	if (status === 'error') return t('pages.runWorkflow.error')
	if (status === 'connecting') return t('pages.runWorkflow.connecting')
	if (status === 'suspended') return t('pages.runWorkflow.suspended')
	return t('pages.runWorkflow.idle')
}

function statusClass(status?: NodeStatus | RunnerStatus | null) {
	if (status === 'success' || status === 'finished') return 'is-success'
	if (status === 'failed' || status === 'error') return 'is-failed'
	if (status === 'running' || status === 'connecting') return 'is-running'
	if (status === 'suspended') return 'is-suspended'
	return 'is-idle'
}
</script>

<template>
	<div class="runner">
		<div class="drawer shadow-2xl" :class="{ 'drawer--open': props.visible }">
			<header class="text-gray-800">
				<div class="w-full flex items-center justify-between">
					<h4 class="flex items-center">{{ t('pages.runWorkflow.drawerTitle') }}</h4>
					<Icon
						icon="lucide:x"
						height="24"
						width="24"
						@click="closeDrawer"
						class="cursor-pointer"
					/>
				</div>
			</header>

			<div class="content">
				<el-tabs v-model="activeTab" class="runner-tabs">
					<el-tab-pane v-if="showInputTab" :label="t('pages.runWorkflow.inputTab')" name="input">
						<InputTab
							:start-node="startNode"
							:visible-variables="visibleVariables"
							:input-values="inputValues"
							:json-drafts="jsonDrafts"
							:validation-errors="validationErrors"
							:is-running="isRunning"
							@run="handleRunWorkflow"
						/>
					</el-tab-pane>
					<el-tab-pane v-if="showTriggerTab" :label="t('pages.runWorkflow.triggerTab')" name="trigger">
						<TriggerTab
							:node-type="activeNodeType"
							:is-listening="isRunning"
							:webhook-debug-url="triggerWebhookDebugUrl"
							:next-schedule-run-text="triggerScheduleNextRunText"
							@stop="handleStopTriggerListening"
						/>
					</el-tab-pane>
					<el-tab-pane
						v-if="!props.inputOnly"
						:label="t('pages.runWorkflow.resultTab')"
						name="result"
						:disabled="runnerStore.status === 'idle'"
					>
						<ResultTab
							:has-execution-data="hasExecutionData"
							:is-running="isRunning"
							:final-result-text="finalResultText"
							:final-result-value="finalResultValue"
						/>
					</el-tab-pane>
					<el-tab-pane
						v-if="!props.inputOnly"
						:label="t('pages.runWorkflow.detailTab')"
						name="detail"
						:disabled="runnerStore.status === 'idle'"
					>
						<DetailTab
							:has-execution-data="hasExecutionData"
							:status-class-name="detailStatusClassName"
							:execution-status-text="executionStatusText"
							:execution-duration-text="executionDurationText"
							:total-token-text="totalTokenText"
							:detail-meta="detailMeta"
							:submitted-params="submittedParams"
							:detail-input-text="detailInputText"
							:final-result-value="finalResultValue"
							:detail-output-text="detailOutputText"
						/>
					</el-tab-pane>
					<el-tab-pane
						v-if="!props.inputOnly"
						:label="t('pages.runWorkflow.traceTab')"
						name="trace"
						:disabled="runnerStore.status === 'idle'"
					>
						<TraceTab :trace-nodes="traceNodes" />
					</el-tab-pane>
				</el-tabs>
			</div>
		</div>
	</div>
</template>

<style lang="less" scoped>
.runner {
	z-index: 999;
	font-size: 13px;
	.drawer {
		position: fixed;
		top: 60px;
		right: 5px;
		bottom: 10px;
		width: 420px;
		background: #fff;
		z-index: 1001;
		border-radius: 8px;
		display: flex;
		flex-direction: column;
		border: 1px solid #e4e4e4;
		transform: translateX(110%);
		transition: transform 0.25s ease;
	}

	.drawer--open {
		transform: translateX(0);
	}

	.drawer header {
		height: 66px;
		padding: 16px 16px 0;

		h4 {
			margin: 0;
			font-size: 15px;
			font-weight: 600;
		}
	}

	.drawer .content {
		flex: 1;
		min-height: 0;
		overflow: hidden;
	}

	:deep(.runner-tabs) {
		height: 100%;
		display: flex;
		flex-direction: column;
	}

	:deep(.el-tabs__header) {
		margin-bottom: 0;
	}

	:deep(.el-tabs__item) {
		font-size: 13px;
	}

	:deep(.el-tabs__nav-scroll) {
		padding-left: 20px;
	}

	:deep(.el-tabs__content) {
		flex: 1;
		min-height: 0;
		overflow: hidden;
	}

	:deep(.el-tab-pane) {
		height: 98%;
	}
}
</style>
