import { PACKAGE_VERSION, init_package_version } from "../package-version.js";
import { createLogger } from "../utils/logger/index.js";
import { init_logger } from "../utils/logger.js";
import { clearTunnelState, loadTunnelState, saveTunnelState, updateTunnelState } from "./tunnel-state.js";
import { buildMobileConnectQrPayload, resolveLanGatewayUrl } from "./tunnel-qr.js";
import { TunnelBrokerClient, resolveBrokerApiBase } from "./broker-client.js";
import { clearFrpcPathForProcess, ensureFrpcBinary, publishFrpcPathForProcess } from "./frpc-binary.js";
import { writeFrpcConfig } from "./frpc-config.js";
import { spawnFrpcProcess } from "./frpc-process.js";
import { createPairingSecret } from "./pairing.js";
import { canResumePersistedTunnel, persistedFromRegistration, registrationFromPersisted } from "./tunnel-persist.js";
import { logTunnelAudit } from "./tunnel-audit.js";
import { createHash } from "node:crypto";
import { EventEmitter } from "node:events";
//#region src/tunnel/tunnel-service.ts
init_package_version();
init_logger();
const log = createLogger("Tunnel");
function hashGatewayToken(token) {
	return createHash("sha256").update(token, "utf8").digest("hex");
}
function platformLabel() {
	return `${process.platform}-${process.arch}`;
}
let singleton = null;
function getTunnelService() {
	if (!singleton) singleton = new TunnelService();
	return singleton;
}
var TunnelService = class extends EventEmitter {
	serviceConfig = null;
	frpcHandle = null;
	heartbeatTimer = null;
	connectedSince = null;
	lastHeartbeatAt = null;
	lastError = null;
	state = "disconnected";
	reconnectAttempt = 0;
	stopping = false;
	startContext = null;
	frpcDownload = null;
	startProgress = null;
	configure(cfg) {
		this.serviceConfig = cfg;
	}
	getStatus() {
		const cfg = this.serviceConfig;
		const persisted = loadTunnelState();
		return {
			enabled: this.state === "connected" || this.state === "connecting" || this.state === "reconnecting",
			state: this.state,
			subdomain: persisted?.subdomain ?? null,
			publicUrl: persisted?.publicUrl ?? null,
			connectedSince: this.connectedSince,
			frpcPid: this.frpcHandle?.pid ?? null,
			lastHeartbeatAt: this.lastHeartbeatAt,
			lastError: this.lastError,
			frpcDownload: this.frpcDownload,
			startProgress: this.startProgress,
			config: {
				autoStart: cfg?.autoStart ?? false,
				brokerUrl: cfg?.brokerUrl ?? "https://frp.xopc.ai/api",
				transport: { tls: "broker_terminated" }
			}
		};
	}
	setStartPhase(phase, patch) {
		const prev = this.startProgress;
		const samePhase = prev?.phase === phase;
		this.startProgress = {
			phase,
			startedAt: samePhase && prev ? prev.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
			publicUrl: patch?.publicUrl !== void 0 ? patch.publicUrl : samePhase ? prev?.publicUrl ?? null : prev?.publicUrl ?? null
		};
		this.emit("tunnel:progress");
	}
	clearStartProgress() {
		if (!this.startProgress) return;
		this.startProgress = null;
		this.emit("tunnel:progress");
	}
	async buildQr(gatewayPort, gatewayHost) {
		const publicUrl = loadTunnelState()?.publicUrl ?? null;
		if (!publicUrl) return {
			qrPayload: "",
			publicUrl: null,
			lanUrl: null
		};
		const { secret, expiresAt } = createPairingSecret();
		return buildMobileConnectQrPayload({
			publicUrl,
			lanUrl: resolveLanGatewayUrl(gatewayHost, gatewayPort),
			pairingSecret: secret,
			expiresAt: expiresAt.toISOString()
		});
	}
	async start(gatewayPort, gatewayToken) {
		const cfg = this.serviceConfig;
		if (!cfg) throw new Error("Tunnel service not configured");
		this.stopping = false;
		this.startContext = {
			gatewayPort,
			gatewayToken
		};
		this.state = "connecting";
		this.lastError = null;
		this.frpcDownload = null;
		this.startProgress = null;
		this.setStartPhase("preparing_frpc");
		this.emit("tunnel:connecting");
		let frpcBin;
		try {
			frpcBin = await ensureFrpcBinary({ onProgress: (progress) => {
				this.frpcDownload = progress;
				this.setStartPhase("preparing_frpc");
			} });
			this.frpcDownload = null;
			this.emit("tunnel:progress");
		} catch (err) {
			this.frpcDownload = null;
			this.clearStartProgress();
			this.state = "error";
			this.lastError = err instanceof Error ? err.message : String(err);
			this.emit("tunnel:error", this.lastError);
			throw err;
		}
		publishFrpcPathForProcess(frpcBin);
		const persisted = loadTunnelState();
		const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));
		let registration;
		try {
			this.setStartPhase("registering");
			registration = await this.resolveRegistration(broker, cfg, gatewayToken, persisted);
		} catch (err) {
			this.clearStartProgress();
			this.state = "error";
			this.lastError = err instanceof Error ? err.message : String(err);
			this.emit("tunnel:error", this.lastError);
			throw err;
		}
		const state = persistedFromRegistration(registration);
		saveTunnelState(state);
		this.setStartPhase("registering", { publicUrl: registration.publicUrl });
		try {
			const configPath = writeFrpcConfig(registration, gatewayPort, "127.0.0.1", "http");
			this.setStartPhase("starting_frpc", { publicUrl: registration.publicUrl });
			await this.spawnAndWait(frpcBin, configPath, broker, state, registration.heartbeatIntervalMs);
		} catch (err) {
			this.clearStartProgress();
			this.state = "error";
			this.lastError = err instanceof Error ? err.message : String(err);
			this.emit("tunnel:error", this.lastError);
			throw err;
		}
		this.clearStartProgress();
		this.state = "connected";
		this.connectedSince = (/* @__PURE__ */ new Date()).toISOString();
		this.reconnectAttempt = 0;
		this.emit("tunnel:connected");
		const qr = await this.buildQr(gatewayPort, cfg.gatewayHost);
		logTunnelAudit("tunnel.start", {
			subdomain: registration.subdomain,
			publicUrl: registration.publicUrl,
			tunnelId: registration.tunnelId,
			gatewayTokenHash: hashGatewayToken(gatewayToken).slice(0, 12)
		}, "Remote access tunnel started");
		return qr;
	}
	async stop(opts) {
		const release = opts?.release === true;
		this.stopping = true;
		this.clearHeartbeat();
		if (this.frpcHandle) {
			await this.frpcHandle.kill();
			this.frpcHandle = null;
		}
		clearFrpcPathForProcess();
		let released = false;
		const persisted = loadTunnelState();
		const cfg = this.serviceConfig;
		if (release && persisted && cfg) {
			const broker = new TunnelBrokerClient(resolveBrokerApiBase(cfg.brokerUrl));
			try {
				await broker.deregister(persisted.tunnelId, persisted.tunnelToken);
				released = true;
			} catch (err) {
				const em = err instanceof Error ? err.message : String(err);
				log.warn({
					err,
					tunnelId: persisted.tunnelId,
					phase: "tunnel_release"
				}, `Tunnel deregister failed: ${em}`);
			}
			clearTunnelState();
			logTunnelAudit("tunnel.release", {
				tunnelId: persisted.tunnelId,
				subdomain: persisted.subdomain
			}, "Released tunnel registration and cleared local credentials");
		} else {
			updateTunnelState({ enabled: false });
			logTunnelAudit("tunnel.stop", {
				tunnelId: persisted?.tunnelId ?? null,
				subdomain: persisted?.subdomain ?? null
			}, "Remote access tunnel stopped (registration retained)");
		}
		this.state = "disconnected";
		this.connectedSince = null;
		this.startContext = null;
		this.clearStartProgress();
		this.emit("tunnel:disconnected");
		return { released };
	}
	async resolveRegistration(broker, cfg, gatewayToken, persisted) {
		if (canResumePersistedTunnel(persisted)) try {
			await broker.heartbeat(persisted.tunnelId, persisted.tunnelToken);
			const resumed = registrationFromPersisted(persisted);
			if (resumed) {
				log.info({
					tunnelId: persisted.tunnelId,
					subdomain: persisted.subdomain,
					phase: "tunnel_resume"
				}, "Resumed tunnel from persisted credentials");
				return resumed;
			}
		} catch (err) {
			log.info({
				err,
				tunnelId: persisted.tunnelId,
				phase: "tunnel_resume"
			}, "Persisted tunnel not resumable — registering again");
		}
		return broker.register({
			brokerUrl: resolveBrokerApiBase(cfg.brokerUrl),
			registrationSecret: cfg.registrationSecret,
			gatewayVersion: PACKAGE_VERSION,
			platform: platformLabel(),
			gatewayTokenHash: hashGatewayToken(gatewayToken),
			preferredSubdomain: persisted?.subdomain
		});
	}
	async spawnAndWait(frpcBin, configPath, broker, state, heartbeatIntervalMs) {
		if (this.frpcHandle) {
			await this.frpcHandle.kill();
			this.frpcHandle = null;
		}
		const handle = spawnFrpcProcess(frpcBin, configPath);
		this.frpcHandle = handle;
		handle.onExit((code, signal) => {
			if (this.stopping) return;
			log.warn({
				code,
				signal,
				pid: handle.pid
			}, "frpc process exited");
			this.clearHeartbeat();
			this.frpcHandle = null;
			this.scheduleReconnect(frpcBin, configPath, broker, state, heartbeatIntervalMs);
		});
		await handle.waitForLoginSuccess;
		this.startHeartbeat(broker, state, heartbeatIntervalMs);
	}
	async scheduleReconnect(frpcBin, configPath, broker, state, heartbeatIntervalMs) {
		if (this.stopping) return;
		const maxAttempts = 5;
		this.reconnectAttempt += 1;
		if (this.reconnectAttempt > maxAttempts) {
			this.state = "error";
			this.lastError = "frpc reconnect failed after maximum attempts";
			log.error({ attempts: maxAttempts }, this.lastError);
			this.emit("tunnel:error", this.lastError);
			return;
		}
		this.state = "reconnecting";
		this.setStartPhase("reconnecting_frpc", { publicUrl: state.publicUrl ?? null });
		const delayMs = Math.min(16e3, 1e3 * 2 ** (this.reconnectAttempt - 1));
		log.info({
			attempt: this.reconnectAttempt,
			delayMs
		}, "Scheduling frpc reconnect");
		await new Promise((r) => setTimeout(r, delayMs));
		if (this.stopping) return;
		try {
			await this.spawnAndWait(frpcBin, configPath, broker, state, heartbeatIntervalMs);
			this.clearStartProgress();
			this.state = "connected";
			this.reconnectAttempt = 0;
			this.emit("tunnel:connected");
		} catch (err) {
			this.lastError = err instanceof Error ? err.message : String(err);
			this.scheduleReconnect(frpcBin, configPath, broker, state, heartbeatIntervalMs);
		}
	}
	startHeartbeat(broker, state, intervalMs) {
		this.clearHeartbeat();
		const tick = async () => {
			try {
				await broker.heartbeat(state.tunnelId, state.tunnelToken);
				this.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
			} catch (err) {
				log.warn({
					err,
					tunnelId: state.tunnelId
				}, "Tunnel heartbeat failed");
				if (this.stopping) return;
				this.clearHeartbeat();
				const ctx = this.startContext;
				const cfg = this.serviceConfig;
				if (!ctx || !cfg) return;
				try {
					const registration = await broker.register({
						brokerUrl: resolveBrokerApiBase(cfg.brokerUrl),
						registrationSecret: cfg.registrationSecret,
						gatewayVersion: PACKAGE_VERSION,
						platform: platformLabel(),
						gatewayTokenHash: hashGatewayToken(ctx.gatewayToken),
						preferredSubdomain: state.subdomain
					});
					const next = persistedFromRegistration(registration);
					saveTunnelState(next);
					const frpcBin = await ensureFrpcBinary();
					const configPath = writeFrpcConfig(registration, ctx.gatewayPort, "127.0.0.1", "http");
					await this.spawnAndWait(frpcBin, configPath, broker, next, registration.heartbeatIntervalMs);
				} catch (reErr) {
					this.lastError = reErr instanceof Error ? reErr.message : String(reErr);
					this.state = "error";
					this.emit("tunnel:error", this.lastError);
				}
			}
		};
		tick();
		this.heartbeatTimer = setInterval(() => void tick(), intervalMs);
	}
	clearHeartbeat() {
		if (this.heartbeatTimer) {
			clearInterval(this.heartbeatTimer);
			this.heartbeatTimer = null;
		}
	}
};
//#endregion
export { TunnelService, getTunnelService, hashGatewayToken };

//# sourceMappingURL=tunnel-service.js.map