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

import { Injectable } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
import { getErrorData, getErrorException, getErrorStatus, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.js';
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
import { ApiFilterMastodon } from '@/server/api/mastodon/endpoints/filter.js';
import { ApiInstanceMastodon } from '@/server/api/mastodon/endpoints/instance.js';
import { ApiStatusMastodon } from '@/server/api/mastodon/endpoints/status.js';
import { ApiNotificationsMastodon } from '@/server/api/mastodon/endpoints/notifications.js';
import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js';
import { ApiSearchMastodon } from '@/server/api/mastodon/endpoints/search.js';
import { ApiError } from '@/server/api/error.js';
import { ServerUtilityService } from '@/server/ServerUtilityService.js';
import { parseTimelineArgs, TimelineArgs, toBoolean } from './argsUtils.js';
import { convertAnnouncement, convertAttachment, MastodonConverters, convertRelationship } from './MastodonConverters.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';

@Injectable()
export class MastodonApiServerService {
	constructor(
		private readonly mastoConverters: MastodonConverters,
		private readonly logger: MastodonLogger,
		private readonly clientService: MastodonClientService,
		private readonly apiAccountMastodon: ApiAccountMastodon,
		private readonly apiAppsMastodon: ApiAppsMastodon,
		private readonly apiFilterMastodon: ApiFilterMastodon,
		private readonly apiInstanceMastodon: ApiInstanceMastodon,
		private readonly apiNotificationsMastodon: ApiNotificationsMastodon,
		private readonly apiSearchMastodon: ApiSearchMastodon,
		private readonly apiStatusMastodon: ApiStatusMastodon,
		private readonly apiTimelineMastodon: ApiTimelineMastodon,
		private readonly serverUtilityService: ServerUtilityService,
	) {}

	@bindThis
	public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
		this.serverUtilityService.addMultipartFormDataContentType(fastify);
		this.serverUtilityService.addFormUrlEncodedContentType(fastify);
		this.serverUtilityService.addCORS(fastify);
		this.serverUtilityService.addFlattenedQueryType(fastify);

		// Convert JS exceptions into error responses
		fastify.setErrorHandler((error, request, reply) => {
			const data = getErrorData(error);
			const status = getErrorStatus(error);
			const exception = getErrorException(error);

			if (exception) {
				this.logger.exception(request, exception);
			}

			return reply.code(status).send(data);
		});

		// Log error responses (including converted JSON exceptions)
		fastify.addHook('onSend', (request, reply, payload, done) => {
			if (reply.statusCode >= 400) {
				if (typeof(payload) === 'string' && String(reply.getHeader('content-type')).toLowerCase().includes('application/json')) {
					const body = JSON.parse(payload);
					const data = getErrorData(body);
					this.logger.error(request, data, reply.statusCode);
				}
			}
			done();
		});

		// External endpoints
		this.apiAccountMastodon.register(fastify);
		this.apiAppsMastodon.register(fastify);
		this.apiFilterMastodon.register(fastify);
		this.apiInstanceMastodon.register(fastify);
		this.apiNotificationsMastodon.register(fastify);
		this.apiSearchMastodon.register(fastify);
		this.apiStatusMastodon.register(fastify);
		this.apiTimelineMastodon.register(fastify);

		fastify.get('/v1/custom_emojis', async (_request, reply) => {
			const client = this.clientService.getClient(_request);
			const data = await client.getInstanceCustomEmojis();
			return reply.send(data.data);
		});

		fastify.get('/v1/announcements', async (_request, reply) => {
			const client = this.clientService.getClient(_request);
			const data = await client.getInstanceAnnouncements();
			const response = data.data.map((announcement) => convertAnnouncement(announcement));

			return reply.send(response);
		});

		fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
			if (!_request.body.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "id"' });

			const client = this.clientService.getClient(_request);
			const data = await client.dismissInstanceAnnouncement(_request.body.id);

			return reply.send(data.data);
		});

		fastify.post('/v1/media', async (_request, reply) => {
			const multipartData = _request.savedRequestFiles?.[0];
			if (!multipartData) {
				return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'No image' });
			}

			const client = this.clientService.getClient(_request);
			const data = await client.uploadMedia(multipartData);
			const response = convertAttachment(data.data as Entity.Attachment);

			return reply.send(response);
		});

		fastify.post<{ Body: { description?: string; focus?: string } }>('/v2/media', async (_request, reply) => {
			const multipartData = _request.savedRequestFiles?.[0];
			if (!multipartData) {
				return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'No image' });
			}

			const client = this.clientService.getClient(_request);
			const data = await client.uploadMedia(multipartData, _request.body);
			const response = convertAttachment(data.data as Entity.Attachment);

			return reply.send(response);
		});

		fastify.get('/v1/trends', async (_request, reply) => {
			const client = this.clientService.getClient(_request);
			const data = await client.getInstanceTrends();
			return reply.send(data.data);
		});

		fastify.get('/v1/trends/tags', async (_request, reply) => {
			const client = this.clientService.getClient(_request);
			const data = await client.getInstanceTrends();
			return reply.send(data.data);
		});

		fastify.get('/v1/trends/links', async (_request, reply) => {
			// As we do not have any system for news/links this will just return empty
			return reply.send([]);
		});

		fastify.get('/v1/preferences', async (_request, reply) => {
			const client = this.clientService.getClient(_request);
			const data = await client.getPreferences();
			return reply.send(data.data);
		});

		fastify.get('/v1/followed_tags', async (_request, reply) => {
			const client = this.clientService.getClient(_request);
			const data = await client.getFollowedTags();
			return reply.send(data.data);
		});

		fastify.get<{ Querystring: TimelineArgs }>('/v1/bookmarks', async (_request, reply) => {
			const { client, me } = await this.clientService.getAuthClient(_request);

			const data = await client.getBookmarks(parseTimelineArgs(_request.query));
			const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));

			return reply.send(response);
		});

		fastify.get<{ Querystring: TimelineArgs }>('/v1/favourites', async (_request, reply) => {
			const { client, me } = await this.clientService.getAuthClient(_request);

			if (!me) {
				throw new ApiError({
					message: 'Credential required.',
					code: 'CREDENTIAL_REQUIRED',
					id: '1384574d-a912-4b81-8601-c7b1c4085df1',
					httpStatusCode: 401,
				});
			}

			const args = {
				...parseTimelineArgs(_request.query),
				userId: me.id,
			};
			const data = await client.getFavourites(args);
			const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));

			return reply.send(response);
		});

		fastify.get<{ Querystring: TimelineArgs }>('/v1/mutes', async (_request, reply) => {
			const client = this.clientService.getClient(_request);

			const data = await client.getMutes(parseTimelineArgs(_request.query));
			const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));

			return reply.send(response);
		});

		fastify.get<{ Querystring: TimelineArgs }>('/v1/blocks', async (_request, reply) => {
			const client = this.clientService.getClient(_request);

			const data = await client.getBlocks(parseTimelineArgs(_request.query));
			const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));

			return reply.send(response);
		});

		fastify.get<{ Querystring: { limit?: string } }>('/v1/follow_requests', async (_request, reply) => {
			const client = this.clientService.getClient(_request);

			const limit = _request.query.limit ? parseInt(_request.query.limit) : 20;
			const data = await client.getFollowRequests(limit);
			const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account as Entity.Account)));

			return reply.send(response);
		});

		fastify.post<{ Params: { id?: string } }>('/v1/follow_requests/:id/authorize', async (_request, reply) => {
			if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });

			const client = this.clientService.getClient(_request);
			const data = await client.acceptFollowRequest(_request.params.id);
			const response = convertRelationship(data.data);

			return reply.send(response);
		});

		fastify.post<{ Params: { id?: string } }>('/v1/follow_requests/:id/reject', async (_request, reply) => {
			if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });

			const client = this.clientService.getClient(_request);
			const data = await client.rejectFollowRequest(_request.params.id);
			const response = convertRelationship(data.data);

			return reply.send(response);
		});
		//#endregion

		fastify.put<{
			Params: {
				id?: string,
			},
			Body: {
				file?: unknown,
				description?: string,
				focus?: string,
				is_sensitive?: string,
			},
		}>('/v1/media/:id', async (_request, reply) => {
			if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });

			const options = {
				..._request.body,
				is_sensitive: toBoolean(_request.body.is_sensitive),
			};
			const client = this.clientService.getClient(_request);
			const data = await client.updateMedia(_request.params.id, options);
			const response = convertAttachment(data.data);

			return reply.send(response);
		});

		done();
	}
}
