import { randomBytes } from "node:crypto";
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, users } from "../db/schema.js";
import { getAnonymousUserId, requireAuth } from "../plugins/auth.js";
import { env } from "../plugins/env.js";
import { buildSharedMedicationOverview } from "../services/coverage.js";
import type { AuthUser } from "../types/fastify.js";
import {
	applyOpenApiRouteStandards,
	genericErrorSchema,
	tokenParamsSchema,
	validationErrorSchema,
} from "../utils/openapi-route-standards.js";
import { isAmountBasedPackageType, normalizePackageType } from "../utils/package-profiles.js";
import {
	getAllTakenByForMedication,
	parseIntakesJson,
	parseTakenByJson,
	personTakesMedication,
} from "../utils/scheduler-utils.js";

// =============================================================================
// Validation Schemas
// =============================================================================
const createShareSchema = z.object({
	takenBy: z.string().min(1, "takenBy is required"),
	scheduleDays: z.number().int().min(1).max(365).default(30),
});

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

const shareTokenPattern = /^[a-f0-9]{16}$/;

const createShareBodyOpenApiSchema = {
	type: "object",
	properties: {
		takenBy: { type: "string" },
		scheduleDays: { type: "integer", minimum: 1, maximum: 365, default: 30 },
	},
	example: {
		takenBy: "Daniel",
		scheduleDays: 14,
	},
} as const;

const shareReadResponseSchema = {
	type: "object",
	properties: {
		takenBy: { type: "string" },
		sharedBy: { type: "string" },
		scheduleDays: { type: "integer" },
		medications: { type: "array", items: { type: "object", additionalProperties: true } },
		shareMedicationOverview: { type: "boolean" },
		medicationOverview: {
			anyOf: [{ type: "array", items: { type: "object", additionalProperties: true } }, { type: "null" }],
		},
		stockThresholds: { type: "object", additionalProperties: { type: "number" } },
		stockCalculationMode: { type: "string", enum: ["automatic", "manual"] },
		upcomingTodayOnly: { type: "boolean" },
		shareScheduleTodayOnly: { type: "boolean" },
	},
} as const;

const shareExpiredResponseSchema = {
	type: "object",
	properties: {
		error: { type: "string" },
		code: { type: "string" },
		ownerUsername: { type: "string" },
		takenBy: { type: "string" },
		expiredAt: { type: "string", format: "date-time" },
	},
} as const;

const shareOverviewExpiredResponseSchema = {
	type: "object",
	properties: {
		error: { type: "string" },
		expiredAt: { type: "string", format: "date-time" },
	},
} as const;

const shareOverviewResponseSchema = {
	type: "object",
	properties: {
		takenBy: { type: "string" },
		sharedBy: { type: "string" },
		generatedAt: { type: "string", format: "date-time" },
		medications: { type: "array", items: { type: "object", additionalProperties: true } },
	},
} as const;

// 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;
}

// =============================================================================
// Share Routes
// =============================================================================
export async function shareRoutes(app: FastifyInstance) {
	applyOpenApiRouteStandards(app, {
		tag: "share",
		protectedByDefault: false,
		protectedPaths: [/^\/share$/, /^\/share\/people$/],
	});

	// ---------------------------------------------------------------------------
	// GET /share/:token - PUBLIC: Get shared schedule by token
	// ---------------------------------------------------------------------------
	app.get<{ Params: { token: string } }>(
		"/share/:token",
		{
			schema: {
				params: tokenParamsSchema,
				response: {
					200: shareReadResponseSchema,
					404: genericErrorSchema,
					410: shareExpiredResponseSchema,
				},
			},
			config: {
				rateLimit: {
					max: 60,
					timeWindow: "1 minute",
					errorResponseBuilder: () => ({ error: "rate_limited" }),
				},
			},
		},
		async (request, reply) => {
			const { token } = request.params;

			// Find share token
			const [share] = await db.select().from(shareTokens).where(eq(shareTokens.token, token));
			if (!share) {
				request.log.warn(`[Share] Invalid share token requested: token=${token}`);
				return reply.status(404).send({
					error: "Share link not found",
					code: "NOT_FOUND",
				});
			}

			// Check if token has expired
			if (share.expiresAt && share.expiresAt.getTime() < Date.now()) {
				request.log.warn(
					`[Share] Expired token requested: token=${token}, ownerUserId=${share.userId}, takenBy=${share.takenBy}`
				);
				// Get the username of the owner to show in the expired message
				const [owner] = await db.select({ username: users.username }).from(users).where(eq(users.id, share.userId));
				return reply.status(410).send({
					error: "Share link has expired",
					code: "EXPIRED",
					ownerUsername: owner?.username ?? "the owner",
					takenBy: share.takenBy,
					expiredAt: share.expiresAt.toISOString(),
				});
			}

			// Get user settings for stock thresholds
			const [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, share.userId));

			// Get the username of the owner who created this share link
			const [owner] = await db.select({ username: users.username }).from(users).where(eq(users.id, share.userId));

			// Get medications for this user filtered by takenBy (search in JSON array)
			// Use SQLite JSON function to check if takenBy is in the array
			const allMeds = await db
				.select()
				.from(medications)
				.where(and(eq(medications.userId, share.userId), eq(medications.isObsolete, false)));

			// Filter medications where takenBy matches either medication-level OR any intake-level takenBy
			const meds = allMeds.filter((med) => {
				const takenByArray = parseTakenByJson(med.takenByJson);
				const intakes = parseIntakesJson(
					med.intakesJson,
					{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
					med.intakeRemindersEnabled ?? false
				);
				return personTakesMedication(share.takenBy, takenByArray, intakes);
			});

			// Parse blisters and build schedule data
			const medicationsWithBlisters = meds.map((med) => {
				// Parse intakes from new format, falling back to legacy
				const intakes = parseIntakesJson(
					med.intakesJson,
					{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
					med.intakeRemindersEnabled ?? false
				);

				// Convert to legacy blisters format for backward compat
				const blisters = intakes.map((i) => ({
					usage: i.usage,
					every: i.every,
					start: i.start,
				}));

				// Parse takenBy JSON array
				const takenByArray = parseTakenByJson(med.takenByJson);

				const totalPills = isAmountBasedPackageType(med.packageType)
					? med.looseTablets + (med.stockAdjustment ?? 0)
					: med.packCount * med.blistersPerPack * med.pillsPerBlister + med.looseTablets + (med.stockAdjustment ?? 0);
				return {
					id: med.id,
					name: med.name,
					genericName: med.genericName,
					pillWeightMg: med.pillWeightMg,
					doseUnit: med.doseUnit ?? "mg",
					imageUrl: med.imageUrl,
					totalPills,
					packageType: normalizePackageType(med.packageType),
					packCount: med.packCount,
					blistersPerPack: med.blistersPerPack,
					looseTablets: med.looseTablets,
					pillsPerBlister: med.pillsPerBlister,
					takenBy: takenByArray,
					intakes, // New unified format with per-intake takenBy
					blisters, // Legacy format for backward compat
					dismissedUntil: med.dismissedUntil,
					updatedAt: med.updatedAt, // For filtering out doses from previous schedule configurations
					lastStockCorrectionAt: med.lastStockCorrectionAt?.getTime() ?? null,
					stockAdjustment: med.stockAdjustment ?? 0,
				};
			});

			const shareMedicationOverview = settings?.shareMedicationOverview ?? false;
			const medicationOverview = shareMedicationOverview
				? buildSharedMedicationOverview({
						medications: meds,
						doses: await db.select().from(doseTracking).where(eq(doseTracking.userId, share.userId)),
						thresholdDays: settings?.lowStockDays ?? 30,
					})
				: null;

			return {
				takenBy: share.takenBy,
				sharedBy: owner?.username ?? null,
				scheduleDays: share.scheduleDays,
				medications: medicationsWithBlisters,
				shareMedicationOverview,
				medicationOverview,
				stockThresholds: {
					lowStockDays: settings?.lowStockDays ?? 30,
					normalStockDays: settings?.normalStockDays ?? 60,
					highStockDays: settings?.highStockDays ?? 90,
					reminderDaysBefore: settings?.reminderDaysBefore ?? 7,
					expiryWarningDays: settings?.expiryWarningDays ?? 90,
				},
				stockCalculationMode: (settings?.stockCalculationMode as "automatic" | "manual") ?? "automatic",
				upcomingTodayOnly: settings?.upcomingTodayOnly ?? false,
				shareScheduleTodayOnly: settings?.shareScheduleTodayOnly ?? false,
			};
		}
	);

	// ---------------------------------------------------------------------------
	// GET /share/:token/overview - PUBLIC: Read-only medication overview by token
	// ---------------------------------------------------------------------------
	app.get<{ Params: { token: string } }>(
		"/share/:token/overview",
		{
			schema: {
				params: tokenParamsSchema,
				response: {
					200: shareOverviewResponseSchema,
					404: genericErrorSchema,
					410: shareOverviewExpiredResponseSchema,
				},
			},
			config: {
				rateLimit: {
					max: 60,
					timeWindow: "1 minute",
					errorResponseBuilder: () => ({ error: "rate_limited" }),
				},
			},
		},
		async (request, reply) => {
			reply.header("Cache-Control", "no-store");

			const { token } = request.params;
			if (!shareTokenPattern.test(token)) {
				request.log.warn(`[ShareOverview] Rejected invalid token format: token=${token}`);
				return reply.status(404).send({ error: "not_found" });
			}

			const [share] = await db.select().from(shareTokens).where(eq(shareTokens.token, token));
			if (!share) {
				request.log.warn(`[ShareOverview] Unknown token requested: token=${token}`);
				return reply.status(404).send({ error: "not_found" });
			}

			if (share.expiresAt && share.expiresAt.getTime() < Date.now()) {
				request.log.warn(
					`[ShareOverview] Expired token requested: token=${token}, ownerUserId=${share.userId}, takenBy=${share.takenBy}`
				);
				return reply.status(410).send({
					error: "expired",
					expiredAt: share.expiresAt.toISOString(),
				});
			}

			const [settings] = await db.select().from(userSettings).where(eq(userSettings.userId, share.userId));
			const [owner] = await db.select({ username: users.username }).from(users).where(eq(users.id, share.userId));

			const allMeds = await db
				.select()
				.from(medications)
				.where(and(eq(medications.userId, share.userId), eq(medications.isObsolete, false)));
			const meds = allMeds.filter((med) => {
				const takenByArray = parseTakenByJson(med.takenByJson);
				const intakes = parseIntakesJson(
					med.intakesJson,
					{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
					med.intakeRemindersEnabled ?? false
				);
				return personTakesMedication(share.takenBy, takenByArray, intakes);
			});

			const doses = await db.select().from(doseTracking).where(eq(doseTracking.userId, share.userId));

			const overview = buildSharedMedicationOverview({
				medications: meds,
				doses,
				thresholdDays: settings?.lowStockDays ?? 30,
			});

			return {
				takenBy: share.takenBy,
				sharedBy: owner?.username ?? null,
				generatedAt: new Date().toISOString(),
				medications: overview,
			};
		}
	);

	// ---------------------------------------------------------------------------
	// POST /share - PROTECTED: Create a new share link
	// ---------------------------------------------------------------------------
	app.post<{ Body: z.infer<typeof createShareSchema> }>(
		"/share",
		{
			preHandler: requireAuth,
			schema: {
				tags: ["share"],
				security: protectedEndpointSecurity,
				body: createShareBodyOpenApiSchema,
				response: {
					200: {
						type: "object",
						properties: {
							reused: { type: "boolean" },
							token: { type: "string" },
							shareUrl: { type: "string" },
							expiresAt: { type: ["string", "null"] },
						},
					},
					400: { anyOf: [genericErrorSchema, validationErrorSchema] },
					401: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

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

			const { takenBy, scheduleDays } = parsed.data;

			// Check if user has medications for this takenBy (search in both medication-level and intake-level)
			const allMeds = await db
				.select()
				.from(medications)
				.where(and(eq(medications.userId, userId), eq(medications.isObsolete, false)));
			const medsForPerson = allMeds.filter((med) => {
				const takenByArray = parseTakenByJson(med.takenByJson);
				const intakes = parseIntakesJson(
					med.intakesJson,
					{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
					med.intakeRemindersEnabled ?? false
				);
				return personTakesMedication(takenBy, takenByArray, intakes);
			});

			if (medsForPerson.length === 0) {
				return reply.status(400).send({
					error: "No medications found for this person",
					code: "NO_MEDICATIONS",
				});
			}

			// Keep exactly one active share link per person/user.
			// If a link already exists, return the same token and only update settings.
			const [existingShare] = await db
				.select()
				.from(shareTokens)
				.where(and(eq(shareTokens.userId, userId), eq(shareTokens.takenBy, takenBy)));

			if (existingShare) {
				await db.update(shareTokens).set({ scheduleDays, expiresAt: null }).where(eq(shareTokens.id, existingShare.id));

				request.log.info(
					`[Share] Reused existing share token: token=${existingShare.token}, ownerUserId=${userId}, takenBy=${takenBy}, scheduleDays=${scheduleDays}`
				);

				return {
					reused: true,
					token: existingShare.token,
					shareUrl: `/share/${existingShare.token}`,
					expiresAt: null,
				};
			}

			const token = randomBytes(8).toString("hex");

			await db.insert(shareTokens).values({
				userId,
				token,
				takenBy,
				scheduleDays,
				expiresAt: null,
			});

			request.log.info(
				`[Share] Created new share token: token=${token}, ownerUserId=${userId}, takenBy=${takenBy}, scheduleDays=${scheduleDays}`
			);

			return {
				reused: false,
				token,
				shareUrl: `/share/${token}`,
				expiresAt: null,
			};
		}
	);

	// ---------------------------------------------------------------------------
	// GET /share/people - PROTECTED: Get list of unique takenBy values
	// ---------------------------------------------------------------------------
	app.get(
		"/share/people",
		{
			preHandler: requireAuth,
			schema: {
				tags: ["share"],
				security: protectedEndpointSecurity,
				response: {
					200: {
						type: "object",
						properties: {
							people: { type: "array", items: { type: "string" } },
						},
					},
					401: genericErrorSchema,
				},
			},
		},
		async (request, reply) => {
			const userId = await getUserId(request, reply);

			// Get all unique takenBy values for this user (from both medication-level and intake-level)
			const meds = await db
				.select({
					takenByJson: medications.takenByJson,
					intakesJson: medications.intakesJson,
					usageJson: medications.usageJson,
					everyJson: medications.everyJson,
					startJson: medications.startJson,
					intakeRemindersEnabled: medications.intakeRemindersEnabled,
				})
				.from(medications)
				.where(and(eq(medications.userId, userId), eq(medications.isObsolete, false)));

			// Collect all unique person names from medication-level AND intake-level takenBy
			const allPeople = new Set<string>();
			for (const med of meds) {
				const takenByArray = parseTakenByJson(med.takenByJson);
				const intakes = parseIntakesJson(
					med.intakesJson,
					{ usageJson: med.usageJson, everyJson: med.everyJson, startJson: med.startJson },
					med.intakeRemindersEnabled ?? false
				);
				const allForMed = getAllTakenByForMedication(takenByArray, intakes);
				for (const person of allForMed) {
					if (person) allPeople.add(person);
				}
			}

			return { people: [...allPeople].sort() };
		}
	);
}
