/* eslint-disable @typescript-eslint/no-explicit-any */
import express from 'express';
import * as crypto from 'crypto';
import { Transaction } from 'sequelize';
import { OidcService } from './oidcService';
import { AuditLoggerService } from '#audit/auditLoggerService';
import { AUDIT_EVENTS } from '#audit/auditEvents';
import { PasswordService } from '#services/auth/passwordService';
import EmailVerificationService from '#services/auth/emailVerificationService';
import { User } from '#models/User';
import { Role } from '#models/Role';
import { UserRole } from '#models/UserRole';
import { UserStatus, UserRoleEnum } from '#constants/auth';
import { UserProvider } from '#providers/UserProvider';
import { UserAuthProvider } from '#providers/UserAuthProvider';
import Config from '#config';
import Logger from '#utils/logger';

const router = express.Router();

const MIN_PASSWORD_LENGTH = 12;
const DEFAULT_TTL_HOURS = 24;

// GET /oidc/interaction/:uid — render login or consent page
router.get('/:uid', async (req, res) => {
	try {
		const details = await OidcService.interactionDetails(req, res);

		if (details.prompt.name === 'consent') {
			return res.render('consent', {
				uid: req.params.uid,
				client: details.params.client_id,
				csrfToken: csrfToken(req),
			});
		}

		return res.render('login', {
			uid: req.params.uid,
			client: details.params.client_id,
			error: undefined,
			csrfToken: csrfToken(req),
		});
	} catch (err) {
		Logger.error('[OIDC] GET /:uid failed', err);
		return res.status(500).send('Internal Server Error');
	}
});

// GET /oidc/interaction/:uid/register — render registration page
router.get('/:uid/register', async (req, res) => {
	const emailPrefill = typeof req.query.email === 'string' ? req.query.email : '';
	return res.render('register', {
		uid: req.params.uid,
		client: '',
		prefill: {
			email: emailPrefill,
			first_name: '',
			last_name: '',
			phone: '',
			address: '',
			gender: '',
			date_of_birth: '',
		},
		error: undefined,
		csrfToken: csrfToken(req),
	});
});

// POST /oidc/interaction/:uid/login — submit login credentials
router.post('/:uid/login', async (req, res) => {
	const { email, password, remember } = req.body as { email?: string; password?: string; remember?: string };

	try {
		const user = await validateCredentials(email ?? '', password ?? '');
		await logAudit(AUDIT_EVENTS.LOGIN_SUCCESS, user.id, req);

		const result = {
			login: {
				accountId: user.id,
				remember: Boolean(remember),
				ts: Math.floor(Date.now() / 1000),
			},
		};

		return OidcService.interactionFinished(req, res, result, { mergeWithLastSubmission: false });
	} catch (err: any) {
		await logAudit(AUDIT_EVENTS.LOGIN_FAILED, undefined, req);

		if (err?.status === 429) {
			return res.status(429).render('login', {
				uid: req.params.uid,
				client: (req.query.client_id as string) ?? '',
				error: err.message,
				csrfToken: csrfToken(req),
			});
		}

		return res.status(401).render('login', {
			uid: req.params.uid,
			client: (req.query.client_id as string) ?? '',
			error: 'Email or password is invalid',
			csrfToken: csrfToken(req),
		});
	}
});

// POST /oidc/interaction/:uid/register — create account in pending_verification
// status and render the verify-pending screen. Auto-login is intentionally
// skipped; the user must click the link in the verification email first.
router.post('/:uid/register', async (req, res) => {
	const uid = req.params.uid;
	const {
		email,
		username,
		password,
		confirmPassword,
		first_name,
		last_name,
		phone,
		address,
		gender,
		date_of_birth,
		terms_accepted,
	} = req.body as {
		email?: string;
		username?: string;
		password?: string;
		confirmPassword?: string;
		first_name?: string;
		last_name?: string;
		phone?: string;
		address?: string;
		gender?: string;
		date_of_birth?: string;
		terms_accepted?: string | boolean;
	};

	const trimmedEmail = (email ?? '').trim().toLowerCase();
	const trimmedUsername = username?.trim() || null;
	const trimmedFirstName = first_name?.trim() || null;
	const trimmedLastName = last_name?.trim() || null;
	const trimmedPhone = phone?.trim() || null;
	const trimmedAddress = address?.trim() || null;
	const trimmedGender = gender?.trim() || null;
	const trimmedDob = date_of_birth?.trim() || null;
	const termsAccepted = terms_accepted === '1' || terms_accepted === true || terms_accepted === 'on';
	const derivedUsername = trimmedUsername ?? trimmedEmail.split('@')[0] ?? null;

	const clientId = await getClientIdFromUid(uid);

	const renderRegisterError = (error: string) =>
		res.status(400).render('register', {
			uid,
			client: clientId,
			prefill: {
				email: trimmedEmail,
				first_name: trimmedFirstName ?? '',
				last_name: trimmedLastName ?? '',
				phone: trimmedPhone ?? '',
				address: trimmedAddress ?? '',
				gender: trimmedGender ?? '',
				date_of_birth: trimmedDob ?? '',
			},
			error,
			csrfToken: csrfToken(req),
		});

	if (!trimmedEmail || !password) {
		return renderRegisterError('Email and password are required');
	}
	if (password !== confirmPassword) {
		return renderRegisterError('Passwords do not match');
	}
	if (password.length < MIN_PASSWORD_LENGTH) {
		return renderRegisterError(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
	}
	if (!termsAccepted) {
		return renderRegisterError('You must accept the terms to create an account');
	}

	const ttlHours = getVerificationTtlHours();

	try {
		const userProvider = new UserProvider();
		const existing = await userProvider.getAll({
			where: { email: trimmedEmail },
			page: 1,
			pageSize: 1,
		});
		if (existing.rows.length > 0) {
			return renderRegisterError('Email already registered. Please sign in instead.');
		}

		const passwordHash = await PasswordService.hashPassword(password);
		const userId = crypto.randomUUID();
		const authId = crypto.randomUUID();
		const userRoleId = crypto.randomUUID() as string;
		const parsedDob = trimmedDob ? parseIsoDate(trimmedDob) : null;

		const transaction: Transaction = await userProvider.transaction();
		let createdUserId: ReturnType<typeof crypto.randomUUID> = userId;
		let createdUser: User | null = null;
		try {
			const userInstance = (await userProvider.create(
				{
					id: userId,
					email: trimmedEmail,
					username: derivedUsername,
					first_name: trimmedFirstName,
					last_name: trimmedLastName,
					phone: trimmedPhone,
					address: trimmedAddress,
					date_of_birth: parsedDob as never,
					gender: trimmedGender as never,
					status: UserStatus.PENDING_VERIFICATION,
					email_verified_at: null,
				} as never,
				{ transaction, skipUniqueCheck: true },
			)) as unknown as User;
			createdUserId = userInstance.id as ReturnType<typeof crypto.randomUUID>;
			createdUser = userInstance;

			const userAuthProvider = new UserAuthProvider();
			await userAuthProvider.create(
				{
					id: authId,
					user_id: createdUserId,
					password_hash: passwordHash,
				} as never,
				{ transaction, skipUniqueCheck: true },
			);

			// Assign default 'user' role if it has been seeded. Missing role
			// is non-fatal: the user lands without role-bound claims until
			// the role is provisioned. Surfacing the warning keeps the gap
			// observable without blocking sign-up.
			const defaultRole = await Role.findOne({
				where: { name: UserRoleEnum.USER },
				transaction,
			});
			if (defaultRole) {
				await UserRole.create(
					{
						id: userRoleId,
						user_id: createdUserId,
						role_id: defaultRole.id,
						is_primary: true,
						assigned_at: new Date(),
					} as never,
					{ transaction },
				);
			} else {
				Logger.warn(`[OIDC register] default role '${UserRoleEnum.USER}' not seeded; user ${createdUserId} has no role`);
			}

			await transaction.commit();
		} catch (txErr) {
			await transaction.rollback();
			throw txErr;
		}

		await logAudit(AUDIT_EVENTS.REGISTER_SUCCESS, createdUserId, req);

		let devVerifyUrl: string | null = null;
		if (createdUser) {
			try {
				const issued = await EmailVerificationService.getInstance().sendVerificationEmail(createdUser, ttlHours);
				devVerifyUrl = isDevMode() ? EmailVerificationService.getInstance().buildVerificationUrl(issued.token) : null;
			} catch (mailErr) {
				// Email transport is best-effort: token row was already inserted
				// inside sendVerificationEmail before the SMTP attempt, so the
				// user can still resend or get a fresh link.
				Logger.error(
					`[OIDC register] Failed to dispatch verification email for user ${createdUserId}. ` +
						`Account created but no email was sent.`,
					mailErr,
				);
			}
		}

		return res.status(200).render('verify-pending', {
			uid,
			client: clientId,
			email: trimmedEmail,
			ttlHours,
			devVerifyUrl,
			error: undefined,
			csrfToken: csrfToken(req),
		});
	} catch (err: any) {
		await logAudit(AUDIT_EVENTS.REGISTER_FAILED, undefined, req);
		const isUniqueViolation = err?.code === '23505' || err?.original?.code === '23505';
		Logger.warn(`[OIDC register] failed: ${err?.message ?? err}`);
		return renderRegisterError(
			isUniqueViolation
				? 'Email or username already exists. Please try again.'
				: 'Unable to create account. Please try again.',
		);
	}
});

// POST /oidc/interaction/:uid/resend-verification — mint a fresh token and
// re-render the verify-pending screen. The response is identical for known
// and unknown emails to avoid leaking account existence.
router.post('/:uid/resend-verification', async (req, res) => {
	const uid = req.params.uid;
	const emailRaw = (req.body as { email?: string }).email ?? '';
	const trimmedEmail = emailRaw.trim().toLowerCase();

	const clientId = await getClientIdFromUid(uid);
	const ttlHours = getVerificationTtlHours();

	const renderPending = (body: {
		email: string;
		ttlHours: number;
		devVerifyUrl?: string | null;
		error?: string;
	}) =>
		res.status(200).render('verify-pending', {
			uid,
			client: clientId,
			email: body.email,
			ttlHours: body.ttlHours,
			devVerifyUrl: body.devVerifyUrl ?? null,
			error: body.error,
			csrfToken: csrfToken(req),
		});

	if (!trimmedEmail) {
		return renderPending({
			email: '',
			ttlHours,
			error: 'Missing email. Please re-open the verification screen.',
		});
	}

	try {
		const existing = await User.findOne({ where: { email: trimmedEmail } });

		// Intentionally do not leak whether the account exists: any caller
		// receives the same "check your inbox" screen, regardless of whether
		// the user is unknown, already verified, or not pending.
		if (!existing || existing.status !== UserStatus.PENDING_VERIFICATION) {
			return renderPending({ email: trimmedEmail, ttlHours });
		}

		const issued = await EmailVerificationService.getInstance().sendVerificationEmail(existing, ttlHours);
		const devVerifyUrl = isDevMode() ? EmailVerificationService.getInstance().buildVerificationUrl(issued.token) : null;

		return renderPending({
			email: trimmedEmail,
			ttlHours,
			devVerifyUrl,
		});
	} catch (err) {
		Logger.error('[OIDC resend-verification] failed', err);
		return renderPending({
			email: trimmedEmail,
			ttlHours,
			error: 'Could not resend verification email. Please try again later.',
		});
	}
});

// POST /oidc/interaction/:uid/confirm — approve consent
router.post('/:uid/confirm', async (req, res) => {
	try {
		const details = await OidcService.interactionDetails(req, res);
		const { prompt, grantId, session, params } = details;

		let grant: any;
		if (grantId) {
			grant = await OidcService.getInstance().Grant.find(grantId);
		} else {
			grant = new (OidcService.getInstance().Grant as any)({
				accountId: session.accountId,
				clientId: params.client_id,
			});
		}

		if (prompt.details?.missingOIDCScope) {
			grant.addOIDCScope(prompt.details.missingOIDCScope.join(' '));
		}
		if (prompt.details?.missingOIDCClaims) {
			grant.addOIDCClaims(prompt.details.missingOIDCClaims);
		}
		if (prompt.details?.missingResourceScopes) {
			for (const [indicator, scopes] of Object.entries(prompt.details.missingResourceScopes as Record<string, string[]>)) {
				grant.addResourceScope(indicator, scopes.join(' '));
			}
		}

		const result = { consent: { grantId: await grant.save() } };
		return OidcService.interactionFinished(req, res, result, { mergeWithLastSubmission: true });
	} catch (err) {
		Logger.error('[OIDC] POST /:uid/confirm failed', err);
		return res.status(500).send('Internal Server Error');
	}
});

// POST /oidc/interaction/:uid/cancel — deny consent
router.post('/:uid/cancel', async (req, res) => {
	return OidcService.interactionFinished(
		req,
		res,
		{ error: 'access_denied', error_description: 'User cancelled the request' },
		{ mergeWithLastSubmission: false },
	);
});

// ── Helpers ──────────────────────────────────────────────────────────────────

function csrfToken(req: express.Request): string {
	return (req as { csrfToken?: () => string }).csrfToken?.() ?? '';
}

function isDevMode(): boolean {
	return process.env.NODE_ENV !== 'production';
}

function getVerificationTtlHours(): number {
	const fromConfig = (Config as unknown as { emailVerification?: { tokenTtlHours?: number } })
		?.emailVerification?.tokenTtlHours;
	return typeof fromConfig === 'number' && fromConfig > 0 ? fromConfig : DEFAULT_TTL_HOURS;
}

function parseIsoDate(value: string): Date | null {
	if (!value) return null;
	const d = new Date(value);
	return Number.isNaN(d.getTime()) ? null : d;
}

/**
 * Resolve the OIDC client_id for a given interaction uid. The cookie-based
 * `interactionDetails` flow needs the original request, which we do not have
 * from a POST handler; instead we go straight to the provider's Interaction
 * model which persists the raw params. Failure here is non-fatal — the
 * client label is decorative on the verify-pending screen.
 */
async function getClientIdFromUid(uid: string): Promise<string> {
	try {
		const provider = OidcService.getInstance();
		const interaction = await provider.Interaction.find(uid);
		const params = (interaction?.params as { client_id?: string } | undefined) ?? undefined;
		return params?.client_id ?? '';
	} catch (err) {
		Logger.warn(
			`[OIDC] getClientIdFromUid failed for ${uid}: ${err instanceof Error ? err.message : String(err)}`,
		);
		return '';
	}
}

async function validateCredentials(
	email: string,
	password: string,
): Promise<{ id: string; email: string; username: string; status: string }> {
	// TODO: Wire with UserProvider + PasswordService + brute-force protection.
	// For now, throw a generic error to indicate not yet implemented.
	const err: any = new Error('Authentication not yet wired');
	err.status = 401;
	throw err;
}

async function logAudit(eventType: string, userId: string | undefined, req: express.Request): Promise<void> {
	if (!Config.audit.enabled) return;
	try {
		const audit = new AuditLoggerService();
		const logEntry: {
			eventType: string;
			userId?: string;
			status: 'success' | 'failed';
			ip?: string;
			userAgent?: string;
			requestId?: string;
		} = {
			eventType,
			status: eventType.endsWith('_SUCCESS') ? 'success' : 'failed',
		};
		if (userId) logEntry.userId = userId;
		if (req.ip) logEntry.ip = req.ip;
		if (req.headers['user-agent']) logEntry.userAgent = req.headers['user-agent'];
		if ((req as any)?.requestId) logEntry.requestId = (req as any).requestId;
		await audit.log(logEntry);
	} catch (err) {
		// Don't let audit failures break the auth flow.
		Logger.warn(`[OIDC] audit log failed for ${eventType}: ${err instanceof Error ? err.message : String(err)}`);
	}
}

export default router;
