import type { FrontendSettings, ITelemetrySettings, N8nEnvFeatFlags } from '@n8n/api-types';
import { LicenseState, Logger, ModuleRegistry } from '@n8n/backend-common';
import { GlobalConfig, SecurityConfig } from '@n8n/config';
import { LICENSE_FEATURES, LICENSE_QUOTAS } from '@n8n/constants';
import { Container, Service } from '@n8n/di';
import { createWriteStream } from 'fs';
import { mkdir } from 'fs/promises';
import uniq from 'lodash/uniq';
import { BinaryDataConfig, InstanceSettings } from 'n8n-core';
import type { ICredentialType, INodeTypeBaseDescription, INodeTypeDescription } from 'n8n-workflow';
import path from 'path';

import config from '@/config';
import { inE2ETests, N8N_VERSION } from '@/constants';
import { CredentialTypes } from '@/credential-types';
import { CredentialsOverwrites } from '@/credentials-overwrites';
import { resolveEvaluationConcurrencyLimit } from '@/evaluation.ee/evaluation-concurrency.helper';
import { License } from '@/license';
import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials';
import { MfaService } from '@/mfa/mfa.service';
import { CommunityPackagesConfig } from '@/modules/community-packages/community-packages.config';
import type { CommunityPackagesService } from '@/modules/community-packages/community-packages.service';
import { isApiEnabled } from '@/public-api';
import { PushConfig } from '@/push/push.config';
import { OwnershipService } from '@/services/ownership.service';
import { getSamlLoginLabel, getCurrentAuthenticationMethod } from '@/sso.ee/sso-helpers';
import { UserManagementMailer } from '@/user-management/email';
import { resolveFrontendHealthEndpointPath } from '@/utils/health-endpoint.util';
import {
	getWorkflowHistoryLicensePruneTime,
	getWorkflowHistoryPruneTime,
} from '@/workflows/workflow-history/workflow-history-helper';
import { AiUsageService } from './ai-usage.service';
import { UrlService } from './url.service';

/**
 * IMPORTANT: Only add settings that are absolutely necessary for non-authenticated pages
 */
export type PublicFrontendSettings = {
	/** Controls initialization flow in settings store */
	settingsMode: FrontendSettings['settingsMode'];

	/** Used to localize login page UI */
	defaultLocale: FrontendSettings['defaultLocale'];

	/** Used to bypass authentication on the workflows/demo page */
	previewMode: FrontendSettings['previewMode'];

	authCookie: {
		/** Blocks insecure access incompatible with the authentication cookie. */
		secure: FrontendSettings['authCookie']['secure'];
	};

	userManagement: {
		/** Used to control login page UI behaviour and conditional SSO Login display */
		authenticationMethod: FrontendSettings['userManagement']['authenticationMethod'];

		/** Enables initial owner setup */
		showSetupOnFirstLoad: FrontendSettings['userManagement']['showSetupOnFirstLoad'];

		/** Determines forgot password page UX */
		smtpSetup: FrontendSettings['userManagement']['smtpSetup'];

		/** Configurable minimum password length for password requirement display */
		passwordMinLength: FrontendSettings['userManagement']['passwordMinLength'];
	};

	enterprise: {
		/** License check for SAML for SSO button visibility */
		saml: FrontendSettings['enterprise']['saml'];

		/** License check for OIDC for SSO button visibility */
		oidc: FrontendSettings['enterprise']['oidc'];

		/** License check for LDAP authentication */
		ldap: FrontendSettings['enterprise']['ldap'];
	};

	sso: {
		saml: {
			/** Config flag for SSO button*/
			loginEnabled: FrontendSettings['sso']['saml']['loginEnabled'];
		};
		ldap: {
			/** Config flag for LDAP authentication */
			loginEnabled: FrontendSettings['sso']['ldap']['loginEnabled'];

			/** Customizes login form label (defaults to "Email") */
			loginLabel: FrontendSettings['sso']['ldap']['loginLabel'];
		};
		oidc: {
			/** Config flag for SSO button*/
			loginEnabled: FrontendSettings['sso']['oidc']['loginEnabled'];

			/** Required for OIDC authentication redirect URL */
			loginUrl: FrontendSettings['sso']['oidc']['loginUrl'];
		};
	};
	/** Used to fetch community nodes on preview instance */
	communityNodesEnabled: FrontendSettings['communityNodesEnabled'];

	mfa?: {
		enabled: boolean;
		enforced: boolean;
	};
};

@Service()
export class FrontendService {
	private settings: FrontendSettings;

	private communityPackagesService?: CommunityPackagesService;

	constructor(
		private readonly globalConfig: GlobalConfig,
		private readonly logger: Logger,
		private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
		private readonly credentialTypes: CredentialTypes,
		private readonly credentialsOverwrites: CredentialsOverwrites,
		private readonly license: License,
		private readonly mailer: UserManagementMailer,
		private readonly instanceSettings: InstanceSettings,
		private readonly urlService: UrlService,
		private readonly securityConfig: SecurityConfig,
		private readonly pushConfig: PushConfig,
		private readonly binaryDataConfig: BinaryDataConfig,
		private readonly licenseState: LicenseState,
		private readonly moduleRegistry: ModuleRegistry,
		private readonly mfaService: MfaService,
		private readonly ownershipService: OwnershipService,
		private readonly aiUsageService: AiUsageService,
	) {
		loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes());
		void this.generateTypes();
		// @TODO: Move to community-packages module
		if (Container.get(CommunityPackagesConfig).enabled) {
			void import('@/modules/community-packages/community-packages.service').then(
				({ CommunityPackagesService }) => {
					this.communityPackagesService = Container.get(CommunityPackagesService);
				},
			);
		}
	}

	private collectEnvFeatureFlags(): N8nEnvFeatFlags {
		const envFeatureFlags: N8nEnvFeatFlags = {};

		for (const [key, value] of Object.entries(process.env)) {
			if (key.startsWith('N8N_ENV_FEAT_') && value !== undefined) {
				envFeatureFlags[key as keyof N8nEnvFeatFlags] = value;
			}
		}

		return envFeatureFlags;
	}

	private async getShowSetupOnFirstLoad() {
		const previewMode = process.env.N8N_PREVIEW_MODE === 'true';
		const hasInstanceOwner = await this.ownershipService.hasInstanceOwner();
		// In preview mode, skip the setup redirect to allow accessing demo routes
		return previewMode ? false : !hasInstanceOwner;
	}

	private async initSettings() {
		const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
		const restEndpoint = this.globalConfig.endpoints.rest;

		const telemetrySettings: ITelemetrySettings = {
			enabled: this.globalConfig.diagnostics.enabled,
		};

		if (telemetrySettings.enabled) {
			const conf = this.globalConfig.diagnostics.frontendConfig;
			const [key, url] = conf.split(';');
			const proxy = `${instanceBaseUrl}/${restEndpoint}/telemetry/proxy`;
			const sourceConfig = `${instanceBaseUrl}/${restEndpoint}/telemetry/rudderstack`;

			if (!key || !url) {
				this.logger.warn('Diagnostics frontend config is invalid');
				telemetrySettings.enabled = false;
			}

			telemetrySettings.config = { key, url, proxy, sourceConfig };
		}

		const previewMode = process.env.N8N_PREVIEW_MODE === 'true';

		this.settings = {
			settingsMode: 'authenticated',
			inE2ETests,
			isDocker: this.instanceSettings.isDocker,
			databaseType: this.globalConfig.database.type,
			previewMode,
			endpointForm: this.globalConfig.endpoints.form,
			endpointFormTest: this.globalConfig.endpoints.formTest,
			endpointFormWaiting: this.globalConfig.endpoints.formWaiting,
			endpointMcp: this.globalConfig.endpoints.mcp,
			endpointMcpTest: this.globalConfig.endpoints.mcpTest,
			endpointWebhook: this.globalConfig.endpoints.webhook,
			endpointWebhookTest: this.globalConfig.endpoints.webhookTest,
			endpointWebhookWaiting: this.globalConfig.endpoints.webhookWaiting,
			endpointHealth: resolveFrontendHealthEndpointPath(this.globalConfig),
			saveDataErrorExecution: this.globalConfig.executions.saveDataOnError,
			saveDataSuccessExecution: this.globalConfig.executions.saveDataOnSuccess,
			saveManualExecutions: this.globalConfig.executions.saveDataManualExecutions,
			saveExecutionProgress: this.globalConfig.executions.saveExecutionProgress,
			executionTimeout: this.globalConfig.executions.timeout,
			maxExecutionTimeout: this.globalConfig.executions.maxTimeout,
			workflowCallerPolicyDefaultOption: this.globalConfig.workflows.callerPolicyDefaultOption,
			timezone: this.globalConfig.generic.timezone,
			urlBaseWebhook: this.urlService.getWebhookBaseUrl(),
			urlBaseEditor: instanceBaseUrl,
			binaryDataMode: this.binaryDataConfig.mode,
			nodeJsVersion: process.version.replace(/^v/, ''),
			nodeEnv: process.env.NODE_ENV,
			versionCli: N8N_VERSION,
			concurrency: this.globalConfig.executions.concurrency.productionLimit,
			evaluationConcurrencyLimit: resolveEvaluationConcurrencyLimit(
				this.globalConfig.executions,
				this.license,
			),
			authCookie: {
				secure: this.globalConfig.auth.cookie.secure,
			},
			releaseChannel: this.globalConfig.generic.releaseChannel,
			oauthCallbackUrls: {
				oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,
				oauth2: `${instanceBaseUrl}/${restEndpoint}/oauth2-credential/callback`,
			},
			jwksUri: `${instanceBaseUrl}/${restEndpoint}/.well-known/jwks.json`,
			versionNotifications: {
				enabled: this.globalConfig.versionNotifications.enabled,
				endpoint: this.globalConfig.versionNotifications.endpoint,
				whatsNewEnabled: this.globalConfig.versionNotifications.whatsNewEnabled,
				whatsNewEndpoint: this.globalConfig.versionNotifications.whatsNewEndpoint,
				infoUrl: this.globalConfig.versionNotifications.infoUrl,
			},
			dynamicBanners: {
				endpoint: this.globalConfig.dynamicBanners.endpoint,
				enabled: this.globalConfig.dynamicBanners.enabled && this.globalConfig.diagnostics.enabled,
			},
			instanceId: this.instanceSettings.instanceId,
			telemetry: telemetrySettings,
			posthog: {
				enabled: this.globalConfig.diagnostics.enabled,
				apiHost: this.globalConfig.diagnostics.posthogConfig.apiHost,
				apiKey: this.globalConfig.diagnostics.posthogConfig.apiKey,
				autocapture: false,
				disableSessionRecording: this.globalConfig.deployment.type !== 'cloud',
				proxy: `${instanceBaseUrl}/${restEndpoint}/ph`,
				debug: this.globalConfig.logging.level === 'debug',
			},
			personalizationSurveyEnabled:
				this.globalConfig.personalization.enabled && this.globalConfig.diagnostics.enabled,
			defaultLocale: this.globalConfig.defaultLocale,
			userManagement: {
				quota: this.license.getUsersLimit(),
				showSetupOnFirstLoad: await this.getShowSetupOnFirstLoad(),
				smtpSetup: this.mailer.isEmailSetUp,
				authenticationMethod: getCurrentAuthenticationMethod(),
				passwordMinLength: this.globalConfig.userManagement.password.minLength,
			},
			sso: {
				managedByEnv: this.globalConfig.instanceSettingsLoader.ssoManagedByEnv,
				saml: {
					loginEnabled: false,
					loginLabel: '',
				},
				ldap: {
					loginEnabled: false,
					loginLabel: '',
				},
				oidc: {
					loginEnabled: false,
					loginUrl: `${instanceBaseUrl}/${restEndpoint}/sso/oidc/login`,
					callbackUrl: `${instanceBaseUrl}/${restEndpoint}/sso/oidc/callback`,
				},
			},
			logStreaming: {
				managedByEnv: this.globalConfig.instanceSettingsLoader.logStreamingManagedByEnv,
			},
			dataTables: {
				maxSize: this.globalConfig.dataTable.maxSize,
			},
			publicApi: {
				enabled: isApiEnabled(),
				latestVersion: 1,
				path: this.globalConfig.publicApi.path,
				swaggerUi: {
					enabled: !this.globalConfig.publicApi.swaggerUiDisabled,
				},
			},
			workflowTagsDisabled: this.globalConfig.tags.disabled,
			workflowsAutosaveDisabled: this.globalConfig.workflows.autosaveDisabled,
			logLevel: this.globalConfig.logging.level,
			hiringBannerEnabled: this.globalConfig.hiringBanner.enabled,
			aiAssistant: {
				enabled: false,
				setup: false,
			},
			templates: {
				enabled: this.globalConfig.templates.enabled,
				host: this.globalConfig.templates.host,
			},
			executionMode: this.globalConfig.executions.mode,
			isMultiMain: this.instanceSettings.isMultiMain,
			pushBackend: this.pushConfig.backend,

			// @TODO: Move to community-packages module
			communityNodesEnabled: Container.get(CommunityPackagesConfig).enabled,
			unverifiedCommunityNodesEnabled: Container.get(CommunityPackagesConfig).unverifiedEnabled,
			communityNodesManagedByEnv:
				this.globalConfig.instanceSettingsLoader.communityPackagesManagedByEnv,

			deployment: {
				type: this.globalConfig.deployment.type,
			},
			allowedModules: {
				builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN?.split(',') ?? undefined,
				external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL?.split(',') ?? undefined,
			},
			enterprise: {
				sharing: false,
				ldap: false,
				saml: false,
				oidc: false,
				mfaEnforcement: false,
				logStreaming: false,
				advancedExecutionFilters: false,
				variables: false,
				sourceControl: false,
				auditLogs: false,
				externalSecrets: false,
				showNonProdBanner: false,
				debugInEditor: false,
				binaryDataS3: false,
				workerView: false,
				advancedPermissions: false,

				workflowDiffs: false,
				namedVersions: false,
				provisioning: false,
				projects: {
					team: {
						limit: 0,
					},
				},
				customRoles: false,
				personalSpacePolicy: false,
				dataRedaction: false,
			},
			mfa: {
				enabled: false,
				enforced: false,
			},
			hideUsagePage: this.globalConfig.hideUsagePage,
			license: {
				consumerId: 'unknown',
				environment: this.globalConfig.license.tenantId === 1 ? 'production' : 'staging',
			},
			variables: {
				limit: 0,
			},
			banners: {
				dismissed: [],
			},
			askAi: {
				enabled: false,
			},
			aiBuilder: {
				enabled: false,
				setup: false,
			},
			aiCredits: {
				enabled: false,
				credits: 0,
				setup: false,
			},
			ai: {
				allowSendingParameterValues: true,
			},
			workflowHistory: {
				pruneTime: getWorkflowHistoryPruneTime(),
				licensePruneTime: getWorkflowHistoryLicensePruneTime(),
			},
			pruning: {
				isEnabled: this.globalConfig.executions.pruneData,
				maxAge: this.globalConfig.executions.pruneDataMaxAge,
				maxCount: this.globalConfig.executions.pruneDataMaxCount,
			},
			security: {
				blockFileAccessToN8nFiles: this.securityConfig.blockFileAccessToN8nFiles,
			},
			chatTrigger: {
				disablePublicChat: this.globalConfig.chatTrigger.disablePublicChat,
			},
			easyAIWorkflowOnboarded: false,
			folders: {
				enabled: false,
			},
			evaluation: {
				quota: this.licenseState.getMaxWorkflowsWithEvaluations(),
			},
			activeModules: this.moduleRegistry.getActiveModules(),
			canvasOnly: this.globalConfig.canvasOnly,
			envFeatureFlags: this.collectEnvFeatureFlags(),
		};
	}

	async generateTypes() {
		this.overwriteCredentialsProperties();

		const { credentials, nodes } = await this.loadNodesAndCredentials.collectTypes();

		const { staticCacheDir } = this.instanceSettings;
		// pre-render all the node and credential types as static json files
		await mkdir(path.join(staticCacheDir, 'types'), { recursive: true });
		await this.writeStaticJSON('nodes', nodes);
		const nodeVersionIdentifiers = this.getNodeVersionIdentifiers(nodes);
		await this.writeStaticJSON('node-versions', nodeVersionIdentifiers);
		await this.writeStaticJSON('credentials', credentials);
	}

	async getSettings(): Promise<FrontendSettings> {
		if (!this.settings) {
			await this.initSettings();
		}
		const restEndpoint = this.globalConfig.endpoints.rest;

		const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
		this.settings.urlBaseWebhook = this.urlService.getWebhookBaseUrl();
		this.settings.urlBaseEditor = instanceBaseUrl;
		this.settings.oauthCallbackUrls = {
			oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,
			oauth2: `${instanceBaseUrl}/${restEndpoint}/oauth2-credential/callback`,
		};
		this.settings.jwksUri = `${instanceBaseUrl}/${restEndpoint}/.well-known/jwks.json`;

		// refresh user management status
		Object.assign(this.settings.userManagement, {
			quota: this.license.getUsersLimit(),
			authenticationMethod: getCurrentAuthenticationMethod(),
			showSetupOnFirstLoad: await this.getShowSetupOnFirstLoad(),
		});

		let dismissedBanners: string[] = [];

		try {
			dismissedBanners = config.getEnv('ui.banners.dismissed') ?? [];
		} catch {
			// not yet in DB
		}

		this.settings.banners.dismissed = dismissedBanners;
		try {
			this.settings.easyAIWorkflowOnboarded = config.getEnv('easyAIWorkflowOnboarded') ?? false;
		} catch {
			this.settings.easyAIWorkflowOnboarded = false;
		}
		try {
			this.settings.ai.allowSendingParameterValues = await this.aiUsageService.getAiUsageSettings();
		} catch {
			this.settings.ai.allowSendingParameterValues = true;
		}

		const isS3Selected = this.binaryDataConfig.mode === 's3';
		const isS3Available = this.binaryDataConfig.availableModes.includes('s3');
		const isS3Licensed = this.license.isBinaryDataS3Licensed();
		const isAiAssistantEnabled = this.license.isAiAssistantEnabled();
		const isAskAiEnabled = this.license.isAskAiEnabled();
		const isAiCreditsEnabled = this.license.isAiCreditsEnabled();
		const isAiBuilderEnabled = this.license.isLicensed(LICENSE_FEATURES.AI_BUILDER);

		this.settings.license.planName = this.license.getPlanName();
		this.settings.license.consumerId = this.license.getConsumerId();

		// Re-resolve on every settings fetch so a license upgrade/downgrade
		// (and the tier-default it implies) propagates to the FE without an
		// instance restart. The env override still wins inside the resolver.
		this.settings.evaluationConcurrencyLimit = resolveEvaluationConcurrencyLimit(
			this.globalConfig.executions,
			this.license,
		);

		// refresh enterprise status
		Object.assign(this.settings.enterprise, {
			sharing: this.license.isSharingEnabled(),
			logStreaming: this.license.isLogStreamingEnabled(),
			ldap: this.license.isLdapEnabled(),
			saml: this.license.isSamlEnabled(),
			oidc: this.licenseState.isOidcLicensed(),
			mfaEnforcement: this.licenseState.isMFAEnforcementLicensed(),
			provisioning: false, // temporarily disabled until this feature is ready for release
			advancedExecutionFilters: this.license.isAdvancedExecutionFiltersEnabled(),
			variables: this.license.isVariablesEnabled(),
			sourceControl: this.license.isSourceControlLicensed(),
			externalSecrets: this.license.isExternalSecretsEnabled(),
			showNonProdBanner: this.license.isLicensed(LICENSE_FEATURES.SHOW_NON_PROD_BANNER),
			debugInEditor: this.license.isDebugInEditorLicensed(),
			binaryDataS3: isS3Available && isS3Selected && isS3Licensed,
			workerView: this.license.isWorkerViewLicensed(),
			advancedPermissions: this.license.isAdvancedPermissionsLicensed(),

			workflowDiffs: this.licenseState.isWorkflowDiffsLicensed(),
			namedVersions: this.license.isLicensed(LICENSE_FEATURES.NAMED_VERSIONS),
			customRoles: this.licenseState.isCustomRolesLicensed(),
			personalSpacePolicy: this.licenseState.isPersonalSpacePolicyLicensed(),
			dataRedaction: this.licenseState.isDataRedactionLicensed(),
		});

		if (this.license.isLdapEnabled()) {
			Object.assign(this.settings.sso.ldap, {
				loginLabel: this.globalConfig.sso.ldap.loginLabel,
				loginEnabled: this.globalConfig.sso.ldap.loginEnabled,
			});
		}

		if (this.license.isSamlEnabled()) {
			Object.assign(this.settings.sso.saml, {
				loginLabel: getSamlLoginLabel(),
				loginEnabled: this.globalConfig.sso.saml.loginEnabled,
			});
		}

		if (this.licenseState.isOidcLicensed()) {
			Object.assign(this.settings.sso.oidc, {
				loginEnabled: this.globalConfig.sso.oidc.loginEnabled,
			});
		}

		if (this.license.isVariablesEnabled()) {
			this.settings.variables.limit = this.license.getVariablesLimit();
		}

		if (this.communityPackagesService) {
			this.settings.missingPackages = this.communityPackagesService.hasMissingPackages;
		}

		if (isAiAssistantEnabled) {
			this.settings.aiAssistant.enabled = isAiAssistantEnabled;
			this.settings.aiAssistant.setup =
				!!this.globalConfig.aiAssistant.baseUrl || !!process.env.N8N_AI_ANTHROPIC_KEY;
		}

		if (isAskAiEnabled) {
			this.settings.askAi.enabled = isAskAiEnabled;
		}

		if (isAiCreditsEnabled) {
			this.settings.aiCredits.enabled = isAiCreditsEnabled;
			this.settings.aiCredits.credits = this.license.getAiCredits();
			this.settings.aiCredits.setup = !!this.globalConfig.aiAssistant.baseUrl;
		}

		const isAiGatewayEnabled =
			this.licenseState.isAiGatewayLicensed() && !!this.globalConfig.aiAssistant.baseUrl;
		if (isAiGatewayEnabled) {
			this.settings.aiGateway = {
				enabled: true,
				budget: this.license.getValue(LICENSE_QUOTAS.AI_GATEWAY_BUDGET) ?? 0,
			};
		}

		if (isAiBuilderEnabled) {
			this.settings.aiBuilder.enabled = isAiBuilderEnabled;
			this.settings.aiBuilder.setup =
				!!this.globalConfig.aiAssistant.baseUrl || !!this.globalConfig.aiBuilder.apiKey;
		}

		this.settings.mfa.enabled = this.globalConfig.mfa.enabled;

		// TODO: read from settings
		this.settings.mfa.enforced = await this.mfaService.isMFAEnforced();

		this.settings.executionMode = this.globalConfig.executions.mode;

		this.settings.binaryDataMode = this.binaryDataConfig.mode;

		this.settings.enterprise.projects.team.limit = this.license.getTeamProjectLimit();

		this.settings.folders.enabled = this.license.isFoldersEnabled();

		// Refresh evaluation settings
		this.settings.evaluation.quota = this.licenseState.getMaxWorkflowsWithEvaluations();

		// Refresh environment feature flags
		this.settings.envFeatureFlags = this.collectEnvFeatureFlags();

		return this.settings;
	}

	/**
	 * Only add settings that are absolutely necessary for non-authenticated pages
	 * @returns Public settings for unauthenticated users
	 */
	async getPublicSettings(includeMfaSettings: boolean): Promise<PublicFrontendSettings> {
		// Get full settings to ensure all required properties are initialized
		const {
			defaultLocale,
			userManagement: { authenticationMethod, showSetupOnFirstLoad, smtpSetup, passwordMinLength },
			sso: { saml: ssoSaml, ldap: ssoLdap, oidc: ssoOidc },
			authCookie,
			previewMode,
			enterprise: { saml, ldap, oidc },
			mfa,
			communityNodesEnabled,
		} = await this.getSettings();
		const publicSettings: PublicFrontendSettings = {
			settingsMode: 'public',
			defaultLocale,
			userManagement: {
				authenticationMethod,
				showSetupOnFirstLoad,
				smtpSetup,
				passwordMinLength,
			},
			sso: {
				saml: {
					loginEnabled: ssoSaml.loginEnabled,
				},
				ldap: ssoLdap,
				oidc: {
					loginEnabled: ssoOidc.loginEnabled,
					loginUrl: ssoOidc.loginUrl,
				},
			},
			authCookie,
			previewMode,
			enterprise: { saml, ldap, oidc },
			communityNodesEnabled,
		};
		if (includeMfaSettings) {
			publicSettings.mfa = mfa;
		}
		return publicSettings;
	}

	getModuleSettings() {
		return Object.fromEntries(this.moduleRegistry.settings);
	}

	getNodeVersionIdentifiers(nodes: INodeTypeDescription[]): string[] {
		const identifiers = new Set<string>();

		for (const node of nodes) {
			if (!node?.name || node.version === undefined) continue;

			const versions = Array.isArray(node.version) ? node.version : [node.version];
			for (const version of versions) {
				if (version === undefined) continue;
				identifiers.add(`${node.name}@${String(version)}`);
			}
		}

		return Array.from(identifiers);
	}

	private async writeStaticJSON(
		name: string,
		data: Array<INodeTypeBaseDescription | ICredentialType | string>,
	): Promise<void> {
		const { staticCacheDir } = this.instanceSettings;
		const filePath = path.join(staticCacheDir, `types/${name}.json`);
		const stream = createWriteStream(filePath, 'utf-8');

		return await new Promise<void>((resolve, reject) => {
			stream.on('error', reject);
			stream.on('finish', resolve);

			stream.write('[\n');
			data.forEach((entry, index) => {
				stream.write(JSON.stringify(entry));
				if (index !== data.length - 1) stream.write(',');
				stream.write('\n');
			});
			stream.write(']\n');
			stream.end();
		});
	}

	private overwriteCredentialsProperties() {
		const { credentials } = this.loadNodesAndCredentials.types;
		const credentialsOverwrites = this.credentialsOverwrites.getAll();
		const { skipTypes } = this.globalConfig.credentials.overwrite;
		for (const credential of credentials) {
			// Clear any existing overwritten properties to prevent stale data
			delete credential.__overwrittenProperties;
			delete credential.__skipManagedCreation;

			const overwrittenProperties = [];
			this.credentialTypes
				.getParentTypes(credential.name)
				.reverse()
				.map((name) => credentialsOverwrites[name])
				.forEach((overwrite) => {
					if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
				});

			if (credential.name in credentialsOverwrites) {
				overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
			}

			if (overwrittenProperties.length) {
				credential.__overwrittenProperties = uniq(overwrittenProperties);
			}

			// For skip-list types, prevent managed credential creation in the frontend
			// (overwrite is conditional on stored data; users should provide their own credentials)
			if (skipTypes.includes(credential.name)) {
				credential.__skipManagedCreation = true;
			}

			// Inject the per-instance JWKS URI as the default of any `jwksUri`
			// property on `oAuth2Api` itself or any credential that extends it
			// and explicitly re-declares the field. Inheritance is blocked by
			// `doNotInherit: true` on both `jweEnabled` and `jwksUri` so that
			// extending credentials don't silently inherit half a dependency
			// pair (which would crash `getParameterResolveOrder`); custom
			// JWE-aware OAuth2 extensions can re-declare both fields together.
			const isOAuth2Credential =
				credential.name === 'oAuth2Api' ||
				this.credentialTypes.getParentTypes(credential.name).includes('oAuth2Api');
			if (isOAuth2Credential && credential.properties) {
				const jwksUri = this.urlService.getInstanceJwksUri();
				credential.properties = credential.properties.map((property) =>
					property.name === 'jwksUri' ? { ...property, default: jwksUri } : property,
				);
			}
		}
	}
}
