/**
 * Script: Sync Relying Party clients from .env files into the DB clients table.
 *
 * Reads .env of each RP project and UPSERTs rows in the `clients` table so that
 * the OIDC provider config stays in sync with the RP env vars without manual SQL.
 *
 * Usage:
 *   npx tsx scripts/sync-rp-clients.ts
 *
 * The script is idempotent — running it multiple times is safe.
 * It only writes when values differ from what's already in the DB.
 */
import { readFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { config as readEnv } from 'dotenv';
import sequelize from '../src/services/database/sequelize/sequelizeService.js';

interface RpConfig {
	name: string;
	port: number;
	dbName: string;
	envPath: string;
	clientId: string;
	clientSecret: string;
	redirectUri: string;
	postLogoutUri: string;
}

const RP_PROJECTS: RpConfig[] = [
	{
		name: 'project-a-demo',
		port: 4001,
		dbName: 'demo_project_a',
		envPath: resolve(__dirname, '../../project-a-demo/.env'),
		clientId: 'project-a-demo',
		clientSecret: '',
		redirectUri: 'http://localhost:4001/auth/oidc/callback',
		postLogoutUri: 'http://localhost:4001',
	},
	{
		name: 'project-b-demo',
		port: 4002,
		dbName: 'demo_project_b',
		envPath: resolve(__dirname, '../../project-b-demo/.env'),
		clientId: 'project-b-demo',
		clientSecret: '',
		redirectUri: 'http://localhost:4002/auth/oidc/callback',
		postLogoutUri: 'http://localhost:4002',
	},
];

function loadEnv(path: string): Record<string, string> {
	if (!existsSync(path)) {
		console.warn(`  [WARN] .env not found: ${path}`);
		return {};
	}
	const result = readEnv({ path });
	return result.parsed ?? {};
}

async function upsertClient(rp: RpConfig, env: Record<string, string>): Promise<void> {
	const clientId = env.SSO_CLIENT_ID ?? rp.clientId;
	const clientSecret = env.SSO_CLIENT_SECRET ?? rp.clientSecret;
	const redirectUri = env.SSO_REDIRECT_URI ?? rp.redirectUri;
	const postLogoutUri = env.SSO_POST_LOGOUT_URI ?? rp.postLogoutUri;

	if (!clientId || !clientSecret) {
		console.warn(`  [SKIP] ${rp.name}: SSO_CLIENT_ID or SSO_CLIENT_SECRET not set in .env`);
		return;
	}

	const [row] = await sequelize.query<{ id: string; client_id: string }>(
		`SELECT id, client_id FROM clients WHERE client_id = :clientId`,
		{ replacements: { clientId }, type: 'SELECT' as any },
	);
	const exists = Boolean(row);

	const fields = {
		redirect_uris: [redirectUri],
		post_logout_redirect_uris: [postLogoutUri],
		token_endpoint_auth_method: 'client_secret_post',
		require_pkce: true,
		scopes: ['openid', 'profile', 'email'],
		grant_types: ['authorization_code', 'refresh_token'],
		response_types: ['code'],
		status: 'active',
	};

	if (exists) {
		// Only update if changed (idempotent)
		const [currentRows] = await sequelize.query<Record<string, unknown>>(
			`SELECT client_id, redirect_uris, post_logout_redirect_uris, require_pkce, token_endpoint_auth_method
         FROM clients WHERE client_id = :clientId`,
			{ replacements: { clientId }, type: 'SELECT' as any },
		);
		const current = currentRows ?? null;
		if (current) {
			const currentRedirect = Array.isArray(current.redirect_uris) ? current.redirect_uris : [];
			const currentPostLogout = Array.isArray(current.post_logout_redirect_uris) ? current.post_logout_redirect_uris : [];
			const changed =
				JSON.stringify(currentRedirect) !== JSON.stringify(fields.redirect_uris) ||
				JSON.stringify(currentPostLogout) !== JSON.stringify(fields.post_logout_redirect_uris) ||
				current.require_pkce !== fields.require_pkce ||
				current.token_endpoint_auth_method !== fields.token_endpoint_auth_method;

			if (changed) {
				await sequelize.query(
					`UPDATE clients SET redirect_uris = :redirectUris::text[], post_logout_redirect_uris = :postLogoutUris::text[],
             token_endpoint_auth_method = :authMethod, require_pkce = :pkce, scopes = :scopes::text[],
             grant_types = :grantTypes::text[], response_types = :responseTypes::text[], status = :status, updated_at = NOW()
             WHERE client_id = :clientId`,
					{
						replacements: {
							clientId,
							redirectUris: JSON.stringify(fields.redirect_uris),
							postLogoutUris: JSON.stringify(fields.post_logout_redirect_uris),
							authMethod: fields.token_endpoint_auth_method,
							pkce: fields.require_pkce,
							scopes: JSON.stringify(fields.scopes),
							grantTypes: JSON.stringify(fields.grant_types),
							responseTypes: JSON.stringify(fields.response_types),
							status: fields.status,
						},
						type: 'UPDATE' as any,
					},
				);
				console.log(`  [UPDATE] ${clientId} — fields updated`);
			} else {
				console.log(`  [OK]     ${clientId} — already in sync`);
			}
		}
	} else {
		// INSERT — generate bcrypt hash for client_secret
		// eslint-disable-next-line no-restricted-syntax
		const bcrypt = await import('bcryptjs');
		const hash = await bcrypt.hash(clientSecret, 10);

		await sequelize.query(
			`INSERT INTO clients (id, client_id, client_secret_hash, redirect_uris, post_logout_redirect_uris,
        token_endpoint_auth_method, require_pkce, scopes, grant_types, response_types, status, created_at, updated_at)
       VALUES (gen_random_uuid(), :clientId, :hash, :redirectUris::text[], :postLogoutUris::text[],
        'client_secret_post', true, :scopes::text[], :grantTypes::text[], :responseTypes::text[],
        'active', NOW(), NOW())
       ON CONFLICT (client_id) DO UPDATE SET
        redirect_uris = EXCLUDED.redirect_uris,
        post_logout_redirect_uris = EXCLUDED.post_logout_redirect_uris,
        token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,
        require_pkce = EXCLUDED.require_pkce,
        scopes = EXCLUDED.scopes,
        grant_types = EXCLUDED.grant_types,
        response_types = EXCLUDED.response_types,
        status = EXCLUDED.status,
        updated_at = NOW()`,
			{
				replacements: {
					clientId,
					hash,
					redirectUris: JSON.stringify(fields.redirect_uris),
					postLogoutUris: JSON.stringify(fields.post_logout_redirect_uris),
					scopes: JSON.stringify(fields.scopes),
					grantTypes: JSON.stringify(fields.grant_types),
					responseTypes: JSON.stringify(fields.response_types),
				},
				type: 'INSERT' as any,
			},
		);
		console.log(`  [INSERT] ${clientId} — new row created`);
	}
}

async function main(): Promise<void> {
	console.log('\n[SyncRPClients] Starting...\n');

	await sequelize.authenticate();
	console.log('[SyncRPClients] DB connected.\n');

	for (const rp of RP_PROJECTS) {
		console.log(`Processing: ${rp.name}`);
		const env = loadEnv(rp.envPath);
		await upsertClient(rp, env);
	}

	// Verify final state
	console.log('\n[SyncRPClients] Final state:\n');
	const results = await sequelize.query<Record<string, unknown>>(
		`SELECT client_id, redirect_uris::text AS redirect_uris, require_pkce,
        token_endpoint_auth_method, status FROM clients
     WHERE client_id IN (:ids)`,
		{ replacements: { ids: RP_PROJECTS.map((r) => r.clientId) }, type: 'SELECT' as any },
	);
	const rows = Array.isArray(results) ? results : (results as unknown as { rows?: unknown[] }).rows ?? [];
	console.table(rows.map((r) => ({
		client_id: String(r.client_id),
		redirect_uris: String(r.redirect_uris),
		require_pkce: String(r.require_pkce),
		auth_method: String(r.token_endpoint_auth_method),
		status: String(r.status),
	})));

	await sequelize.close();
	console.log('\n[SyncRPClients] Done.\n');
}

main().catch((err) => {
	console.error('[SyncRPClients] Error:', err);
	process.exit(1);
});
