import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core'
import { SchedulerRegistry } from '@nestjs/schedule'
import { CronJob } from 'cron'
import { AppConfigService } from '../../global'
import { OnEvent } from '@nestjs/event-emitter'
import { context } from '@opentelemetry/api'
import tracer from 'dd-trace'
import { setSpanMetadata } from '../../common'
import { CRON_METADATA, CronOptions } from './cron.decorator'

@Injectable()
export class CronService implements OnModuleInit, OnModuleDestroy {
	private readonly logger = new Logger(this.constructor.name)
	private readonly cronJobs = new Map<string, { options: CronOptions; instance: any; methodName: string }>()

	constructor(
		private readonly reflector: Reflector,
		private readonly metadataScanner: MetadataScanner,
		private readonly appConfigService: AppConfigService,
		private readonly discoveryService: DiscoveryService,
		private readonly schedulerRegistry: SchedulerRegistry,
	) {}

	onModuleInit() {
		this.explore()
	}

	onModuleDestroy() {
		for (const jobName of this.cronJobs.keys()) {
			if (this.schedulerRegistry.doesExist('cron', jobName)) {
				this.schedulerRegistry.deleteCronJob(jobName)
			}
		}
	}

	@OnEvent('app-config.updated')
	onConfigUpdated(config: Record<string, any>) {
		for (const [jobName, jobInfo] of this.cronJobs.entries()) {
			if (!jobInfo.options.appconfigScheduleKey) continue

			const newSchedule = config[jobInfo.options.appconfigScheduleKey]
			if (newSchedule) {
				this.scheduleJob(jobName, newSchedule)
			}
		}
	}

	/** Retrieves cron jobs from application and registers them for execution */
	private explore() {
		const providers = this.discoveryService.getProviders()

		for (const wrapper of providers) {
			const { instance } = wrapper
			if (!instance || !Object.getPrototypeOf(instance)) continue

			for (const methodName of this.metadataScanner.getAllMethodNames(Object.getPrototypeOf(instance))) {
				const metadata = this.reflector.get<CronOptions>(CRON_METADATA, instance[methodName])
				if (metadata) this.registerCronJob(instance, methodName, metadata)
			}
		}
	}

	private registerCronJob(instance: any, methodName: string, options: CronOptions) {
		const jobName = options.name

		let schedule = options.schedule
		if (options.appconfigScheduleKey) {
			schedule = this.appConfigService.get(options.appconfigScheduleKey, options.schedule)
		}

		this.cronJobs.set(jobName, { options, instance, methodName })
		this.scheduleJob(jobName, schedule)
	}

	private scheduleJob(jobName: string, schedule: string) {
		const jobInfo = this.cronJobs.get(jobName)
		if (!jobInfo) return

		try {
			if (this.schedulerRegistry.doesExist('cron', jobName)) {
				this.schedulerRegistry.deleteCronJob(jobName)
			}

			const job = new CronJob(schedule, async () => {
				await this.runTracedJob(jobName, jobInfo)
			})

			this.schedulerRegistry.addCronJob(jobName, job)
			job.start()

			this.logger.log(`Scheduled cron job ${jobName} with schedule ${schedule}`)
		} catch (error) {
			this.logger.error({ message: `Failed to schedule cron job ${jobName} with schedule ${schedule}`, error })
		}
	}

	private async runTracedJob(jobName: string, jobInfo: { instance: any; methodName: string }) {
		const name = jobName
		await new Promise<void>((resolve, reject) => {
			tracer.trace(
				'workerJob',
				{
					tags: {
						'resource.name': `cron_${name}`,
						'span.kind': 'worker',
						'sampling.priority': '1',
					},
				},
				async (span) => {
					const logger = new Logger(jobInfo.instance.constructor.name)

					setSpanMetadata(name, span)

					span?.addTags({
						'job.name': name,
						'processor.name': jobInfo.instance.constructor.name,
						'sampling.priority': '1',
					})

					logger.log(`Executing traced cron '${name}'`)

					try {
						await context.with(context.active(), async () => {
							const result = await jobInfo.instance[jobInfo.methodName]()
							resolve(result)
						})
					} catch (error) {
						logger.error(error)
						span?.setTag('error', true)
						span?.setTag('error.msg', error.message)
						span?.setTag('error.type', error.name)
						span?.setTag('error.stack', error.stack)
						reject(error)
					}
				},
			)
		})
	}
}
