import fs from 'fs'
import crypto from 'crypto'
import nodemailer from 'nodemailer'
import jwt from 'jsonwebtoken'
import twilio from 'twilio'
import { PutObjectCommand, DeleteObjectCommand, ListObjectsV2Command, DeleteObjectsCommand } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { SNSClient, PublishCommand } from '@aws-sdk/client-sns'
import config from './config'
import { OAuth2Client } from 'google-auth-library'
import { google } from 'googleapis'
import SMTPTransport from 'nodemailer/lib/smtp-transport'
import { IUser } from '../api/users/models'
import constants from './constants'
import { IFile } from './types'
import ffmpeg from 'fluent-ffmpeg'
import path from 'path'
import https from 'https'
import http from 'http'
import { CreateScheduleCommand, DeleteScheduleCommand } from '@aws-sdk/client-scheduler'

function deleteOldFiles(directory = 'uploads', maxAgeInMinutes = 5) {
	try {
		console.log('🕒  Deleting old files!')
		const currentTime = new Date()
		const maxAge = maxAgeInMinutes * 60 * 1000
		const files = fs.readdirSync(directory)

		let deletedCount = 0
		let totalFreedBytes = 0

		for (const file of files) {
			const filePath = directory + '/' + file
			const stats = fs.statSync(filePath)
			const fileAge = currentTime.getTime() - stats.birthtime.getTime()

			if (fileAge > maxAge) {
				fs.unlinkSync(filePath)
				console.log(`Deleted: ${filePath}`)
				deletedCount++
				totalFreedBytes += stats.size
			}
		}
		console.log(`Total files deleted: ${deletedCount}`)
		console.log(`Total space freed: ${formatBytes(totalFreedBytes)}`)
		console.log('✅  Done')
	} catch (err) {
		console.error(err)
	}
}

function formatBytes(bytes: number) {
	if (bytes === 0) return '0B'
	const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
	const i = Math.floor(Math.log(bytes) / Math.log(1024))
	const value = bytes / Math.pow(1024, i)
	return `${value.toFixed(2)}${sizes[i]}`
}

function getHash(password: string) {
	const salt = process.env.PASSWORD_SALT!
	const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512')
	return hash.toString('hex')
}

function getToken(user: IUser, remember: boolean = true): string {
	const { _id, auth } = user
	const expiry = remember ? { expiresIn: '90d' } : { expiresIn: '24h' }
	const payload = { _id: `${_id}`, hash: auth.password.slice(-10) }
	// @ts-ignore
	const token = jwt.sign(payload, process.env.JWT_SECRET!, expiry)
	return token
}

function deleteFile(file: Express.Multer.File | string) {
	try {
		if (typeof file === 'string') {
			fs.unlinkSync(file)
			console.log(`File ${file} deleted!`)
		} else {
			fs.unlinkSync(file.path)
			console.log(`File ${file.filename} deleted!`)
		}
	} catch { }
}

async function uploadFile(file: NonNullable<IFile> | Buffer, cloudFilePath: string): Promise<string | undefined> {
	try {
		// @ts-ignore
		const fileBuffer = file instanceof Buffer ? file : await fs.promises.readFile(file.path)

		const command = new PutObjectCommand({
			Bucket: process.env.R2_BUCKET!,
			Key: cloudFilePath,
			Body: fileBuffer,
			ACL: 'public-read'
		})

		await config.R2.send(command)
		return getR2FileUrl(cloudFilePath)
	} catch (error) {
		console.log(error)
	}
}

async function deleteR2File(path: string, completeUrl = true) {
	try {
		if (completeUrl) {
			const initial = getR2FileUrl('')
			path = path.slice(initial.length)
		}
		const command = new DeleteObjectCommand({
			Bucket: process.env.R2_BUCKET!,
			Key: path
		})
		await config.R2.send(command)
		console.log(`File deleted successfully.`)
	} catch (error) {
		console.log(error)
	}
}

async function deleteR2FilesWithPrefix(prefix: string, paginationLimit = 500) {
	try {
		let ContinuationToken: string | undefined
		do {
			const listCommand = new ListObjectsV2Command({
				Bucket: process.env.R2_BUCKET!,
				Prefix: prefix,
				ContinuationToken,
				MaxKeys: paginationLimit
			})
			const res = await config.R2.send(listCommand)
			const keys = res.Contents?.map(obj => ({ Key: obj.Key! })) ?? []
			if (keys.length > 0) {
				const deleteCommand = new DeleteObjectsCommand({
					Bucket: process.env.R2_BUCKET!,
					Delete: { Objects: keys },
				})
				await config.R2.send(deleteCommand)
				console.log(`✅ Deleted: ${keys.length} files under "${prefix}"`)
			}
			ContinuationToken = res.IsTruncated ? res.NextContinuationToken : undefined
		} while (ContinuationToken)
	} catch (error) {
		console.log(error)
	}
}

function getR2FileUrl(path: string) {
	return `${process.env.R2_PUBLIC_URL}/${path}`
}

function getRandomOtp(digits: number = 4): string {
	if (config.isDevelopment) {
		return '1234'
	}
	let otp = `${Math.random()}`.slice(-digits)
	return otp
}

async function sendAWSSms(phone: string, message: string) {
	if (config.isDevelopment) {
		return
	}
	const snsClient = new SNSClient({
		region: process.env.R2_REGION!,
		credentials: {
			accessKeyId: process.env.R2_ACCESS_KEY_ID!,
			secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!
		}
	})

	const command = new PublishCommand({
		PhoneNumber: phone,
		Message: message
	})

	try {
		const result = await snsClient.send(command)
		console.log('Message sent with MessageId:', result)
		return result
		return result.MessageId
	} catch (error) {
		console.error('Error sending SNS message:', error)
		throw error
	}
}

async function sendSms(phone: string, message: string) {
	if (config.isDevelopment) {
		return
	}
	console.log(`✅ SMS Sent: ${phone} - "${message}"`)
	return
	const twilioClient = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN)
	const response = await twilioClient.messages.create({
		to: phone,
		from: process.env.TWILIO_PHONE,
		body: message
	})
	return response
}

async function sendOauthGmail(email: string, subject: string, body?: string, html?: string) {
	try {
		console.log(`✅ Email Sent: ${email} - "${subject}" - "${body || html}"`)
		if (config.isDevelopment) {
			return
		}
		const oAuth2Client = new google.auth.OAuth2(
			process.env.GMAIL_CLIENT_ID,
			process.env.GMAIL_CLIENT_SECRET,
			'https://developers.google.com/oauthplayground'
		)
		oAuth2Client.setCredentials({ refresh_token: process.env.GMAIL_REFRESH_TOKEN })

		const accessToken = await oAuth2Client.getAccessToken()
		if (!accessToken?.token) {
			throw new Error('Failed to retrieve access token')
		}

		const transporter = nodemailer.createTransport({
			service: 'gmail',
			auth: {
				type: 'OAuth2',
				user: process.env.GMAIL_SENDER_EMAIL,
				clientId: process.env.GMAIL_CLIENT_ID,
				clientSecret: process.env.GMAIL_CLIENT_SECRET,
				refreshToken: process.env.GMAIL_REFRESH_TOKEN,
				accessToken: accessToken?.token
			}
		} as SMTPTransport.Options)

		const info = await transporter.sendMail({
			from: `${process.env.GMAIL_SENDER_NAME} <${process.env.GMAIL_SENDER_EMAIL}>`,
			to: email,
			subject,
			text: body,
			html
		})

		return {
			ok: info.response.includes('250'),
			response: info.response
		}
	} catch (error) {
		console.log(error)
		return { ok: false, response: 'server error' }
	}
}

function encryptAes(message: string) {
	let cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(config.key), config.iv)
	let encrypted = cipher.update(message)
	encrypted = Buffer.concat([encrypted, cipher.final()])
	return encrypted.toString('base64')
}

function decryptAes(cipherText: string) {
	let encryptedText = Buffer.from(cipherText, 'base64')
	let decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(config.key), config.iv)
	let decrypted = decipher.update(encryptedText)
	decrypted = Buffer.concat([decrypted, decipher.final()])
	return decrypted.toString()
}

async function decodeGoogleToken(googleToken: string, googleClientId: string) {
	try {
		const client = new OAuth2Client(googleClientId)
		const ticket = await client.verifyIdToken({
			idToken: googleToken,
			audience: googleClientId
		})
		const payload = ticket.getPayload()
		return payload
	} catch (error) {
		console.log(error)
	}
}

async function sendPushNotification(
	fcmToken: string,
	title: string,
	body: string,
	data: any
) {
	try {
		// console.log(JSON.stringify({title, body, data}, null ,2));
		data = JSON.stringify(data)
		// ?DOCS: https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#resource:-message
		const res = await constants.firebaseAdmin!.messaging().send({
			data: { title, body, data },
			notification: {
				title,
				body,
			},
			android: {
				priority: 'high',
				notification: {
					sound: 'default',
					icon: 'ic_notification',
					color: '#00A9A7',
					channelId: 'default_channel',
				},
			},
			apns: {
				headers: {
					'apns-priority': '10',
				},
				payload: {
					aps: {
						sound: 'default',
						badge: 1,
						'mutable-content': 1,
						'content-available': 1,
					},
				},
			},
			token: fcmToken,
		})
	} catch (error: any) {
		const errorCodes = ['messaging/invalid-argument', 'messaging/invalid-recipient', 'messaging/invalid-registration-token', 'messaging/registration-token-not-registered']
		console.log(error)
		if (errorCodes.includes(error.code)) {
			return error
		}
	}
}

async function generateSignedUrl(path: string, expiresInMinutes = 20) {
	try {
		const command = new PutObjectCommand({ Bucket: process.env.R2_BUCKET, Key: path })
		// max expiry is 7 days
		const upload = await getSignedUrl(config.R2, command, { expiresIn: expiresInMinutes * 60 })
		const download = getR2FileUrl(path)
		return { upload, download }
	} catch (error) {
		console.log(error)
	}
}

function validateRecord(data: Record<string, any>, field: string, hasStringValues = true, maxLength = 100) {
	try {
		if (typeof data !== 'object') {
			return `${field} must be an object`
		}
		const keys = Object.keys(data)
		for (const e of keys) {
			const value = data[e]
			if (typeof e !== 'string') {
				return `${e} in keyof ${field} must be a string`
			}
			if (!e.length) {
				return `${e} in keyof ${field} must have a length of atleast 1`
			}
			if (e.length > maxLength) {
				return `${e} in keyof ${field} must have a length of atmost ${maxLength}`
			}
			if (!hasStringValues) {
				continue
			}
			if (typeof value !== 'string') {
				return `${field}['${e}'] must be a string`
			}
			if (!value.length) {
				return `${field}['${e}'] must have a length of atleast 1`
			}
			if (value.length > maxLength) {
				return `${field}['${e}'] must have a length of atmost ${maxLength}`
			}
		}
	} catch (error) {
		console.log(error)
		return 'unknown error occurred'
	}
}

/*
	Requires ffmpeg:
	Mac: "brew install ffmpeg"
	Ubuntu: "sudo apt install ffmpeg"
*/
async function compressVideo(input: string, output: string): Promise<boolean> {
	return await new Promise((resolve, reject) => {
		ffmpeg(input)
			// Video settings
			.videoCodec('libx265')
			.outputOptions([
				'-crf 28',
				'-preset slower',
				'-tag:v hvc1'
			])
			// Audio settings
			.audioCodec('aac')
			.audioBitrate('96k')
			.save(output)
			// Events
			.on('end', () => {
				resolve(true)
			})
			.on('error', (err) => {
				reject(err)
			})
	})
}

function downloadFile(url: string, filePath: string) {
	return new Promise((resolve, reject) => {
		try {
			fs.mkdirSync(path.dirname(filePath), { recursive: true })
			const protocol = url.startsWith('https') ? https : http
			const fileStream = fs.createWriteStream(filePath)

			const request = protocol.get(url, response => {
				if (response.statusCode !== 200) {
					reject(new Error(`Failed to download file. Status: ${response.statusCode}`))
					return
				}
				response.pipe(fileStream)
				fileStream.on('finish', () => {
					fileStream.close()
					resolve(null)
				})
			})

			request.on('error', err => {
				fs.unlink(filePath, () => { })
				reject(err)
			})
		} catch (error) {
			console.log(error)
			reject(error)
		}
	})
}

async function createEventBridgeSchedule(name: string, at: Date, payload: any, autoDelete = true) {
	const toSchedulerTime = (date: Date) => {
		const d = new Date(date)
		return (
			d.getUTCFullYear() +
			"-" +
			String(d.getUTCMonth() + 1).padStart(2, "0") +
			"-" +
			String(d.getUTCDate()).padStart(2, "0") +
			"T" +
			String(d.getUTCHours()).padStart(2, "0") +
			":" +
			String(d.getUTCMinutes()).padStart(2, "0") +
			":" +
			String(d.getUTCSeconds()).padStart(2, "0")
		)
	}
	const command = new CreateScheduleCommand({
		Name: name,
		ScheduleExpression: `at(${toSchedulerTime(at)})`,
		FlexibleTimeWindow: { Mode: 'OFF' },
		ActionAfterCompletion: autoDelete ? 'DELETE' : undefined,
		Target: {
			Arn: process.env.AWS_EVENTBRIDGE_LAMBDA_ARN,
			RoleArn: process.env.AWS_EVENTBRIDGE_ROLE_ARN,
			Input: JSON.stringify(payload)
		}
	})
	const resp = await config.schedulerClient.send(command)
	return resp
}

async function deleteEventBridgeSchedule(name: string) {
	const command = new DeleteScheduleCommand({ Name: name })
	const resp = await config.schedulerClient.send(command)
	return resp
}

// createEventBridgeSchedule('job-123', new Date('2025-12-03T17:39:00+05:30'), {message: 'hello world'}, false)

// deleteEventBridgeSchedule('job-123')

const helpers = {
	deleteOldFiles,
	getHash,
	getToken,
	deleteFile,
	uploadFile,
	deleteR2File,
	deleteR2FilesWithPrefix,
	getR2FileUrl,
	generateSignedUrl,
	getRandomOtp,
	sendAWSSms,
	sendSms,
	sendOauthGmail,
	encryptAes,
	decryptAes,
	decodeGoogleToken,
	sendPushNotification,
	validateRecord,
	compressVideo,
	downloadFile,
	createEventBridgeSchedule,
	deleteEventBridgeSchedule
}

export default helpers
