import crypto from 'crypto';
import { Transaction } from 'sequelize';
import sequelize from '#services/database/sequelize/sequelizeService';
import { EmailVerifyToken } from '#models/EmailVerifyToken';
import { User } from '#models/User';
import MailService from '#services/notification/notificationEmailService';
import { getEmailVerificationEmail } from '#templates/email/emailVerification';
import { GenericError } from '#interfaces/error/generic';
import Config from '#config';

const TOKEN_BYTES = 32; // 64 hex chars — matches `email_verify_tokens.token_hash` CHAR(64)
const DEFAULT_TTL_HOURS = 24;
const HASH_ALGO = 'sha256';

function hashToken(plain: string): string {
	return crypto.createHash(HASH_ALGO).update(plain).digest('hex');
}

export interface CreateTokenResult {
	token: string; // plain token — only returned to the caller, never stored
	expiresAt: Date;
}

export type VerifyTokenOutcome =
	| { kind: 'ok'; user: User }
	| { kind: 'expired' }
	| { kind: 'used' }
	| { kind: 'not_found' };

class EmailVerificationService {
	private static instance: EmailVerificationService;

	static getInstance(): EmailVerificationService {
		EmailVerificationService.instance ??= new EmailVerificationService();
		return EmailVerificationService.instance;
	}

	/**
	 * Create a fresh verification token for a user. Old (unused) tokens for the
	 * same user are invalidated so only the most recent email is honoured.
	 */
	async createToken(userId: string, ttlHours = Config.emailVerification?.tokenTtlHours ?? DEFAULT_TTL_HOURS): Promise<CreateTokenResult> {
		const plain = crypto.randomBytes(TOKEN_BYTES).toString('hex');
		const tokenHash = hashToken(plain);
		const expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000);

		await sequelize.transaction(async (tx: Transaction) => {
			// Invalidate any outstanding tokens so a single user can only verify
			// via the latest email we sent out.
			await EmailVerifyToken.update(
				{ used_at: new Date() },
				{
					where: { user_id: userId, used_at: null },
					transaction: tx,
				}
			);

			await EmailVerifyToken.create(
				{
					id: crypto.randomUUID(),
					user_id: userId,
					token_hash: tokenHash,
					expires_at: expiresAt,
				},
				{ transaction: tx }
			);
		});

		return { token: plain, expiresAt };
	}

	/**
	 * Verify a plain token from a verification link. Marks the token used and
	 * flips the user to `active` + `email_verified_at` on success.
	 */
	async verifyToken(plain: string): Promise<VerifyTokenOutcome> {
		const tokenHash = hashToken(plain);

		return sequelize.transaction(async (tx: Transaction) => {
			const record = await EmailVerifyToken.findOne({
				where: { token_hash: tokenHash },
				transaction: tx,
			});

			if (!record) return { kind: 'not_found' };
			if (record.used_at) return { kind: 'used' };
			if (record.expires_at.getTime() < Date.now()) return { kind: 'expired' };

			const user = await User.findByPk(record.user_id, { transaction: tx });
			if (!user) return { kind: 'not_found' };

			record.used_at = new Date();
			await record.save({ transaction: tx });

			user.email_verified_at = new Date();
			user.status = 'active';
			await user.save({ transaction: tx });

			return { kind: 'ok', user };
		});
	}

	/**
	 * Build the verification link for a given plain token. Centralised so the
	 * route/path is defined in exactly one place.
	 */
	buildVerificationUrl(plain: string): string {
		const base = (Config.server.backendUrl || 'http://localhost:3001').replace(/\/+$/, '');
		return `${base}/api/v1/auth/verify-email?token=${encodeURIComponent(plain)}`;
	}

	/**
	 * Send a verification email for a newly-created user. In dev (no SMTP
	 * configured) the link is appended to `dev-mail.log` so it can be clicked
	 * manually.
	 */
	async sendVerificationEmail(user: User, ttlHours = Config.emailVerification?.tokenTtlHours ?? DEFAULT_TTL_HOURS): Promise<{ token: string; expiresAt: Date }> {
		const { token, expiresAt } = await this.createToken(user.id, ttlHours);
		const url = this.buildVerificationUrl(token);

		const fullName = [user.first_name, user.last_name].filter(Boolean).join(' ').trim() || null;
		const html = getEmailVerificationEmail({
			email: user.email,
			fullName,
			verificationUrl: url,
			expiresInHours: ttlHours,
		});

		await MailService.getInstance().sendmail({
			from: Config.email.from,
			to: user.email,
			subject: '[SSO VietProDev] Xác thực email / Verify your email',
			text: `Xin chào ${user.email},\n\nVui lòng truy cập liên kết sau để xác thực email (hết hạn sau ${ttlHours} giờ):\n${url}\n\nNếu bạn không đăng ký, vui lòng bỏ qua email này.`,
			html,
		});

		return { token, expiresAt };
	}

	/**
	 * Resend a verification email for an existing user. If the user is already
	 * verified this throws `USER_ALREADY_VERIFIED`. If an unexpired token was
	 * issued very recently we surface `VERIFICATION_RESEND_COOLDOWN` so the
	 * client can show a friendly "wait a moment" message.
	 *
	 * Returns the new token's expiry and the mail transport mode so the
	 * controller can echo them back to the caller.
	 */
	async resendVerificationEmail(
		user: User,
		ttlHours = Config.emailVerification?.tokenTtlHours ?? DEFAULT_TTL_HOURS,
		cooldownSeconds = 60,
	): Promise<{ expiresAt: Date; mode: 'smtp' | 'fallback' }> {
		if (user.email_verified_at) {
			throw new GenericError('USER_ALREADY_VERIFIED');
		}

		// Cooldown: if the most recent unused token was created within the
		// cooldown window, refuse to spam the user's inbox.
		const recent = await EmailVerifyToken.findOne({
			where: { user_id: user.id, used_at: null },
			order: [['expires_at', 'DESC']],
		});
		if (recent) {
			// Reuse the existing token if it's still valid — no need to issue a new one
			if (recent.expires_at.getTime() - Date.now() > ttlHours * 60 * 60 * 1000 * 0.5) {
				const url = this.buildVerificationUrl(
					// We can't return the original plaintext token (only the hash is stored).
					// Generate a new token to be safe; the previous token is invalidated
					// inside createToken().
					(await this.createToken(user.id, ttlHours)).token,
				);
				const result = await this.dispatchMail(user, url, ttlHours);
				return { expiresAt: recent.expires_at, mode: result };
			}
		}

		const { token, expiresAt } = await this.createToken(user.id, ttlHours);
		const url = this.buildVerificationUrl(token);
		const mode = await this.dispatchMail(user, url, ttlHours);
		return { expiresAt, mode };
	}

	private async dispatchMail(
		user: User,
		url: string,
		ttlHours: number,
	): Promise<'smtp' | 'fallback'> {
		const fullName = [user.first_name, user.last_name].filter(Boolean).join(' ').trim() || null;
		const html = getEmailVerificationEmail({
			email: user.email,
			fullName,
			verificationUrl: url,
			expiresInHours: ttlHours,
		});
		const result = await MailService.getInstance().sendmail({
			from: Config.email.from,
			to: user.email,
			subject: '[SSO VietProDev] Xác thực email / Verify your email',
			text: `Xin chào ${user.email},\n\nVui lòng truy cập liên kết sau để xác thực email (hết hạn sau ${ttlHours} giờ):\n${url}\n\nNếu bạn không đăng ký, vui lòng bỏ qua email này.`,
			html,
		});
		return result.mode;
	}
}

export default EmailVerificationService;
