import { Container, Service } from '@n8n/di';
import type { Scope } from '@n8n/permissions';
import type { FindManyOptions, SelectQueryBuilder } from '@n8n/typeorm';
import { DataSource, In, Like, Not, QueryFailedError } from '@n8n/typeorm';

import { CredentialsEntity, type User } from '../entities';
import { BaseRepository } from './base-repository';
import {
	addCredentialDependencyExistsFilter,
	type CredentialDependencyFilter,
} from './credential-dependency.repository';
import { InstanceCredentialAssignmentRepository } from './instance-credential-assignment.repository';
import { SharedCredentialsRepository } from './shared-credentials.repository';
import type { ICredentialsDb, ListQuery } from '../entities/types-db';
import type { OperationContext } from '../services/transaction';
import { TransactionRunner } from '../services/transaction';
import { parseListQuerySortBy } from '../utils/list-query-sort';

const SORTABLE_COLUMNS = new Set(['id', 'name', 'createdAt', 'updatedAt']);

export type CredentialSharingRelation =
	| 'shared'
	| 'shared.project'
	| 'shared.project.projectRelations';

const DEFAULT_CREDENTIAL_RELATIONS: CredentialSharingRelation[] = [
	'shared',
	'shared.project',
	'shared.project.projectRelations',
];

type CredentialsListQueryOptions = ListQuery.Options & {
	includeData?: boolean;
	user?: User;
	relations?: CredentialSharingRelation[];
};

@Service()
export class CredentialsRepository extends BaseRepository<CredentialsEntity> {
	constructor(
		dataSource: DataSource,
		private readonly instanceCredentialAssignmentRepository: InstanceCredentialAssignmentRepository,
		transactionRunner: TransactionRunner,
	) {
		super(CredentialsEntity, dataSource.manager, transactionRunner);
	}

	async findStartingWith(credentialName: string) {
		return await this.find({
			select: ['name'],
			where: { name: Like(`${credentialName}%`), usageScope: 'project' },
		});
	}

	async findNonProjectCredentialsByIds(ids: string[]): Promise<CredentialsEntity[]> {
		return await this.find({
			where: { id: In(ids), usageScope: Not('project') },
			select: ['id'],
		});
	}

	async findDanglingProjectCredentials(): Promise<CredentialsEntity[]> {
		return await this.createQueryBuilder('credentials')
			.leftJoinAndSelect('credentials.shared', 'shared')
			.where('shared.credentialsId is null')
			.andWhere('credentials.usageScope = :usageScope', { usageScope: 'project' })
			.getMany();
	}

	async findInstanceCredentialById(
		credentialId: string,
		ctx: OperationContext,
	): Promise<CredentialsEntity | null> {
		return await this.managerFor(ctx).findOneBy(CredentialsEntity, {
			id: credentialId,
			usageScope: 'instance',
		});
	}

	async saveInstanceCredential(
		credential: CredentialsEntity,
		ctx: OperationContext,
	): Promise<CredentialsEntity> {
		return await this.managerFor(ctx).save(CredentialsEntity, credential);
	}

	async updateInstanceCredential(
		credentialId: string,
		data: Pick<ICredentialsDb, 'id' | 'name' | 'type' | 'data'>,
		ctx: OperationContext,
	): Promise<CredentialsEntity | null> {
		const manager = this.managerFor(ctx);
		await manager.update(CredentialsEntity, { id: credentialId, usageScope: 'instance' }, data);
		return await manager.findOneBy(CredentialsEntity, {
			id: credentialId,
			usageScope: 'instance',
		});
	}

	async deleteInstanceCredentialIfUnassigned(
		credentialId: string,
		ctx: OperationContext = {},
	): Promise<
		| { status: 'deleted' }
		| { status: 'notFound' }
		| { status: 'assigned'; credentialUseIds: string[] }
	> {
		const manager = this.managerFor(ctx);
		const credential = await manager.findOneBy(CredentialsEntity, {
			id: credentialId,
			usageScope: 'instance',
		});
		if (!credential) return { status: 'notFound' };

		const credentialUseIds = await this.instanceCredentialAssignmentRepository.findCredentialUseIds(
			credentialId,
			ctx,
		);
		if (credentialUseIds.length > 0) return { status: 'assigned', credentialUseIds };

		try {
			const result = await manager.delete(CredentialsEntity, {
				id: credentialId,
				usageScope: 'instance',
			});
			return result.affected === 0 ? { status: 'notFound' } : { status: 'deleted' };
		} catch (error) {
			if (error instanceof QueryFailedError && !ctx.trx) {
				const concurrentCredentialUseIds =
					await this.instanceCredentialAssignmentRepository.findCredentialUseIds(credentialId, ctx);
				if (concurrentCredentialUseIds.length > 0) {
					return { status: 'assigned', credentialUseIds: concurrentCredentialUseIds };
				}
			}
			throw error;
		}
	}

	async findManyAndCount(
		listQueryOptions?: CredentialsListQueryOptions,
		credentialIds?: string[],
	): Promise<[CredentialsEntity[], number]> {
		const findManyOptions = this.toFindManyOptions(listQueryOptions);

		if (credentialIds) {
			findManyOptions.where = { ...findManyOptions.where, id: In(credentialIds) };
		}

		return await this.findAndCount(this.onlyProjectCredentials(findManyOptions));
	}

	private onlyProjectCredentials(
		findManyOptions: FindManyOptions<CredentialsEntity>,
	): FindManyOptions<CredentialsEntity> {
		findManyOptions.where = { ...findManyOptions.where, usageScope: 'project' };
		return findManyOptions;
	}

	private toFindManyOptions(listQueryOptions?: CredentialsListQueryOptions) {
		const findManyOptions: FindManyOptions<CredentialsEntity> = {};

		type Select = Array<keyof CredentialsEntity>;

		const relations = listQueryOptions?.relations ?? DEFAULT_CREDENTIAL_RELATIONS;
		const defaultSelect: Select = [
			'id',
			'name',
			'type',
			'isManaged',
			'createdAt',
			'updatedAt',
			'isGlobal',
			'isResolvable',
			'resolverId',
		];

		if (!listQueryOptions) {
			return {
				select: defaultSelect,
				relations,
			} as FindManyOptions<CredentialsEntity>;
		}

		const { filter, select, take, skip, sortBy } = listQueryOptions;

		if (typeof filter?.name === 'string' && filter?.name !== '') {
			filter.name = Like(`%${filter.name}%`);
		}

		if (typeof filter?.type === 'string' && filter?.type !== '') {
			filter.type = Like(`%${filter.type}%`);
		}

		this.handleSharedFilters(listQueryOptions);

		if (filter) findManyOptions.where = filter;
		if (select) findManyOptions.select = select;
		if (take) findManyOptions.take = take;
		if (skip) findManyOptions.skip = skip;

		if (take && select && !select?.id) {
			findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
		}

		// the credential:connect scope check needs isResolvable whenever isGlobal is selected
		if (select?.isGlobal && !select?.isResolvable) {
			findManyOptions.select = { ...findManyOptions.select, isResolvable: true };
		}

		if (!findManyOptions.select) {
			findManyOptions.select = defaultSelect;
			findManyOptions.relations = relations;
		}

		if (sortBy) {
			const { column, direction } = parseListQuerySortBy(sortBy);
			if (SORTABLE_COLUMNS.has(column)) {
				findManyOptions.order = { [column]: direction };
			}
		}

		if (listQueryOptions.includeData) {
			if (Array.isArray(findManyOptions.select)) {
				findManyOptions.select.push('data');
			} else {
				findManyOptions.select.data = true;
			}
		}

		return findManyOptions;
	}

	private handleSharedFilters(listQueryOptions?: CredentialsListQueryOptions): void {
		if (!listQueryOptions?.filter) return;

		const { filter } = listQueryOptions;

		if (typeof filter.projectId === 'string' && filter.projectId !== '') {
			filter.shared = {
				projectId: filter.projectId,
			};
			delete filter.projectId;
		}

		if (typeof filter.withRole === 'string' && filter.withRole !== '') {
			filter.shared = {
				...(filter?.shared ? filter.shared : {}),
				role: filter.withRole,
			};
			delete filter.withRole;
		}

		if (
			filter.user &&
			typeof filter.user === 'object' &&
			'id' in filter.user &&
			typeof filter.user.id === 'string'
		) {
			filter.shared = {
				...(filter?.shared ? filter.shared : {}),
				project: {
					projectRelations: {
						userId: filter.user.id,
					},
				},
			};
			delete filter.user;
		}
	}

	async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
		const findManyOptions: FindManyOptions<CredentialsEntity> = {
			where: { id: In(ids), usageScope: 'project' },
		};

		if (withSharings) {
			findManyOptions.relations = {
				shared: {
					project: true,
				},
			};
		}

		return await this.find(findManyOptions);
	}

	/**
	 * Find all global credentials, optionally narrowed by credential type.
	 */
	async findAllGlobalCredentials(
		options: {
			includeData?: boolean;
			type?: string;
			filters?: {
				dependency?: CredentialDependencyFilter;
			};
		} = {},
	): Promise<CredentialsEntity[]> {
		const { includeData = false, type, filters } = options;

		const dependencyFilter = filters?.dependency;
		if (dependencyFilter) {
			return await this.findAllGlobalCredentialsByDependencyFilter({
				dependencyFilter,
				includeData,
				type,
			});
		}

		const findManyOptions = this.toFindManyOptions({ includeData });
		findManyOptions.where = {
			...findManyOptions.where,
			isGlobal: true,
			usageScope: 'project',
			...(type ? { type: Like(`%${type}%`) } : {}),
		};
		return await this.find(findManyOptions);
	}

	private async findAllGlobalCredentialsByDependencyFilter(options: {
		dependencyFilter: CredentialDependencyFilter;
		includeData?: boolean;
		type?: string;
	}): Promise<CredentialsEntity[]> {
		const { includeData, dependencyFilter, type } = options;

		const qb = this.createQueryBuilder('credential');
		qb.where('credential.isGlobal = :isGlobal', { isGlobal: true });
		qb.andWhere('credential.usageScope = :usageScope', { usageScope: 'project' });
		if (type) {
			qb.andWhere('credential.type LIKE :type', { type: `%${type}%` });
		}
		addCredentialDependencyExistsFilter(qb, dependencyFilter);

		const defaultSelect: Array<keyof CredentialsEntity> = [
			'id',
			'name',
			'type',
			'isManaged',
			'createdAt',
			'updatedAt',
			'isGlobal',
			'isResolvable',
			'resolverId',
		];
		const selectColumns = defaultSelect.map((k) => `credential.${k}`);
		if (includeData) {
			selectColumns.push('credential.data');
		}

		qb.select(selectColumns);
		qb.leftJoinAndSelect('credential.shared', 'shared');
		qb.leftJoinAndSelect('shared.project', 'project');
		qb.leftJoinAndSelect('project.projectRelations', 'projectRelations');

		return await qb.getMany();
	}

	/**
	 * Find all credentials that are owned by a personal project.
	 */
	async findAllPersonalCredentials(): Promise<CredentialsEntity[]> {
		return await this.findBy({
			usageScope: 'project',
			shared: { project: { type: 'personal' } },
		});
	}

	/**
	 * Find all credentials that are part of any project that the workflow is
	 * part of.
	 *
	 * This is useful to for finding credentials that can be used in the
	 * workflow.
	 */
	async findAllCredentialsForWorkflow(workflowId: string): Promise<CredentialsEntity[]> {
		return await this.findBy({
			usageScope: 'project',
			shared: { project: { sharedWorkflows: { workflowId } } },
		});
	}

	/**
	 * Find all credentials that are part of that project.
	 *
	 * This is useful for finding credentials that can be used in workflows that
	 * are part of this project.
	 */
	async findAllCredentialsForProject(projectId: string): Promise<CredentialsEntity[]> {
		return await this.findBy({ usageScope: 'project', shared: { projectId } });
	}

	/**
	 * Find credentials by name and type, scoped to a specific project.
	 * Used by replaceInvalidCredentials to prevent cross-project credential resolution.
	 */
	async findByNameAndTypeInProject(
		name: string,
		type: string,
		projectId: string,
	): Promise<CredentialsEntity[]> {
		return await this.findBy({ name, type, usageScope: 'project', shared: { projectId } });
	}

	/**
	 * Get credentials with sharing permissions using a subquery instead of pre-fetched IDs.
	 * This combines the credential and sharing queries into a single database query.
	 */
	async getManyAndCountWithSharingSubquery(
		user: User,
		sharingOptions: {
			scopes?: Scope[];
			projectRoles?: string[];
			credentialRoles?: string[];
			isPersonalProject?: boolean;
			personalProjectOwnerId?: string;
			onlySharedWithMe?: boolean;
		},
		options: CredentialsListQueryOptions & {
			filters?: {
				dependency?: CredentialDependencyFilter;
			};
		} = {},
	) {
		const query = this.getManyQueryWithSharingSubquery(user, sharingOptions, options);

		// Get credentials with pagination
		const credentials = await query.getMany();

		// Build count query without pagination and relations
		const countQuery = this.getManyQueryWithSharingSubquery(user, sharingOptions, {
			...options,
			take: undefined,
			skip: undefined,
			select: undefined,
		});

		// Remove relations and select for count
		const count = await countQuery.select('credential.id').getCount();

		return { credentials, count };
	}

	/**
	 * Build a query that filters credentials based on sharing permissions using a subquery.
	 */
	private getManyQueryWithSharingSubquery(
		user: User,
		sharingOptions: {
			scopes?: Scope[];
			projectRoles?: string[];
			credentialRoles?: string[];
			isPersonalProject?: boolean;
			personalProjectOwnerId?: string;
			onlySharedWithMe?: boolean;
		},
		options: CredentialsListQueryOptions & {
			filters?: {
				dependency?: CredentialDependencyFilter;
			};
		} = {},
	): SelectQueryBuilder<CredentialsEntity> {
		const qb = this.createQueryBuilder('credential');
		qb.andWhere('credential.usageScope = :usageScope', { usageScope: 'project' });

		if (options.filters?.dependency) {
			addCredentialDependencyExistsFilter(qb, options.filters.dependency);
		}

		// Pass projectId from options to sharing options
		const projectId =
			typeof options.filter?.projectId === 'string' ? options.filter.projectId : undefined;
		const sharingOptionsWithProjectId = {
			...sharingOptions,
			projectId,
		};

		// Build the subquery for shared credential IDs
		const sharedCredentialsRepository = Container.get(SharedCredentialsRepository);
		const sharedCredentialSubquery = sharedCredentialsRepository.buildSharedCredentialIdsSubquery(
			user,
			sharingOptionsWithProjectId,
		);

		// Apply the sharing filter using the subquery
		qb.andWhere(`credential.id IN (${sharedCredentialSubquery.getQuery()})`);
		qb.setParameters(sharedCredentialSubquery.getParameters());

		// Apply other filters
		// projectId is always handled in the subquery, so skip it to avoid issues
		const filtersToApply =
			options.filter && typeof options.filter.projectId !== 'undefined'
				? { ...options.filter, projectId: undefined }
				: options.filter;

		// Apply name filter
		if (typeof filtersToApply?.name === 'string' && filtersToApply.name !== '') {
			qb.andWhere('credential.name LIKE :name', { name: `%${filtersToApply.name}%` });
		}

		// Apply type filter
		if (typeof filtersToApply?.type === 'string' && filtersToApply.type !== '') {
			qb.andWhere('credential.type LIKE :type', { type: `%${filtersToApply.type}%` });
		}

		// Apply select
		const defaultSelect: Array<keyof CredentialsEntity> = [
			'id',
			'name',
			'type',
			'isManaged',
			'createdAt',
			'updatedAt',
			'isGlobal',
			'isResolvable',
			'resolverId',
		];

		if (options.select) {
			// User provided custom select
			const selectFields = options.select;
			// Ensure id is included for pagination
			if (options.take && !selectFields.id) {
				qb.select([...Object.keys(selectFields).map((k) => `credential.${k}`), 'credential.id']);
			} else {
				qb.select(Object.keys(selectFields).map((k) => `credential.${k}`));
			}
		} else {
			// Use default select
			qb.select(defaultSelect.map((k) => `credential.${k}`));
		}

		// Add data field if requested
		if (options.includeData) {
			qb.addSelect('credential.data');
		}

		// Apply relations
		if (!options.select) {
			qb.leftJoinAndSelect('credential.shared', 'shared')
				.leftJoinAndSelect('shared.project', 'project')
				.leftJoinAndSelect('project.projectRelations', 'projectRelations');
		}

		if (options.sortBy) {
			const { column, direction } = parseListQuerySortBy(options.sortBy);
			if (SORTABLE_COLUMNS.has(column)) {
				qb.addOrderBy(`credential.${column}`, direction);
			}
		}

		// Apply pagination
		if (options.take) {
			qb.take(options.take);
		}
		if (options.skip) {
			qb.skip(options.skip);
		}

		return qb;
	}
}
