import { WorkflowEntity } from '@n8n/db';
import { Service } from '@n8n/di';

import {
	WorkflowCreationService,
	type WorkflowCreateBatchContext,
} from '@/workflows/workflow-creation.service';
import { WorkflowService } from '@/workflows/workflow.service';

import { workflowReferences } from './references/workflow-references';
import { decideWorkflowConflictAction } from './workflow-conflict-policy';
import { decideWorkflowId } from './workflow-id-policy';
import {
	WorkflowImportMatchService,
	type WorkflowIdConflict,
} from './workflow-import-match.service';
import type {
	PersistedWorkflowOutcome,
	PersistedWorkflowPlanItem,
	PreparedWorkflow,
	WorkflowConflict,
	WorkflowFolderConflict,
	WorkflowImportContext,
	WorkflowImportPlan,
	WorkflowPlanItem,
	WorkflowPlannedAction,
} from './workflow-import.types';
import type {
	ImportBindingMap,
	ImportContext,
	ImportWorkflowProperties,
	PackageImportBindings,
	WorkflowIdPolicy,
} from '../../n8n-packages.types';
import { visitWorkflowCredentials } from '../credential/workflow-credential-references';

export interface WorkflowImportResult {
	outcomes: PersistedWorkflowOutcome[];
	bindings: PackageImportBindings;
}

/**
 * Imports a batch of prepared workflows in two phases:
 * {@link plan} matches each workflow against the destination project and decides what action create/update/skip
 * {@link apply} writes that plan into n8n. Publishing is a separate package-wide sweep.
 */
@Service()
export class WorkflowImporter {
	constructor(
		private readonly workflowImportMatchService: WorkflowImportMatchService,
		private readonly workflowCreationService: WorkflowCreationService,
		private readonly workflowService: WorkflowService,
	) {}

	async plan(
		context: ImportContext,
		prepared: PreparedWorkflow[],
		options: ImportWorkflowProperties,
	): Promise<WorkflowImportPlan> {
		const existingBySourceWorkflowId =
			await this.workflowImportMatchService.findBySourceWorkflowIds(
				context.projectId,
				prepared.map(({ sourceWorkflowId }) => sourceWorkflowId),
			);

		const items: WorkflowPlanItem[] = [];
		const conflicts: WorkflowConflict[] = [];
		const folderConflicts: WorkflowFolderConflict[] = [];
		// `source`-policy ids that would be freshly created — candidates for a
		// global id collision check below. Blocked creates are excluded: they
		// already report a workflow-conflict for the same workflow.
		const sourceCreateIds: string[] = [];

		for (const workflow of prepared) {
			const existing = existingBySourceWorkflowId.get(workflow.sourceWorkflowId) ?? null;
			const { action, blocked } = decideWorkflowConflictAction(
				options.workflowConflictPolicy,
				existing,
			);

			const item = toPlanItem(workflow, existing, action, options.workflowIdPolicy);
			items.push(item);

			if (item.action === 'create' && options.workflowIdPolicy === 'source' && !blocked) {
				sourceCreateIds.push(item.decidedId);
			}

			if (blocked && existing) {
				conflicts.push({
					sourceWorkflowId: workflow.sourceWorkflowId,
					existingWorkflowId: existing.id,
					name: existing.name,
				});
			}

			const targetFolderId = workflow.parentFolderId ?? context.folderId;
			if (targetFolderId && existing) {
				const existingParentFolderId = existing.parentFolder?.id ?? null;
				if (existingParentFolderId !== targetFolderId) {
					folderConflicts.push({
						sourceWorkflowId: workflow.sourceWorkflowId,
						existingWorkflowId: existing.id,
						existingParentFolderId,
						targetFolderId,
						name: existing.name,
					});
				}
			}
		}

		const idConflicts = await this.collectIdConflicts(sourceCreateIds);

		return { items, conflicts, idConflicts, folderConflicts };
	}

	/**
	 * For `source`-policy creates, a workflow id is only safe to reuse if it
	 * exists nowhere else in the instance (ids are a global primary key). Any hit
	 * — even in another project — blocks the import.
	 */
	private async collectIdConflicts(candidateIds: string[]): Promise<WorkflowIdConflict[]> {
		const existing =
			await this.workflowImportMatchService.findOwningProjectsByWorkflowId(candidateIds);

		return candidateIds.flatMap((id) => {
			const location = existing.get(id);
			if (!location) return [];
			return [
				{
					sourceWorkflowId: id,
					existingWorkflowId: id,
					existingProjectId: location.projectId,
					isArchived: location.isArchived,
					name: location.name,
				},
			];
		});
	}

	/**
	 * Writes the planned workflows. Publishing is deliberately not done here: activation rejects a
	 * parent whose sub-workflow is not yet published, so it has to wait until every workflow in the
	 * package exists — see `WorkflowPublisher.applyToPackage`.
	 */
	async apply(
		context: WorkflowImportContext,
		plan: WorkflowImportPlan,
		bindings: PackageImportBindings,
	): Promise<WorkflowImportResult> {
		const workflowBindings = new Map([
			...bindings.workflows,
			...collectPlannedWorkflowBindings(plan.items),
		]);
		const resolvedBindings: PackageImportBindings = { ...bindings, workflows: workflowBindings };
		const createItems = plan.items.filter((item) => item.action === 'create');
		const batchContext =
			createItems.length === 0
				? undefined
				: await this.workflowCreationService.prepareBatchContext(
						context.user,
						context.projectId,
						createItems.flatMap((item) => {
							const folderId = item.parentFolderId ?? context.folderId;
							return folderId ? [folderId] : [];
						}),
						createItems.map(({ entity }) => entity),
						resolvedBindings.credentials,
					);

		const outcomes: PersistedWorkflowOutcome[] = [];
		for (const item of plan.items) {
			outcomes.push(await this.applyItem(context, item, resolvedBindings, batchContext));
		}

		return { outcomes, bindings: resolvedBindings };
	}

	private async applyItem(
		context: WorkflowImportContext,
		item: WorkflowPlanItem,
		bindings: PackageImportBindings,
		batchContext: WorkflowCreateBatchContext | undefined,
	): Promise<PersistedWorkflowOutcome> {
		if (item.action === 'skip') {
			return {
				status: 'skipped',
				workflow: item.existing,
				sourceWorkflowId: item.sourceWorkflowId,
			};
		}

		return {
			status: item.action === 'create' ? 'created' : 'updated',
			workflow: await this.persistWorkflow(context, item, bindings, batchContext),
			sourceWorkflowId: item.sourceWorkflowId,
			item,
		};
	}

	private async persistWorkflow(
		context: WorkflowImportContext,
		item: PersistedWorkflowPlanItem,
		bindings: PackageImportBindings,
		batchContext: WorkflowCreateBatchContext | undefined,
	): Promise<WorkflowEntity> {
		const tagIds =
			item.tagIds && [...new Set(item.tagIds)].filter((id) => !context.droppedTagIds.has(id));

		if (item.action === 'create') {
			const entity = prepareEntityForPersist(item.entity, bindings, item.decidedId);
			return await this.workflowCreationService.createWorkflow(context.user, entity, {
				projectId: context.projectId,
				parentFolderId: item.parentFolderId ?? context.folderId ?? undefined,
				publicApi: true,
				source: 'import',
				sourceWorkflowId: item.sourceWorkflowId,
				...(batchContext ? { batchContext } : {}),
				...(tagIds !== undefined ? { tagIds } : {}),
			});
		}

		const entity = prepareEntityForPersist(item.entity, bindings);
		return await this.workflowService.update(context.user, entity, item.existing.id, {
			publicApi: true,
			source: 'import',
			...(tagIds !== undefined ? { tagIds } : {}),
		});
	}
}

function planItemTargetId(item: WorkflowPlanItem): string {
	return item.action === 'create' ? item.decidedId : item.existing.id;
}

export function collectPlannedWorkflowBindings(items: WorkflowPlanItem[]): ImportBindingMap {
	return new Map(items.map((item) => [item.sourceWorkflowId, planItemTargetId(item)]));
}

/** Clones package content for persistence without mutating the import plan. */
function prepareEntityForPersist(
	source: WorkflowEntity,
	bindings: PackageImportBindings,
	decidedId?: string,
): WorkflowEntity {
	const entity = Object.assign(new WorkflowEntity(), source, {
		nodes: structuredClone(source.nodes),
		...(source.settings ? { settings: structuredClone(source.settings) } : {}),
		...(decidedId !== undefined ? { id: decidedId } : {}),
	});
	applyCredentialBindingsInPlace(entity, bindings.credentials);
	for (const reference of workflowReferences) reference.apply(entity, bindings);
	return entity;
}

/** Mutates node credential ids on `entity` using the resolved import binding map. */
function applyCredentialBindingsInPlace(
	entity: WorkflowEntity,
	credentialBindings: ImportBindingMap,
): void {
	visitWorkflowCredentials(entity.nodes, (_credentialType, details) => {
		if (!details.id) return false;

		const targetId = credentialBindings.get(details.id);
		if (!targetId || targetId === details.id) return false;

		details.id = targetId;
		return true;
	});
}

function toPlanItem(
	prepared: PreparedWorkflow,
	existing: WorkflowEntity | null,
	action: WorkflowPlannedAction,
	idPolicy: WorkflowIdPolicy,
): WorkflowPlanItem {
	if (existing === null) {
		return {
			action: 'create',
			decidedId: decideWorkflowId(idPolicy, prepared.sourceWorkflowId),
			...prepared,
		};
	}

	switch (action) {
		case 'update':
			return { action, ...prepared, existing };
		case 'skip':
			return { action, ...prepared, existing };
		case 'create':
			// Only `fail` reaches here with a match; it records a conflict the gate rejects first.
			return {
				action,
				decidedId: decideWorkflowId(idPolicy, prepared.sourceWorkflowId),
				...prepared,
			};
	}
}
