// ── Logto Management API Client ─────────────────────────────────────
// Machine-to-Machine (M2M) client for proxying account management
// operations through GearBox's backend, per D-04.
// Users never interact with Logto directly.

interface LogtoManagementConfig {
	issuer: string;
	m2mAppId: string;
	m2mAppSecret: string;
	apiResource: string;
}

interface LogtoUser {
	id: string;
	primaryEmail: string | null;
	name: string | null;
	avatar: string | null;
	createdAt: number;
}

export class LogtoManagementClient {
	private config: LogtoManagementConfig | null = null;
	private accessToken: string | null = null;
	private tokenExpiry = 0;

	constructor(config?: LogtoManagementConfig) {
		if (config) {
			this.config = config;
			return;
		}
		const issuer = process.env.OIDC_ISSUER;
		const m2mAppId = process.env.LOGTO_M2M_APP_ID;
		const m2mAppSecret = process.env.LOGTO_M2M_APP_SECRET;
		const apiResource =
			process.env.LOGTO_API_RESOURCE ?? "https://default.logto.app/api";

		if (issuer && m2mAppId && m2mAppSecret) {
			this.config = { issuer, m2mAppId, m2mAppSecret, apiResource };
		}
		// config stays null if env vars missing — methods will throw
	}

	private ensureConfigured(): LogtoManagementConfig {
		if (!this.config) {
			throw new Error(
				"Logto M2M not configured. Set LOGTO_M2M_APP_ID and LOGTO_M2M_APP_SECRET env vars.",
			);
		}
		return this.config;
	}

	/**
	 * Derive the Management API base URL from the OIDC issuer.
	 * Strip /oidc suffix if present, to get the Logto base URL.
	 */
	private getBaseUrl(): string {
		const config = this.ensureConfigured();
		const issuer = config.issuer.replace(/\/oidc\/?$/, "");
		return issuer;
	}

	/**
	 * Get a cached M2M access token, refreshing if expired.
	 * Per T-28-01: never log the token or secret.
	 * Per T-28-02: cache with TTL (expiry minus 60s buffer).
	 */
	async getAccessToken(): Promise<string> {
		const config = this.ensureConfigured();

		// Return cached token if still valid (with 60s buffer)
		if (this.accessToken && Date.now() < this.tokenExpiry - 60_000) {
			return this.accessToken;
		}

		const tokenUrl = `${this.getBaseUrl()}/oidc/token`;
		const credentials = Buffer.from(
			`${config.m2mAppId}:${config.m2mAppSecret}`,
		).toString("base64");

		const res = await fetch(tokenUrl, {
			method: "POST",
			headers: {
				"Content-Type": "application/x-www-form-urlencoded",
				Authorization: `Basic ${credentials}`,
			},
			body: new URLSearchParams({
				grant_type: "client_credentials",
				resource: config.apiResource,
				scope: "all",
			}).toString(),
		});

		if (!res.ok) {
			const errorBody = await res.text();
			console.error(
				`[Logto M2M] Token request failed: HTTP ${res.status}`,
				`\n  URL: ${tokenUrl}`,
				`\n  Resource: ${config.apiResource}`,
				`\n  App ID: ${config.m2mAppId.slice(0, 8)}...`,
				`\n  Response: ${errorBody}`,
			);
			throw new Error(
				`Logto M2M token request failed: HTTP ${res.status} — ${errorBody}`,
			);
		}

		const data = (await res.json()) as {
			access_token: string;
			expires_in: number;
		};
		this.accessToken = data.access_token;
		this.tokenExpiry = Date.now() + data.expires_in * 1000;

		return this.accessToken;
	}

	private async apiRequest(
		method: string,
		path: string,
		body?: unknown,
	): Promise<Response> {
		const token = await this.getAccessToken();
		const url = `${this.getBaseUrl()}${path}`;

		const headers: Record<string, string> = {
			Authorization: `Bearer ${token}`,
		};
		if (body !== undefined) {
			headers["Content-Type"] = "application/json";
		}

		return fetch(url, {
			method,
			headers,
			body: body !== undefined ? JSON.stringify(body) : undefined,
		});
	}

	async getUser(logtoSub: string): Promise<LogtoUser> {
		const res = await this.apiRequest("GET", `/api/users/${logtoSub}`);
		if (!res.ok) {
			throw new Error(`Failed to get Logto user: HTTP ${res.status}`);
		}
		return res.json() as Promise<LogtoUser>;
	}

	/**
	 * Verify a user's current password.
	 * Returns true if password is correct, false otherwise.
	 */
	async verifyPassword(logtoSub: string, password: string): Promise<boolean> {
		const res = await this.apiRequest(
			"POST",
			`/api/users/${logtoSub}/password/verify`,
			{ password },
		);
		if (res.status === 204 || res.status === 200) return true;
		if (res.status === 422 || res.status === 400) return false;
		throw new Error(`Logto verifyPassword unexpected: HTTP ${res.status}`);
	}

	/**
	 * Set a new password for the user.
	 */
	async updatePassword(logtoSub: string, password: string): Promise<void> {
		const res = await this.apiRequest(
			"PATCH",
			`/api/users/${logtoSub}/password`,
			{ password },
		);
		if (!res.ok) {
			throw new Error(`Failed to update password: HTTP ${res.status}`);
		}
	}

	/**
	 * Check if user has a password set (social-only accounts may not).
	 */
	async hasPassword(logtoSub: string): Promise<boolean> {
		const res = await this.apiRequest(
			"GET",
			`/api/users/${logtoSub}/has-password`,
		);
		if (!res.ok) {
			throw new Error(`Failed to check password status: HTTP ${res.status}`);
		}
		const data = (await res.json()) as { hasPassword: boolean };
		return data.hasPassword;
	}

	/**
	 * Update the user's primary email on Logto.
	 */
	async updateEmail(logtoSub: string, email: string): Promise<void> {
		const res = await this.apiRequest("PATCH", `/api/users/${logtoSub}`, {
			primaryEmail: email,
		});
		if (!res.ok) {
			throw new Error(`Failed to update email: HTTP ${res.status}`);
		}
	}

	/**
	 * Delete the user from Logto entirely.
	 */
	async deleteUser(logtoSub: string): Promise<void> {
		const res = await this.apiRequest("DELETE", `/api/users/${logtoSub}`);
		if (!res.ok) {
			throw new Error(`Failed to delete Logto user: HTTP ${res.status}`);
		}
	}
}

export const logtoClient = new LogtoManagementClient();
