// services/api-helper.ts
import { request, type APIRequestContext } from '@playwright/test';
import { setTimeout as wait } from 'node:timers/promises';

import type { UserCredentials } from '../config/test-users';
import {
	INSTANCE_OWNER_CREDENTIALS,
	INSTANCE_MEMBER_CREDENTIALS,
	INSTANCE_ADMIN_CREDENTIALS,
	INSTANCE_CHAT_CREDENTIALS,
} from '../config/test-users';
import { TestError } from '../Types';
import { CredentialApiHelper } from './credential-api-helper';
import { DynamicCredentialApiHelper } from './dynamic-credential-api-helper';
import { ExternalSecretsApiHelper } from './external-secrets-api-helper';
import { McpApiHelper } from './mcp-api-helper';
import { ProjectApiHelper } from './project-api-helper';
import { PublicApiHelper } from './public-api-helper';
import { RoleApiHelper } from './role-api-helper';
import { SourceControlApiHelper } from './source-control-api-helper';
import { TagApiHelper } from './tag-api-helper';
import { UserApiHelper, type TestUser } from './user-api-helper';
import { VariablesApiHelper } from './variables-api-helper';
import { WebhookApiHelper } from './webhook-api-helper';
import { WorkflowApiHelper } from './workflow-api-helper';

export interface LoginResponseData {
	id: string;
	[key: string]: unknown;
}

export type UserRole = 'owner' | 'admin' | 'member' | 'chat';
export type TestState = 'fresh' | 'reset' | 'signin-only';

const AUTH_TAGS = {
	ADMIN: '@auth:admin',
	OWNER: '@auth:owner',
	MEMBER: '@auth:member',
	CHAT: '@auth:chat',
	NONE: '@auth:none',
} as const;

const DB_TAGS = {
	RESET: '@db:reset',
} as const;

export class ApiHelpers {
	request: APIRequestContext;
	workflows: WorkflowApiHelper;
	webhooks: WebhookApiHelper;
	mcp: McpApiHelper;
	projects: ProjectApiHelper;
	credentials: CredentialApiHelper;
	dynamicCredentials: DynamicCredentialApiHelper;
	variables: VariablesApiHelper;
	externalSecrets: ExternalSecretsApiHelper;
	users: UserApiHelper;
	tags: TagApiHelper;
	roles: RoleApiHelper;
	sourceControl: SourceControlApiHelper;

	publicApi: PublicApiHelper;

	constructor(requestContext: APIRequestContext) {
		this.request = requestContext;
		this.workflows = new WorkflowApiHelper(this);
		this.webhooks = new WebhookApiHelper(this);
		this.mcp = new McpApiHelper(this);
		this.projects = new ProjectApiHelper(this);
		this.credentials = new CredentialApiHelper(this);
		this.dynamicCredentials = new DynamicCredentialApiHelper(this);
		this.variables = new VariablesApiHelper(this);
		this.externalSecrets = new ExternalSecretsApiHelper(this);
		this.users = new UserApiHelper(this);
		this.tags = new TagApiHelper(this);
		this.roles = new RoleApiHelper(this);
		this.sourceControl = new SourceControlApiHelper(this);

		this.publicApi = new PublicApiHelper(this);
	}

	// ===== MAIN SETUP METHODS =====

	/**
	 * Setup test environment based on test tags
	 * @param tags - Array of test tags (e.g., ['@db:reset', '@auth:owner'])
	 * @param memberIndex - Which member to use (if auth role is 'member')
	 *
	 * Examples:
	 * - ['@db:reset', '@auth:owner'] = reset DB + signin as owner
	 * - ['@auth:admin'] = signin as admin (no reset)
	 * - ['@auth:none'] = no signin (unauthenticated)
	 */
	async setupFromTags(tags: string[], memberIndex: number = 0): Promise<LoginResponseData | null> {
		const shouldReset = this.shouldResetDatabase(tags);
		const role = this.getRoleFromTags(tags);

		if (shouldReset && role) {
			// Reset + signin
			await this.resetDatabase();
			return await this.signin(role, memberIndex);
		} else if (shouldReset) {
			// Reset only, manual signin required
			await this.resetDatabase();
			return null;
		} else if (role) {
			// Signin only
			return await this.signin(role, memberIndex);
		}

		// No setup required
		return null;
	}

	/**
	 * Check if database should be reset based on tags
	 */
	private shouldResetDatabase(tags: string[]): boolean {
		const lowerTags = tags.map((tag) => tag.toLowerCase());
		return lowerTags.includes(DB_TAGS.RESET.toLowerCase());
	}

	/**
	 * Setup test environment based on desired state (programmatic approach)
	 * @param state - 'fresh': new container, 'reset': reset DB + signin, 'signin-only': just signin
	 * @param role - User role to sign in as
	 * @param memberIndex - Which member to use (if role is 'member')
	 */
	async setupTest(
		state: TestState,
		role: UserRole = 'owner',
		memberIndex: number = 0,
	): Promise<LoginResponseData | null> {
		switch (state) {
			case 'fresh':
				// For fresh docker container - just reset, no signin needed yet
				await this.resetDatabase();
				return null;

			case 'reset':
				// Reset database then sign in
				await this.resetDatabase();
				return await this.signin(role, memberIndex);

			case 'signin-only':
				// Just sign in without reset
				return await this.signin(role, memberIndex);

			default:
				throw new TestError('Unknown test state');
		}
	}

	// ===== CORE METHODS =====

	async resetDatabase(): Promise<void> {
		const response = await this.request.post('/rest/e2e/reset', {
			data: {
				owner: INSTANCE_OWNER_CREDENTIALS,
				members: INSTANCE_MEMBER_CREDENTIALS,
				admin: INSTANCE_ADMIN_CREDENTIALS,
				chat: INSTANCE_CHAT_CREDENTIALS,
			},
		});

		if (!response.ok()) {
			const errorText = await response.text();
			throw new TestError(errorText);
		}
		// Adding small delay to ensure database is reset
		await wait(1000);
	}

	async signin(role: UserRole, memberIndex: number = 0): Promise<LoginResponseData> {
		const credentials = this.getCredentials(role, memberIndex);
		return await this.loginAndSetCookies(credentials);
	}

	async login(credentials: { email: string; password: string }): Promise<LoginResponseData> {
		return await this.loginAndSetCookies(credentials);
	}

	// ===== CONFIGURATION METHODS =====

	async setFeature(feature: string, enabled: boolean): Promise<void> {
		await this.request.patch('/rest/e2e/feature', {
			data: { feature: `feat:${feature}`, enabled },
		});
	}

	async setQuota(quotaName: string, value: number | string): Promise<void> {
		await this.request.patch('/rest/e2e/quota', {
			data: { feature: `quota:${quotaName}`, value },
		});
	}

	async setQueueMode(enabled: boolean): Promise<void> {
		await this.request.patch('/rest/e2e/queue-mode', {
			data: { enabled },
		});
	}

	// ===== FEATURE FLAG METHODS =====

	async setEnvFeatureFlags(flags: Record<string, string>): Promise<{
		data: {
			success: boolean;
			message: string;
			flags: Record<string, string>;
		};
	}> {
		const response = await this.request.patch('/rest/e2e/env-feature-flags', {
			data: { flags },
		});
		return await response.json();
	}

	async clearEnvFeatureFlags(): Promise<{
		data: {
			success: boolean;
			message: string;
			flags: Record<string, string>;
		};
	}> {
		const response = await this.request.patch('/rest/e2e/env-feature-flags', {
			data: { flags: {} },
		});
		return await response.json();
	}

	async getEnvFeatureFlags(): Promise<{
		data: Record<string, string>;
	}> {
		const response = await this.request.get('/rest/e2e/env-feature-flags');
		return await response.json();
	}

	// ===== CONVENIENCE METHODS =====

	async enableFeature(feature: string): Promise<void> {
		await this.setFeature(feature, true);
	}

	/**
	 * Enable all project features (sharing, folders, advancedPermissions, projectRoles)
	 * Use this in API-only tests - the n8n fixture enables these via withProjectFeatures()
	 */
	async enableProjectFeatures(): Promise<void> {
		await this.enableFeature('sharing');
		await this.enableFeature('folders');
		await this.enableFeature('advancedPermissions');
		await this.enableFeature('projectRole:admin');
		await this.enableFeature('projectRole:editor');
	}

	async disableFeature(feature: string): Promise<void> {
		await this.setFeature(feature, false);
	}

	async setMaxTeamProjectsQuota(value: number | string): Promise<void> {
		await this.setQuota('maxTeamProjects', value);
	}

	/**
	 * Create an isolated API context for a specific user.
	 * Returns an ApiHelpers instance logged in as the specified user.
	 * Use this for API-only operations without needing a browser context.
	 */
	async createApiForUser(user: Pick<TestUser, 'email' | 'password'>): Promise<ApiHelpers> {
		const userContext = await request.newContext();
		const userApi = new ApiHelpers(userContext);
		await userApi.login({ email: user.email, password: user.password });
		return userApi;
	}

	/**
	 * Check if n8n is healthy
	 * @returns True if n8n is healthy, false otherwise
	 */
	async isHealthy(probe: 'liveness' | 'readiness' = 'liveness'): Promise<boolean> {
		const url = probe === 'liveness' ? '/healthz' : '/healthz/readiness';
		const response = await this.request.get(url);
		const data = await response.json();
		return data.status === 'ok';
	}

	// ===== LOG STREAMING METHODS =====

	/**
	 * Create a syslog destination for log streaming.
	 * Requires the logStreaming feature to be enabled.
	 *
	 * @param config - Syslog destination configuration
	 * @returns Created destination data
	 */
	async createSyslogDestination(config: {
		host: string;
		port: number;
		protocol?: 'tcp' | 'udp';
		facility?: number;
		app_name?: string;
		label?: string;
		subscribedEvents?: string[];
	}): Promise<{ id: string }> {
		const response = await this.request.post('/rest/eventbus/destination', {
			data: {
				__type: '$$MessageEventBusDestinationSyslog',
				host: config.host,
				port: config.port,
				protocol: config.protocol ?? 'tcp',
				facility: config.facility ?? 16, // Local0
				app_name: config.app_name ?? 'n8n',
				label: config.label ?? 'VictoriaLogs Syslog',
				subscribedEvents: config.subscribedEvents ?? ['*'], // All events
			},
		});

		if (!response.ok()) {
			throw new TestError(
				`Failed to create syslog destination: ${response.status()} ${await response.text()}`,
			);
		}

		const result = await response.json();
		// Handle both direct response and {data: ...} wrapped response
		return result.data ?? result;
	}

	/**
	 * Delete a log streaming destination.
	 *
	 * @param id - Destination ID to delete
	 */
	async deleteLogStreamingDestination(id: string): Promise<void> {
		const response = await this.request.delete(`/rest/eventbus/destination?id=${id}`);
		if (!response.ok()) {
			throw new TestError(
				`Failed to delete log streaming destination: ${response.status()} ${await response.text()}`,
			);
		}
	}

	/**
	 * Get all log streaming destinations.
	 *
	 * @returns Array of destination configurations
	 */
	// eslint-disable-next-line @typescript-eslint/naming-convention
	async getLogStreamingDestinations(): Promise<Array<{ id: string; __type: string }>> {
		const response = await this.request.get('/rest/eventbus/destination');
		if (!response.ok()) {
			throw new TestError(
				`Failed to get log streaming destinations: ${response.status()} ${await response.text()}`,
			);
		}
		const result = await response.json();
		// Handle both direct response and {data: ...} wrapped response
		return result.data ?? result;
	}

	/**
	 * Send a test message to a log streaming destination.
	 *
	 * @param id - Destination ID to test
	 * @returns True if test was successful
	 */
	async testLogStreamingDestination(id: string): Promise<boolean> {
		const response = await this.request.get(`/rest/eventbus/testmessage?id=${id}`);
		if (!response.ok()) {
			return false;
		}
		const result = await response.json();
		// Handle both direct response and {data: ...} wrapped response
		return result.data ?? result;
	}

	/**
	 * Delete all log streaming destinations.
	 */
	async deleteAllLogStreamingDestinations(): Promise<void> {
		const destinations = await this.getLogStreamingDestinations();
		for (const destination of destinations) {
			await this.deleteLogStreamingDestination(destination.id);
		}
	}

	// ===== MCP API KEY METHODS =====

	/**
	 * Rotate the MCP API key for the authenticated user.
	 * Creates a new API key and invalidates the old one.
	 *
	 * @returns The new MCP API key data
	 */
	async rotateMcpApiKey(): Promise<{ id: string; apiKey: string; userId: string }> {
		const response = await this.request.post('/rest/mcp/api-key/rotate');

		if (!response.ok()) {
			throw new TestError(
				`Failed to rotate MCP API key: ${response.status()} ${await response.text()}`,
			);
		}

		const result = await response.json();
		return result.data ?? result;
	}

	/**
	 * Enable or disable MCP access for the instance.
	 * Uses the MCP settings endpoint to toggle access.
	 *
	 * @param enabled - Whether MCP access should be enabled
	 */
	async setMcpAccess(enabled: boolean): Promise<void> {
		const response = await this.request.patch('/rest/mcp/settings', {
			data: { mcpAccessEnabled: enabled },
		});

		if (!response.ok()) {
			throw new TestError(
				`Failed to set MCP access: ${response.status()} ${await response.text()}`,
			);
		}
	}

	// ===== PRIVATE METHODS =====

	private async loginAndSetCookies(
		credentials: Pick<UserCredentials, 'email' | 'password'>,
	): Promise<LoginResponseData> {
		const response = await this.request.post('/rest/login', {
			data: {
				emailOrLdapLoginId: credentials.email,
				password: credentials.password,
			},
			maxRetries: 3,
		});

		if (!response.ok()) {
			const errorText = await response.text();
			throw new TestError(errorText);
		}

		let responseData: unknown;
		try {
			responseData = await response.json();
		} catch (error: unknown) {
			const errorText = await response.text();
			throw new TestError(errorText);
		}

		const loginData: LoginResponseData = (responseData as { data: LoginResponseData }).data;

		if (!loginData?.id) {
			throw new TestError('Login did not return expected user data (missing user ID)');
		}

		return loginData;
	}

	private getCredentials(role: UserRole, memberIndex: number = 0): UserCredentials {
		switch (role) {
			case 'owner':
				return INSTANCE_OWNER_CREDENTIALS;
			case 'admin':
				return INSTANCE_ADMIN_CREDENTIALS;
			case 'member':
				if (!INSTANCE_MEMBER_CREDENTIALS || memberIndex >= INSTANCE_MEMBER_CREDENTIALS.length) {
					throw new TestError(`No member credentials found for index ${memberIndex}`);
				}
				return INSTANCE_MEMBER_CREDENTIALS[memberIndex];
			case 'chat':
				return INSTANCE_CHAT_CREDENTIALS;
			default:
				throw new TestError(`Unknown role: ${role as string}`);
		}
	}

	// ===== TAG PARSING METHODS =====

	/**
	 * Get the role from the tags
	 * @param tags - Array of test tags (e.g., ['@auth:owner'])
	 * @returns The role from the tags, or 'owner' if no role is found
	 */
	getRoleFromTags(tags: string[]): UserRole | null {
		const lowerTags = tags.map((tag) => tag.toLowerCase());

		if (lowerTags.includes(AUTH_TAGS.ADMIN.toLowerCase())) return 'admin';
		if (lowerTags.includes(AUTH_TAGS.OWNER.toLowerCase())) return 'owner';
		if (lowerTags.includes(AUTH_TAGS.MEMBER.toLowerCase())) return 'member';
		if (lowerTags.includes(AUTH_TAGS.CHAT.toLowerCase())) return 'chat';
		if (lowerTags.includes(AUTH_TAGS.NONE.toLowerCase())) return null;
		return 'owner';
	}
}
