/*
 * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
 * SPDX-License-Identifier: AGPL-3.0-only
 */

import { Injectable } from '@nestjs/common';
import { isAxiosError } from 'axios';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import { ApiError } from '@/server/api/error.js';
import { getBaseUrl } from '@/server/api/mastodon/MastodonClientService.js';
import { AuthenticationError } from '@/server/api/AuthenticateService.js';
import type { FastifyRequest } from 'fastify';

@Injectable()
export class MastodonLogger {
	public readonly logger: Logger;

	constructor(
		loggerService: LoggerService,
	) {
		this.logger = loggerService.getLogger('masto-api');
	}

	public error(request: FastifyRequest, error: MastodonError, status: number): void {
		const path = getPath(request);

		if (status >= 400 && status <= 499) { // Client errors
			this.logger.debug(`Error in mastodon endpoint ${request.method} ${path}:`, error);
		} else { // Server errors
			this.logger.error(`Error in mastodon endpoint ${request.method} ${path}:`, error);
		}
	}

	public exception(request: FastifyRequest, ex: Error): void {
		const path = getPath(request);

		// Exceptions are always server errors, and should therefore always be logged.
		this.logger.error(`Exception in mastodon endpoint ${request.method} ${path}:`, ex);
	}
}

function getPath(request: FastifyRequest): string {
	try {
		return new URL(request.url, getBaseUrl(request)).pathname;
	} catch {
		return request.url;
	}
}

// TODO move elsewhere
export interface MastodonError {
	error: string;
	error_description?: string;
}

export function getErrorException(error: unknown): Error | null {
	if (!(error instanceof Error)) {
		return null;
	}

	// AxiosErrors need special decoding
	if (isAxiosError(error)) {
		// Axios errors with a response are from the remote
		if (error.response) {
			return null;
		}

		// This is the inner exception, basically
		if (error.cause && !isAxiosError(error.cause)) {
			if (!error.cause.stack) {
				error.cause.stack = error.stack;
			}

			return error.cause;
		}

		const ex = new Error();
		ex.name = error.name;
		ex.stack = error.stack;
		ex.message = error.message;
		ex.cause = error.cause;
		return ex;
	}

	// AuthenticationError is a client error
	if (error instanceof AuthenticationError) {
		return null;
	}

	return error;
}

export function getErrorData(error: unknown): MastodonError {
	// Axios wraps errors from the backend
	error = unpackAxiosError(error);

	if (!error || typeof(error) !== 'object') {
		return {
			error: 'UNKNOWN_ERROR',
			error_description: String(error),
		};
	}

	if (error instanceof ApiError) {
		return convertApiError(error);
	}

	if ('code' in error && typeof (error.code) === 'string') {
		if ('message' in error && typeof (error.message) === 'string') {
			return convertApiError(error as ApiError);
		}
	}

	if ('error' in error && typeof (error.error) === 'string') {
		if ('message' in error && typeof (error.message) === 'string') {
			return convertErrorMessageError(error as { error: string, message: string });
		}
	}

	if (error instanceof Error) {
		return convertGenericError(error);
	}

	if ('error' in error && typeof(error.error) === 'string') {
		// "error_description" is string, undefined, or not present.
		if (!('error_description' in error) || typeof(error.error_description) === 'string' || typeof(error.error_description) === 'undefined') {
			return convertMastodonError(error as MastodonError);
		}
	}

	return {
		error: 'INTERNAL_ERROR',
		error_description: 'Internal error occurred. Please contact us if the error persists.',
	};
}

function unpackAxiosError(error: unknown): unknown {
	if (isAxiosError(error)) {
		if (error.response) {
			if (error.response.data && typeof(error.response.data) === 'object') {
				if ('error' in error.response.data && error.response.data.error && typeof(error.response.data.error) === 'object') {
					return error.response.data.error;
				}

				return error.response.data;
			}

			// No data - this is a fallback to avoid leaking request/response details in the error
			return undefined;
		}

		if (error.cause && !isAxiosError(error.cause)) {
			if (!error.cause.stack) {
				error.cause.stack = error.stack;
			}

			return error.cause;
		}

		// No data - this is a fallback to avoid leaking request/response details in the error
		return String(error);
	}

	return error;
}

function convertApiError(apiError: ApiError): MastodonError {
	return {
		error: apiError.code,
		error_description: apiError.message,
	};
}

function convertErrorMessageError(error: { error: string, message: string }): MastodonError {
	return {
		error: error.error,
		error_description: error.message,
	};
}

function convertGenericError(error: Error): MastodonError {
	return {
		error: 'INTERNAL_ERROR',
		error_description: String(error),
	};
}

function convertMastodonError(error: MastodonError): MastodonError {
	return {
		error: error.error,
		error_description: error.error_description,
	};
}

export function getErrorStatus(error: unknown): number {
	if (error && typeof(error) === 'object') {
		// Axios wraps errors from the backend
		if ('response' in error && typeof (error.response) === 'object' && error.response) {
			if ('status' in error.response && typeof(error.response.status) === 'number') {
				return error.response.status;
			}
		}

		if ('httpStatusCode' in error && typeof(error.httpStatusCode) === 'number') {
			return error.httpStatusCode;
		}

		if ('statusCode' in error && typeof(error.statusCode) === 'number') {
			return error.statusCode;
		}
	}

	return 500;
}
