import { and, eq } from "drizzle-orm";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { z } from "zod";
import { db } from "../db/client.js";
import { doseTracking, medications, shareTokens, userSettings } from "../db/schema.js";
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
import { env } from "../plugins/env.js";
import { computeMedicationCurrentStock } from "../services/current-stock.js";
import type { AuthUser } from "../types/fastify.js";
import {
	applyOpenApiRouteStandards,
	genericErrorSchema,
	tokenParamsSchema,
	validationErrorSchema,
} from "../utils/openapi-route-standards.js";
import {
	parseIntakesJson,
	parseLocalDateTime,
	parseTakenByJson,
	personTakesMedication,
} from "../utils/scheduler-utils.js";

// =============================================================================
// Validation Schemas
// =============================================================================
const markDoseSchema = z.object({
	doseId: z.string().min(1, "doseId is required"),
});

const shareDoseSchema = z.object({
	doseId: z.string().min(1, "doseId is required"),
});

const dismissDosesSchema = z.object({
	doseIds: z.array(z.string().min(1)).min(1, "At least one doseId is required"),
});

const protectedEndpointSecurity: ReadonlyArray<Record<string, readonly string[]>> = [
	{ bearerAuth: [] },
	{ cookieAuth: [] },
];

const doseIdPattern = /^(\d+)-(\d+)-(\d+)(?:-(.+))?$/;

const doseReadResponseSchema = {
	type: "object",
	properties: {
		doses: {
			type: "array",
			items: {
				type: "object",
				properties: {
					doseId: { type: "string" },
					takenAt: { type: "number" },
					markedBy: { type: ["string", "null"] },
					takenSource: { type: "string" },
					dismissed: { type: "boolean" },
				},
			},
		},
	},
} as const;

function maskToken(token: string): string {
	if (token.length <= 8) return token;
	return `${token.slice(0, 4)}...${token.slice(-4)}`;
}

// Helper to get user ID from request
// Returns anonymous user ID when auth is disabled
async function getUserId(request: FastifyRequest, reply: FastifyReply): Promise<number> {
	// If auth is disabled, use the anonymous user
	if (!env.AUTH_ENABLED) {
		return getAnonymousUserId();
	}

	const authUser = request.user as unknown as AuthUser | null;
	if (!authUser) {
		reply.status(401).send({ error: "Not authenticated" });
		throw new Error("AUTH_REQUIRED");
	}
	return authUser.id;
}

type ParsedDoseId = {
	medicationId: number;
	intakeIndex: number;
	timestampMs: number;
	personSuffix: string | null;
};

function parseDoseId(doseId: string): ParsedDoseId | null {
	const match = doseIdPattern.exec(doseId);
	if (!match) return null;

	const medicationId = Number.parseInt(match[1], 10);
	const intakeIndex = Number.parseInt(match[2], 10);
	const timestampMs = Number.parseInt(match[3], 10);
	const personSuffix = match[4] ? match[4].trim() : null;

	if (Number.isNaN(medicationId) || Number.isNaN(intakeIndex) || Number.isNaN(timestampMs) || intakeIndex < 0) {
		return null;
	}

	return {
		medicationId,
		intakeIndex,
		timestampMs,
		personSuffix,
	};
}

async function getActiveShareToken(token: string): Promise<{
	share: typeof shareTokens.$inferSelect | null;
	reason: "not_found" | "expired" | "ok";
}> {
	const [share] = await db.select().from(shareTokens).where(eq(shareTokens.token, token));
	if (!share) return { share: null, reason: "not_found" };

	if (share.expiresAt && share.expiresAt.getTime() < Date.now()) {
		return { share: null, reason: "expired" };
	}

	return { share, reason: "ok" };
}

async function validateShareDoseId(share: typeof shareTokens.$inferSelect, doseId: string): Promise<boolean> {
	const parsedDose = parseDoseId(doseId);
	if (!parsedDose) {
		return false;
	}

	const [medication] = await db
		.select()
		.from(medications)
		.where(and(eq(medications.id, parsedDose.medicationId), eq(medications.userId, share.userId)));

	if (!medication) {
		return false;
	}

	const medTakenBy = parseTakenByJson(medication.takenByJson);
	const intakes = parseIntakesJson(
		medication.intakesJson,
		{ usageJson: medication.usageJson, everyJson: medication.everyJson, startJson: medication.startJson },
		medication.intakeRemindersEnabled ?? false
	);

	if (!personTakesMedication(share.takenBy, medTakenBy, intakes)) {
		return false;
	}

	const intake = intakes[parsedDose.intakeIndex];
	if (!intake) {
		return false;
	}

	const expectedPersons = intake.takenBy ? [intake.takenBy] : medTakenBy;
	if (expectedPersons.length === 0) {
		return parsedDose.personSuffix === null;
	}

	if (!parsedDose.personSuffix) {
		return intake.takenBy === null;
	}

	return expectedPersons.includes(parsedDose.personSuffix);
}

async function isDoseOutOfStock(options: {
	userId: number;
	doseId: string;
	stockCalculationMode: "automatic" | "manual";
}): Promise<boolean> {
	const parsedDose = parseDoseId(options.doseId);
	if (!parsedDose) {
		return false;
	}

	const [medication] = await db
		.select()
		.from(medications)
		.where(and(eq(medications.id, parsedDose.medicationId), eq(medications.userId, options.userId)));

	if (!medication) {
		return false;
	}

	const intakes = parseIntakesJson(
		medication.intakesJson,
		{ usageJson: medication.usageJson, everyJson: medication.everyJson, startJson: medication.startJson },
		medication.intakeRemindersEnabled ?? false
	);
	const intake = intakes[parsedDose.intakeIndex];

	const scheduledOccurrenceMs = intake
		? (() => {
				const doseDate = new Date(parsedDose.timestampMs);
				const intakeStart = parseLocalDateTime(intake.start);
				return new Date(
					doseDate.getFullYear(),
					doseDate.getMonth(),
					doseDate.getDate(),
					intakeStart.getHours(),
					intakeStart.getMinutes(),
					intakeStart.getSeconds(),
					intakeStart.getMilliseconds()
				).getTime();
			})()
		: parsedDose.timestampMs;

	const doses = await db.select().from(doseTracking).where(eq(doseTracking.userId, options.userId));
	const stockBeforeDoseMs = Math.max(0, scheduledOccurrenceMs - 1);
	return (
		computeMedicationCurrentStock({
			medication,
			doses,
			stockCalculationMode: options.stockCalculationMode,
			nowMs: stockBeforeDoseMs,
		}) <= 0
	);
}

// =============================================================================
// Dose Tracking Routes
// =============================================================================
export async function doseRoutes(app: FastifyInstance) {
	applyOpenApiRouteStandards(app, {
		tag: "doses",
		protectedByDefault: false,
		protectedPaths: [/^\/doses\/taken$/, /^\/doses\/taken\/:doseId$/, /^\/doses\/dismiss$/],
	});

	// ---------------------------------------------------------------------------
	// GET /doses/taken - PROTECTED: Get all taken doses for the user
	// Suppress request logs — polled every 5s by frontend
	// ---------------------------------------------------------------------------
	app.get(
		"/doses/taken",
		{
			preHandler: requireAuth,
			logLevel: "warn",
			schema: {
				tags: ["doses"],
				security: protectedEndpointSecurity,
				response: {
					200: doseReadResponseSchema,
					401: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			// Get all taken doses for this user (no time limit)
			const doses = await db.select().from(doseTracking).where(eq(doseTracking.userId, userId));

			return {
				doses: doses.map((d) => ({
					doseId: d.doseId,
					takenAt: d.takenAt?.getTime() ?? Date.now(),
					markedBy: d.markedBy,
					takenSource: d.takenSource ?? "manual",
					dismissed: d.dismissed ?? false,
				})),
			};
		}
	);

	// ---------------------------------------------------------------------------
	// POST /doses/taken - PROTECTED: Mark a dose as taken
	// ---------------------------------------------------------------------------
	app.post<{ Body: z.infer<typeof markDoseSchema> }>(
		"/doses/taken",
		{
			preHandler: requireAuth,
			schema: {
				tags: ["doses"],
				security: protectedEndpointSecurity,
				body: {
					type: "object",
					properties: {
						doseId: { type: "string" },
					},
					example: {
						doseId: "1:2026-03-11T08:00:00.000Z:Daniel",
					},
				},
				response: {
					200: {
						type: "object",
						properties: {
							success: { type: "boolean" },
							message: { type: "string" },
						},
					},
					400: { anyOf: [genericErrorSchema, validationErrorSchema] },
					409: genericErrorSchema,
					401: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			const parsed = markDoseSchema.safeParse(request.body);
			if (!parsed.success) {
				return reply.status(400).send({
					error: parsed.error.errors[0]?.message ?? "Invalid input",
				});
			}

			const { doseId } = parsed.data;

			// Check if already marked
			const [existing] = await db
				.select()
				.from(doseTracking)
				.where(and(eq(doseTracking.userId, userId), eq(doseTracking.doseId, doseId)));

			if (existing) {
				return { success: true, message: "Already marked" };
			}

			const [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, userId));
			const outOfStock = await isDoseOutOfStock({
				userId,
				doseId,
				stockCalculationMode: (settings?.stockCalculationMode as "automatic" | "manual") ?? "automatic",
			});
			if (outOfStock) {
				return reply.status(409).send({ error: "Medication is out of stock", code: "OUT_OF_STOCK" });
			}

			// Insert new record
			await db.insert(doseTracking).values({
				userId,
				doseId,
				markedBy: null, // Marked by the user themselves
				takenSource: "manual",
			});

			return { success: true };
		}
	);

	// ---------------------------------------------------------------------------
	// DELETE /doses/taken/:doseId - PROTECTED: Unmark a dose
	// ---------------------------------------------------------------------------
	app.delete<{ Params: { doseId: string } }>(
		"/doses/taken/:doseId",
		{
			preHandler: requireAuth,
			schema: {
				tags: ["doses"],
				security: protectedEndpointSecurity,
				params: {
					type: "object",
					required: ["doseId"],
					properties: {
						doseId: { type: "string", minLength: 1 },
					},
				},
				response: {
					200: { type: "object", properties: { success: { type: "boolean" } } },
					401: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			const { doseId } = request.params;

			// Check if this dose was dismissed
			const [existing] = await db
				.select()
				.from(doseTracking)
				.where(and(eq(doseTracking.userId, userId), eq(doseTracking.doseId, doseId)));

			if (existing?.dismissed) {
				// Already dismissed - keep the record as-is
				// The dose stays dismissed, we just acknowledge the undo request
			} else {
				// Not dismissed - delete the record entirely
				await db.delete(doseTracking).where(and(eq(doseTracking.userId, userId), eq(doseTracking.doseId, doseId)));
			}

			return { success: true };
		}
	);

	// ---------------------------------------------------------------------------
	// POST /doses/dismiss - PROTECTED: Dismiss missed doses without deducting stock
	// ---------------------------------------------------------------------------
	app.post<{ Body: z.infer<typeof dismissDosesSchema> }>(
		"/doses/dismiss",
		{
			preHandler: requireAuth,
			schema: {
				tags: ["doses"],
				security: protectedEndpointSecurity,
				body: {
					type: "object",
					properties: {
						doseIds: { type: "array", items: { type: "string" } },
					},
					example: {
						doseIds: ["1:2026-03-11T08:00:00.000Z:Daniel", "1:2026-03-11T20:00:00.000Z:Daniel"],
					},
				},
				response: {
					200: {
						type: "object",
						properties: {
							success: { type: "boolean" },
							dismissedCount: { type: "integer" },
						},
					},
					400: { anyOf: [genericErrorSchema, validationErrorSchema] },
					401: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			const parsed = dismissDosesSchema.safeParse(request.body);
			if (!parsed.success) {
				return reply.status(400).send({
					error: parsed.error.errors[0]?.message ?? "Invalid input",
				});
			}

			const { doseIds } = parsed.data;

			// Insert dismissed records for each dose that doesn't exist yet
			let dismissedCount = 0;
			for (const doseId of doseIds) {
				// Check if already exists (taken or dismissed)
				const [existing] = await db
					.select()
					.from(doseTracking)
					.where(and(eq(doseTracking.userId, userId), eq(doseTracking.doseId, doseId)));

				if (existing) {
					// Already exists - update to dismissed if not already
					if (!existing.dismissed) {
						await db
							.update(doseTracking)
							.set({ dismissed: true })
							.where(and(eq(doseTracking.userId, userId), eq(doseTracking.doseId, doseId)));
						dismissedCount++;
					}
				} else {
					// Create new dismissed record
					await db.insert(doseTracking).values({
						userId,
						doseId,
						markedBy: null,
						takenAt: new Date(0),
						dismissed: true,
					});
					dismissedCount++;
				}
			}

			return { success: true, dismissedCount };
		}
	);

	// ---------------------------------------------------------------------------
	// DELETE /doses/dismiss - PROTECTED: Clear all dismissed doses (un-dismiss)
	// ---------------------------------------------------------------------------
	app.delete(
		"/doses/dismiss",
		{
			preHandler: requireAuth,
			schema: {
				tags: ["doses"],
				security: protectedEndpointSecurity,
				response: {
					200: {
						type: "object",
						properties: {
							success: { type: "boolean" },
							clearedCount: { type: "integer" },
						},
					},
					401: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			// Delete all dismissed-only records (not taken ones)
			// For taken+dismissed, just remove the dismissed flag
			const dismissed = await db
				.select()
				.from(doseTracking)
				.where(and(eq(doseTracking.userId, userId), eq(doseTracking.dismissed, true)));

			for (const d of dismissed) {
				const hasRealTakenTimestamp = d.takenAt instanceof Date ? d.takenAt.getTime() > 0 : Boolean(d.takenAt);

				if (d.markedBy !== null || hasRealTakenTimestamp) {
					// This was also marked as taken - just remove dismissed flag
					await db.update(doseTracking).set({ dismissed: false }).where(eq(doseTracking.id, d.id));
				} else {
					// This was only dismissed - delete it
					await db.delete(doseTracking).where(eq(doseTracking.id, d.id));
				}
			}

			return { success: true, clearedCount: dismissed.length };
		}
	);

	// ---------------------------------------------------------------------------
	// GET /share/:token/doses - PUBLIC: Get taken doses for a share link
	// Suppress request logs — polled every 5s by SharedSchedule
	// ---------------------------------------------------------------------------
	app.get<{ Params: { token: string } }>(
		"/share/:token/doses",
		{
			schema: {
				params: tokenParamsSchema,
				response: {
					200: doseReadResponseSchema,
					404: genericErrorSchema,
				},
			},
			logLevel: "warn",
			config: {
				rateLimit: {
					max: 60,
					timeWindow: "1 minute",
					errorResponseBuilder: () => ({ error: "rate_limited" }),
				},
			},
		},
		async (request, reply) => {
			const { token } = request.params;

			const { share, reason } = await getActiveShareToken(token);
			if (!share) {
				request.log.warn(`[ShareDose] Rejected read for token ${maskToken(token)} (reason=${reason})`);
				return reply.notFound("Share link not found");
			}

			// Get all taken doses for this user (no time limit)
			const doses = await db.select().from(doseTracking).where(eq(doseTracking.userId, share.userId));

			return {
				doses: doses.map((d) => ({
					doseId: d.doseId,
					takenAt: d.takenAt?.getTime() ?? Date.now(),
					markedBy: d.markedBy,
					takenSource: d.takenSource ?? "manual",
					dismissed: d.dismissed ?? false,
				})),
			};
		}
	);

	// ---------------------------------------------------------------------------
	// POST /share/:token/doses - PUBLIC: Mark a dose as taken via share link
	// ---------------------------------------------------------------------------
	app.post<{ Params: { token: string }; Body: z.infer<typeof shareDoseSchema> }>(
		"/share/:token/doses",
		{
			schema: {
				params: tokenParamsSchema,
				body: {
					type: "object",
					properties: {
						doseId: { type: "string" },
					},
					example: {
						doseId: "1:2026-03-11T08:00:00.000Z:Daniel",
					},
				},
				response: {
					200: { type: "object", properties: { success: { type: "boolean" }, message: { type: "string" } } },
					400: { anyOf: [genericErrorSchema, validationErrorSchema] },
					409: genericErrorSchema,
					404: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const { token } = request.params;

			const parsed = shareDoseSchema.safeParse(request.body);
			if (!parsed.success) {
				return reply.status(400).send({
					error: parsed.error.errors[0]?.message ?? "Invalid input",
				});
			}

			const { doseId } = parsed.data;

			const { share, reason } = await getActiveShareToken(token);
			if (!share) {
				request.log.warn(`[ShareDose] Rejected mark for token ${maskToken(token)} (reason=${reason})`);
				return reply.notFound("Share link not found");
			}

			const isValidShareDoseId = await validateShareDoseId(share, doseId);
			if (!isValidShareDoseId) {
				request.log.warn(
					`[ShareDose] Rejected invalid doseId in mark request (owner=${share.userId}, takenBy=${share.takenBy}, doseId=${doseId})`
				);
				return reply.status(400).send({ error: "Invalid or unauthorized doseId" });
			}

			// Check if already marked
			const [existing] = await db
				.select()
				.from(doseTracking)
				.where(and(eq(doseTracking.userId, share.userId), eq(doseTracking.doseId, doseId)));

			if (existing) {
				request.log.debug(`[ShareDose] Duplicate mark ignored (owner=${share.userId}, doseId=${doseId})`);
				return { success: true, message: "Already marked" };
			}

			const [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, share.userId));
			const outOfStock = await isDoseOutOfStock({
				userId: share.userId,
				doseId,
				stockCalculationMode: (settings?.stockCalculationMode as "automatic" | "manual") ?? "automatic",
			});
			if (outOfStock) {
				request.log.info(
					`[ShareDose] Rejected out-of-stock mark request (owner=${share.userId}, takenBy=${share.takenBy}, doseId=${doseId})`
				);
				return reply.status(409).send({ error: "Medication is out of stock", code: "OUT_OF_STOCK" });
			}

			// Insert new record - marked by the shared person, or the concrete intake person for an "all" link.
			const parsedShareDose = parseDoseId(doseId);
			const markedBy = share.takenBy === "all" ? (parsedShareDose?.personSuffix ?? share.takenBy) : share.takenBy;

			await db.insert(doseTracking).values({
				userId: share.userId,
				doseId,
				markedBy,
				takenSource: "manual",
			});

			request.log.info(
				`[ShareDose] Dose marked via share link (owner=${share.userId}, takenBy=${share.takenBy}, doseId=${doseId})`
			);

			return { success: true };
		}
	);

	// ---------------------------------------------------------------------------
	// DELETE /share/:token/doses/:doseId - PUBLIC: Unmark a dose via share link
	// ---------------------------------------------------------------------------
	app.delete<{ Params: { token: string; doseId: string } }>(
		"/share/:token/doses/:doseId",
		{
			schema: {
				params: {
					type: "object",
					required: ["token", "doseId"],
					properties: {
						token: tokenParamsSchema.properties.token,
						doseId: { type: "string", minLength: 1 },
					},
				},
				response: {
					200: { type: "object", properties: { success: { type: "boolean" } } },
					400: genericErrorSchema,
					404: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const { token, doseId } = request.params;

			const { share, reason } = await getActiveShareToken(token);
			if (!share) {
				request.log.warn(`[ShareDose] Rejected unmark for token ${maskToken(token)} (reason=${reason})`);
				return reply.notFound("Share link not found");
			}

			const isValidShareDoseId = await validateShareDoseId(share, doseId);
			if (!isValidShareDoseId) {
				request.log.warn(
					`[ShareDose] Rejected invalid doseId in unmark request (owner=${share.userId}, takenBy=${share.takenBy}, doseId=${doseId})`
				);
				return reply.status(400).send({ error: "Invalid or unauthorized doseId" });
			}

			// Check if this dose was dismissed
			const [existing] = await db
				.select()
				.from(doseTracking)
				.where(and(eq(doseTracking.userId, share.userId), eq(doseTracking.doseId, doseId)));

			if (existing?.dismissed) {
				// Already dismissed - keep the record as-is
				request.log.debug(`[ShareDose] Unmark ignored for dismissed dose (owner=${share.userId}, doseId=${doseId})`);
			} else {
				// Not dismissed - delete the record entirely
				await db
					.delete(doseTracking)
					.where(and(eq(doseTracking.userId, share.userId), eq(doseTracking.doseId, doseId)));
				request.log.info(
					`[ShareDose] Dose unmarked via share link (owner=${share.userId}, takenBy=${share.takenBy}, doseId=${doseId})`
				);
			}

			return { success: true };
		}
	);
}
