/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import Config from '@app/Config';
import type {HttpRequestConfig} from '@app/lib/HttpClient';
import HttpClient from '@app/lib/HttpClient';
import {makePersistent} from '@app/lib/MobXPersistence';
import relayClient from '@app/lib/RelayClient';
import DeveloperOptionsStore from '@app/stores/DeveloperOptionsStore';
import {API_CODE_VERSION} from '@fluxer/constants/src/AppConstants';
import {expandWireFormat} from '@fluxer/limits/src/LimitDiffer';
import type {LimitConfigSnapshot, LimitConfigWireFormat} from '@fluxer/limits/src/LimitTypes';
import {makeAutoObservable, reaction, runInAction} from 'mobx';

export interface InstanceFeatures {
	sms_mfa_enabled: boolean;
	voice_enabled: boolean;
	stripe_enabled: boolean;
	self_hosted: boolean;
	manual_review_enabled: boolean;
}

export interface InstanceSsoConfig {
	enabled: boolean;
	enforced: boolean;
	display_name: string | null;
	redirect_uri: string;
}

export interface InstanceEndpoints {
	api: string;
	api_client?: string;
	api_public?: string;
	gateway: string;
	media: string;
	static_cdn: string;
	marketing: string;
	admin: string;
	invite: string;
	gift: string;
	webapp: string;
}

export interface InstanceCaptcha {
	provider: 'hcaptcha' | 'turnstile' | 'none';
	hcaptcha_site_key: string | null;
	turnstile_site_key: string | null;
}

export interface InstancePush {
	public_vapid_key: string | null;
}

export interface InstanceAppPublic {
	sentry_dsn: string;
	sentry_proxy_path: string;
	sentry_report_host: string;
	sentry_project_id: string;
	sentry_public_key: string;
}

export type GifProvider = 'klipy' | 'tenor';

export interface InstanceDiscoveryResponse {
	api_code_version: number;
	endpoints: InstanceEndpoints;
	captcha: InstanceCaptcha;
	features: InstanceFeatures;
	gif?: {provider: GifProvider};
	sso?: InstanceSsoConfig;
	limits: LimitConfigSnapshot | LimitConfigWireFormat;
	push?: InstancePush;
	app_public: InstanceAppPublic;
}

export interface RuntimeConfigSnapshot {
	apiEndpoint: string;
	apiPublicEndpoint: string;
	gatewayEndpoint: string;
	mediaEndpoint: string;
	staticCdnEndpoint: string;
	marketingEndpoint: string;
	adminEndpoint: string;
	inviteEndpoint: string;
	giftEndpoint: string;
	webAppEndpoint: string;
	gifProvider: GifProvider;
	captchaProvider: 'hcaptcha' | 'turnstile' | 'none';
	hcaptchaSiteKey: string | null;
	turnstileSiteKey: string | null;
	apiCodeVersion: number;
	features: InstanceFeatures;
	sso: InstanceSsoConfig | null;
	publicPushVapidKey: string | null;
	limits: LimitConfigSnapshot;
	sentryDsn: string;
	sentryProxyPath: string;
	sentryReportHost: string;
	sentryProjectId: string;
	sentryPublicKey: string;
	relayDirectoryUrl: string | null;
}

type InitState = 'initializing' | 'ready' | 'error';

class RuntimeConfigStore {
	private _initState: InitState = 'initializing';
	private _initError: Error | null = null;

	private _initPromise: Promise<void>;
	private _resolveInit!: () => void;
	private _rejectInit!: (err: Error) => void;

	private _connectSeq = 0;

	apiEndpoint: string = '';
	apiPublicEndpoint: string = '';
	gatewayEndpoint: string = '';
	mediaEndpoint: string = '';
	staticCdnEndpoint: string = '';
	marketingEndpoint: string = '';
	adminEndpoint: string = '';
	inviteEndpoint: string = '';
	giftEndpoint: string = '';
	webAppEndpoint: string = '';

	gifProvider: GifProvider = 'klipy';

	captchaProvider: 'hcaptcha' | 'turnstile' | 'none' = 'none';
	hcaptchaSiteKey: string | null = null;
	turnstileSiteKey: string | null = null;

	apiCodeVersion: number = API_CODE_VERSION;
	features: InstanceFeatures = {
		sms_mfa_enabled: false,
		voice_enabled: false,
		stripe_enabled: false,
		self_hosted: false,
		manual_review_enabled: false,
	};
	sso: InstanceSsoConfig | null = null;
	publicPushVapidKey: string | null = null;
	limits: LimitConfigSnapshot = this.createEmptyLimitConfig();
	currentDefaultsHash: string | null = null;

	sentryDsn: string = '';
	sentryProxyPath: string = '/error-reporting-proxy';
	sentryReportHost: string = '';
	sentryProjectId: string = '';
	sentryPublicKey: string = '';

	relayDirectoryUrl: string | null = Config.PUBLIC_RELAY_DIRECTORY_URL;

	get relayModeEnabled(): boolean {
		return this.relayDirectoryUrl != null;
	}

	constructor() {
		this._initPromise = new Promise<void>((resolve, reject) => {
			this._resolveInit = resolve;
			this._rejectInit = reject;
		});

		makeAutoObservable(this, {}, {autoBind: true});

		this.initialize().catch(() => {});

		reaction(
			() => this.apiEndpoint,
			(endpoint) => {
				if (endpoint) {
					HttpClient.setBaseUrl(endpoint, this.apiCodeVersion);
					this.updateTargetInstanceDomain(endpoint);
				}
			},
			{fireImmediately: true},
		);

		reaction(
			() => this.relayDirectoryUrl,
			(directoryUrl) => {
				HttpClient.setRelayDirectoryUrl(directoryUrl);
				if (directoryUrl) {
					relayClient.setRelayDirectoryUrl(directoryUrl);
				}
			},
			{fireImmediately: true},
		);
	}

	private updateTargetInstanceDomain(endpoint: string): void {
		try {
			const url = new URL(endpoint);
			HttpClient.setTargetInstanceDomain(url.hostname);
		} catch {
			HttpClient.setTargetInstanceDomain(null);
		}
	}

	private async initialize(): Promise<void> {
		try {
			await makePersistent(this, 'runtimeConfig', [
				'apiEndpoint',
				'apiPublicEndpoint',
				'gatewayEndpoint',
				'mediaEndpoint',
				'staticCdnEndpoint',
				'marketingEndpoint',
				'adminEndpoint',
				'inviteEndpoint',
				'giftEndpoint',
				'webAppEndpoint',
				'gifProvider',
				'captchaProvider',
				'hcaptchaSiteKey',
				'turnstileSiteKey',
				'apiCodeVersion',
				'features',
				'sso',
				'publicPushVapidKey',
				'limits',
				'currentDefaultsHash',
				'sentryDsn',
				'sentryProxyPath',
				'sentryReportHost',
				'sentryProjectId',
				'sentryPublicKey',
				'relayDirectoryUrl',
			]);

			const bootstrapEndpoint = this.apiEndpoint || Config.PUBLIC_BOOTSTRAP_API_ENDPOINT;

			await this.connectToEndpoint(bootstrapEndpoint);

			runInAction(() => {
				this._initState = 'ready';
				this._initError = null;
			});

			this._resolveInit();
		} catch (error) {
			const err = error instanceof Error ? error : new Error(String(error));
			runInAction(() => {
				this._initState = 'error';
				this._initError = err;
			});
			this._rejectInit(err);
		}
	}

	waitForInit(): Promise<void> {
		return this._initPromise;
	}

	get initialized(): boolean {
		return this._initState === 'ready';
	}

	get initError(): Error | null {
		return this._initError;
	}

	applySnapshot(snapshot: RuntimeConfigSnapshot): void {
		this.apiEndpoint = snapshot.apiEndpoint;
		this.apiPublicEndpoint = snapshot.apiPublicEndpoint;
		this.gatewayEndpoint = snapshot.gatewayEndpoint;
		this.mediaEndpoint = snapshot.mediaEndpoint;
		this.staticCdnEndpoint = snapshot.staticCdnEndpoint;
		this.marketingEndpoint = snapshot.marketingEndpoint;
		this.adminEndpoint = snapshot.adminEndpoint;
		this.inviteEndpoint = snapshot.inviteEndpoint;
		this.giftEndpoint = snapshot.giftEndpoint;
		this.webAppEndpoint = snapshot.webAppEndpoint;

		this.gifProvider = snapshot.gifProvider;

		this.captchaProvider = snapshot.captchaProvider;
		this.hcaptchaSiteKey = snapshot.hcaptchaSiteKey;
		this.turnstileSiteKey = snapshot.turnstileSiteKey;

		this.apiCodeVersion = snapshot.apiCodeVersion;
		this.features = snapshot.features;
		this.sso = snapshot.sso;
		this.publicPushVapidKey = snapshot.publicPushVapidKey;
		this.limits = this.normalizeLimits(snapshot.limits ?? this.createEmptyLimitConfig());
		this.currentDefaultsHash = null;

		this.sentryDsn = snapshot.sentryDsn;
		this.sentryProxyPath = snapshot.sentryProxyPath;
		this.sentryReportHost = snapshot.sentryReportHost;
		this.sentryProjectId = snapshot.sentryProjectId;
		this.sentryPublicKey = snapshot.sentryPublicKey;

		this.relayDirectoryUrl = snapshot.relayDirectoryUrl;
	}
	getSnapshot(): RuntimeConfigSnapshot {
		return {
			apiEndpoint: this.apiEndpoint,
			apiPublicEndpoint: this.apiPublicEndpoint,
			gatewayEndpoint: this.gatewayEndpoint,
			mediaEndpoint: this.mediaEndpoint,
			staticCdnEndpoint: this.staticCdnEndpoint,
			marketingEndpoint: this.marketingEndpoint,
			adminEndpoint: this.adminEndpoint,
			inviteEndpoint: this.inviteEndpoint,
			giftEndpoint: this.giftEndpoint,
			webAppEndpoint: this.webAppEndpoint,
			gifProvider: this.gifProvider,
			captchaProvider: this.captchaProvider,
			hcaptchaSiteKey: this.hcaptchaSiteKey,
			turnstileSiteKey: this.turnstileSiteKey,
			apiCodeVersion: this.apiCodeVersion,
			features: {...this.features},
			sso: this.sso ? {...this.sso} : null,
			publicPushVapidKey: this.publicPushVapidKey,
			limits: this.cloneLimits(this.limits),
			sentryDsn: this.sentryDsn,
			sentryProxyPath: this.sentryProxyPath,
			sentryReportHost: this.sentryReportHost,
			sentryProjectId: this.sentryProjectId,
			sentryPublicKey: this.sentryPublicKey,
			relayDirectoryUrl: this.relayDirectoryUrl,
		};
	}
	private createEmptyLimitConfig(): LimitConfigSnapshot {
		return {
			version: 1,
			traitDefinitions: [],
			rules: [],
		};
	}

	private cloneLimits(limits: LimitConfigSnapshot): LimitConfigSnapshot {
		return JSON.parse(JSON.stringify(limits));
	}

	private normalizeLimits(limits?: LimitConfigSnapshot): LimitConfigSnapshot {
		const cloned = this.cloneLimits(limits ?? this.createEmptyLimitConfig());
		return {
			...cloned,
			traitDefinitions: cloned.traitDefinitions ?? [],
			rules: cloned.rules ?? [],
		};
	}

	private processLimitsFromApi(limits: LimitConfigSnapshot | LimitConfigWireFormat | undefined): LimitConfigSnapshot {
		if (limits && 'defaultsHash' in limits && limits.version === 2) {
			const expanded = expandWireFormat(limits);
			this.currentDefaultsHash = limits.defaultsHash;
			return this.normalizeLimits(expanded);
		}
		this.currentDefaultsHash = null;
		return this.normalizeLimits(limits as LimitConfigSnapshot | undefined);
	}

	async withSnapshot<T>(snapshot: RuntimeConfigSnapshot, fn: () => Promise<T>): Promise<T> {
		const before = this.getSnapshot();
		this.applySnapshot(snapshot);

		try {
			return await fn();
		} finally {
			this.applySnapshot(before);
		}
	}

	async resetToDefaults(): Promise<void> {
		await this.connectToEndpoint(Config.PUBLIC_BOOTSTRAP_API_ENDPOINT);
	}

	async connectToEndpoint(input: string): Promise<void> {
		const connectId = ++this._connectSeq;

		const apiEndpoint = this.normalizeEndpoint(input);
		const wellKnownUrl = this.buildWellKnownUrl(apiEndpoint);

		const request: HttpRequestConfig = {url: wellKnownUrl};

		const response = await HttpClient.get<InstanceDiscoveryResponse>(request);

		if (connectId !== this._connectSeq) {
			return;
		}

		if (!response.ok) {
			throw new Error(`Failed to reach ${wellKnownUrl} (${response.status})`);
		}

		this.updateFromInstance(response.body);
	}

	private buildWellKnownUrl(apiEndpoint: string): string {
		try {
			const url = new URL(apiEndpoint);
			url.pathname = '/.well-known/fluxer';
			return url.toString();
		} catch {
			return `${apiEndpoint.replace(/\/api\/?$/, '')}/.well-known/fluxer`;
		}
	}

	private normalizeEndpoint(input: string): string {
		const trimmed = input['trim']();
		if (!trimmed) {
			throw new Error('API endpoint is required');
		}

		let candidate = trimmed;

		if (candidate.startsWith('/')) {
			candidate = `${window.location.origin}${candidate}`;
		} else if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//.test(candidate)) {
			candidate = `https://${candidate}`;
		}

		const url = new URL(candidate);
		if (url.pathname === '' || url.pathname === '/') {
			url.pathname = '/api';
		}
		url.pathname = url.pathname.replace(/\/+$/, '');
		return url.toString();
	}

	private updateFromInstance(instance: InstanceDiscoveryResponse): void {
		this.assertCodeVersion(instance.api_code_version);

		const apiEndpoint = instance.endpoints.api_client ?? instance.endpoints.api;
		const apiPublicEndpoint = instance.endpoints.api_public ?? apiEndpoint;
		const sso = instance.sso ?? null;
		const gifProvider: GifProvider = instance.gif?.provider === 'tenor' ? 'tenor' : 'klipy';

		runInAction(() => {
			this.apiEndpoint = apiEndpoint;
			this.apiPublicEndpoint = apiPublicEndpoint;

			this.gatewayEndpoint = instance.endpoints.gateway;
			this.mediaEndpoint = instance.endpoints.media;
			this.staticCdnEndpoint = instance.endpoints.static_cdn;
			this.marketingEndpoint = instance.endpoints.marketing;
			this.adminEndpoint = instance.endpoints.admin;
			this.inviteEndpoint = instance.endpoints.invite;
			this.giftEndpoint = instance.endpoints.gift;
			this.webAppEndpoint = instance.endpoints.webapp;

			this.gifProvider = gifProvider;

			this.captchaProvider = instance.captcha.provider;
			this.hcaptchaSiteKey = instance.captcha.hcaptcha_site_key;
			this.turnstileSiteKey = instance.captcha.turnstile_site_key;

			this.apiCodeVersion = instance.api_code_version;
			this.features = instance.features;
			this.sso = sso;
			this.publicPushVapidKey = instance.push?.public_vapid_key ?? null;
			this.limits = this.processLimitsFromApi(instance.limits);

			if (instance.app_public) {
				this.sentryDsn = instance.app_public.sentry_dsn;
				this.sentryProxyPath = instance.app_public.sentry_proxy_path;
				this.sentryReportHost = instance.app_public.sentry_report_host;
				this.sentryProjectId = instance.app_public.sentry_project_id;
				this.sentryPublicKey = instance.app_public.sentry_public_key;
			}
		});
	}
	private assertCodeVersion(instanceVersion: number): void {
		if (instanceVersion < API_CODE_VERSION) {
			throw new Error(
				`Incompatible server (code version ${instanceVersion}); this client requires ${API_CODE_VERSION}.`,
			);
		}
	}

	get webAppBaseUrl(): string {
		if (this.webAppEndpoint) {
			return this.webAppEndpoint.replace(/\/$/, '');
		}

		try {
			const url = new URL(this.apiEndpoint);
			if (url.pathname.endsWith('/api')) {
				url.pathname = url.pathname.slice(0, -4) || '/';
			}
			return url.toString().replace(/\/$/, '');
		} catch {
			return this.apiEndpoint.replace(/\/api$/, '');
		}
	}

	isSelfHosted(): boolean {
		return DeveloperOptionsStore.selfHostedModeOverride || this.features.self_hosted;
	}

	get marketingHost(): string {
		try {
			return new URL(this.marketingEndpoint).host;
		} catch {
			return '';
		}
	}

	get inviteHost(): string {
		try {
			return new URL(this.inviteEndpoint).host;
		} catch {
			return '';
		}
	}

	get giftHost(): string {
		try {
			return new URL(this.giftEndpoint).host;
		} catch {
			return '';
		}
	}

	get inviteUrlBase(): string {
		try {
			const url = new URL(this.inviteEndpoint);
			const path = url.pathname !== '/' ? url.pathname.replace(/\/$/, '') : '';
			return `${url.host}${path}`;
		} catch {
			return '';
		}
	}

	get giftUrlBase(): string {
		try {
			const url = new URL(this.giftEndpoint);
			const path = url.pathname !== '/' ? url.pathname.replace(/\/$/, '') : '';
			return `${url.host}${path}`;
		} catch {
			return '';
		}
	}

	get localInstanceDomain(): string {
		try {
			const url = new URL(this.apiEndpoint);
			return url.hostname;
		} catch {
			return 'localhost';
		}
	}
}

export function describeApiEndpoint(endpoint: string): string {
	try {
		const url = new URL(endpoint);
		const path = url.pathname === '/api' ? '' : url.pathname;
		return `${url.host}${path}`;
	} catch {
		return endpoint;
	}
}

export default new RuntimeConfigStore();
