import express, { Router, type IRouter } from "express";
import { createHash, createHmac, randomBytes, randomInt, timingSafeEqual } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, basename, extname } from "node:path";
import { asc, count, desc, eq, and, gt, inArray, isNull, lt } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  yeActivityTable,
  yeAttendanceTable,
  yeClientsTable,
  yeDocumentsTable,
  yeEmployeesTable,
  yeEmployeeCredentialsTable,
  yeEquipmentTable,
  yeMaterialsTable,
  yePoliciesTable,
  yeProjectsTable,
  yeQuotesTable,
  yeSecurityEventsTable,
  yeSettingsTable,
  yeUsersTable,
  yeWorksitesTable,
  yeAppointmentsTable,
  yeAlertsTable,
  yeAuditLogsTable,
  yeRolesTable,
  yeDevicesTable,
  yeProjectQrResourcesTable,
  yeQrChallengesTable,
  yeSecurityAttemptsTable,
  yeJobPostingsTable,
  yeJobApplicationsTable,
  yeResourcesTable,
  yeMediaTable,
  yePayrollExportsTable,
  yeLeaveRequestsTable,
  yePublicEntriesTable,
  yeAdminSecondFactorChallengesTable,
} from "@workspace/db";
import * as api from "@workspace/api-zod";
import {
  authenticate,
  clearSessionCookies,
  createSession,
  requireRoles,
  revokeSession,
  setSessionCookies,
  hashPassword,
  verifyPassword,
} from "../middleware/auth";
import { ensurePublicEntries } from "../lib/public-seed";

const router: IRouter = Router();

function idFrom(req: { params: Record<string, string | string[] | undefined> }) {
  const value = req.params.id;
  return Array.isArray(value) ? value[0] : value;
}

function fail(res: any, message: string, status = 400) {
  res.status(status).json({ error: message });
}

type MultipartFile = {
  fieldName: string;
  filename: string;
  contentType: string;
  data: Buffer;
};

const UPLOAD_LIMIT_BYTES = 5 * 1024 * 1024;
const CONTACT_UPLOAD_LIMIT_BYTES = 12 * 1024 * 1024;
const privateUploadRoot = join(process.cwd(), "private_uploads");
const allowedContactFiles: Record<string, string[]> = {
  ".pdf": ["application/pdf"],
  ".doc": ["application/msword", "application/octet-stream"],
  ".docx": ["application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/octet-stream"],
  ".txt": ["text/plain", "application/octet-stream"],
  ".png": ["image/png"],
  ".jpg": ["image/jpeg"],
  ".jpeg": ["image/jpeg"],
  ".webp": ["image/webp"],
  ".gif": ["image/gif"],
};

function parseMultipartBody(req: express.Request): { fields: Record<string, string>; files: MultipartFile[] } {
  const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
  const contentType = String(req.headers["content-type"] ?? "");
  const boundary = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i)?.[1] ?? contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i)?.[2];
  if (!boundary || body.length === 0) return { fields: {}, files: [] };
  const delimiter = Buffer.from(`--${boundary}`);
  const separator = Buffer.from("\r\n\r\n");
  const fields: Record<string, string> = {};
  const files: MultipartFile[] = [];
  let cursor = 0;
  while (cursor < body.length) {
    const start = body.indexOf(delimiter, cursor);
    if (start < 0) break;
    let partStart = start + delimiter.length;
    if (body.subarray(partStart, partStart + 2).toString() === "--") break;
    if (body.subarray(partStart, partStart + 2).toString() === "\r\n") partStart += 2;
    const next = body.indexOf(delimiter, partStart);
    if (next < 0) break;
    let partEnd = next;
    if (body.subarray(partEnd - 2, partEnd).toString() === "\r\n") partEnd -= 2;
    const part = body.subarray(partStart, partEnd);
    const headerEnd = part.indexOf(separator);
    if (headerEnd < 0) { cursor = next; continue; }
    const headers = part.subarray(0, headerEnd).toString("utf8");
    const data = part.subarray(headerEnd + separator.length);
    const name = headers.match(/name="([^"]+)"/i)?.[1];
    if (!name) { cursor = next; continue; }
    const filename = headers.match(/filename="([^"]*)"/i)?.[1];
    if (filename) {
      files.push({
        fieldName: name,
        filename: basename(filename),
        contentType: headers.match(/content-type:\s*([^\r\n]+)/i)?.[1]?.trim().toLowerCase() || "application/octet-stream",
        data,
      });
    } else {
      fields[name] = data.toString("utf8");
    }
    cursor = next;
  }
  return { fields, files };
}

function validateUpload(file: MultipartFile, mode: "resume" | "contact"): string | null {
  if (file.data.length === 0) return "El archivo está vacío.";
  if (file.data.length > UPLOAD_LIMIT_BYTES) return "Cada archivo debe pesar 5 MB o menos.";
  const extension = extname(file.filename).toLowerCase();
  const acceptedTypes = mode === "resume" ? [".pdf"] : Object.keys(allowedContactFiles);
  if (!acceptedTypes.includes(extension)) return mode === "resume" ? "El resume debe estar en formato PDF." : "Este formato de archivo no está permitido.";
  if (mode === "resume" && (file.contentType !== "application/pdf" || file.data.subarray(0, 5).toString() !== "%PDF-")) return "El resume debe ser un PDF válido.";
  if (mode === "contact" && !allowedContactFiles[extension]?.includes(file.contentType) && file.contentType !== "application/octet-stream") return "El tipo de archivo no coincide con su extensión.";
  return null;
}

async function savePrivateUpload(file: MultipartFile): Promise<string> {
  await mkdir(privateUploadRoot, { recursive: true });
  const safeExtension = extname(file.filename).toLowerCase();
  const storedName = `${crypto.randomUUID()}${safeExtension}`;
  await writeFile(join(privateUploadRoot, storedName), file.data, { flag: "wx" });
  return `/api/uploads/${storedName}`;
}

function canWrite(req: any, res: any): boolean {
  const role = req.auth?.user.role;
  if (role === "employee") {
    fail(res, "El rol empleado es de solo lectura", 403);
    return false;
  }
  const scopes: Record<string, string[]> = {
    secretary: ["/quotes", "/clients", "/appointments"],
    accounting: ["/payroll", "/leave-requests"],
    supervisor: ["/worksites", "/attendance", "/alerts"],
  };
  const allowed = scopes[role ?? ""];
  if (allowed && !allowed.some(prefix => req.path === prefix || req.path.startsWith(`${prefix}/`))) {
    fail(res, "Tu rol no tiene alcance para modificar este recurso", 403);
    return false;
  }
  return true;
}

async function activity(action: string, severity = "info") {
  await db.insert(yeActivityTable).values({ action, actor: "Sistema Y.E.", severity });
}

const loginAttempts = new Map<string, { count: number; resetAt: number }>();
const MOBILE_CREDENTIAL_ROLES = new Set(["Web Master", "super_admin", "admin", "hr"]);
const SECURITY_READ_ROLES = new Set(["Web Master", "super_admin", "admin", "hr", "accounting"]);
const ROOT_KEY = process.env.SESSION_SECRET;
if ((!process.env.QR_SIGNING_KEY || !process.env.EMPLOYEE_CREDENTIAL_PEPPER) && !ROOT_KEY) {
  throw new Error("SESSION_SECRET or both dedicated QR_SIGNING_KEY and EMPLOYEE_CREDENTIAL_PEPPER are required");
}
const deriveKey = (dedicated: string | undefined, context: string) => dedicated
  ?? createHmac("sha256", ROOT_KEY!).update(`Y.E./key-derivation/${context}/v1`).digest();
const QR_KEY = deriveKey(process.env.QR_SIGNING_KEY, "qr-signing");
const CREDENTIAL_PEPPER = deriveKey(process.env.EMPLOYEE_CREDENTIAL_PEPPER, "employee-code");
const ADMIN_FACTOR_KEY = deriveKey(process.env.ADMIN_SECOND_FACTOR_PEPPER, "admin-second-factor");
const QR_KEY_VERSION = process.env.QR_KEY_VERSION ?? "v1";

const sha256 = (value: string) => createHash("sha256").update(value).digest("hex");
const credentialDigest = (value: string) => CREDENTIAL_PEPPER
  ? createHmac("sha256", CREDENTIAL_PEPPER).update(value).digest("hex")
  : null;
const secondFactorDigest = (value: string) => createHmac("sha256", ADMIN_FACTOR_KEY).update(value).digest("hex");
const secondFactorMethods = new Set(["daily_code", "simple_math", "captcha"]);
const isSecondFactorMethod = (value: string): value is "daily_code" | "simple_math" | "captcha" =>
  secondFactorMethods.has(value);
const dailyCodeExpiry = () => {
  const expiry = new Date();
  expiry.setUTCHours(23, 59, 59, 999);
  return expiry;
};
const equalDigest = (actual: string, expected: string) => {
  const actualBuffer = Buffer.from(actual, "hex");
  const expectedBuffer = Buffer.from(expected, "hex");
  return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
};
const weakEmployeeCode = (value: string) => {
  const points = [...value].map(char => char.codePointAt(0) ?? 0);
  const monotonic = points.length >= 6 && (points.every((point, index) => index === 0 || point === points[index - 1] + 1)
    || points.every((point, index) => index === 0 || point === points[index - 1] - 1));
  return /^(?:\d{9}|\d{3}-\d{2}-\d{4})$/.test(value)
    || /^(.)\1+$/.test(value)
    || monotonic;
};

type SecondFactorMethod = "daily_code" | "simple_math" | "captcha";

async function secondFactorConfig() {
  const [settings] = await db.select({
    enabled: yeSettingsTable.adminSecondFactorEnabled,
    method: yeSettingsTable.adminSecondFactorMethod,
  }).from(yeSettingsTable).limit(1);
  const method = settings?.method && isSecondFactorMethod(settings.method) ? settings.method : "daily_code";
  return { enabled: settings?.enabled ?? false, method };
}

async function issueDailyCode(userId: string, issuedBy: string) {
  const now = new Date();
  const expiresAt = dailyCodeExpiry();
  await db.update(yeAdminSecondFactorChallengesTable)
    .set({ consumedAt: now })
    .where(and(
      eq(yeAdminSecondFactorChallengesTable.userId, userId),
      eq(yeAdminSecondFactorChallengesTable.method, "daily_code"),
      isNull(yeAdminSecondFactorChallengesTable.consumedAt),
      gt(yeAdminSecondFactorChallengesTable.expiresAt, now),
    ));
  const code = randomInt(100000, 1000000).toString();
  const [challenge] = await db.insert(yeAdminSecondFactorChallengesTable).values({
    userId,
    method: "daily_code",
    responseHash: secondFactorDigest(code),
    expiresAt,
    issuedBy,
  }).returning();
  return { challenge, code };
}

async function createSecondFactorChallenge(userId: string, method: SecondFactorMethod) {
  if (method === "daily_code") {
    const [challenge] = await db.select().from(yeAdminSecondFactorChallengesTable).where(and(
      eq(yeAdminSecondFactorChallengesTable.userId, userId),
      eq(yeAdminSecondFactorChallengesTable.method, method),
      isNull(yeAdminSecondFactorChallengesTable.consumedAt),
      lt(yeAdminSecondFactorChallengesTable.attempts, 5),
      gt(yeAdminSecondFactorChallengesTable.expiresAt, new Date()),
    )).orderBy(desc(yeAdminSecondFactorChallengesTable.createdAt)).limit(1);
    return challenge ?? null;
  }

  const challengeText = method === "simple_math"
    ? `${randomInt(1, 20)} + ${randomInt(1, 20)} = ?`
    : Array.from({ length: 6 }, () => "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"[randomInt(0, 32)]).join("");
  const response = method === "simple_math"
    ? String(challengeText.split(" ").slice(0, 3).filter(Boolean).reduce((sum, value) => sum + (Number(value) || 0), 0))
    : challengeText;
  const [challenge] = await db.insert(yeAdminSecondFactorChallengesTable).values({
    userId,
    method,
    responseHash: secondFactorDigest(response),
    challengeText,
    expiresAt: new Date(Date.now() + 10 * 60 * 1000),
  }).returning();
  return challenge;
}

async function verifySecondFactor(userId: string, method: SecondFactorMethod, challengeId: string, answer: string) {
  const [challenge] = await db.select().from(yeAdminSecondFactorChallengesTable).where(and(
    eq(yeAdminSecondFactorChallengesTable.id, challengeId),
    eq(yeAdminSecondFactorChallengesTable.userId, userId),
    eq(yeAdminSecondFactorChallengesTable.method, method),
  )).limit(1);
  const now = new Date();
  if (!challenge || challenge.consumedAt || challenge.expiresAt <= now || challenge.attempts >= 5) return false;
  const valid = equalDigest(secondFactorDigest(answer.trim().toUpperCase()), challenge.responseHash);
  if (!valid) {
    await db.update(yeAdminSecondFactorChallengesTable)
      .set({ attempts: challenge.attempts + 1 })
      .where(eq(yeAdminSecondFactorChallengesTable.id, challenge.id));
    return false;
  }
  await db.update(yeAdminSecondFactorChallengesTable)
    .set({ consumedAt: now })
    .where(eq(yeAdminSecondFactorChallengesTable.id, challenge.id));
  return true;
}
const codeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
function randomEmployeeCode() {
  const length = randomInt(12, 21);
  return Array.from({ length }, () => codeAlphabet[randomInt(0, codeAlphabet.length)]).join("");
}
const opaque = (bytes = 32) => randomBytes(bytes).toString("base64url");
const safeEqual = (left: string, right: string) => {
  const a = Buffer.from(left, "hex");
  const b = Buffer.from(right, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
};
const credentialRole = (req: any) => MOBILE_CREDENTIAL_ROLES.has(req.auth?.user.role);
const securityReader = (req: any) => SECURITY_READ_ROLES.has(req.auth?.user.role);

function qrSign(payload: Record<string, unknown>) {
  if (!QR_KEY) throw new Error("QR signing key is not configured");
  const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
  const signature = createHmac("sha256", QR_KEY).update(`${QR_KEY_VERSION}.${encoded}`).digest("base64url");
  return { encoded, signature };
}

function qrVerify(encoded: string, signature: string): Record<string, any> | null {
  if (!QR_KEY) return null;
  const expected = createHmac("sha256", QR_KEY).update(`${QR_KEY_VERSION}.${encoded}`).digest();
  const actual = Buffer.from(signature, "base64url");
  if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return null;
  try {
    const payload = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
    return payload && typeof payload === "object" ? payload : null;
  } catch {
    return null;
  }
}

function distanceMeters(lat1: number, lon1: number, lat2: number, lon2: number) {
  const radians = (value: number) => value * Math.PI / 180;
  const a = Math.sin(radians(lat2 - lat1) / 2) ** 2
    + Math.cos(radians(lat1)) * Math.cos(radians(lat2)) * Math.sin(radians(lon2 - lon1) / 2) ** 2;
  return 6371000 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

function inSchedule(now: Date, start: string | null, end: string | null, grace: number) {
  if (!start || !end) return true;
  const minutes = now.getUTCHours() * 60 + now.getUTCMinutes();
  const parse = (value: string) => {
    const [hour, minute] = value.split(":").map(Number);
    return hour * 60 + minute;
  };
  const from = parse(start) - grace;
  const to = parse(end) + grace;
  return from <= to ? minutes >= from && minutes <= to : minutes >= from || minutes <= to;
}

async function securityAttempt(values: {
  employeeId?: string | null; employeeNumber?: string | null; projectId?: string | null;
  worksiteId?: string | null; deviceId?: string | null; platform?: string | null;
  result: string; reason: string; latitude?: number | null; longitude?: number | null;
  riskScore: number; challengeId?: string | null;
}) {
  await db.insert(yeSecurityAttemptsTable).values(values);
}

const crud = ({
  path,
  table,
  listSchema,
  createSchema,
  createResponse,
  updateSchema,
  updateResponse,
  prepare,
  output,
}: any) => {
  router.get(path, async (req, res): Promise<void> => {
    const rows = await db.select().from(table).orderBy(desc(table.createdAt ?? table.updatedAt ?? table.id));
    res.json(listSchema.parse(rows.map(output ?? ((row: any) => row))));
  });
  router.post(path, async (req, res): Promise<void> => {
    if (!canWrite(req, res)) return;
    const parsed = createSchema.safeParse(req.body);
    if (!parsed.success) {
      fail(res, parsed.error.message);
      return;
    }
    const values = prepare ? await prepare(parsed.data) : parsed.data;
    const inserted = await db.insert(table).values(values).returning() as any;
    const row = inserted[0];
    await activity(`Nuevo registro creado en ${path.slice(1)}`);
    res.status(201).json(createResponse.parse(output ? output(row) : row));
  });
  router.patch(`${path}/:id`, async (req, res): Promise<void> => {
    if (!canWrite(req, res)) return;
    const id = idFrom(req);
    const parsed = updateSchema.safeParse(req.body);
    if (!id || !parsed.success) {
      fail(res, parsed.success ? "Identificador inválido" : parsed.error.message);
      return;
    }
    const [row] = await db
      .update(table)
      .set({ ...parsed.data, updatedAt: new Date() })
      .where(eq(table.id, id))
      .returning();
    if (!row) {
      fail(res, "Registro no encontrado", 404);
      return;
    }
    await activity(`Registro actualizado en ${path.slice(1)}`);
    res.json(updateResponse.parse(output ? output(row) : row));
  });
  router.delete(`${path}/:id`, async (req, res): Promise<void> => {
    if (!canWrite(req, res)) return;
    const id = idFrom(req);
    if (!id) {
      fail(res, "Identificador inválido");
      return;
    }
    const deleted = await db.delete(table).where(eq(table.id, id)).returning() as any;
    const row = deleted[0];
    if (!row) {
      fail(res, "Registro no encontrado", 404);
      return;
    }
    await activity(`Registro retirado de ${path.slice(1)}`, "warning");
    res.sendStatus(204);
  });
};

router.get("/public/overview", async (_req, res): Promise<void> => {
  const [equipment, projects] = await Promise.all([
    db.select({ total: count() }).from(yeEquipmentTable),
    db.select({ total: count() }).from(yeProjectsTable).where(eq(yeProjectsTable.status, "active")),
  ]);
  res.json(api.GetPublicOverviewResponse.parse({
    companyName: "Y. E. Transport & Constructors",
    tagline: "Tú alquilas. Nosotros construimos.",
    location: "Puerto Rico",
    serviceAreas: ["Transporte", "Equipo pesado", "Materiales", "Construcción general", "Construcción industrial"],
    fleetCount: Number(equipment[0]?.total ?? 0),
    activeProjects: Number(projects[0]?.total ?? 0),
    featuredImage: null,
  }));
});

router.post("/public/quotes", async (req, res): Promise<void> => {
  const parsed = api.CreateQuoteRequestBody.safeParse(req.body);
  if (!parsed.success) {
    fail(res, parsed.error.message);
    return;
  }
  const [row] = await db.insert(yeQuotesTable).values({
    ...parsed.data,
    quoteNumber: `YE-${new Date().getFullYear()}-${String(Date.now()).slice(-4)}`,
  }).returning();
  await activity(`Nueva solicitud de cotización de ${parsed.data.clientName}`, "warning");
  res.status(201).json(api.CreateQuoteRequestResponse.parse(row));
});

router.post("/public/contact", express.raw({ type: "multipart/form-data", limit: `${CONTACT_UPLOAD_LIMIT_BYTES}b` }), async (req, res): Promise<void> => {
  const multipart = parseMultipartBody(req);
  if (Object.keys(multipart.fields).length > 0) req.body = multipart.fields;
  const parsed = api.CreateContactMessageBody.safeParse(req.body);
  if (!parsed.success) {
    fail(res, parsed.error.message);
    return;
  }
  const attachments = multipart.files.filter(file => file.fieldName === "attachments");
  if (attachments.length > 5) {
    fail(res, "Puedes adjuntar hasta 5 archivos.");
    return;
  }
  const totalBytes = attachments.reduce((total, file) => total + file.data.length, 0);
  if (totalBytes > CONTACT_UPLOAD_LIMIT_BYTES) {
    fail(res, "Los adjuntos no pueden superar 12 MB en total.");
    return;
  }
  for (const file of attachments) {
    const error = validateUpload(file, "contact");
    if (error) { fail(res, error); return; }
  }
  const attachmentUrls = await Promise.all(attachments.map(savePrivateUpload));
  await activity(`Nuevo mensaje de contacto de ${parsed.data.name}${attachmentUrls.length ? ` · Adjuntos: ${attachmentUrls.join(", ")}` : ""}`, "info");
  res.status(201).json(api.CreateContactMessageResponse.parse({
    id: crypto.randomUUID(),
    receivedAt: new Date(),
  }));
});

router.post("/public/jobs", express.raw({ type: "multipart/form-data", limit: `${CONTACT_UPLOAD_LIMIT_BYTES}b` }), async (req, res): Promise<void> => {
  const multipart = parseMultipartBody(req);
  const resume = multipart.files.find(file => file.fieldName === "resume");
  const required = {
    name: multipart.fields.name,
    email: multipart.fields.email,
    phone: multipart.fields.phone,
    service: multipart.fields.service,
    message: multipart.fields.message,
  };
  if (!required.name || !required.email || !required.message || !resume) {
    fail(res, "Nombre, email, detalles y un resume PDF son requeridos.");
    return;
  }
  const resumeError = validateUpload(resume, "resume");
  if (resumeError) { fail(res, resumeError); return; }
  const resumeUrl = await savePrivateUpload(resume);
  const [application] = await db.insert(yeJobApplicationsTable).values({
    applicantName: required.name.trim(),
    email: required.email.trim(),
    phone: required.phone?.trim() || null,
    resumeUrl,
    certifications: required.service?.trim() || null,
    notes: required.message.trim(),
    status: "received",
  }).returning();
  await activity(`Nueva solicitud de empleo de ${required.name.trim()} · Resume: ${resumeUrl}`, "info");
  res.status(201).json({ id: application.id, receivedAt: application.createdAt });
});

/**
 * Employee mobile authentication deliberately has a separate route from the
 * administrative email login. Employee codes are issued by corporate staff,
 * HMAC-digested with a server-side pepper before persistence, and are never
 * accepted as email aliases or passwords.
 */
router.post("/auth/mobile/login", async (req, res): Promise<void> => {
  const suppliedCode = String(req.body?.employeeCode ?? "").trim();
  const key = `${req.ip}:mobile:${credentialDigest(suppliedCode) ?? "unconfigured"}`;
  const now = Date.now();
  const attempt = loginAttempts.get(key);
  if (attempt && attempt.resetAt > now && attempt.count >= 8) {
    res.status(429).json({ error: "Demasiados intentos. Intenta de nuevo más tarde." });
    return;
  }
  const employeeCode = suppliedCode;
  const installationId = String(req.body?.installationId ?? "");
  const deviceCredentialInput = typeof req.body?.deviceCredential === "string" ? req.body.deviceCredential : "";
  const platform = String(req.body?.platform ?? "");
  const codeHash = credentialDigest(employeeCode);
  if (!codeHash || !/^[A-Za-z0-9_-]{6,20}$/.test(employeeCode) || weakEmployeeCode(employeeCode) ||
      installationId.length < 16 || !["ios", "android"].includes(platform)) {
    res.status(401).json({ error: "Credenciales móviles inválidas." });
    return;
  }
  const [credential] = await db.select().from(yeEmployeeCredentialsTable)
    .where(and(eq(yeEmployeeCredentialsTable.employeeCodeHash, codeHash), eq(yeEmployeeCredentialsTable.status, "active")))
    .limit(1);
  const [employee] = credential
    ? await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.id, credential.employeeId)).limit(1)
    : [];
  const [user] = employee?.userId
    ? await db.select().from(yeUsersTable).where(and(eq(yeUsersTable.id, employee.userId), eq(yeUsersTable.active, true))).limit(1)
    : [];
  if (!credential || !employee || !user || employee.status !== "active") {
    const current = loginAttempts.get(key) ?? { count: 0, resetAt: now + 15 * 60 * 1000 };
    current.count += 1;
    loginAttempts.set(key, current);
    res.status(401).json({ error: "Credenciales móviles inválidas." });
    return;
  }
  const installationHash = sha256(installationId);
  const [sameDevice] = await db.select().from(yeDevicesTable)
    .where(and(eq(yeDevicesTable.employeeId, employee.id), eq(yeDevicesTable.installationIdHash, installationHash))).limit(1);
  const [otherActive] = await db.select().from(yeDevicesTable)
    .where(and(eq(yeDevicesTable.employeeId, employee.id), inArray(yeDevicesTable.status, ["approved", "pending"]))).limit(1);
  if (sameDevice && ["revoked", "replaced", "rejected"].includes(sameDevice.status)) {
    res.status(403).json({ error: "Dispositivo no autorizado.", device: { id: sameDevice.id, status: sameDevice.status, platform: sameDevice.platform } });
    return;
  }
  if (sameDevice) {
    if (!deviceCredentialInput || !sameDevice.deviceCredentialHash || !safeEqual(sha256(deviceCredentialInput), sameDevice.deviceCredentialHash)) {
      res.status(403).json({ error: "Credencial de dispositivo requerida o inválida." });
      return;
    }
    if (sameDevice.status !== "approved") {
      res.status(403).json({ error: "El dispositivo aún requiere aprobación.", device: { id: sameDevice.id, status: sameDevice.status, platform: sameDevice.platform } });
      return;
    }
  }
  if (!sameDevice && otherActive) {
    res.status(403).json({ error: "Ya existe otro dispositivo activo. Solicita un reemplazo al personal autorizado.", device: { id: otherActive.id, status: "pending", platform } });
    return;
  }
  let device = sameDevice;
  let deviceCredential: string | undefined;
  if (!device) {
    deviceCredential = opaque(32);
    [device] = await db.insert(yeDevicesTable).values({
      employeeId: employee.id,
      employeeName: employee.name,
      platform,
      model: typeof req.body?.model === "string" ? req.body.model.slice(0, 190) : null,
      osVersion: typeof req.body?.osVersion === "string" ? req.body.osVersion.slice(0, 80) : null,
      appVersion: typeof req.body?.appVersion === "string" ? req.body.appVersion.slice(0, 80) : null,
      installationIdHash: installationHash,
      deviceCredentialHash: sha256(deviceCredential),
      status: "pending",
    }).returning();
  }
  if (!device) { res.status(503).json({ error: "No se pudo registrar el dispositivo." }); return; }
  const session = await createSession(user, "mobile", req);
  await db.update(yeEmployeeCredentialsTable).set({ lastUsedAt: new Date() }).where(eq(yeEmployeeCredentialsTable.id, credential.id));
  loginAttempts.delete(key);
  await db.insert(yeAuditLogsTable).values({
    eventType: "authentication", actorName: user.name, actorRole: user.role,
    resourceType: "mobile_session", resourceId: session.session?.id, action: "login",
    ipAddress: req.ip, device: platform,
  });
  res.json({
    user: { id: user.id, name: user.name, role: user.role, initials: user.initials, permissions: [] },
    token: session.token,
    device: { id: device.id, status: device.status, platform: device.platform, model: device.model, osVersion: device.osVersion, appVersion: device.appVersion },
    ...(deviceCredential ? { deviceCredential } : {}),
  });
});

router.post("/auth/login", async (req, res): Promise<void> => {
  const key = `${req.ip}:${String(req.body?.email ?? "").toLowerCase()}`;
  const now = Date.now();
  const attempt = loginAttempts.get(key);
  if (attempt && attempt.resetAt > now && attempt.count >= 8) {
    res.status(429).json({ error: "Demasiados intentos. Intenta de nuevo más tarde." });
    return;
  }
  if (attempt && attempt.resetAt <= now) loginAttempts.delete(key);
  const parsed = api.LoginBody.safeParse(req.body);
  if (!parsed.success) {
    fail(res, "Correo y contraseña son requeridos", 400);
    return;
  }
  const email = parsed.data.email.toLowerCase().trim();
  const [user] = await db.select().from(yeUsersTable).where(eq(yeUsersTable.email, email)).limit(1);
  const valid = user ? await verifyPassword(parsed.data.password, user.passwordHash) : false;
  if (!valid || !user?.active) {
    const current = loginAttempts.get(key) ?? { count: 0, resetAt: now + 15 * 60 * 1000 };
    current.count += 1;
    loginAttempts.set(key, current);
    res.status(401).json({ error: "Correo o contraseña inválidos" });
    return;
  }
  loginAttempts.delete(key);
  const client = parsed.data.client ?? "web";
  if (client === "mobile") {
    // Mobile clients must use the employee-code flow, which binds their
    // session to a server-issued device credential.
    res.status(400).json({ error: "Usa el acceso móvil con código corporativo." });
    return;
  }
  const factorConfig = await secondFactorConfig();
  if (factorConfig.enabled) {
    const secondFactor = parsed.data.secondFactor;
    if (!secondFactor?.challengeId || !secondFactor.code) {
      const challenge = await createSecondFactorChallenge(user.id, factorConfig.method);
      if (!challenge) {
        fail(res, "No hay un código diario emitido para este usuario. Solicita uno desde una sesión administrativa activa.", 428);
        return;
      }
      res.json(api.LoginResponse.parse({
        requiresSecondFactor: true,
        method: factorConfig.method,
        challengeId: challenge.id,
        ...(challenge.challengeText ? { challenge: challenge.challengeText } : {}),
        expiresAt: challenge.expiresAt,
      }));
      return;
    }
    const validSecondFactor = isSecondFactorMethod(factorConfig.method)
      && await verifySecondFactor(user.id, factorConfig.method, secondFactor.challengeId, secondFactor.code);
    if (!validSecondFactor) {
      await db.insert(yeAuditLogsTable).values({
        eventType: "authentication",
        actorName: user.name,
        actorRole: user.role,
        resourceType: "admin_second_factor",
        resourceId: secondFactor.challengeId,
        action: "verification_failed",
        reason: "Desafío inválido, expirado o agotado",
        ipAddress: req.ip,
        device: req.get("user-agent")?.slice(0, 500),
      });
      res.status(401).json({ error: "Segundo factor inválido o expirado" });
      return;
    }
    await db.insert(yeAuditLogsTable).values({
      eventType: "authentication",
      actorName: user.name,
      actorRole: user.role,
      resourceType: "admin_second_factor",
      resourceId: secondFactor.challengeId,
      action: "verified",
      ipAddress: req.ip,
      device: req.get("user-agent")?.slice(0, 500),
    });
  }
  const session = await createSession(user, client, req);
  if (client === "web") setSessionCookies(res, session.token, session.csrfToken);
  await db.insert(yeAuditLogsTable).values({
    eventType: "authentication",
    actorName: user.name,
    actorRole: user.role,
    resourceType: "session",
    resourceId: session.session?.id,
    action: "login",
    ipAddress: req.ip,
    device: req.get("user-agent")?.slice(0, 500),
  });
  const currentUser = {
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role,
    initials: user.initials,
    permissions: user.role === "Web Master" || user.role === "super_admin" ? ["*"] : [],
  };
  res.json(api.LoginResponse.parse({ user: currentUser, csrfToken: session.csrfToken }));
});

router.get("/public/content", async (_req, res): Promise<void> => {
  const [settings] = await db.select({ siteContent: yeSettingsTable.siteContent }).from(yeSettingsTable).limit(1);
  res.json(api.GetPublicContentResponse.parse(settings?.siteContent ?? {}));
});

async function publicEntries(kind: "showcase" | "post" | "legal") {
  await ensurePublicEntries();
  return db.select().from(yePublicEntriesTable)
    .where(and(eq(yePublicEntriesTable.kind, kind), eq(yePublicEntriesTable.published, true), eq(yePublicEntriesTable.status, "published")))
    .orderBy(asc(yePublicEntriesTable.sortOrder), desc(yePublicEntriesTable.createdAt));
}

router.get("/public/showcase", async (_req, res): Promise<void> => {
  const rows = await publicEntries("showcase");
  res.json(api.ListPublicShowcaseResponse.parse(rows));
});

router.get("/public/posts", async (_req, res): Promise<void> => {
  const rows = await publicEntries("post");
  res.json(api.ListPublicPostsResponse.parse(rows));
});

router.get("/public/legal", async (_req, res): Promise<void> => {
  const rows = await publicEntries("legal");
  res.json(api.ListPublicLegalResponse.parse(rows));
});

router.get("/public/entries/:kind/:slug", async (req, res): Promise<void> => {
  const parsed = api.GetPublicEntryParams.safeParse(req.params);
  if (!parsed.success) { fail(res, parsed.error.message); return; }
  await ensurePublicEntries();
  const [row] = await db.select().from(yePublicEntriesTable).where(and(
    eq(yePublicEntriesTable.kind, parsed.data.kind),
    eq(yePublicEntriesTable.slug, parsed.data.slug),
    eq(yePublicEntriesTable.published, true),
    eq(yePublicEntriesTable.status, "published"),
  )).limit(1);
  if (!row) { fail(res, "Publicación no encontrada", 404); return; }
  res.json(api.GetPublicEntryResponse.parse(row));
});

router.use(authenticate);
router.get("/uploads/:filename", requireRoles("Web Master", "super_admin", "admin", "hr"), async (req, res): Promise<void> => {
  const filename = basename(String(req.params.filename ?? ""));
  if (!filename || filename !== req.params.filename || filename.includes("..")) {
    fail(res, "Archivo inválido", 400);
    return;
  }
  try {
    const contents = await readFile(join(privateUploadRoot, filename));
    res.setHeader("Content-Type", filename.toLowerCase().endsWith(".pdf") ? "application/pdf" : "application/octet-stream");
    res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
    res.setHeader("Cache-Control", "private, no-store");
    res.send(contents);
  } catch {
    fail(res, "Archivo no encontrado", 404);
  }
});

router.post("/auth/second-factor/daily-code", requireRoles("Web Master", "super_admin"), async (req, res): Promise<void> => {
  const issued = await issueDailyCode(req.auth!.user.id, req.auth!.user.id);
  await db.insert(yeAuditLogsTable).values({
    eventType: "authentication",
    actorName: req.auth!.user.name,
    actorRole: req.auth!.user.role,
    resourceType: "admin_second_factor",
    resourceId: issued.challenge.id,
    action: "daily_code_issued",
    reason: "Código mostrado una sola vez",
    ipAddress: req.ip,
    device: req.get("user-agent")?.slice(0, 500),
  });
  res.status(201).json(api.IssueAdminSecondFactorDailyCodeResponse.parse({
    challengeId: issued.challenge.id,
    code: issued.code,
    expiresAt: issued.challenge.expiresAt,
    shownOnce: true,
  }));
});

router.post("/auth/impersonation", requireRoles("Web Master", "super_admin"), async (req, res): Promise<void> => {
  const parsed = api.StartImpersonationBody.safeParse(req.body);
  if (!parsed.success) { fail(res, parsed.error.message); return; }
  const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.id, parsed.data.employeeId)).limit(1);
  if (!employee) { fail(res, "Empleado no encontrado", 404); return; }
  if (employee.status !== "active") { fail(res, "Solo se puede abrir personal activo", 403); return; }

  let [targetUser] = employee.userId
    ? await db.select().from(yeUsersTable).where(eq(yeUsersTable.id, employee.userId)).limit(1)
    : [];
  if (!targetUser) {
    const [createdUser] = await db.insert(yeUsersTable).values({
      name: employee.name,
      email: `employee-${employee.id}@internal.ye`,
      role: "employee",
      initials: employee.name.split(/\s+/).map(part => part[0]).join("").slice(0, 2).toUpperCase() || "YE",
      passwordHash: await hashPassword(randomBytes(24).toString("base64url")),
      active: true,
    }).returning();
    targetUser = createdUser;
    await db.update(yeEmployeesTable).set({ userId: targetUser.id }).where(eq(yeEmployeesTable.id, employee.id));
  }
  if (!targetUser.active) { fail(res, "El usuario del empleado está inactivo", 403); return; }

  const session = await createSession(targetUser, "impersonation", req, {
    impersonatorUserId: req.auth!.user.id,
    impersonatedUserId: targetUser.id,
  });
  await db.insert(yeAuditLogsTable).values({
    eventType: "authentication",
    actorName: req.auth!.user.name,
    actorRole: req.auth!.user.role,
    resourceType: "impersonation_session",
    resourceId: session.session.id,
    action: "impersonation_started",
    reason: `Acceso delegado a ${employee.name}`,
    ipAddress: req.ip,
    device: req.get("user-agent")?.slice(0, 500),
  });
  res.json(api.StartImpersonationResponse.parse({
    token: session.token,
    csrfToken: session.csrfToken,
    expiresAt: session.session.expiresAt,
    user: {
      id: targetUser.id,
      name: targetUser.name,
      email: targetUser.email,
      role: targetUser.role,
      initials: targetUser.initials,
      permissions: [],
    },
  }));
});
router.post("/auth/impersonation/end", async (req, res): Promise<void> => {
  if (req.auth?.sessionType !== "impersonation") {
    fail(res, "No hay una sesión delegada activa", 400);
    return;
  }
  await revokeSession(req.auth.sessionId);
  await db.insert(yeAuditLogsTable).values({
    eventType: "authentication",
    actorName: req.auth.user.name,
    actorRole: req.auth.user.role,
    resourceType: "impersonation_session",
    resourceId: req.auth.sessionId,
    action: "impersonation_ended",
    ipAddress: req.ip,
  });
  res.sendStatus(204);
});
// A mobile bearer session alone is not a device binding. Every subsequent
// mobile call must present the one-time server-issued device credential.
router.use(async (req, res, next) => {
  if (req.auth?.sessionType !== "mobile" || req.path === "/auth/logout") { next(); return; }
  const rawCredential = req.get("x-device-credential");
  if (!rawCredential || rawCredential.length < 32) {
    res.status(401).json({ error: "Credencial de dispositivo requerida" });
    return;
  }
  const [employee] = await db.select({ id: yeEmployeesTable.id }).from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth.user.id)).limit(1);
  const [device] = employee ? await db.select({ id: yeDevicesTable.id, status: yeDevicesTable.status }).from(yeDevicesTable).where(and(
    eq(yeDevicesTable.employeeId, employee.id),
    eq(yeDevicesTable.deviceCredentialHash, sha256(rawCredential)),
  )).limit(1) : [];
  if (!device || ["revoked", "replaced", "rejected"].includes(device.status)) {
    res.status(401).json({ error: "Credencial de dispositivo inválida" });
    return;
  }
  next();
});
router.use((req, res, next) => {
  if (req.auth?.user.role === "supervisor" &&
      !req.path.startsWith("/supervisor/") &&
      !req.path.startsWith("/auth/")) {
    res.status(403).json({ error: "Los supervisores deben usar la superficie asignada de supervisor" });
    return;
  }
  if (req.auth?.user.role !== "employee") { next(); return; }
  const ownRead = req.method === "GET" && (
    req.path === "/employees/me" ||
    req.path === "/attendance/me" ||
    req.path === "/mobile/devices/status" ||
    req.path === "/leave-requests/me" ||
    req.path === "/alerts" ||
    req.path === "/auth/me"
  );
  const ownWrite = req.auth.sessionType === "mobile" &&
    req.method === "POST" &&
    (req.path === "/attendance" || req.path === "/leave-requests");
  const authAction = req.path === "/auth/logout" || req.path === "/auth/csrf" || req.path === "/auth/impersonation/end";
  if (!ownRead && !ownWrite && !authAction) {
    res.status(403).json({ error: "El rol empleado solo puede acceder a su información móvil" });
    return;
  }
  next();
});

router.get("/auth/me", async (req, res): Promise<void> => {
  const user = req.auth?.user;
  if (!user) { fail(res, "Autenticación requerida", 401); return; }
  if (req.auth?.sessionType === "mobile") {
    res.json(api.GetCurrentUserResponse.parse({
      id: user.id,
      name: user.name,
      role: user.role,
      initials: user.initials,
      permissions: [],
    }));
    return;
  }
  res.json(api.GetCurrentUserResponse.parse({
    id: user.id,
    name: user.name,
    email: user.email,
    role: user.role,
    initials: user.initials,
    permissions: user.role === "Web Master" || user.role === "super_admin" ? ["*"] : [],
  }));
});

/**
 * Supervisor mobile endpoints intentionally live under their own namespace.
 * Scope is derived from worksites whose supervisorName matches the authenticated
 * user's name; no unrestricted employee, attendance or leave query is exposed.
 */
async function supervisorScope(user: { name: string }) {
  const allWorksites = await db.select().from(yeWorksitesTable);
  const normalizedName = user.name.trim().toLocaleLowerCase();
  const worksites = allWorksites.filter((site) =>
    (site.supervisorName ?? "").trim().toLocaleLowerCase() === normalizedName ||
    site.supervisorName === user.name,
  );
  const siteIds = new Set(worksites.map((site) => site.id));
  const siteNames = new Set(worksites.map((site) => site.name.trim().toLocaleLowerCase()));
  const allEmployees = await db.select().from(yeEmployeesTable);
  const employees = allEmployees.filter((employee) => {
    const assignment = (employee.assignedWorksite ?? "").trim().toLocaleLowerCase();
    return assignment !== "" && (siteIds.has(assignment) || siteNames.has(assignment));
  });
  const employeeIds = new Set(employees.map((employee) => employee.id));
  const employeeNumbers = new Set(employees.map((employee) => employee.employeeNumber));
  return { worksites, employees, siteIds, siteNames, employeeIds, employeeNumbers };
}

router.get("/supervisor/summary", requireRoles("supervisor"), async (req, res): Promise<void> => {
  const scope = await supervisorScope(req.auth!.user);
  const [allAttendance, allLeaveRequests, allAlerts, allSecurityEvents] = await Promise.all([
    db.select().from(yeAttendanceTable).orderBy(desc(yeAttendanceTable.capturedAt)),
    db.select().from(yeLeaveRequestsTable).orderBy(desc(yeLeaveRequestsTable.createdAt)),
    db.select().from(yeAlertsTable).orderBy(desc(yeAlertsTable.createdAt)),
    db.select().from(yeSecurityEventsTable).orderBy(desc(yeSecurityEventsTable.createdAt)),
  ]);
  const attendance = allAttendance.filter((record) =>
    (record.employeeId && scope.employeeIds.has(record.employeeId)) ||
    (record.worksiteId && scope.siteIds.has(record.worksiteId)) ||
    scope.employeeNumbers.has(record.employeeNumber) ||
    scope.siteNames.has(record.worksiteName.trim().toLocaleLowerCase()),
  );
  const leaveRequests = allLeaveRequests.filter((request) => request.employeeId && scope.employeeIds.has(request.employeeId));
  const worksiteMatches = (name: string | null) =>
    !name || scope.siteNames.has(name.trim().toLocaleLowerCase());
  const alerts = allAlerts.filter((alert) => worksiteMatches(alert.worksiteName));
  const securityEvents = allSecurityEvents.filter((event) => worksiteMatches(event.worksiteName));
  // Keep the dedicated endpoint's payload scoped, while retaining the useful
  // alert/leave fields that the generic PlatformRecord schema permits.
  res.json({ employees: scope.employees, worksites: scope.worksites, attendance, alerts, securityEvents, leaveRequests });
});

router.patch("/supervisor/attendance/:id", requireRoles("supervisor"), async (req, res): Promise<void> => {
  const scope = await supervisorScope(req.auth!.user);
  const id = idFrom(req);
  const [record] = await db.select().from(yeAttendanceTable).where(eq(yeAttendanceTable.id, id ?? ""));
  const inScope = record && (
    (record.employeeId && scope.employeeIds.has(record.employeeId)) ||
    (record.worksiteId && scope.siteIds.has(record.worksiteId)) ||
    scope.employeeNumbers.has(record.employeeNumber) ||
    scope.siteNames.has(record.worksiteName.trim().toLocaleLowerCase())
  );
  if (!record || !inScope) { fail(res, "Marcación fuera del equipo asignado", 403); return; }
  const body = req.body as Record<string, unknown>;
  const changes: Record<string, unknown> = {};
  if (body.action === "check_in" || body.action === "check_out") changes.action = body.action;
  if (typeof body.capturedAt === "string" && !Number.isNaN(Date.parse(body.capturedAt))) changes.capturedAt = new Date(body.capturedAt);
  if (body.worksiteId === null || typeof body.worksiteId === "string") {
    const worksiteId = body.worksiteId as string | null;
    if (worksiteId !== null && !scope.siteIds.has(worksiteId)) { fail(res, "Obra fuera del alcance asignado", 403); return; }
    changes.worksiteId = worksiteId;
    if (worksiteId) changes.worksiteName = scope.worksites.find((site) => site.id === worksiteId)?.name ?? record.worksiteName;
  }
  if (!Object.keys(changes).length) { fail(res, "No hay cambios válidos", 400); return; }
  const [updated] = await db.update(yeAttendanceTable).set(changes as any).where(eq(yeAttendanceTable.id, record.id)).returning();
  await activity(`Asistencia corregida por supervisor: ${record.employeeName}`, "warning");
  res.json(updated);
});

router.patch("/supervisor/leave-requests/:id", requireRoles("supervisor"), async (req, res): Promise<void> => {
  const scope = await supervisorScope(req.auth!.user);
  const id = idFrom(req);
  const [request] = await db.select().from(yeLeaveRequestsTable).where(eq(yeLeaveRequestsTable.id, id ?? ""));
  if (!request || !request.employeeId || !scope.employeeIds.has(request.employeeId)) {
    fail(res, "Solicitud fuera del equipo asignado", 403);
    return;
  }
  const status = req.body?.status;
  if (status !== "approved" && status !== "denied") { fail(res, "Estado de aprobación inválido", 400); return; }
  const [updated] = await db.update(yeLeaveRequestsTable).set({
    status,
    reviewedBy: req.auth!.user.name,
    updatedAt: new Date(),
  }).where(eq(yeLeaveRequestsTable.id, request.id)).returning();
  await activity(`Solicitud de ausencia ${status === "approved" ? "aprobada" : "denegada"} por supervisor`, "info");
  res.json(updated);
});

router.get("/auth/csrf", async (req, res): Promise<void> => {
  if (!req.auth || req.auth.sessionType !== "web") { fail(res, "Se requiere sesión web", 401); return; }
  const token = req.cookies?.ye_csrf;
  if (!token) { fail(res, "Token CSRF no disponible", 403); return; }
  res.json(api.GetCsrfTokenResponse.parse({ csrfToken: token }));
});

router.post("/auth/logout", async (req, res): Promise<void> => {
  if (req.auth) {
    await revokeSession(req.auth.sessionId);
    await db.insert(yeAuditLogsTable).values({
      eventType: "authentication",
      actorName: req.auth.user.name,
      actorRole: req.auth.user.role,
      resourceType: "session",
      resourceId: req.auth.sessionId,
      action: "logout",
      ipAddress: req.ip,
    });
  }
  clearSessionCookies(res);
  res.sendStatus(204);
});

router.post("/employees/:id/mobile-credential", async (req, res): Promise<void> => {
  if (!credentialRole(req)) { fail(res, "No tienes permisos para emitir credenciales", 403); return; }
  if (!CREDENTIAL_PEPPER) { fail(res, "La configuración de seguridad no está disponible", 503); return; }
  const employeeId = idFrom(req);
  const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.id, employeeId ?? "")).limit(1);
  if (!employee || employee.status !== "active") { fail(res, "Empleado no encontrado", 404); return; }
  const suppliedCode = typeof req.body?.employeeCode === "string" ? req.body.employeeCode.trim() : "";
  const employeeCode = suppliedCode || randomEmployeeCode();
  const codeHash = credentialDigest(employeeCode);
  if (!codeHash || !/^[A-Za-z0-9_-]{6,20}$/.test(employeeCode) || weakEmployeeCode(employeeCode)) {
    fail(res, "La credencial no cumple la política de seguridad", 422); return;
  }
  await db.update(yeEmployeeCredentialsTable).set({ status: "revoked", revokedAt: new Date(), rotatedAt: new Date() })
    .where(and(eq(yeEmployeeCredentialsTable.employeeId, employee.id), eq(yeEmployeeCredentialsTable.status, "active")));
  const [credential] = await db.insert(yeEmployeeCredentialsTable).values({
    employeeId: employee.id,
    employeeCodeHash: codeHash,
    issuedBy: req.auth!.user.id,
  }).returning();
  await db.insert(yeAuditLogsTable).values({
    eventType: "credential", actorName: req.auth!.user.name, actorRole: req.auth!.user.role,
    resourceType: "employee_mobile_credential", resourceId: employee.id, action: "issue",
    ipAddress: req.ip,
  });
  // The raw code is intentionally returned only on this response.
  res.status(201).json({ credentialId: credential.id, employeeCode, shownOnce: true });
});

router.delete("/employees/:id/mobile-credential", async (req, res): Promise<void> => {
  if (!credentialRole(req)) { fail(res, "No tienes permisos para revocar credenciales", 403); return; }
  const employeeId = idFrom(req);
  const updated = await db.update(yeEmployeeCredentialsTable).set({ status: "revoked", revokedAt: new Date() })
    .where(and(eq(yeEmployeeCredentialsTable.employeeId, employeeId ?? ""), eq(yeEmployeeCredentialsTable.status, "active"))).returning();
  if (!updated[0]) { fail(res, "Credencial no encontrada", 404); return; }
  await db.insert(yeAuditLogsTable).values({
    eventType: "credential", actorName: req.auth!.user.name, actorRole: req.auth!.user.role,
    resourceType: "employee_mobile_credential", resourceId: employeeId, action: "revoke", ipAddress: req.ip,
  });
  res.sendStatus(204);
});

router.get("/mobile/devices/status", async (req, res): Promise<void> => {
  if (req.auth?.sessionType !== "mobile") { fail(res, "Se requiere sesión móvil", 401); return; }
  const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth.user.id)).limit(1);
  if (!employee) { fail(res, "Perfil de empleado no encontrado", 404); return; }
  const rows = await db.select({
    id: yeDevicesTable.id, status: yeDevicesTable.status, platform: yeDevicesTable.platform,
    model: yeDevicesTable.model, osVersion: yeDevicesTable.osVersion, appVersion: yeDevicesTable.appVersion,
    authorizedAt: yeDevicesTable.authorizedAt, revokedAt: yeDevicesTable.revokedAt, lastActivity: yeDevicesTable.lastActivity,
  }).from(yeDevicesTable).where(eq(yeDevicesTable.employeeId, employee.id));
  res.json(rows);
});

router.post("/mobile/devices/register", async (req, res): Promise<void> => {
  if (req.auth?.sessionType !== "mobile") { fail(res, "Se requiere sesión móvil", 401); return; }
  const installationId = String(req.body?.installationId ?? "");
  const platform = String(req.body?.platform ?? "");
  if (installationId.length < 16 || !["ios", "android"].includes(platform)) { fail(res, "Datos de dispositivo inválidos", 422); return; }
  const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth.user.id)).limit(1);
  if (!employee) { fail(res, "Perfil de empleado no encontrado", 404); return; }
  const installationIdHash = sha256(installationId);
  const [same] = await db.select().from(yeDevicesTable).where(and(eq(yeDevicesTable.employeeId, employee.id), eq(yeDevicesTable.installationIdHash, installationIdHash))).limit(1);
  if (same) { res.json({ id: same.id, status: same.status, platform: same.platform }); return; }
  const [active] = await db.select().from(yeDevicesTable).where(and(eq(yeDevicesTable.employeeId, employee.id), inArray(yeDevicesTable.status, ["approved", "pending"]))).limit(1);
  if (active) { res.status(409).json({ error: "Ya existe un dispositivo activo o pendiente", device: { id: active.id, status: active.status } }); return; }
  const deviceCredential = opaque(32);
  const [device] = await db.insert(yeDevicesTable).values({
    employeeId: employee.id, employeeName: employee.name, platform, installationIdHash,
    deviceCredentialHash: sha256(deviceCredential), status: "pending",
    model: typeof req.body?.model === "string" ? req.body.model.slice(0, 190) : null,
    osVersion: typeof req.body?.osVersion === "string" ? req.body.osVersion.slice(0, 80) : null,
    appVersion: typeof req.body?.appVersion === "string" ? req.body.appVersion.slice(0, 80) : null,
  }).returning();
  res.status(201).json({ id: device.id, status: device.status, platform: device.platform, deviceCredential });
});

router.post("/devices/:id/approve", async (req, res): Promise<void> => {
  if (!credentialRole(req)) { fail(res, "Solo personal autorizado puede aprobar dispositivos", 403); return; }
  const id = idFrom(req);
  const [device] = await db.select().from(yeDevicesTable).where(eq(yeDevicesTable.id, id ?? "")).limit(1);
  if (!device) { fail(res, "Dispositivo no encontrado", 404); return; }
  const [active] = device.employeeId ? await db.select().from(yeDevicesTable).where(and(eq(yeDevicesTable.employeeId, device.employeeId), eq(yeDevicesTable.status, "approved"))).limit(1) : [];
  if (active && active.id !== device.id) { fail(res, "El empleado ya tiene otro dispositivo aprobado", 409); return; }
  const [updated] = await db.update(yeDevicesTable).set({ status: "approved", approvedBy: req.auth!.user.id, authorizedAt: new Date() }).where(eq(yeDevicesTable.id, device.id)).returning();
  await db.insert(yeAuditLogsTable).values({ eventType: "device", actorName: req.auth!.user.name, actorRole: req.auth!.user.role, resourceType: "device", resourceId: device.id, action: "approve", ipAddress: req.ip });
  res.json({ id: updated.id, status: updated.status, platform: updated.platform, model: updated.model, osVersion: updated.osVersion, appVersion: updated.appVersion });
});

router.post("/devices/:id/revoke", async (req, res): Promise<void> => {
  if (!credentialRole(req)) { fail(res, "Solo personal autorizado puede revocar dispositivos", 403); return; }
  const id = idFrom(req);
  const [device] = await db.update(yeDevicesTable).set({ status: req.body?.replace ? "replaced" : "revoked", revokedAt: new Date(), replacedAt: req.body?.replace ? new Date() : null })
    .where(eq(yeDevicesTable.id, id ?? "")).returning();
  if (!device) { fail(res, "Dispositivo no encontrado", 404); return; }
  await db.insert(yeAuditLogsTable).values({ eventType: "device", actorName: req.auth!.user.name, actorRole: req.auth!.user.role, resourceType: "device", resourceId: device.id, action: "revoke", reason: String(req.body?.reason ?? "revocación corporativa"), ipAddress: req.ip });
  res.json({ id: device.id, status: device.status, platform: device.platform, revokedAt: device.revokedAt });
});

router.post("/projects/:id/qr/main", async (req, res): Promise<void> => {
  if (!credentialRole(req)) { fail(res, "Solo personal corporativo autorizado puede emitir el QR principal", 403); return; }
  if (!QR_KEY) { fail(res, "La configuración de seguridad no está disponible", 503); return; }
  const projectId = idFrom(req);
  const [project] = await db.select().from(yeProjectsTable).where(eq(yeProjectsTable.id, projectId ?? "")).limit(1);
  if (!project) { fail(res, "Proyecto no encontrado", 404); return; }
  const [current] = await db.select().from(yeProjectQrResourcesTable).where(eq(yeProjectQrResourcesTable.projectId, project.id)).limit(1);
  const [resource] = current
    ? await db.update(yeProjectQrResourcesTable).set({ status: "active", keyVersion: QR_KEY_VERSION, rotatedAt: new Date() }).where(eq(yeProjectQrResourcesTable.id, current.id)).returning()
    : await db.insert(yeProjectQrResourcesTable).values({ projectId: project.id, keyVersion: QR_KEY_VERSION, status: "active", createdBy: req.auth!.user.id }).returning();
  await db.insert(yeAuditLogsTable).values({ eventType: "qr_resource", actorName: req.auth!.user.name, actorRole: req.auth!.user.role, resourceType: "project_main_qr", resourceId: resource.id, action: current ? "rotate" : "create", ipAddress: req.ip });
  res.json({ id: resource.id, projectId: resource.projectId, worksiteId: resource.worksiteId, keyVersion: resource.keyVersion, status: resource.status, printable: true, printUrl: `/api/projects/${project.id}/qr/main/print` });
});

router.get("/projects/:id/qr/main/print", async (req, res): Promise<void> => {
  if (!credentialRole(req)) { fail(res, "Solo personal corporativo autorizado puede imprimir el QR principal", 403); return; }
  const projectId = idFrom(req);
  const [resource] = await db.select().from(yeProjectQrResourcesTable)
    .where(and(eq(yeProjectQrResourcesTable.projectId, projectId ?? ""), eq(yeProjectQrResourcesTable.status, "active"))).limit(1);
  if (!resource) { fail(res, "QR principal no encontrado", 404); return; }
  res.json({ id: resource.id, projectId: resource.projectId, worksiteId: resource.worksiteId, keyVersion: resource.keyVersion, printable: true });
});

router.post("/worksites/:id/qr/display-challenge", async (req, res): Promise<void> => {
  const role = req.auth?.user.role;
  const worksiteId = idFrom(req);
  const [worksite] = await db.select().from(yeWorksitesTable).where(eq(yeWorksitesTable.id, worksiteId ?? "")).limit(1);
  if (!worksite) { fail(res, "Obra no encontrada", 404); return; }
  const supervisorAllowed = role === "supervisor" && worksite.qrDisplayAuthorized &&
    (worksite.supervisorName ?? "").trim().toLocaleLowerCase() === req.auth!.user.name.trim().toLocaleLowerCase();
  if (!credentialRole(req) && !supervisorAllowed) { fail(res, "No tienes autorización para mostrar este QR", 403); return; }
  if (!QR_KEY) { fail(res, "La configuración de seguridad no está disponible", 503); return; }
  const [project] = await db.select().from(yeProjectsTable).where(eq(yeProjectsTable.id, worksite.projectId)).limit(1);
  if (!project) { fail(res, "Proyecto no encontrado", 404); return; }
  const [resource] = await db.select().from(yeProjectQrResourcesTable)
    .where(and(eq(yeProjectQrResourcesTable.projectId, project.id), eq(yeProjectQrResourcesTable.status, "active"))).limit(1);
  if (!resource) { fail(res, "El QR principal aún no ha sido emitido por personal corporativo", 409); return; }
  const challengeId = crypto.randomUUID();
  const nonce = opaque(24);
  const displaySessionId = opaque(18);
  const issuedAt = new Date();
  const expiresAt = new Date(issuedAt.getTime() + 45_000);
  const payload = { challengeId, projectId: project.id, worksiteId: worksite.id, nonce, displaySessionId, keyVersion: QR_KEY_VERSION, issuedAt: issuedAt.toISOString(), expiresAt: expiresAt.toISOString() };
  const signed = qrSign(payload);
  await db.insert(yeQrChallengesTable).values({
    id: challengeId, resourceId: resource.id, projectId: project.id, worksiteId: worksite.id,
    nonceHash: sha256(nonce), displaySessionId, keyVersion: QR_KEY_VERSION, issuedAt, expiresAt,
  });
  res.json({ challenge: signed.encoded, signature: signed.signature, projectId: project.id, worksiteId: worksite.id, displaySessionId, keyVersion: QR_KEY_VERSION, issuedAt, expiresAt, watermarkMetadata: { seed: opaque(12), frameIntervalMs: 500 } });
});

router.patch("/worksites/:id/security-config", async (req, res): Promise<void> => {
  if (!credentialRole(req)) { fail(res, "Solo personal autorizado puede configurar seguridad", 403); return; }
  const id = idFrom(req);
  const latitude = Number(req.body?.latitude);
  const longitude = Number(req.body?.longitude);
  const radius = Number(req.body?.geofenceRadiusMeters);
  const grace = Number(req.body?.scheduleGraceMinutes);
  if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90 || !Number.isFinite(longitude) || longitude < -180 || longitude > 180 ||
      !Number.isInteger(radius) || radius < 25 || radius > 5000 || !Number.isInteger(grace) || grace < 0 || grace > 180) {
    fail(res, "Configuración de geofence inválida", 422); return;
  }
  const [updated] = await db.update(yeWorksitesTable).set({
    latitude, longitude, geofenceRadiusMeters: radius, scheduleGraceMinutes: grace,
    scheduleStart: typeof req.body?.scheduleStart === "string" ? req.body.scheduleStart : null,
    scheduleEnd: typeof req.body?.scheduleEnd === "string" ? req.body.scheduleEnd : null,
    qrDisplayAuthorized: req.body?.qrDisplayAuthorized === true,
  }).where(eq(yeWorksitesTable.id, id ?? "")).returning();
  if (!updated) { fail(res, "Obra no encontrada", 404); return; }
  await db.insert(yeAuditLogsTable).values({ eventType: "security_config", actorName: req.auth!.user.name, actorRole: req.auth!.user.role, resourceType: "worksite", resourceId: updated.id, action: "update", ipAddress: req.ip });
  res.json({ latitude: updated.latitude, longitude: updated.longitude, geofenceRadiusMeters: updated.geofenceRadiusMeters, scheduleStart: updated.scheduleStart, scheduleEnd: updated.scheduleEnd, scheduleGraceMinutes: updated.scheduleGraceMinutes, qrDisplayAuthorized: updated.qrDisplayAuthorized });
});

router.post("/attendance/scan", async (req, res): Promise<void> => {
  if (req.auth?.sessionType !== "mobile") { fail(res, "Se requiere sesión móvil", 401); return; }
  const reject = async (reason: string, status = 403, riskScore = 75, ids: Record<string, any> = {}) => {
    await securityAttempt({ ...ids, result: "rejected", reason, platform: String(req.body?.platform ?? ""), latitude: Number(req.body?.latitude) || null, longitude: Number(req.body?.longitude) || null, riskScore });
    if (riskScore >= 80) await db.insert(yeSecurityEventsTable).values({ type: "attendance_rejection", message: "Rechazos repetidos o de alto riesgo requieren revisión", severity: "critical", resolved: false });
    fail(res, "Marcación rechazada por las validaciones de seguridad.", status);
  };
  const deviceCredential = String(req.get("x-device-credential") ?? req.body?.deviceCredential ?? "");
  const platform = String(req.body?.platform ?? "");
  if (!deviceCredential || !["ios", "android"].includes(platform)) { await reject("platform_or_device_credential_invalid", 403, 90); return; }
  const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth.user.id)).limit(1);
  if (!employee) { await reject("employee_not_found", 403, 85); return; }
  const [device] = await db.select().from(yeDevicesTable).where(and(eq(yeDevicesTable.employeeId, employee.id), eq(yeDevicesTable.deviceCredentialHash, sha256(deviceCredential)))).limit(1);
  if (!device || device.status !== "approved" || device.platform !== platform) { await reject("device_not_approved_or_platform_mismatch", 403, 90, { employeeId: employee.id, employeeNumber: employee.employeeNumber, deviceId: device?.id }); return; }
  const encoded = String(req.body?.challenge ?? "");
  const signature = String(req.body?.signature ?? "");
  const payload = qrVerify(encoded, signature);
  if (!payload || payload.keyVersion !== QR_KEY_VERSION) { await reject("invalid_qr_signature_or_key_version", 403, 90, { employeeId: employee.id, employeeNumber: employee.employeeNumber, deviceId: device.id }); return; }
  const expires = Date.parse(String(payload.expiresAt ?? ""));
  if (!payload.challengeId || !payload.projectId || !payload.worksiteId || !Number.isFinite(expires) || expires <= Date.now()) { await reject("qr_expired_or_invalid", 403, 90, { employeeId: employee.id, employeeNumber: employee.employeeNumber, deviceId: device.id, challengeId: payload.challengeId }); return; }
  const [challenge] = await db.select().from(yeQrChallengesTable).where(and(eq(yeQrChallengesTable.id, payload.challengeId), isNull(yeQrChallengesTable.consumedAt))).limit(1);
  if (!challenge || challenge.expiresAt.getTime() <= Date.now() || !safeEqual(challenge.nonceHash, sha256(String(payload.nonce ?? "")))) { await reject("qr_replayed_or_expired", 403, 95, { employeeId: employee.id, employeeNumber: employee.employeeNumber, deviceId: device.id, challengeId: payload.challengeId }); return; }
  const consumed = await db.update(yeQrChallengesTable).set({ consumedAt: new Date() }).where(and(eq(yeQrChallengesTable.id, challenge.id), isNull(yeQrChallengesTable.consumedAt))).returning();
  if (!consumed[0]) { await reject("qr_replayed", 409, 95, { employeeId: employee.id, employeeNumber: employee.employeeNumber, deviceId: device.id, challengeId: challenge.id }); return; }
  const [worksite] = await db.select().from(yeWorksitesTable).where(eq(yeWorksitesTable.id, challenge.worksiteId)).limit(1);
  const assigned = worksite && (employee.assignedWorksite === worksite.id || employee.assignedWorksite?.trim().toLocaleLowerCase() === worksite.name.trim().toLocaleLowerCase());
  if (!worksite || !assigned) { await reject("employee_not_assigned_to_worksite", 403, 80, { employeeId: employee.id, employeeNumber: employee.employeeNumber, worksiteId: challenge.worksiteId, projectId: challenge.projectId, deviceId: device.id, challengeId: challenge.id }); return; }
  const latitude = Number(req.body?.latitude);
  const longitude = Number(req.body?.longitude);
  const distance = worksite.latitude == null || worksite.longitude == null ? Number.POSITIVE_INFINITY : distanceMeters(latitude, longitude, worksite.latitude, worksite.longitude);
  if (!Number.isFinite(latitude) || !Number.isFinite(longitude) || distance > worksite.geofenceRadiusMeters) { await reject("outside_server_geofence", 85, 90, { employeeId: employee.id, employeeNumber: employee.employeeNumber, worksiteId: worksite.id, projectId: challenge.projectId, deviceId: device.id, challengeId: challenge.id }); return; }
  const serverNow = new Date();
  if (!inSchedule(serverNow, worksite.scheduleStart, worksite.scheduleEnd, worksite.scheduleGraceMinutes)) { await reject("outside_schedule_grace", 403, 70, { employeeId: employee.id, employeeNumber: employee.employeeNumber, worksiteId: worksite.id, projectId: challenge.projectId, deviceId: device.id, challengeId: challenge.id }); return; }
  const action = req.body?.action === "check_out" ? "check_out" : req.body?.action === "check_in" ? "check_in" : null;
  if (!action) { await reject("invalid_attendance_action", 422, 70, { employeeId: employee.id, employeeNumber: employee.employeeNumber, worksiteId: worksite.id, projectId: challenge.projectId, deviceId: device.id, challengeId: challenge.id }); return; }
  const [attendance] = await db.insert(yeAttendanceTable).values({
    employeeId: employee.id, employeeName: employee.name, employeeNumber: employee.employeeNumber,
    worksiteId: worksite.id, worksiteName: worksite.name, deviceId: device.id, platform,
    challengeId: challenge.id, deviceCredentialHash: device.deviceCredentialHash, action,
    latitude, longitude, capturedAt: serverNow, riskScore: 0,
    validation: { identity: true, device: true, location: true, schedule: true, antiReplay: true, signature: true },
  }).returning();
  await db.update(yeDevicesTable).set({ lastActivity: serverNow }).where(eq(yeDevicesTable.id, device.id));
  await securityAttempt({ employeeId: employee.id, employeeNumber: employee.employeeNumber, projectId: challenge.projectId, worksiteId: worksite.id, deviceId: device.id, platform, result: "accepted", reason: "validated", latitude, longitude, riskScore: 0, challengeId: challenge.id });
  res.status(201).json({ attendanceId: attendance.id, status: "accepted", serverTime: serverNow, riskScore: 0 });
});

router.get("/security/attempts", async (req, res): Promise<void> => {
  if (!securityReader(req)) { fail(res, "No tienes permiso para consultar eventos de seguridad", 403); return; }
  const rows = await db.select().from(yeSecurityAttemptsTable).orderBy(desc(yeSecurityAttemptsTable.createdAt)).limit(500);
  res.json(rows);
});

router.get("/employees/me", async (req, res): Promise<void> => {
  const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth?.user.id ?? "")).limit(1);
  if (!employee) { fail(res, "Perfil de empleado no encontrado", 404); return; }
  res.json(api.GetMyEmployeeProfileResponse.parse(employee));
});

router.get("/attendance/me", async (req, res): Promise<void> => {
  const [employee] = await db.select({ id: yeEmployeesTable.id }).from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth?.user.id ?? "")).limit(1);
  if (!employee) { res.json([]); return; }
  const rows = await db.select().from(yeAttendanceTable).where(eq(yeAttendanceTable.employeeId, employee.id)).orderBy(desc(yeAttendanceTable.capturedAt));
  res.json(api.ListMyAttendanceResponse.parse(rows));
});

router.get("/leave-requests/me", async (req, res): Promise<void> => {
  const [employee] = await db.select({ id: yeEmployeesTable.id }).from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth?.user.id ?? "")).limit(1);
  if (!employee) { res.json([]); return; }
  const rows = await db.select().from(yeLeaveRequestsTable).where(eq(yeLeaveRequestsTable.employeeId, employee.id)).orderBy(desc(yeLeaveRequestsTable.createdAt));
  res.json(api.ListMyLeaveRequestsResponse.parse(rows));
});

router.get("/dashboard/summary", async (_req, res): Promise<void> => {
  const [projects, worksites, quotes, equipment, attendance, events, maintenance, recentActivity] = await Promise.all([
    db.select({ total: count() }).from(yeProjectsTable).where(eq(yeProjectsTable.status, "active")),
    db.select({ total: count() }).from(yeWorksitesTable).where(eq(yeWorksitesTable.status, "active")),
    db.select({ total: count() }).from(yeQuotesTable).where(eq(yeQuotesTable.status, "new")),
    db.select({ total: count() }).from(yeEquipmentTable).where(eq(yeEquipmentTable.status, "available")),
    db.select({ total: count() }).from(yeAttendanceTable),
    db.select({ total: count() }).from(yeSecurityEventsTable).where(eq(yeSecurityEventsTable.resolved, false)),
    db.select({ total: count() }).from(yeEquipmentTable).where(eq(yeEquipmentTable.status, "maintenance")),
    db.select().from(yeActivityTable).orderBy(desc(yeActivityTable.timestamp)).limit(8),
  ]);
  res.json(api.GetDashboardSummaryResponse.parse({
    activeProjects: Number(projects[0]?.total ?? 0),
    activeWorksites: Number(worksites[0]?.total ?? 0),
    openQuotes: Number(quotes[0]?.total ?? 0),
    fleetAvailable: Number(equipment[0]?.total ?? 0),
    checkedInToday: Number(attendance[0]?.total ?? 0),
    securityEvents: Number(events[0]?.total ?? 0),
    maintenanceDue: Number(maintenance[0]?.total ?? 0),
    recentActivity,
  }));
});

router.get("/dashboard/activity", async (_req, res): Promise<void> => {
  const rows = await db.select().from(yeActivityTable).orderBy(desc(yeActivityTable.timestamp)).limit(20);
  res.json(api.ListActivityResponse.parse(rows));
});

router.get("/quotes/:id", async (req, res): Promise<void> => {
  const [row] = await db.select().from(yeQuotesTable).where(eq(yeQuotesTable.id, idFrom(req) ?? ""));
  if (!row) { fail(res, "Cotización no encontrada", 404); return; }
  res.json(api.GetQuoteResponse.parse(row));
});

router.patch("/quotes/:id", async (req, res): Promise<void> => {
  const parsed = api.UpdateQuoteBody.safeParse(req.body);
  if (!parsed.success) { fail(res, parsed.error.message); return; }
  const [row] = await db.update(yeQuotesTable).set({ ...parsed.data, updatedAt: new Date() }).where(eq(yeQuotesTable.id, idFrom(req) ?? "")).returning();
  if (!row) { fail(res, "Cotización no encontrada", 404); return; }
  res.json(api.UpdateQuoteResponse.parse(row));
});

router.delete("/quotes/:id", async (req, res): Promise<void> => {
  const [row] = await db.delete(yeQuotesTable).where(eq(yeQuotesTable.id, idFrom(req) ?? "")).returning();
  if (!row) { fail(res, "Cotización no encontrada", 404); return; }
  res.sendStatus(204);
});

crud({
  path: "/quotes",
  table: yeQuotesTable,
  listSchema: api.ListQuotesResponse,
  createSchema: api.CreateQuoteBody,
  createResponse: api.CreateQuoteResponse,
  updateSchema: api.UpdateQuoteBody,
  updateResponse: api.UpdateQuoteResponse,
});
crud({
  path: "/clients",
  table: yeClientsTable,
  listSchema: api.ListClientsResponse,
  createSchema: api.CreateClientBody,
  createResponse: api.CreateClientResponse,
  updateSchema: api.UpdateClientBody,
  updateResponse: api.UpdateClientResponse,
});
crud({
  path: "/projects",
  table: yeProjectsTable,
  listSchema: api.ListProjectsResponse,
  createSchema: api.CreateProjectBody,
  createResponse: api.CreateProjectResponse,
  updateSchema: api.UpdateProjectBody,
  updateResponse: api.UpdateProjectResponse,
});
router.get("/projects/:id", async (req, res): Promise<void> => {
  const [project] = await db.select().from(yeProjectsTable).where(eq(yeProjectsTable.id, idFrom(req) ?? ""));
  if (!project) { fail(res, "Proyecto no encontrado", 404); return; }
  const worksites = await db.select().from(yeWorksitesTable).where(eq(yeWorksitesTable.projectId, project.id));
  res.json(api.GetProjectResponse.parse({ ...project, worksites }));
});
crud({
  path: "/worksites",
  table: yeWorksitesTable,
  listSchema: api.ListWorksitesResponse,
  createSchema: api.CreateWorksiteBody,
  createResponse: api.CreateWorksiteResponse,
  updateSchema: api.UpdateWorksiteBody,
  updateResponse: api.UpdateWorksiteResponse,
  prepare: async (data: any) => {
    const [project] = await db.select().from(yeProjectsTable).where(eq(yeProjectsTable.id, data.projectId));
    return { ...data, projectNumber: project?.projectNumber ?? "SIN-PROYECTO" };
  },
});
crud({
  path: "/equipment",
  table: yeEquipmentTable,
  listSchema: api.ListEquipmentResponse,
  createSchema: api.CreateEquipmentBody,
  createResponse: api.CreateEquipmentResponse,
  updateSchema: api.UpdateEquipmentBody,
  updateResponse: api.UpdateEquipmentResponse,
});
crud({
  path: "/materials",
  table: yeMaterialsTable,
  listSchema: api.ListMaterialsResponse,
  createSchema: api.CreateMaterialBody,
  createResponse: api.CreateMaterialResponse,
  updateSchema: api.UpdateMaterialBody,
  updateResponse: api.UpdateMaterialResponse,
  output: (row: any) => ({
    ...row,
    status: row.stock <= 0 ? "out_of_stock" : row.stock <= row.reorderLevel ? "low" : "in_stock",
  }),
});
router.get("/employees", async (req, res): Promise<void> => {
  const query = typeof req.query.q === "string" ? req.query.q.trim().toLocaleLowerCase() : "";
  const rows = await db.select().from(yeEmployeesTable).orderBy(desc(yeEmployeesTable.id));
  const filtered = query
    ? rows.filter((row) => Object.values(row).some((value) => {
      if (value == null) return false;
      return String(value).toLocaleLowerCase().includes(query);
    }))
    : rows;
  res.json(api.ListEmployeesResponse.parse(filtered));
});
crud({
  path: "/employees",
  table: yeEmployeesTable,
  listSchema: api.ListEmployeesResponse,
  createSchema: api.CreateEmployeeBody,
  createResponse: api.CreateEmployeeResponse,
  updateSchema: api.UpdateEmployeeBody,
  updateResponse: api.UpdateEmployeeResponse,
});
router.get("/attendance", async (_req, res): Promise<void> => {
  const rows = await db.select().from(yeAttendanceTable).orderBy(desc(yeAttendanceTable.capturedAt));
  res.json(api.ListAttendanceResponse.parse(rows));
});
router.post("/attendance", async (req, res): Promise<void> => {
  if (req.auth?.sessionType === "mobile") {
    fail(res, "La asistencia móvil requiere un desafío QR vigente", 403);
    return;
  }
  const parsed = api.RecordAttendanceBody.safeParse(req.body);
  if (!parsed.success) { fail(res, parsed.error.message); return; }
  let employeeId = parsed.data.employeeId;
  let employeeName = "Empleado de campo";
  let employeeNumber = parsed.data.employeeId;
  if (req.auth?.user.role === "employee") {
    const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth.user.id)).limit(1);
    if (!employee || employee.id !== parsed.data.employeeId) {
      fail(res, "Solo puedes registrar tu propia asistencia", 403);
      return;
    }
    employeeId = employee.id;
    employeeName = employee.name;
    employeeNumber = employee.employeeNumber;
  }
  const [row] = await db.insert(yeAttendanceTable).values({
    ...parsed.data,
    employeeId,
    employeeName,
    employeeNumber,
    worksiteName: "Obra verificada",
    capturedAt: new Date(parsed.data.capturedAt),
    validation: { identity: true, device: true, location: true, schedule: true, antiReplay: true },
  }).returning();
  await activity("Marcación de asistencia validada", "info");
  res.status(201).json(api.RecordAttendanceResponse.parse(row));
});
router.get("/security/events", async (_req, res): Promise<void> => {
  const rows = await db.select().from(yeSecurityEventsTable).orderBy(desc(yeSecurityEventsTable.createdAt));
  res.json(api.ListSecurityEventsResponse.parse(rows));
});
crud({
  path: "/policies",
  table: yePoliciesTable,
  listSchema: api.ListPoliciesResponse,
  createSchema: api.CreatePolicyBody,
  createResponse: api.CreatePolicyResponse,
  updateSchema: api.UpdatePolicyBody,
  updateResponse: api.UpdatePolicyResponse,
});
crud({
  path: "/documents",
  table: yeDocumentsTable,
  listSchema: api.ListDocumentsResponse,
  createSchema: api.CreateDocumentBody,
  createResponse: api.CreateDocumentResponse,
  updateSchema: api.UpdateDocumentBody,
  updateResponse: api.UpdateDocumentResponse,
});
const DEFAULT_INTEGRATION_SETTINGS = {
  ai: { enabled: false, provider: "none", model: "", baseUrl: "" },
  smtp: { enabled: false, host: "", port: 587, secure: true, fromName: "", fromEmail: "" },
  sms: { enabled: false, provider: "none", senderId: "" },
  chat: { enabled: true, provider: "visual", handoffEmail: "" },
  whatsapp: { enabled: true, phone: "+19394412095" },
  maps: { enabled: false, provider: "none" },
  analytics: { enabled: false, provider: "none" },
  webhooks: { enabled: false, quoteUrl: "", contactUrl: "" },
};

router.get("/settings", async (_req, res): Promise<void> => {
  const [settings] = await db.select().from(yeSettingsTable).limit(1);
  res.json(api.GetSettingsResponse.parse(settings ?? {
    companyName: "Y. E. Transport & Constructors",
    legalName: "Y. E. Transport & Constructors, LLC",
    phone: "+1 (787) 555-0148",
    email: "info@yetransport.com",
    address: "Puerto Rico",
    defaultLanguage: "es",
    quoteNotificationEmail: "cotizaciones@yetransport.com",
    siteContent: {},
    integrationSettings: DEFAULT_INTEGRATION_SETTINGS,
    adminSecondFactorEnabled: false,
    adminSecondFactorMethod: "daily_code",
  }));
});
router.patch("/settings", requireRoles("Web Master", "super_admin"), async (req, res): Promise<void> => {
  const parsed = api.UpdateSettingsBody.safeParse(req.body);
  if (!parsed.success) { fail(res, parsed.error.message); return; }
  const [current] = await db.select().from(yeSettingsTable).limit(1);
  const [settings] = current
    ? await db.update(yeSettingsTable).set({ ...parsed.data }).where(eq(yeSettingsTable.id, current.id)).returning()
    : await db.insert(yeSettingsTable).values({
      companyName: parsed.data.companyName ?? "Y. E. Transport & Constructors",
      legalName: parsed.data.legalName ?? "Y. E. Transport & Constructors, LLC",
      phone: parsed.data.phone ?? "",
      email: parsed.data.email ?? "",
      address: parsed.data.address ?? "Puerto Rico",
      defaultLanguage: parsed.data.defaultLanguage ?? "es",
      quoteNotificationEmail: parsed.data.quoteNotificationEmail ?? "",
      siteContent: parsed.data.siteContent ?? {},
      integrationSettings: parsed.data.integrationSettings ?? DEFAULT_INTEGRATION_SETTINGS,
      adminSecondFactorEnabled: parsed.data.adminSecondFactorEnabled ?? false,
      adminSecondFactorMethod: parsed.data.adminSecondFactorMethod ?? "daily_code",
    }).returning();
  res.json(api.UpdateSettingsResponse.parse(settings));
});

const platformCrud = (path: string, table: any, config: {
  list: any;
  createBody?: any;
  createResponse?: any;
  updateBody?: any;
  updateResponse?: any;
  deleteHook?: boolean;
}) => {
  router.get(path, async (_req, res): Promise<void> => {
    if (path === "/roles" && !["Web Master", "super_admin"].includes(_req.auth?.user.role ?? "")) {
      fail(res, "Solo Web Master o super_admin pueden administrar roles", 403);
      return;
    }
    const rows = await db.select().from(table).orderBy(desc(table.createdAt ?? table.updatedAt ?? table.id));
    res.json(config.list.parse(rows));
  });
  if (config.createBody && config.createResponse) {
    router.post(path, async (req, res): Promise<void> => {
      if (path === "/roles" && !["Web Master", "super_admin"].includes(req.auth?.user.role ?? "")) {
        fail(res, "Solo Web Master o super_admin pueden administrar roles", 403);
        return;
      }
      if (!canWrite(req, res)) return;
      const parsed = config.createBody.safeParse(req.body);
      if (!parsed.success) { fail(res, parsed.error.message); return; }
      const inserted = await db.insert(table).values(parsed.data as any).returning() as any;
      const row = inserted[0];
      await activity(`Nuevo registro creado en ${path.slice(1)}`);
      await db.insert(yeAuditLogsTable).values({
        eventType: "data_change",
        actorName: "Sistema Y.E.",
        actorRole: "admin",
        resourceType: path.slice(1),
        resourceId: row.id,
        action: "create",
        afterValue: row,
      });
      res.status(201).json(config.createResponse.parse(row));
    });
  }
  if (config.updateBody && config.updateResponse) {
    router.patch(`${path}/:id`, async (req, res): Promise<void> => {
      if (path === "/roles" && !["Web Master", "super_admin"].includes(req.auth?.user.role ?? "")) {
        fail(res, "Solo Web Master o super_admin pueden administrar roles", 403);
        return;
      }
      if (!canWrite(req, res)) return;
      const id = idFrom(req);
      const parsed = config.updateBody.safeParse(req.body);
      if (!id || !parsed.success) { fail(res, parsed.success ? "Identificador inválido" : parsed.error.message); return; }
      const [before] = await db.select().from(table).where(eq(table.id, id));
      if (!before) { fail(res, "Registro no encontrado", 404); return; }
      const updated = await db.update(table).set(parsed.data as any).where(eq(table.id, id)).returning() as any;
      const row = updated[0];
      await db.insert(yeAuditLogsTable).values({
        eventType: "data_change",
        actorName: "Sistema Y.E.",
        actorRole: "admin",
        resourceType: path.slice(1),
        resourceId: id,
        action: "update",
        beforeValue: before,
        afterValue: row,
      });
      res.json(config.updateResponse.parse(row));
    });
  }
  if (config.deleteHook) {
    router.delete(`${path}/:id`, async (req, res): Promise<void> => {
      if (!canWrite(req, res)) return;
      const id = idFrom(req);
      const deleted = await db.delete(table).where(eq(table.id, id ?? "")).returning() as any;
      if (!deleted[0]) { fail(res, "Registro no encontrado", 404); return; }
      await activity(`Registro eliminado de ${path.slice(1)}`, "warning");
      res.sendStatus(204);
    });
  }
};

platformCrud("/appointments", yeAppointmentsTable, { list: api.ListAppointmentsResponse, createBody: api.CreateAppointmentBody, createResponse: api.CreateAppointmentResponse, updateBody: api.UpdateAppointmentBody, updateResponse: api.UpdateAppointmentResponse, deleteHook: true });
platformCrud("/alerts", yeAlertsTable, { list: api.ListAlertsResponse, createBody: api.CreateAlertBody, createResponse: api.CreateAlertResponse, updateBody: api.UpdateAlertBody, updateResponse: api.UpdateAlertResponse });
platformCrud("/audit", yeAuditLogsTable, { list: api.ListAuditLogsResponse });
platformCrud("/roles", yeRolesTable, { list: api.ListRolesResponse, createBody: api.CreateRoleBody, createResponse: api.CreateRoleResponse, updateBody: api.UpdateRoleBody, updateResponse: api.UpdateRoleResponse });
platformCrud("/devices", yeDevicesTable, { list: api.ListDevicesResponse, updateBody: api.UpdateDeviceBody, updateResponse: api.UpdateDeviceResponse });
platformCrud("/jobs", yeJobPostingsTable, { list: api.ListJobPostingsResponse, createBody: api.CreateJobPostingBody, createResponse: api.CreateJobPostingResponse, updateBody: api.UpdateJobPostingBody, updateResponse: api.UpdateJobPostingResponse });
platformCrud("/applications", yeJobApplicationsTable, { list: api.ListJobApplicationsResponse, createBody: api.CreateJobApplicationBody, createResponse: api.CreateJobApplicationResponse, updateBody: api.UpdateJobApplicationBody, updateResponse: api.UpdateJobApplicationResponse });
platformCrud("/resources", yeResourcesTable, { list: api.ListResourcesResponse, createBody: api.CreateResourceBody, createResponse: api.CreateResourceResponse, updateBody: api.UpdateResourceBody, updateResponse: api.UpdateResourceResponse });
platformCrud("/media", yeMediaTable, { list: api.ListMediaResponse, createBody: api.CreateMediaBody, createResponse: api.CreateMediaResponse });
platformCrud("/public-entries", yePublicEntriesTable, {
  list: api.ListPublicEntriesResponse,
  createBody: api.CreatePublicEntryBody,
  createResponse: api.CreatePublicEntryResponse,
  updateBody: api.UpdatePublicEntryBody,
  updateResponse: api.UpdatePublicEntryResponse,
  deleteHook: true,
});
platformCrud("/payroll/exports", yePayrollExportsTable, { list: api.ListPayrollExportsResponse, createBody: api.CreatePayrollExportBody, createResponse: api.CreatePayrollExportResponse });
router.post("/leave-requests", async (req, res, next): Promise<void> => {
  if (req.auth?.user.role !== "employee") { next(); return; }
  const [employee] = await db.select().from(yeEmployeesTable).where(eq(yeEmployeesTable.userId, req.auth.user.id)).limit(1);
  if (!employee) { fail(res, "Perfil de empleado no encontrado", 404); return; }
  const body = req.body as Record<string, unknown>;
  const [row] = await db.insert(yeLeaveRequestsTable).values({
    employeeId: employee.id,
    employeeName: employee.name,
    leaveType: String(body.leaveType ?? "personal"),
    startDate: String(body.startDate ?? new Date().toISOString().slice(0, 10)),
    endDate: String(body.endDate ?? new Date().toISOString().slice(0, 10)),
    reason: String(body.reason ?? ""),
  }).returning();
  res.status(201).json(api.CreateLeaveRequestResponse.parse(row));
});
platformCrud("/leave-requests", yeLeaveRequestsTable, { list: api.ListLeaveRequestsResponse, createBody: api.CreateLeaveRequestBody, createResponse: api.CreateLeaveRequestResponse, updateBody: api.UpdateLeaveRequestBody, updateResponse: api.UpdateLeaveRequestResponse });

export default router;