import { z } from 'zod';
import { isRecord } from '@n8n/utils/is-record';

import type { ChatInstance } from '../../chat-integration.service';
import { stringValue, unsupportedQuery } from '../../integration-helpers';
import type { IntegrationContextQuery } from '../../integration-tools';

const PLATFORM = 'slack';

const getUserSchema = z.object({ userId: z.string().min(1) });
const getChannelInfoSchema = z.object({ channelId: z.string().min(1) });
const searchUsersSchema = z
	.object({
		query: z.string().min(1).optional(),
		email: z.string().min(1).optional(),
		limit: z.number().int().min(1).max(50).default(10),
		cursor: z.string().min(1).optional(),
		includeBots: z.boolean().default(false),
		includeDeleted: z.boolean().default(false),
	})
	.refine((input) => input.query !== undefined || input.email !== undefined, {
		message: 'Provide query or email.',
	});
const searchChannelsSchema = z.object({
	query: z.string().min(1),
	limit: z.number().int().min(1).max(50).default(10),
	cursor: z.string().min(1).optional(),
	includeArchived: z.boolean().default(false),
});

type SearchUsersInput = z.infer<typeof searchUsersSchema>;
type SearchChannelsInput = z.infer<typeof searchChannelsSchema>;

interface SlackProfile {
	display_name?: unknown;
	email?: unknown;
	image_192?: unknown;
	real_name?: unknown;
}

interface SlackUser {
	deleted?: unknown;
	id?: unknown;
	is_bot?: unknown;
	name?: unknown;
	profile?: SlackProfile;
	real_name?: unknown;
}

interface SlackChannel {
	id?: unknown;
	is_archived?: unknown;
	is_member?: unknown;
	is_private?: unknown;
	name?: unknown;
	num_members?: unknown;
}

interface SlackUsersListResponse {
	members?: SlackUser[];
	response_metadata?: { next_cursor?: string };
}

interface SlackUserLookupResponse {
	user?: SlackUser;
}

interface SlackConversationsListResponse {
	channels?: SlackChannel[];
	response_metadata?: { next_cursor?: string };
}

interface SlackUsersApi {
	list?(options: Record<string, unknown>): Promise<SlackUsersListResponse>;
	lookupByEmail?(options: Record<string, unknown>): Promise<SlackUserLookupResponse>;
}

interface SlackConversationsApi {
	list?(options: Record<string, unknown>): Promise<SlackConversationsListResponse>;
}

interface SlackWebClient {
	conversations?: SlackConversationsApi;
	users?: SlackUsersApi;
}

interface SlackAdapter {
	client?: SlackWebClient;
	withToken?: (options: Record<string, unknown>) => Promise<Record<string, unknown>>;
}

interface SlackUserSearchResult {
	userId: string;
	userName: string;
	fullName: string;
	email?: string;
	avatarUrl?: string;
	isBot: boolean;
}

interface SlackChannelSearchResult {
	channelId: string;
	name: string;
	isArchived: boolean;
	isMember: boolean;
	isPrivate: boolean;
	memberCount?: number;
}

export async function executeSlackContextQuery(params: {
	chat: ChatInstance;
	query: IntegrationContextQuery;
	input: Record<string, unknown>;
}): Promise<unknown> {
	if (params.query === 'get_user') {
		const input = getUserSchema.parse(params.input);
		const user = await params.chat.getUser(input.userId);
		return { ok: true, user };
	}

	if (params.query === 'get_channel_info') {
		const input = getChannelInfoSchema.parse(params.input);
		const channel = params.chat.channel(normalizeChannelId(input.channelId));
		const channelInfo = await channel.fetchMetadata();
		return { ok: true, channel: channelInfo };
	}

	if (params.query === 'search_users') {
		return await searchSlackUsers(params.chat, searchUsersSchema.parse(params.input));
	}

	if (params.query === 'search_channels') {
		return await searchSlackChannels(params.chat, searchChannelsSchema.parse(params.input));
	}

	return unsupportedQuery(PLATFORM, params.query);
}

async function searchSlackUsers(chat: ChatInstance, input: SearchUsersInput): Promise<unknown> {
	const adapter = getSlackAdapter(chat);
	const usersApi = adapter?.client?.users;
	if (!adapter || !usersApi?.list) {
		return unsupportedQuery(PLATFORM, 'search_users');
	}

	const usersById = new Map<string, SlackUserSearchResult>();
	const searchTerm = normalizeSearchTerm(input.query ?? input.email ?? '');
	const email = input.email?.trim();

	// Only try users.lookupByEmail on the first page (cursor=undefined). Email
	// lookup is a single-result direct API; paging never replays it.
	if (!input.cursor && email && usersApi.lookupByEmail) {
		try {
			const response = await usersApi.lookupByEmail(await withSlackToken(adapter, { email }));
			const user = normalizeSlackUser(response.user);
			if (
				user &&
				shouldIncludeUser(response.user, input) &&
				matchesUserSearch(response.user, searchTerm, email)
			) {
				usersById.set(user.userId, user);
			}
		} catch {
			// Fall back to users.list, which can still match workspaces/scopes
			// that do not support users.lookupByEmail for this token.
		}
	}

	let cursor: string | undefined = input.cursor;
	while (usersById.size < input.limit) {
		const remainingLimit = input.limit - usersById.size;
		const response = await usersApi.list(
			await withSlackToken(adapter, {
				limit: remainingLimit,
				...(cursor ? { cursor } : {}),
			}),
		);
		for (const rawUser of response.members ?? []) {
			if (!shouldIncludeUser(rawUser, input) || !matchesUserSearch(rawUser, searchTerm, email)) {
				continue;
			}
			const user = normalizeSlackUser(rawUser);
			if (user) usersById.set(user.userId, user);
		}
		cursor = response.response_metadata?.next_cursor || undefined;
		if (!cursor) break;
	}

	const users = [...usersById.values()].slice(0, input.limit);
	return {
		ok: true,
		users,
		resultCount: users.length,
		...(cursor ? { nextCursor: cursor } : {}),
	};
}

async function searchSlackChannels(
	chat: ChatInstance,
	input: SearchChannelsInput,
): Promise<unknown> {
	const adapter = getSlackAdapter(chat);
	const conversationsApi = adapter?.client?.conversations;
	if (!adapter || !conversationsApi?.list) {
		return unsupportedQuery(PLATFORM, 'search_channels');
	}

	const channelsById = new Map<string, SlackChannelSearchResult>();
	const searchTerm = normalizeChannelSearchTerm(input.query);
	let cursor: string | undefined = input.cursor;

	while (channelsById.size < input.limit) {
		const remainingLimit = input.limit - channelsById.size;
		const response = await conversationsApi.list(
			await withSlackToken(adapter, {
				exclude_archived: !input.includeArchived,
				limit: remainingLimit,
				types: 'public_channel,private_channel',
				...(cursor ? { cursor } : {}),
			}),
		);

		for (const rawChannel of response.channels ?? []) {
			if (!input.includeArchived && rawChannel.is_archived === true) continue;
			if (!matchesChannelSearch(rawChannel, searchTerm)) continue;
			const channel = normalizeSlackChannel(rawChannel);
			if (channel) channelsById.set(channel.channelId, channel);
		}
		cursor = response.response_metadata?.next_cursor || undefined;
		if (!cursor) break;
	}

	const channels = [...channelsById.values()].slice(0, input.limit);
	return {
		ok: true,
		channels,
		resultCount: channels.length,
		...(cursor ? { nextCursor: cursor } : {}),
	};
}

export async function subscribeSlackThread(thread: {
	subscribe?: () => Promise<void>;
}): Promise<void> {
	await thread.subscribe?.();
}

function getSlackAdapter(chat: ChatInstance): SlackAdapter | undefined {
	const adapter = chat.getAdapter('slack');
	if (!isRecord(adapter)) return undefined;
	const candidate = adapter as {
		client?: unknown;
		withToken?: unknown;
	};
	if (candidate.client !== undefined && !isRecord(candidate.client)) return undefined;
	if (candidate.withToken !== undefined && typeof candidate.withToken !== 'function') {
		return undefined;
	}
	return adapter as unknown as SlackAdapter;
}

async function withSlackToken(
	adapter: SlackAdapter,
	options: Record<string, unknown>,
): Promise<Record<string, unknown>> {
	if (!adapter.withToken) return options;
	return await adapter.withToken(options);
}

function normalizeChannelId(id: string): string {
	return id.includes(':') ? id : `slack:${id}`;
}

function normalizeSlackUser(user: SlackUser | undefined): SlackUserSearchResult | undefined {
	const userId = stringValue(user?.id);
	if (!userId) return undefined;
	const profile = user?.profile;
	const userName = stringValue(user?.name) ?? userId;
	const fullName =
		stringValue(user?.real_name) ??
		stringValue(profile?.real_name) ??
		stringValue(profile?.display_name) ??
		userName;

	const email = stringValue(profile?.email);
	const avatarUrl = stringValue(profile?.image_192);

	return {
		userId,
		userName,
		fullName,
		...(email ? { email } : {}),
		...(avatarUrl ? { avatarUrl } : {}),
		isBot: user?.is_bot === true,
	};
}

function normalizeSlackChannel(
	channel: SlackChannel | undefined,
): SlackChannelSearchResult | undefined {
	if (!channel) return undefined;
	const channelId = stringValue(channel.id);
	const name = stringValue(channel.name);
	if (!channelId || !name) return undefined;

	return {
		channelId,
		name: `#${name}`,
		isArchived: channel.is_archived === true,
		isMember: channel.is_member === true,
		isPrivate: channel.is_private === true,
		...(typeof channel.num_members === 'number' ? { memberCount: channel.num_members } : {}),
	};
}

function shouldIncludeUser(user: SlackUser | undefined, input: SearchUsersInput): boolean {
	if (!input.includeDeleted && user?.deleted === true) return false;
	if (!input.includeBots && user?.is_bot === true) return false;
	return true;
}

function matchesUserSearch(
	user: SlackUser | undefined,
	searchTerm: string,
	email: string | undefined,
): boolean {
	const fields = [
		stringValue(user?.id),
		stringValue(user?.name),
		stringValue(user?.real_name),
		stringValue(user?.profile?.display_name),
		stringValue(user?.profile?.real_name),
		stringValue(user?.profile?.email),
	]
		.filter((field): field is string => field !== undefined)
		.map(normalizeSearchTerm);

	if (email) {
		const normalizedEmail = normalizeSearchTerm(email);
		if (fields.some((field) => field === normalizedEmail)) return true;
	}

	return fields.some((field) => field.includes(searchTerm));
}

function matchesChannelSearch(channel: SlackChannel | undefined, searchTerm: string): boolean {
	const fields = [stringValue(channel?.id), stringValue(channel?.name)]
		.filter((field): field is string => field !== undefined)
		.map(normalizeChannelSearchTerm);
	return fields.some((field) => field.includes(searchTerm));
}

function normalizeSearchTerm(value: string): string {
	return value.trim().toLowerCase().replace(/^[#@]/, '');
}

function normalizeChannelSearchTerm(value: string): string {
	return normalizeSearchTerm(value);
}
