import { Time } from '@n8n/constants';
import { AuthenticatedRequest, CredentialsEntity } from '@n8n/db';
import { Delete, Options, Param, Post, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di';
import { Request, Response } from 'express';
import { Cipher } from 'n8n-core';
import { jsonParse } from 'n8n-workflow';

import { CredentialsFinderService } from '@/credentials/credentials-finder.service';
import { EnterpriseCredentialsService } from '@/credentials/credentials.service.ee';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { EventService } from '@/events/event.service';
import { CreateCsrfStateData, OauthService } from '@/oauth/oauth.service';

import { DynamicCredentialResolverRepository } from './database/repositories/credential-resolver.repository';
import { DynamicCredentialsConfig } from './dynamic-credentials.config';
import { CredentialConnectionStatusService, DynamicCredentialResolverRegistry } from './services';
import { DynamicCredentialCorsService } from './services/dynamic-credential-cors.service';
import { DynamicCredentialWebService } from './services/dynamic-credential-web.service';
import { getDynamicCredentialMiddlewares } from './utils';

const dynamicCredentialsConfig = Container.get(DynamicCredentialsConfig);

@RestController('/credentials')
export class DynamicCredentialsController {
	constructor(
		private readonly enterpriseCredentialsService: EnterpriseCredentialsService,
		private readonly oauthService: OauthService,
		private readonly resolverRepository: DynamicCredentialResolverRepository,
		private readonly resolverRegistry: DynamicCredentialResolverRegistry,
		private readonly cipher: Cipher,
		private readonly dynamicCredentialCorsService: DynamicCredentialCorsService,
		private readonly dynamicCredentialWebService: DynamicCredentialWebService,
		private readonly credentialsFinderService: CredentialsFinderService,
		private readonly credentialConnectionStatusService: CredentialConnectionStatusService,
		private readonly eventService: EventService,
	) {}

	private async findCredentialToUse(credentialId: string): Promise<CredentialsEntity> {
		const credential = await this.enterpriseCredentialsService.getOne(credentialId);

		if (!credential) {
			throw new NotFoundError('Credential not found');
		}

		if (
			!credential.type.toLowerCase().includes('oauth2') &&
			!credential.type.toLowerCase().includes('oauth1')
		) {
			throw new BadRequestError('Credential type not supported');
		}
		return credential;
	}

	private async getResolverInstance(resolverId: string | undefined) {
		if (!resolverId) {
			throw new BadRequestError('Missing resolverId query parameter');
		}

		const resolverEntity = await this.resolverRepository.findOneBy({
			id: resolverId,
		});

		if (!resolverEntity) {
			throw new NotFoundError('Resolver not found');
		}

		// Get resolver instance from registry
		const resolver = this.resolverRegistry.getResolverByTypename(resolverEntity.type);

		if (!resolver) {
			throw new NotFoundError('Resolver type not found');
		}
		return { resolver, resolverEntity };
	}

	/**
	 * OPTIONS /credentials/:id/revoke
	 *
	 * Handles CORS preflight requests
	 */
	@Options('/:id/revoke', { skipAuth: true })
	handlePreflightCredentialRevoke(req: Request, res: Response): void {
		this.dynamicCredentialCorsService.preflightHandler(req, res, ['delete', 'options']);
	}

	@Delete('/:id/revoke', {
		allowUnauthenticated: true,
		middlewares: getDynamicCredentialMiddlewares(),
		ipRateLimit: {
			limit: dynamicCredentialsConfig.rateLimitPerMinute,
			windowMs: 1 * Time.minutes.toMilliseconds,
		},
	})
	async revokeCredential(req: Request, res: Response): Promise<void> {
		this.dynamicCredentialCorsService.applyCorsHeadersIfEnabled(req, res, ['delete', 'options']);
		const credentialContext = this.dynamicCredentialWebService.getCredentialContextFromRequest(req);
		const credential = await this.findCredentialToUse(req.params.id);

		const resolverId = req.query.resolverId as string | undefined;
		const { resolver, resolverEntity } = await this.getResolverInstance(resolverId);

		if (resolver.deleteSecret) {
			// Decrypt and parse resolver configuration
			const decryptedConfig = await this.cipher.decryptV2(resolverEntity.config);
			const resolverConfig = jsonParse<Record<string, unknown>>(decryptedConfig);

			await resolver.deleteSecret(credential.id, credentialContext, {
				configuration: resolverConfig,
				resolverId: resolverEntity.id,
				resolverName: resolverEntity.type,
			});
		}

		res.status(204).send(); // 204 No Content indicates successful deletion
	}

	/**
	 * OPTIONS /credentials/:id/authorize
	 *
	 * Handles CORS preflight requests
	 */
	@Options('/:id/authorize', { skipAuth: true })
	handlePreflightCredentialAuthorize(req: Request, res: Response): void {
		this.dynamicCredentialCorsService.preflightHandler(req, res, ['post', 'options']);
	}

	@Post('/:id/authorize', {
		allowUnauthenticated: true,
		middlewares: getDynamicCredentialMiddlewares(),
		ipRateLimit: {
			limit: dynamicCredentialsConfig.rateLimitAuthorizePerMinute,
			windowMs: 1 * Time.minutes.toMilliseconds,
		},
	})
	async authorizeCredential(req: Request, res: Response): Promise<string> {
		this.dynamicCredentialCorsService.applyCorsHeadersIfEnabled(req, res, ['post', 'options']);
		const credentialContext = this.dynamicCredentialWebService.getCredentialContextFromRequest(req);
		const credential = await this.findCredentialToUse(req.params.id);

		const resolverId = req.query.resolverId as string | undefined;
		const { resolver, resolverEntity } = await this.getResolverInstance(resolverId);

		if (resolver.validateIdentity) {
			// Decrypt and parse resolver configuration
			const decryptedConfig = await this.cipher.decryptV2(resolverEntity.config);
			const resolverConfig = jsonParse<Record<string, unknown>>(decryptedConfig);

			await resolver.validateIdentity(credentialContext, {
				resolverId: resolverEntity.id,
				resolverName: resolverEntity.type,
				configuration: resolverConfig,
			});
		}

		const callerData: [CredentialsEntity, CreateCsrfStateData] = [
			credential,
			{
				cid: credential.id,
				origin: 'dynamic-credential',
				authorizationHeader: req.headers.authorization || `Bearer ${credentialContext.identity}`,
				authMetadata: credentialContext.metadata,
				credentialResolverId: req.query.resolverId,
			},
		];

		if (credential.type.toLowerCase().includes('oauth2')) {
			return await this.oauthService.generateAOauth2AuthUri(...callerData);
		}

		if (credential.type.toLowerCase().includes('oauth1')) {
			return await this.oauthService.generateAOauth1AuthUri(...callerData);
		}

		throw new BadRequestError('Credential type not supported');
	}

	/**
	 * DELETE /credentials/:credentialId/my-connection
	 *
	 * Deletes the running user's per-user connection row(s) for the given
	 * credential. Users can only disconnect their own connection — the absence
	 * of a `:userId` route segment is intentional.
	 *
	 * Not gated by `credential:read`: a user who has since lost access to the
	 * project (or to the credential) must still be able to clear their own
	 * stored OAuth tokens. The delete is keyed on the caller's userId server
	 * side, so the worst a user can do is clear their own row.
	 */
	@Delete('/:credentialId/my-connection')
	async deleteMyConnection(
		req: AuthenticatedRequest,
		res: Response,
		@Param('credentialId') credentialId: string,
	): Promise<void> {
		const credential = await this.credentialsFinderService.findCredentialById(credentialId);

		if (!credential) {
			throw new NotFoundError('Credential not found');
		}

		const affected = await this.credentialConnectionStatusService.deleteMyConnection(
			req.user.id,
			credentialId,
		);

		if (affected === 0) {
			throw new NotFoundError('No connection to disconnect');
		}

		this.eventService.emit('credentials-user-disconnected', {
			user: req.user,
			credentialId,
			credentialType: credential.type,
		});

		res.status(204).send();
	}
}
