import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { RouterClient } from "@orpc/server";
import type { resumePatchOperationsSchema } from "@reactive-resume/ai/tools/resume-tool-contracts";
import type router from "@reactive-resume/api/routers";
import type z from "zod";
import { Buffer } from "node:buffer";
import { resolveUserFromRequestHeaders } from "@reactive-resume/api/context";
import { createResumePdfDownloadUrl } from "@reactive-resume/api/features/resume/export";
import { env } from "@reactive-resume/env/server";
import { resumeDataSchema } from "@reactive-resume/schema/resume/data";
import { MCP_TOOL_NAME } from "./mcp-tool-names";
import { TOOL_META } from "./tool-meta";

type PatchOperation = z.infer<typeof resumePatchOperationsSchema>[number];

// ── Shared Helpers ───────────────���──────────────────────────────

function errorMessage(error: unknown): string {
	return error instanceof Error ? error.message : String(error);
}

function errorHint(error: unknown): string {
	const msg = errorMessage(error);
	const { unlockResume, listResumes, getResume } = MCP_TOOL_NAME;
	if (msg.includes("slug already exists")) return "\n\nHint: The slug is already in use. Try a different one.";
	if (msg.includes("locked")) return `\n\nHint: This resume is locked. Use \`${unlockResume}\` first.`;
	if (msg.includes("404") || msg.includes("not found"))
		return `\n\nHint: Resume not found. Use \`${listResumes}\` to find valid IDs.`;
	if (msg.includes("400"))
		return `\n\nHint: Invalid request. Check the input parameters or use \`${getResume}\` to inspect the resume structure.`;
	if (msg.includes("403"))
		return `\n\nHint: Permission denied. The resume may be locked — use \`${unlockResume}\` first.`;
	return "";
}

/**
 * Wraps an async tool handler with consistent error formatting.
 * On success, returns the handler's result directly.
 * On failure, returns `{ isError: true, content: [{ type: "text", text }] }` with actionable hints.
 */
function withErrorHandling<T>(label: string, handler: (params: T) => Promise<CallToolResult>) {
	return async (params: T): Promise<CallToolResult> => {
		try {
			return await handler(params);
		} catch (error) {
			return {
				isError: true,
				content: [{ type: "text", text: `Error ${label}: ${errorMessage(error)}${errorHint(error)}` }],
			};
		}
	};
}

function text(value: string): CallToolResult {
	return { content: [{ type: "text", text: value }] };
}

function json(value: unknown): CallToolResult {
	return text(JSON.stringify(value, null, 2));
}

function fileFromBase64(input: { fileName: string; contentType: string; dataBase64: string }): File {
	if (input.contentType !== "application/pdf") throw new Error("Application documents must be PDF files.");

	const bytes = Buffer.from(input.dataBase64, "base64");
	if (bytes.length === 0) throw new Error("Application document cannot be empty.");

	return new File([bytes], input.fileName, { type: input.contentType });
}

function coerceFollowUpAt(input: Record<string, unknown>): Record<string, unknown> {
	if (!("followUpAt" in input)) return input;

	const followUpAt = input.followUpAt;
	if (followUpAt === undefined || followUpAt === null || followUpAt instanceof Date) return input;

	return { ...input, followUpAt: new Date(String(followUpAt)) };
}

function buildResumeShareUrl(username: string, slug: string): string {
	const base = env.APP_URL.replace(/\/$/, "");
	return `${base}/${encodeURIComponent(username)}/${encodeURIComponent(slug)}`;
}

function resumeShareUrlNotes(input: { isPublic: boolean; hasPassword: boolean }): string {
	const lines = [
		"Anyone can open this link without signing in only when the resume is public (`isPublic: true`).",
		input.isPublic
			? "This resume is currently public."
			: "This resume is currently private; the URL is still your canonical share link if you make it public later.",
	];
	if (input.hasPassword)
		lines.push(
			"Password protection is enabled in the web app; visitors may need that password before content is shown.",
		);
	return lines.join("\n");
}

// ── Shared Zod Fragments ─────────────��──────────────────────────

const T = MCP_TOOL_NAME;

// ── Tool Registration ────────────────────���──────────────────────

export function registerTools(server: McpServer, client: RouterClient<typeof router>, requestHeaders: Headers) {
	// ── List Resumes ──────────────────���───────────────────────────
	server.registerTool(
		T.listResumes,
		TOOL_META[T.listResumes],
		withErrorHandling(
			"listing resumes",
			async ({ tags, sort }: { tags: string[]; sort: "lastUpdatedAt" | "createdAt" | "name" }) => {
				const resumes = await client.resume.list({ tags, sort });

				if (resumes.length === 0) return text(`No resumes found. Use \`${T.createResume}\` to create one.`);

				return text(JSON.stringify(resumes, null, 2));
			},
		),
	);

	// ── List Resume Tags ───────────────────���──────────────────────
	server.registerTool(
		T.listResumeTags,
		TOOL_META[T.listResumeTags],
		withErrorHandling("listing resume tags", async () => {
			const tags = await client.resume.tags.list();

			if (tags.length === 0) return text("No tags in use yet. Add tags when creating or updating a resume.");

			return text(JSON.stringify(tags, null, 2));
		}),
	);

	// ── Read Resume ────────────────���──────────────────────────────
	server.registerTool(
		T.getResume,
		TOOL_META[T.getResume],
		withErrorHandling("getting resume", async ({ id }: { id: string }) => {
			const resume = await client.resume.getById({ id });

			return text(JSON.stringify(resume.data, null, 2));
		}),
	);

	// ── Get Resume Analysis ────────────────────���──────────────────
	server.registerTool(
		T.getResumeAnalysis,
		TOOL_META[T.getResumeAnalysis],
		withErrorHandling("getting resume analysis", async ({ id }: { id: string }) => {
			const analysis = await client.resume.analysis.getById({ id });

			if (!analysis) return text("No saved analysis for this resume yet.");

			return text(JSON.stringify(analysis, null, 2));
		}),
	);

	// ── Download Resume PDF ────────��──────────────────────────────
	server.registerTool(
		T.downloadResumePdf,
		TOOL_META[T.downloadResumePdf],
		withErrorHandling("creating PDF download URL", async ({ id }: { id: string }) => {
			const resume = await client.resume.getById({ id });
			const user = await resolveUserFromRequestHeaders(requestHeaders);
			if (!user) throw new Error("Unauthorized");

			const signedUrl = createResumePdfDownloadUrl({ resumeId: id, userId: user.id });

			return text(
				JSON.stringify(
					{
						resumeId: id,
						name: resume.name,
						downloadUrl: signedUrl.url,
						expiresAt: signedUrl.expiresAt,
						expiresInSeconds: signedUrl.expiresInSeconds,
						contentType: "application/pdf",
					},
					null,
					2,
				),
			);
		}),
	);

	// ── Create Resume ─────────────────────────────────────────────
	server.registerTool(
		T.createResume,
		TOOL_META[T.createResume],
		withErrorHandling(
			"creating resume",
			async ({
				name,
				slug,
				tags,
				withSampleData,
			}: {
				name: string;
				slug: string;
				tags: string[];
				withSampleData: boolean;
			}) => {
				const id = await client.resume.create({ name, slug, tags, withSampleData });

				return text(
					`Created resume "${name}" (ID: ${id}) with slug "${slug}".${withSampleData ? " Pre-filled with sample data." : ""}\n\nNext steps: Use \`${T.getResume}\` to view it, or \`${T.patchResume}\` to start editing.`,
				);
			},
		),
	);

	// ── Import Resume ─────────────��───────────────────────────────
	server.registerTool(
		T.importResume,
		TOOL_META[T.importResume],
		withErrorHandling("importing resume", async ({ data }: { data: unknown }) => {
			const parsed = resumeDataSchema.safeParse(data);
			if (!parsed.success)
				return {
					isError: true,
					content: [
						{
							type: "text",
							text: `Invalid ResumeData: ${parsed.error.message}\n\nHint: Ensure the JSON matches the schema at resume://_meta/schema`,
						},
					],
				};

			const id = await client.resume.import({ data: parsed.data });

			return text(
				`Imported resume (ID: ${id}).\n\nNext steps: Use \`${T.getResume}\` to inspect metadata (name/slug were auto-generated), or \`${T.updateResume}\` / \`${T.patchResume}\` to adjust.`,
			);
		}),
	);

	// ── Duplicate Resume ────────────────────���─────────────────────
	server.registerTool(
		T.duplicateResume,
		TOOL_META[T.duplicateResume],
		withErrorHandling(
			"duplicating resume",
			async ({ id, name, slug, tags }: { id: string; name: string; slug: string; tags: string[] }) => {
				const newId = await client.resume.duplicate({ id, name, slug, tags });

				return text(
					`Duplicated resume as "${name}" (ID: ${newId}) with slug "${slug}".\n\nNext steps: Use \`${T.getResume}\` to view it, or \`${T.patchResume}\` to customize.`,
				);
			},
		),
	);

	// ── Apply Resume Patch ────────────────────────────────────────
	server.registerTool(
		T.patchResume,
		TOOL_META[T.patchResume],
		withErrorHandling("patching resume", async ({ id, operations }: { id: string; operations: PatchOperation[] }) => {
			const resume = await client.resume.patch({ id, operations });
			const summary = operations.map((op) => `${op.op} ${op.path}`).join(", ");

			return text(`Applied ${operations.length} operation(s) to "${resume.name}": ${summary}`);
		}),
	);

	// ── Update Resume (metadata) ─────────────────��───────────────
	server.registerTool(
		T.updateResume,
		TOOL_META[T.updateResume],
		withErrorHandling("updating resume", async (params) => {
			const { id, name, slug, tags, isPublic } = params as {
				id: string;
				name?: string;
				slug?: string;
				tags?: string[];
				isPublic?: boolean;
			};
			if (name === undefined && slug === undefined && tags === undefined && isPublic === undefined)
				throw new Error("Provide at least one of: name, slug, tags, isPublic.");

			const resume = await client.resume.update({
				id,
				...(name !== undefined ? { name } : {}),
				...(slug !== undefined ? { slug } : {}),
				...(tags !== undefined ? { tags } : {}),
				...(isPublic !== undefined ? { isPublic } : {}),
			});

			const user = await resolveUserFromRequestHeaders(requestHeaders);
			const username =
				user && "username" in user && typeof (user as { username: unknown }).username === "string"
					? (user as { username: string }).username
					: "";
			const shareUrl =
				username !== ""
					? buildResumeShareUrl(username, resume.slug)
					: "(could not build share URL — missing username on account)";

			const payload = {
				id: resume.id,
				name: resume.name,
				slug: resume.slug,
				tags: resume.tags,
				isPublic: resume.isPublic,
				hasPassword: resume.hasPassword,
				shareUrl,
			};

			return text(
				[
					JSON.stringify(payload, null, 2),
					"",
					resumeShareUrlNotes({ isPublic: resume.isPublic, hasPassword: resume.hasPassword }),
				].join("\n"),
			);
		}),
	);

	// ── Delete Resume ────────────────────────────────────────��────
	server.registerTool(
		T.deleteResume,
		TOOL_META[T.deleteResume],
		withErrorHandling("deleting resume", async ({ id }: { id: string }) => {
			await client.resume.delete({ id });

			return text(`Successfully deleted resume (${id}) and all associated files.`);
		}),
	);

	// ── Lock Resume ────────────────���──────────────────────────────
	server.registerTool(
		T.lockResume,
		TOOL_META[T.lockResume],
		withErrorHandling("locking resume", async ({ id }: { id: string }) => {
			await client.resume.setLocked({ id, isLocked: true });

			return text(`Resume (${id}) is now locked. It cannot be edited, patched, or deleted until unlocked.`);
		}),
	);

	// ── Unlock Resume ───────────────���─────────────────────────────
	server.registerTool(
		T.unlockResume,
		TOOL_META[T.unlockResume],
		withErrorHandling("unlocking resume", async ({ id }: { id: string }) => {
			await client.resume.setLocked({ id, isLocked: false });

			return text(`Resume (${id}) is now unlocked. It can be edited, patched, and deleted.`);
		}),
	);

	// ── Get Resume Statistics ────────────────────────────────────
	server.registerTool(
		T.getResumeStatistics,
		TOOL_META[T.getResumeStatistics],
		withErrorHandling("getting resume statistics", async ({ id }: { id: string }) => {
			const stats = await client.resume.statistics.getById({ id });

			return text(JSON.stringify(stats, null, 2));
		}),
	);

	// ── Applications ──────────────────────────────────────────────
	server.registerTool(
		T.listApplications,
		TOOL_META[T.listApplications],
		withErrorHandling("listing applications", async (params) => json(await client.applications.list(params as never))),
	);

	server.registerTool(
		T.readApplication,
		TOOL_META[T.readApplication],
		withErrorHandling("reading application", async ({ id }: { id: string }) =>
			json(await client.applications.getById({ id })),
		),
	);

	server.registerTool(
		T.listApplicationTags,
		TOOL_META[T.listApplicationTags],
		withErrorHandling("listing application tags", async () => json(await client.applications.tags())),
	);

	server.registerTool(
		T.getApplicationStats,
		TOOL_META[T.getApplicationStats],
		withErrorHandling("getting application stats", async () => json(await client.applications.stats())),
	);

	server.registerTool(
		T.createApplication,
		TOOL_META[T.createApplication],
		withErrorHandling("creating application", async (params) => {
			const id = await client.applications.create(coerceFollowUpAt(params as Record<string, unknown>) as never);
			return json({ id });
		}),
	);

	server.registerTool(
		T.updateApplication,
		TOOL_META[T.updateApplication],
		withErrorHandling("updating application", async (params) => {
			return json(await client.applications.update(coerceFollowUpAt(params as Record<string, unknown>) as never));
		}),
	);

	server.registerTool(
		T.addApplicationNote,
		TOOL_META[T.addApplicationNote],
		withErrorHandling(
			"adding application note",
			async ({ id, text: noteText, date }: { id: string; text: string; date?: string | undefined }) => {
				return json(await client.applications.addNote({ id, text: noteText, date }));
			},
		),
	);

	server.registerTool(
		T.updateApplicationTimelineEntry,
		TOOL_META[T.updateApplicationTimelineEntry],
		withErrorHandling("updating application timeline entry", async (params) => {
			return json(await client.applications.updateTimelineEntry(params as never));
		}),
	);

	server.registerTool(
		T.deleteApplicationTimelineEntry,
		TOOL_META[T.deleteApplicationTimelineEntry],
		withErrorHandling(
			"deleting application timeline entry",
			async ({ id, entryId }: { id: string; entryId: string }) => {
				return json(await client.applications.deleteTimelineEntry({ id, entryId }));
			},
		),
	);

	server.registerTool(
		T.deleteApplication,
		TOOL_META[T.deleteApplication],
		withErrorHandling("deleting application", async ({ id }: { id: string }) => {
			await client.applications.delete({ id });
			return text(`Deleted application (${id}).`);
		}),
	);

	server.registerTool(
		T.bulkUpdateApplications,
		TOOL_META[T.bulkUpdateApplications],
		withErrorHandling("bulk updating applications", async (params) =>
			json(await client.applications.bulkUpdate(params as never)),
		),
	);

	server.registerTool(
		T.bulkDeleteApplications,
		TOOL_META[T.bulkDeleteApplications],
		withErrorHandling("bulk deleting applications", async ({ ids }: { ids: string[] }) => {
			return json(await client.applications.bulkDelete({ ids }));
		}),
	);

	server.registerTool(
		T.importApplications,
		TOOL_META[T.importApplications],
		withErrorHandling("importing applications", async (params) => {
			const input = params as { items: Array<Record<string, unknown>> };
			return json(
				await client.applications.import({ items: input.items.map((item) => coerceFollowUpAt(item)) } as never),
			);
		}),
	);

	server.registerTool(
		T.attachApplicationDocument,
		TOOL_META[T.attachApplicationDocument],
		withErrorHandling(
			"attaching application document",
			async ({
				id,
				kind,
				fileName,
				contentType,
				dataBase64,
			}: {
				id: string;
				kind: "resume" | "cover-letter";
				fileName: string;
				contentType: string;
				dataBase64: string;
			}) => {
				const file = fileFromBase64({ fileName, contentType, dataBase64 });
				return json(await client.applications.attachDocument({ id, kind, file }));
			},
		),
	);

	server.registerTool(
		T.removeApplicationDocument,
		TOOL_META[T.removeApplicationDocument],
		withErrorHandling(
			"removing application document",
			async ({ id, kind }: { id: string; kind: "resume" | "cover-letter" }) => {
				return json(await client.applications.removeDocument({ id, kind }));
			},
		),
	);

	server.registerTool(
		T.autofillApplicationFromJob,
		TOOL_META[T.autofillApplicationFromJob],
		withErrorHandling("autofilling application from job", async (params) =>
			json(await client.applications.ai.autofill(params as never)),
		),
	);

	server.registerTool(
		T.scoreApplicationMatch,
		TOOL_META[T.scoreApplicationMatch],
		withErrorHandling("scoring application match", async ({ id }: { id: string }) => {
			return json(await client.applications.ai.matchScore({ id }));
		}),
	);

	server.registerTool(
		T.tailorResumeForApplication,
		TOOL_META[T.tailorResumeForApplication],
		withErrorHandling("tailoring resume for application", async ({ id }: { id: string }) => {
			return json(await client.applications.ai.tailorResume({ id }));
		}),
	);

	server.registerTool(
		T.draftApplicationMessage,
		TOOL_META[T.draftApplicationMessage],
		withErrorHandling(
			"drafting application message",
			async ({ id, kind }: { id: string; kind: "cover-letter" | "follow-up" }) =>
				json(await client.applications.ai.draftMessage({ id, kind })),
		),
	);
}
