// SPDX-License-Identifier: AGPL-3.0-or-later

import {SudoVerificationSchema} from '@fluxer/schema/src/domains/auth/AuthSchemas';
import {
	ApplicationAuthorizationIdParam,
	ApplicationIdParam,
} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
import {
	ApplicationPublicResponse,
	ApplicationResponse,
	ApplicationsMeResponse,
	AuthorizeConsentRequest,
	AuthorizeRequest,
	BotTokenResetResponse,
	IntrospectRequestForm,
	OAuth2AuthorizationsBulkRevokeRequest,
	OAuth2AuthorizationsListResponse,
	OAuth2ConsentResponse,
	OAuth2IntrospectResponse,
	OAuth2MeResponse,
	OAuth2TokenResponse,
	OAuth2UserInfoResponse,
	RevokeRequestForm,
	TokenRequest,
} from '@fluxer/schema/src/domains/oauth/OAuthSchemas';
import type {z} from 'zod';
import {Config} from '../Config';
import {DefaultUserOnly, LoginRequiredAllowSuspicious} from '../middleware/AuthMiddleware';
import {requireOAuth2BearerToken, requireOAuth2Scope} from '../middleware/OAuth2ScopeMiddleware';
import {RateLimitMiddleware} from '../middleware/RateLimitMiddleware';
import {OpenAPI} from '../middleware/ResponseTypeMiddleware';
import {SudoModeMiddleware} from '../middleware/SudoModeMiddleware';
import {RateLimitConfigs} from '../RateLimitConfig';
import type {HonoApp} from '../types/HonoEnv';
import {Validator} from '../Validator';

export function OAuth2Controller(app: HonoApp) {
	app.get(
		'/oauth2/authorize',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_AUTHORIZE),
		Validator('query', AuthorizeRequest),
		async (ctx) => {
			const q = ctx.req.valid('query');
			if (q.prompt === 'none') {
				const user = ctx.get('user');
				const errorRedirectBase = await ctx
					.get('oauth2Service')
					.resolveErrorRedirectBase(q.client_id.toString(), q.redirect_uri ?? undefined);
				const errorUrl = new URL(errorRedirectBase);
				if (q.state) errorUrl.searchParams.set('state', q.state);
				if (!user) {
					errorUrl.searchParams.set('error', 'login_required');
					return ctx.redirect(errorUrl.toString(), 302);
				}
				const requestedScopes = new Set(q.scope.split(/[\s+]+/).filter(Boolean));
				const nonBotScopes = [...requestedScopes].filter((s) => s !== 'bot');
				if (nonBotScopes.length > 0) {
					const refreshTokens = await ctx.get('oauth2TokenRepository').listRefreshTokensForUser(user.id);
					const authorizedScopes = new Set<string>();
					for (const token of refreshTokens) {
						if (token.applicationId.toString() === q.client_id.toString()) {
							for (const scope of token.scope) {
								authorizedScopes.add(scope);
							}
						}
					}
					if (!nonBotScopes.every((s) => authorizedScopes.has(s))) {
						errorUrl.searchParams.set('error', 'consent_required');
						return ctx.redirect(errorUrl.toString(), 302);
					}
				}
				try {
					const {redirectTo} = await ctx.get('oauth2Service').authorizeAndConsent({
						clientId: q.client_id.toString(),
						redirectUri: q.redirect_uri,
						scope: q.scope,
						state: q.state ?? undefined,
						codeChallenge: q.code_challenge,
						codeChallengeMethod: q.code_challenge_method as 'S256' | 'plain' | undefined,
						responseType: (q.response_type ?? 'code') as 'code',
						userId: user.id,
					});
					return ctx.redirect(redirectTo, 302);
				} catch {
					errorUrl.searchParams.set('error', 'consent_required');
					return ctx.redirect(errorUrl.toString(), 302);
				}
			}
			const consentUrl = new URL(`${Config.endpoints.webApp}/oauth2/authorize`);
			consentUrl.searchParams.set('client_id', q.client_id.toString());
			consentUrl.searchParams.set('scope', q.scope);
			if (q.response_type) consentUrl.searchParams.set('response_type', q.response_type);
			if (q.redirect_uri) consentUrl.searchParams.set('redirect_uri', q.redirect_uri);
			if (q.state) consentUrl.searchParams.set('state', q.state);
			if (q.prompt) consentUrl.searchParams.set('prompt', q.prompt);
			if (q.guild_id) consentUrl.searchParams.set('guild_id', q.guild_id.toString());
			if (q.channel_id) consentUrl.searchParams.set('channel_id', q.channel_id.toString());
			if (q.permissions) consentUrl.searchParams.set('permissions', q.permissions);
			if (q.disable_guild_select) consentUrl.searchParams.set('disable_guild_select', q.disable_guild_select);
			if (q.code_challenge) consentUrl.searchParams.set('code_challenge', q.code_challenge);
			if (q.code_challenge_method) consentUrl.searchParams.set('code_challenge_method', q.code_challenge_method);
			return ctx.redirect(consentUrl.toString(), 302);
		},
	);
	app.post(
		'/oauth2/authorize/consent',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_AUTHORIZE),
		LoginRequiredAllowSuspicious,
		DefaultUserOnly,
		Validator('json', AuthorizeConsentRequest),
		OpenAPI({
			operationId: 'provide_oauth2_consent',
			summary: 'Grant OAuth2 consent',
			responseSchema: OAuth2ConsentResponse,
			statusCode: 200,
			security: ['bearerToken', 'sessionToken'],
			tags: ['OAuth2'],
			description:
				'User grants permission for an OAuth2 application to access authorized scopes. Used in authorization code flow to complete the authorization process after user review.',
		}),
		async (ctx) => {
			const body: z.infer<typeof AuthorizeConsentRequest> = ctx.req.valid('json');
			const user = ctx.get('user');
			return ctx.json(
				await ctx.get('oauth2RequestService').authorizeConsent({
					body,
					userId: user.id,
					requestCache: ctx.get('requestCache'),
				}),
			);
		},
	);
	app.post(
		'/oauth2/token',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_TOKEN),
		Validator('form', TokenRequest),
		OpenAPI({
			operationId: 'exchange_oauth2_token',
			summary: 'Exchange OAuth2 token',
			responseSchema: OAuth2TokenResponse,
			statusCode: 200,
			security: [],
			tags: ['OAuth2'],
			description:
				'Exchanges authorization code or other grant type for access tokens. Supports authorization code, refresh token, and client credentials flows. Client authentication via authorization header or client credentials.',
		}),
		async (ctx) => {
			const form = ctx.req.valid('form');
			const result = await ctx.get('oauth2RequestService').tokenExchange({
				form,
				authorizationHeader: ctx.req.header('authorization') ?? undefined,
				logPrefix: 'OAuth2',
			});
			return ctx.json(result);
		},
	);
	app.get(
		'/oauth2/userinfo',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_INTROSPECT),
		requireOAuth2Scope('identify'),
		OpenAPI({
			operationId: 'get_oauth2_userinfo',
			summary: 'Get OAuth2 user information',
			responseSchema: OAuth2UserInfoResponse,
			statusCode: 200,
			security: ['bearerToken'],
			tags: ['OAuth2'],
			description:
				'Retrieves authenticated user information using a valid access token. Requires identify scope and supports email scope for email fields.',
		}),
		async (ctx) => {
			return ctx.json(await ctx.get('oauth2RequestService').userInfo(ctx.req.header('authorization') ?? undefined));
		},
	);
	app.post(
		'/oauth2/token/revoke',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_INTROSPECT),
		Validator('form', RevokeRequestForm),
		OpenAPI({
			operationId: 'revoke_oauth2_token',
			summary: 'Revoke OAuth2 token',
			responseSchema: null,
			statusCode: 200,
			security: [],
			tags: ['OAuth2'],
			description:
				'Revokes an access or refresh token, immediately invalidating it. Client authentication required via authorization header or client credentials. Returns 200 on success.',
		}),
		async (ctx) => {
			await ctx.get('oauth2RequestService').revoke({
				form: ctx.req.valid('form'),
				authorizationHeader: ctx.req.header('authorization') ?? undefined,
			});
			return ctx.body(null, 200);
		},
	);
	app.post(
		'/oauth2/introspect',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_INTROSPECT),
		Validator('form', IntrospectRequestForm),
		OpenAPI({
			operationId: 'introspect_oauth2_token',
			summary: 'Introspect OAuth2 token',
			responseSchema: OAuth2IntrospectResponse,
			statusCode: 200,
			security: [],
			tags: ['OAuth2'],
			description:
				'Verifies token validity and retrieves metadata. Returns active status, scope, expiration, and user information. Client authentication via authorization header or client credentials.',
		}),
		async (ctx) => {
			const result = await ctx.get('oauth2RequestService').introspect({
				form: ctx.req.valid('form'),
				authorizationHeader: ctx.req.header('authorization') ?? undefined,
			});
			return ctx.json(result);
		},
	);
	app.get(
		'/oauth2/@me',
		requireOAuth2BearerToken(),
		LoginRequiredAllowSuspicious,
		DefaultUserOnly,
		OpenAPI({
			operationId: 'get_current_user_oauth2',
			summary: 'Get current OAuth2 user',
			responseSchema: OAuth2MeResponse,
			statusCode: 200,
			security: ['bearerToken'],
			tags: ['OAuth2'],
			description:
				'Retrieves current authorization details for a valid OAuth2 bearer token. Includes OAuth2 metadata and user details when identify is present.',
		}),
		async (ctx) => {
			const response = await ctx.get('oauth2RequestService').getMe(ctx.req.header('authorization') ?? undefined);
			return ctx.json(response);
		},
	);
	app.get(
		'/oauth2/applications/:id/public',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_DEV_CLIENTS_LIST),
		Validator('param', ApplicationIdParam),
		OpenAPI({
			operationId: 'get_public_application',
			summary: 'Get public application',
			responseSchema: ApplicationPublicResponse,
			statusCode: 200,
			security: [],
			tags: ['OAuth2'],
			description:
				'Retrieves public information about an OAuth2 application without authentication. Allows clients to discover application metadata before initiating authorization.',
		}),
		async (ctx) => {
			const appId = ctx.req.valid('param').id;
			const user = ctx.get('user');
			const response = await ctx.get('oauth2RequestService').getApplicationPublic(appId, user?.id);
			return ctx.json(response);
		},
	);
	app.get(
		'/applications/@me',
		OpenAPI({
			operationId: 'get_current_user_applications',
			summary: 'List current user applications',
			responseSchema: ApplicationsMeResponse,
			statusCode: 200,
			security: [],
			tags: ['OAuth2'],
			description:
				'Lists all OAuth2 applications registered by the authenticated user. Includes application credentials and metadata. Requires valid OAuth2 access token.',
		}),
		async (ctx) => {
			const response = await ctx
				.get('oauth2RequestService')
				.getApplicationsMe(ctx.req.header('authorization') ?? undefined);
			return ctx.json(response);
		},
	);
	app.post(
		'/oauth2/applications/:id/bot/reset-token',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_DEV_CLIENT_ROTATE_SECRET),
		LoginRequiredAllowSuspicious,
		DefaultUserOnly,
		SudoModeMiddleware,
		Validator('param', ApplicationIdParam),
		Validator('json', SudoVerificationSchema),
		OpenAPI({
			operationId: 'reset_bot_token',
			summary: 'Reset bot token',
			responseSchema: BotTokenResetResponse,
			statusCode: 200,
			security: ['bearerToken', 'sessionToken'],
			tags: ['OAuth2'],
			description:
				'Rotates the bot token for an OAuth2 application. Requires sudo mode authentication. Invalidates all previously issued bot tokens. Used for security rotation and compromise mitigation.',
		}),
		async (ctx) => {
			const user = ctx.get('user');
			const body = ctx.req.valid('json');
			const applicationId = ctx.req.valid('param').id;
			const response = await ctx.get('oauth2RequestService').resetBotToken({
				ctx,
				userId: user.id,
				body,
				applicationId,
			});
			return ctx.json(response);
		},
	);
	app.post(
		'/oauth2/applications/:id/client-secret/reset',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_DEV_CLIENT_ROTATE_SECRET),
		LoginRequiredAllowSuspicious,
		DefaultUserOnly,
		SudoModeMiddleware,
		Validator('param', ApplicationIdParam),
		Validator('json', SudoVerificationSchema),
		OpenAPI({
			operationId: 'reset_client_secret',
			summary: 'Reset client secret',
			responseSchema: ApplicationResponse,
			statusCode: 200,
			security: ['bearerToken', 'sessionToken'],
			tags: ['OAuth2'],
			description:
				'Rotates the client secret for an OAuth2 application. Requires sudo mode authentication. Essential security operation for protecting client credentials. Existing access tokens remain valid.',
		}),
		async (ctx) => {
			const user = ctx.get('user');
			const body = ctx.req.valid('json');
			const applicationId = ctx.req.valid('param').id;
			const response = await ctx.get('oauth2RequestService').resetClientSecret({
				ctx,
				userId: user.id,
				body,
				applicationId,
			});
			return ctx.json(response);
		},
	);
	app.get(
		'/oauth2/@me/authorizations',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_DEV_CLIENTS_LIST),
		LoginRequiredAllowSuspicious,
		DefaultUserOnly,
		OpenAPI({
			operationId: 'list_user_oauth2_authorizations',
			summary: 'List user OAuth2 authorizations',
			responseSchema: OAuth2AuthorizationsListResponse,
			statusCode: 200,
			security: ['bearerToken', 'sessionToken'],
			tags: ['OAuth2'],
			description:
				'Lists all third-party applications the user has authorized. Shows granted scopes and authorization metadata. Allows user to review and manage delegated access.',
		}),
		async (ctx) => {
			const user = ctx.get('user');
			const authorizations = await ctx.get('oauth2RequestService').listAuthorizations(user.id);
			return ctx.json(authorizations);
		},
	);
	app.delete(
		'/oauth2/@me/authorizations/:applicationId',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_INTROSPECT),
		LoginRequiredAllowSuspicious,
		DefaultUserOnly,
		Validator('param', ApplicationAuthorizationIdParam),
		OpenAPI({
			operationId: 'delete_user_oauth2_authorization',
			summary: 'Revoke OAuth2 authorization',
			responseSchema: null,
			statusCode: 204,
			security: ['bearerToken', 'sessionToken'],
			tags: ['OAuth2'],
			description:
				'Revokes user authorization for a third-party application. Immediately invalidates all tokens issued to that application. User regains control of delegated access.',
		}),
		async (ctx) => {
			const user = ctx.get('user');
			const applicationId = ctx.req.valid('param').applicationId;
			await ctx.get('oauth2RequestService').deleteAuthorization(user.id, applicationId);
			return ctx.body(null, 204);
		},
	);
	app.post(
		'/oauth2/@me/authorizations/revoke',
		RateLimitMiddleware(RateLimitConfigs.OAUTH_INTROSPECT),
		LoginRequiredAllowSuspicious,
		DefaultUserOnly,
		Validator('json', OAuth2AuthorizationsBulkRevokeRequest),
		OpenAPI({
			operationId: 'bulk_delete_user_oauth2_authorizations',
			summary: 'Bulk revoke OAuth2 authorizations',
			responseSchema: null,
			statusCode: 204,
			security: ['bearerToken', 'sessionToken'],
			tags: ['OAuth2'],
			description:
				'Revokes user authorizations for multiple third-party applications. Immediately invalidates all tokens issued to those applications.',
		}),
		async (ctx) => {
			const user = ctx.get('user');
			const body = ctx.req.valid('json');
			await ctx.get('oauth2RequestService').deleteAuthorizations(user.id, body.application_ids);
			return ctx.body(null, 204);
		},
	);
}
