import { Router, Request, Response } from "express";
import uuidv4 from "uuid/v4";
import {createActivity, getActor, signAndSend} from "./federate";
import { Activity, FollowActivity, AcceptActivity, UndoActivity, CreateActivity, NoteObject, DeleteActivity } from "./activity";
import sanitizeHtml from "sanitize-html";
import Note from "../entity/Note";
import { getConnection } from "typeorm";
import Actor from "../entity/Actor";
import Article from "../entity/Article";

const domain = process.env.DOMAIN;

export default function inbox(router: Router) {
	router.post("/ap/inbox", handleInbox);
	router.post("/inbox", handleInbox);
}

function handleInbox(req: Request, res: Response) {
	const activity = req.body as Activity;
	if (activity.type === "Follow") {
		handleFollow(activity, req, res);
	} else if (activity.type === "Create") {
		handleCreate(activity, req, res);
	} else if (activity.type === "Undo") {
		handleUndo(activity, req, res);
	} else if (activity.type === "Delete") {
		handleDelete(activity, req, res);
	} else {
		res.end(); // TODO: handle this better
	}
}

async function handleFollow(activity: Activity, req: Request, res: Response) {
	if (typeof req.body.object !== "string") {
		res.end(); // TODO: handle this better
		return;
	}
	const follow = activity as FollowActivity;
	if (follow.object !== `https://${domain}/ap/actor`) {
		res.end();
		return;
	}
	const actor = await getActor(follow.actor, true); // always force re-fetch the actor on follow
	if (!actor) {
		// if the actor ceases existing between the time the Follow is sent and when receive it, ignore it and end the request
		res.end();
		return;
	}
	const acceptObject = {
		"@context": "https://www.w3.org/ns/activitystreams",
		"id": `https://${domain}/ap/${uuidv4()}`,
		"type": "Accept",
		"actor": `https://${domain}/ap/actor`,
		"object": follow
	} as AcceptActivity;
	try {
		await signAndSend(acceptObject, actor.inbox);
		// prefer shared server inbox
		const serverInbox = actor.endpoints && actor.endpoints.sharedInbox ? actor.endpoints.sharedInbox : actor.inbox;
		await getConnection().createQueryBuilder()
			.update(Actor)
			.set({
				isFollower: true,
				inbox: serverInbox
			})
			.where("id = :id", {id: actor.id})
			.execute();
		res.end();
	} catch (err) {
		res.status(500).json({ message: "Encountered error handling follow.", error: err }).end();
	}
}

async function handleCreate(activity: Activity, req: Request, res: Response) {
	const create = activity as CreateActivity;
	if (create.object.type == "Note") {
		handleCreateNote(create, req, res);
	} else {
		res.end();
	}
}

function sanitizeStatus(content: string): string {
	return sanitizeHtml(content, {
		allowedTags: ["a", "span", "p", "br", "b", "strong", "i", "em", "s", "del", "u", "code", "pre", "ul", "ol", "li", "blockquote", "img"],
		allowedAttributes: {
			"a": ["href", "data-user"],
			"img": ["src"]
		},
		transformTags: {
			"a": sanitizeHtml.simpleTransform("a", { rel: "noopener", target: "_blank" })
		}
	});
}

async function handleCreateNote(create: CreateActivity, req: Request, res: Response) {
	const noteObject = create.object as NoteObject;
	const actor = await getActor(noteObject.attributedTo); // get and cache the actor if it's not already cached

	const asPublic = "https://www.w3.org/ns/activitystreams#Public";
	const article = await getConnection().getRepository(Article).findOne({ where: { conversation: noteObject.conversation } });
	if (!article) {
		console.log(`Ignoring message not in response to an article: ${noteObject.id}`);
		res.end();
	} else if (!process.env.ACCEPT_NON_PUBLIC_NOTES && !noteObject.to.includes(asPublic) && !noteObject.cc.includes(asPublic)) {
		console.log(`Ignoring non-public post: ${noteObject.id}`);

		try {
			const note: NoteObject = {
				type: "Note",
				id: `https://${domain}/ap/object/${uuidv4()}`,
				to: [actor.id],
				cc: [],
				directMessage: true,
				attributedTo: `https://${domain}/ap/actor`,
				content: `<a href="${actor.url}" class="mention">@<span>${actor.preferredUsername}</span></a> Non-public posts are not accepted. To respond to a blog post, use either Public or Unlisted.`,
				published: new Date().toISOString(),
				inReplyTo: noteObject.id,
				conversation: noteObject.conversation || `https://${domain}/ap/conversation/${uuidv4()}`
			};
			const responseCreate = createActivity(note);
			signAndSend(responseCreate, actor.inbox);
		} catch (err) {
			console.error(`Couldn't send non-public reply Note to ${noteObject.id}`, err);
		}

		res.end();
	} else {
		const sanitizedContent = sanitizeStatus(noteObject.content);
		const note = new Note();
		note.id = noteObject.id;
		note.actor = await getConnection().getRepository(Actor).findOne(actor.id);
		note.content = sanitizedContent;
		note.attributedTo = noteObject.attributedTo;
		note.inReplyTo = noteObject.inReplyTo;
		note.conversation = noteObject.conversation;
		note.published = noteObject.published;
		try {
			await getConnection().getRepository(Note).save(note);
			res.end();
		} catch (err) {
			console.error(`Encountered error storing reply ${noteObject.id}`, err);
			res.status(500).end();
		}
	}
}

async function handleDelete(activity: Activity, req: Request, res: Response) {
	const deleteActivity = activity as DeleteActivity;
	try {
		await getConnection().getRepository(Note).createQueryBuilder()
			.delete()
			.from(Note, "note")
			.where("note.id = :id", { id: deleteActivity.object })
			.andWhere("note.\"actorId\" = :actor", { actor: deleteActivity.actor })
			.execute();
	} catch (err) {
		console.error(`Encountered error deleting ${deleteActivity.object}`, err);
	}
	res.end();
}

function handleUndo(activity: Activity, req: Request, res: Response) {
	const undo = activity as UndoActivity;
	if (undo.object.type === "Follow") {
		handleUndoFollow(undo, req, res);
	} else {
		res.end();
	}
}

async function handleUndoFollow(undo: UndoActivity, req: Request, res: Response) {
	const follow = undo.object as FollowActivity;
	if (follow.object !== `https://${domain}/ap/actor` || undo.actor !== follow.actor) {
		res.end();
		return;
	}
	try {
		await getConnection().createQueryBuilder()
			.update(Actor)
			.set({ isFollower: false })
			.where("id = :id", { id: follow.actor })
			.execute();
	} catch (err) {
		console.error(`Error handling unfollow from ${follow.actor}`, err);
	}
	res.end();
}
