import bcrypt from "bcryptjs";

import User, { USER_ROLES } from "@/models/user";
import { getDb } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { requireUserManagement } from "@/lib/auth/permissions";
import { generateAdminTemporaryPassword } from "@/lib/auth/adminTempPassword";
import {
	withErrorHandling,
	json,
	badRequest,
	unauthorized,
	notFound,
} from "@/lib/api/errors";

export const dynamic = "force-dynamic";

const BRANCH_RE = /^NL\d+$/;
const OBJECT_ID_RE = /^[a-f0-9]{24}$/i;

const USERNAME_RE = /^[a-z0-9][a-z0-9._-]{2,31}$/; // 3..32, conservative
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const BCRYPT_SALT_ROUNDS = 12;

const ALLOWED_ROLES = new Set(Object.values(USER_ROLES));
const ALLOWED_UPDATE_FIELDS = Object.freeze([
	"username",
	"email",
	"role",
	"branchId",
	"mustChangePassword",
]);

function isPlainObject(value) {
	return Boolean(value && typeof value === "object" && !Array.isArray(value));
}

function isNonEmptyString(value) {
	return typeof value === "string" && value.trim().length > 0;
}

function normalizeUsername(value) {
	return String(value || "")
		.trim()
		.toLowerCase();
}

function normalizeEmail(value) {
	return String(value || "")
		.trim()
		.toLowerCase();
}

function normalizeBranchId(value) {
	return String(value || "")
		.trim()
		.toUpperCase();
}

function toIsoOrNull(value) {
	if (!value) return null;
	try {
		return new Date(value).toISOString();
	} catch {
		return null;
	}
}

function toSafeUser(doc) {
	return {
		id: String(doc?._id),
		username: typeof doc?.username === "string" ? doc.username : "",
		email: typeof doc?.email === "string" ? doc.email : "",
		role: typeof doc?.role === "string" ? doc.role : "",
		branchId: doc?.branchId ?? null,
		mustChangePassword: Boolean(doc?.mustChangePassword),
		createdAt: toIsoOrNull(doc?.createdAt),
		updatedAt: toIsoOrNull(doc?.updatedAt),
	};
}

function pickDuplicateField(err) {
	if (!err || typeof err !== "object") return null;

	const keyValue =
		err.keyValue && typeof err.keyValue === "object" ? err.keyValue : null;
	if (keyValue) {
		const keys = Object.keys(keyValue);
		if (keys.length > 0) return keys[0];
	}

	const keyPattern =
		err.keyPattern && typeof err.keyPattern === "object"
			? err.keyPattern
			: null;
	if (keyPattern) {
		const keys = Object.keys(keyPattern);
		if (keys.length > 0) return keys[0];
	}

	return null;
}

async function resolveValidatedUserId(ctx) {
	const { userId } = await ctx.params;

	if (!userId) {
		throw badRequest(
			"VALIDATION_MISSING_PARAM",
			"Missing required route parameter(s)",
			{ params: ["userId"] },
		);
	}

	if (!OBJECT_ID_RE.test(String(userId))) {
		throw badRequest("VALIDATION_INVALID_FIELD", "Invalid userId", {
			field: "userId",
			value: userId,
		});
	}

	return String(userId);
}

export const PATCH = withErrorHandling(
	async function PATCH(request, ctx) {
		const session = await getSession();

		if (!session) {
			throw unauthorized("AUTH_UNAUTHENTICATED", "Unauthorized");
		}

		requireUserManagement(session);
		const userId = await resolveValidatedUserId(ctx);

		let body;
		try {
			body = await request.json();
		} catch {
			throw badRequest("VALIDATION_INVALID_JSON", "Invalid request body");
		}

		if (!isPlainObject(body)) {
			throw badRequest("VALIDATION_INVALID_BODY", "Invalid request body");
		}

		const hasUpdateField = Object.keys(body).some((k) =>
			ALLOWED_UPDATE_FIELDS.includes(k),
		);

		if (!hasUpdateField) {
			throw badRequest("VALIDATION_MISSING_FIELD", "Missing fields to update", {
				fields: [...ALLOWED_UPDATE_FIELDS],
			});
		}

		await getDb();

		const user = await User.findById(String(userId)).exec();

		if (!user) {
			throw notFound("USER_NOT_FOUND", "Not found", { userId: String(userId) });
		}

		const patch = {};

		// username (optional)
		if (Object.prototype.hasOwnProperty.call(body, "username")) {
			if (!isNonEmptyString(body.username)) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid username", {
					field: "username",
					value: body.username,
				});
			}

			const username = normalizeUsername(body.username);

			if (!USERNAME_RE.test(username)) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid username", {
					field: "username",
					value: username,
					pattern: String(USERNAME_RE),
				});
			}

			const existing = await User.findOne({
				username,
				_id: { $ne: String(userId) },
			})
				.select("_id")
				.exec();

			if (existing) {
				throw badRequest(
					"VALIDATION_INVALID_FIELD",
					"Username already exists",
					{
						field: "username",
						value: username,
					},
				);
			}

			patch.username = username;
		}

		// email (optional)
		if (Object.prototype.hasOwnProperty.call(body, "email")) {
			if (!isNonEmptyString(body.email)) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid email", {
					field: "email",
					value: body.email,
				});
			}

			const email = normalizeEmail(body.email);

			if (!EMAIL_RE.test(email)) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid email", {
					field: "email",
					value: email,
				});
			}

			const existing = await User.findOne({
				email,
				_id: { $ne: String(userId) },
			})
				.select("_id")
				.exec();

			if (existing) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Email already exists", {
					field: "email",
					value: email,
				});
			}

			patch.email = email;
		}

		// role (optional)
		if (Object.prototype.hasOwnProperty.call(body, "role")) {
			if (!isNonEmptyString(body.role)) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid role", {
					field: "role",
					value: body.role,
					allowed: Array.from(ALLOWED_ROLES),
				});
			}

			const role = String(body.role).trim();

			if (!ALLOWED_ROLES.has(role)) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid role", {
					field: "role",
					value: role,
					allowed: Array.from(ALLOWED_ROLES),
				});
			}

			patch.role = role;
		}

		// branchId (optional, can be null)
		if (Object.prototype.hasOwnProperty.call(body, "branchId")) {
			if (body.branchId === null) {
				patch.branchId = null;
			} else if (isNonEmptyString(body.branchId)) {
				patch.branchId = normalizeBranchId(body.branchId);
			} else {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid branchId", {
					field: "branchId",
					value: body.branchId,
					pattern: "^NL\\d+$",
				});
			}
		}

		// mustChangePassword (optional)
		if (Object.prototype.hasOwnProperty.call(body, "mustChangePassword")) {
			if (typeof body.mustChangePassword !== "boolean") {
				throw badRequest(
					"VALIDATION_INVALID_FIELD",
					"Invalid mustChangePassword",
					{
						field: "mustChangePassword",
						value: body.mustChangePassword,
					},
				);
			}
			patch.mustChangePassword = body.mustChangePassword;
		}

		// --- Enforce role <-> branchId consistency --------------------------------
		const nextRole = patch.role ?? user.role;

		const nextBranchId = Object.prototype.hasOwnProperty.call(patch, "branchId")
			? patch.branchId
			: (user.branchId ?? null);

		if (nextRole === USER_ROLES.BRANCH) {
			if (!isNonEmptyString(nextBranchId)) {
				throw badRequest(
					"VALIDATION_MISSING_FIELD",
					"Missing required fields",
					{
						fields: ["branchId"],
					},
				);
			}

			const normalized = normalizeBranchId(nextBranchId);

			if (!BRANCH_RE.test(normalized)) {
				throw badRequest("VALIDATION_INVALID_FIELD", "Invalid branchId", {
					field: "branchId",
					value: normalized,
					pattern: "^NL\\d+$",
				});
			}

			patch.branchId = normalized;
		} else {
			// For non-branch users, always clear branchId
			patch.branchId = null;
		}

		// --- Apply patch ----------------------------------------------------------
		if (Object.prototype.hasOwnProperty.call(patch, "username"))
			user.username = patch.username;

		if (Object.prototype.hasOwnProperty.call(patch, "email"))
			user.email = patch.email;

		if (Object.prototype.hasOwnProperty.call(patch, "role"))
			user.role = patch.role;

		if (Object.prototype.hasOwnProperty.call(patch, "branchId"))
			user.branchId = patch.branchId;

		if (Object.prototype.hasOwnProperty.call(patch, "mustChangePassword"))
			user.mustChangePassword = patch.mustChangePassword;

		try {
			await user.save();
		} catch (err) {
			if (err && err.code === 11000) {
				const field = pickDuplicateField(err) || "unknown";
				throw badRequest("VALIDATION_INVALID_FIELD", "Duplicate key", {
					field,
				});
			}
			throw err;
		}

		return json({ ok: true, user: toSafeUser(user) }, 200);
	},
	{ logPrefix: "[api/admin/users/[userId]]" },
);

export const POST = withErrorHandling(
	async function POST(_request, ctx) {
		const session = await getSession();

		if (!session) {
			throw unauthorized("AUTH_UNAUTHENTICATED", "Unauthorized");
		}

		requireUserManagement(session);

		const userId = await resolveValidatedUserId(ctx);

		if (String(session.userId) === userId) {
			throw badRequest(
				"VALIDATION_INVALID_FIELD",
				"Cannot reset current user password",
				{
					field: "userId",
					reason: "SELF_PASSWORD_RESET_FORBIDDEN",
				},
			);
		}

		await getDb();

		const user = await User.findById(userId).exec();
		if (!user) {
			throw notFound("USER_NOT_FOUND", "Not found", { userId });
		}

		const temporaryPassword = generateAdminTemporaryPassword();
		const passwordHash = await bcrypt.hash(
			temporaryPassword,
			BCRYPT_SALT_ROUNDS,
		);

		user.passwordHash = passwordHash;
		user.mustChangePassword = true;
		user.passwordResetToken = null;
		user.passwordResetExpiresAt = null;

		await user.save();

		return json(
			{
				ok: true,
				user: toSafeUser(user),
				temporaryPassword,
			},
			200,
		);
	},
	{ logPrefix: "[api/admin/users/[userId]]" },
);

export const DELETE = withErrorHandling(
	async function DELETE(request, ctx) {
		const session = await getSession();

		if (!session) {
			throw unauthorized("AUTH_UNAUTHENTICATED", "Unauthorized");
		}

		requireUserManagement(session);
		const userId = await resolveValidatedUserId(ctx);

		// Safety: prevent deleting your own currently active account.
		if (String(session.userId) === userId) {
			throw badRequest(
				"VALIDATION_INVALID_FIELD",
				"Cannot delete current user",
				{
					field: "userId",
					reason: "SELF_DELETE_FORBIDDEN",
				},
			);
		}

		await getDb();

		const deleted = await User.findByIdAndDelete(String(userId))
			.select(
				"_id username email role branchId mustChangePassword createdAt updatedAt",
			)
			.exec();

		if (!deleted) {
			throw notFound("USER_NOT_FOUND", "Not found", { userId: String(userId) });
		}

		return json({ ok: true, user: toSafeUser(deleted) }, 200);
	},
	{ logPrefix: "[api/admin/users/[userId]]" },
);
