import { Service } from '@n8n/di';
import {
	hasGlobalScope,
	PROJECT_OWNER_ROLE_SLUG,
	type Scope,
	type WorkflowSharingRole,
} from '@n8n/permissions';
import { DataSource, In, Not } from '@n8n/typeorm';
import type {
	EntityManager,
	FindManyOptions,
	FindOptionsWhere,
	SelectQueryBuilder,
} from '@n8n/typeorm';

import { BaseRepository } from './base-repository';
import type { User } from '../entities';
import { Project, ProjectRelation, SharedWorkflow } from '../entities';
import { type OperationContext, TransactionRunner } from '../services/transaction';
import { chunkIds } from '../utils/chunk-ids';

@Service()
export class SharedWorkflowRepository extends BaseRepository<SharedWorkflow> {
	constructor(dataSource: DataSource, transactionRunner: TransactionRunner) {
		super(SharedWorkflow, dataSource.manager, transactionRunner);
	}

	/**
	 * SharedWorkflow maps workflows to projects, so user access is checked through
	 * project relations with the supplied project roles.
	 */
	async findWorkflowIdsInUserProjects(
		workflowIds: string[],
		userId: string,
		projectRoleSlugs: string[],
	): Promise<Set<string>> {
		if (workflowIds.length === 0 || projectRoleSlugs.length === 0) return new Set();

		const found = new Set<string>();
		for (const chunk of chunkIds(workflowIds)) {
			const rows = await this.find({
				select: { workflowId: true },
				where: {
					workflowId: In(chunk),
					project: { projectRelations: { userId, role: { slug: In(projectRoleSlugs) } } },
				},
			});
			for (const row of rows) {
				found.add(row.workflowId);
			}
		}

		return found;
	}

	async findByWorkflowIds(workflowIds: string[]) {
		const rows = new Map<string, SharedWorkflow>();

		for (const chunk of chunkIds(workflowIds)) {
			const found = await this.find({
				where: {
					role: 'workflow:owner',
					workflowId: In(chunk),
				},
				relations: { project: { projectRelations: { user: true, role: true } } },
			});
			for (const row of found) rows.set(row.workflowId, row);
		}

		return [...rows.values()];
	}

	/** Owner project of each workflow, keyed by workflow id. */
	async findOwnerProjectsByWorkflowIds(workflowIds: string[]): Promise<Map<string, Project>> {
		const ownerRows: SharedWorkflow[] = [];

		for (const chunk of chunkIds(workflowIds)) {
			ownerRows.push(
				...(await this.find({
					where: { workflowId: In(chunk), role: 'workflow:owner' },
					relations: { project: true },
				})),
			);
		}

		return new Map(ownerRows.map(({ workflowId, project }) => [workflowId, project]));
	}

	async findSharingRole(
		userId: string,
		workflowId: string,
	): Promise<WorkflowSharingRole | undefined> {
		const sharing = await this.findOne({
			// NOTE: We have to select everything that is used in the `where` clause. Otherwise typeorm will create an invalid query and we get this error:
			//       QueryFailedError: SQLITE_ERROR: no such column: distinctAlias.SharedWorkflow_...
			select: {
				role: true,
				workflowId: true,
				projectId: true,
			},
			where: {
				workflowId,
				project: { projectRelations: { role: { slug: PROJECT_OWNER_ROLE_SLUG }, userId } },
			},
		});

		return sharing?.role;
	}

	async makeOwnerOfAllWorkflows(project: Project) {
		return await this.update(
			{
				projectId: Not(project.id),
				role: 'workflow:owner',
			},
			{ project },
		);
	}

	async makeOwner(workflowIds: string[], projectId: string, trx?: EntityManager) {
		trx = trx ?? this.manager;

		return await trx.upsert(
			SharedWorkflow,
			workflowIds.map(
				(workflowId) =>
					({
						workflowId,
						projectId,
						role: 'workflow:owner',
					}) as const,
			),

			['projectId', 'workflowId'],
		);
	}

	async findWithFields(
		workflowIds: string[],
		{ select }: Pick<FindManyOptions<SharedWorkflow>, 'select'>,
	) {
		return await this.find({
			where: {
				workflowId: In(workflowIds),
			},
			select,
		});
	}

	async deleteByIds(sharedWorkflowIds: string[], projectId: string, trx?: EntityManager) {
		trx = trx ?? this.manager;

		return await trx.delete(SharedWorkflow, {
			projectId,
			workflowId: In(sharedWorkflowIds),
		});
	}

	/**
	 * Find the IDs of all the projects where a workflow is accessible.
	 */
	async findProjectIds(workflowId: string) {
		const rows = await this.find({ where: { workflowId }, select: ['projectId'] });

		const projectIds = rows.reduce<string[]>((acc, row) => {
			if (row.projectId) acc.push(row.projectId);
			return acc;
		}, []);

		return [...new Set(projectIds)];
	}

	/**
	 * Find the IDs of all the projects where a workflow is shared with one of
	 * the given sharing roles.
	 */
	async findProjectIdsByRole(workflowId: string, roles: string[]) {
		const rows = await this.find({
			where: { workflowId, role: In(roles) },
			select: ['projectId'],
		});

		const projectIds = rows.reduce<string[]>((acc, row) => {
			if (row.projectId) acc.push(row.projectId);
			return acc;
		}, []);

		return [...new Set(projectIds)];
	}

	/**
	 * Pass `ctx` when calling from inside a transaction — the read then runs on that
	 * transaction's connection instead of checking out a second one, which would
	 * deadlock a single-connection pool.
	 */
	async getWorkflowOwningProject(workflowId: string, ctx: OperationContext = {}) {
		return (
			await this.managerFor(ctx).findOne(SharedWorkflow, {
				where: { workflowId, role: 'workflow:owner' },
				relations: { project: true },
			})
		)?.project;
	}

	async getRelationsByWorkflowIdsAndProjectIds(workflowIds: string[], projectIds: string[]) {
		return await this.find({
			where: {
				workflowId: In(workflowIds),
				projectId: In(projectIds),
			},
		});
	}

	async getAllRelationsForWorkflows(workflowIds: string[]) {
		return await this.find({
			where: {
				workflowId: In(workflowIds),
			},
			relations: ['project'],
		});
	}

	async getSharedPersonalWorkflowsCount(): Promise<number> {
		return await this.createQueryBuilder('sw')
			.innerJoin('sw.project', 'project')
			.where('sw.role = :role', { role: 'workflow:owner' })
			.andWhere('project.type = :type', { type: 'personal' })
			.andWhere((qb) => {
				const subQuery = qb
					.subQuery()
					.select('1')
					.from(SharedWorkflow, 'other')
					.where('other.workflowId = sw.workflowId')
					.andWhere('other.projectId != sw.projectId')
					.getQuery();
				return `EXISTS ${subQuery}`;
			})
			.getCount();
	}

	async findWorkflowWithOptions(
		workflowId: string,
		options: {
			where?: FindOptionsWhere<SharedWorkflow>;
			includeTags?: boolean;
			includeParentFolder?: boolean;
			includeActiveVersion?: boolean;
			em?: EntityManager;
		} = {},
	) {
		const {
			where = {},
			includeTags = false,
			includeParentFolder = false,
			includeActiveVersion = false,
			em = this.manager,
		} = options;

		return await em.findOne(SharedWorkflow, {
			where: {
				workflowId,
				...where,
			},
			relations: {
				workflow: {
					shared: { project: true },
					tags: includeTags,
					parentFolder: includeParentFolder,
					activeVersion: includeActiveVersion ? { workflowPublishHistory: true } : false,
				},
			},
		});
	}

	/**
	 * Build a subquery that returns workflow IDs based on sharing permissions.
	 * This replicates the logic from WorkflowSharingService but as a subquery.
	 */
	buildSharedWorkflowIdsSubquery(
		user: User,
		sharingOptions: {
			scopes?: Scope[];
			projectRoles?: string[];
			workflowRoles?: string[];
			isPersonalProject?: boolean;
			personalProjectOwnerId?: string;
			onlySharedWithMe?: boolean;
			projectId?: string;
		},
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
	): SelectQueryBuilder<any> {
		const {
			projectRoles,
			workflowRoles,
			isPersonalProject,
			personalProjectOwnerId,
			onlySharedWithMe,
			projectId,
		} = sharingOptions;

		const subquery = this.manager
			.createQueryBuilder()
			.select('sw.workflowId')
			.from(SharedWorkflow, 'sw');

		// Handle different sharing scenarios
		// Check explicit filters first (isPersonalProject, onlySharedWithMe) before falling back to user's global permissions
		if (isPersonalProject) {
			// Personal project - get owned workflows using the project owner's ID (not the requesting user's)
			const ownerUserId = personalProjectOwnerId ?? user.id;
			subquery
				.innerJoin(Project, 'p', 'sw.projectId = p.id')
				.innerJoin(ProjectRelation, 'pr', 'pr.projectId = p.id')
				.where('sw.role = :ownerRole', { ownerRole: 'workflow:owner' })
				.andWhere('pr.userId = :subqueryUserId', { subqueryUserId: ownerUserId })
				.andWhere('pr.role = :projectOwnerRole', { projectOwnerRole: PROJECT_OWNER_ROLE_SLUG });
		} else if (onlySharedWithMe) {
			// Shared with me - workflows shared (as editor) to user's personal project
			subquery
				.innerJoin(Project, 'p', 'sw.projectId = p.id')
				.innerJoin(ProjectRelation, 'pr', 'pr.projectId = p.id')
				.where('sw.role = :editorRole', { editorRole: 'workflow:editor' })
				.andWhere('pr.userId = :subqueryUserId', { subqueryUserId: user.id })
				.andWhere('pr.role = :projectOwnerRole', { projectOwnerRole: PROJECT_OWNER_ROLE_SLUG });
		} else if (!hasGlobalScope(user, 'workflow:read')) {
			// Standard sharing based on roles (global-scope users need no additional filtering)
			if (!workflowRoles || !projectRoles) {
				throw new Error('workflowRoles and projectRoles are required when not using special cases');
			}

			subquery
				.innerJoin(Project, 'p', 'sw.projectId = p.id')
				.innerJoin(ProjectRelation, 'pr', 'pr.projectId = p.id')
				.where('sw.role IN (:...workflowRoles)', { workflowRoles })
				.andWhere('pr.userId = :subqueryUserId', { subqueryUserId: user.id })
				.andWhere('pr.role IN (:...projectRoles)', { projectRoles });
		}

		// Apply project filter across all branches (except onlySharedWithMe which is personal-project scoped)
		if (!onlySharedWithMe && projectId) {
			subquery.andWhere('sw.projectId = :subqueryProjectId', { subqueryProjectId: projectId });
		}

		return subquery;
	}
}
