import { and, eq } from "drizzle-orm";
import { Router } from "express";
import { z } from "zod";

import { db } from "../db/index.js";
import { member, user } from "../db/schema/auth.js";
import { HttpError } from "../lib/http-error.js";
import { labSchema, patientInputSchema } from "../lib/patient-validation.js";
import { isReceptionOnly, providerScope } from "../lib/role-scope.js";
import {
  requireAuth,
  requireOrg,
  requirePermission,
} from "../middleware/auth.js";
import { emitToUser } from "../realtime.js";
import { recordActivity } from "../services/activity.js";
import { listClinicMembers } from "../services/messaging.js";
import { createNotification } from "../services/notifications.js";
import * as service from "../services/patients.js";

export const patientsRouter = Router();

const transferInputSchema = z.object({
  providerId: z.string().trim().min(1),
});

const labsAppendSchema = z.object({
  labs: z.array(labSchema).min(1).max(50),
});

const labDeleteSchema = z.object({
  name: z.string().trim().min(1),
  value: z.string(),
  takenAt: z.string(),
});

// Notify the rest of the clinic about a patient record change (best-effort,
// pushed live over the socket).
async function notifyClinic(
  orgId: string,
  actor: { id: string; name: string },
  text: string,
  fileNumber: string,
): Promise<void> {
  try {
    const members = await listClinicMembers(orgId, actor.id);
    for (const m of members) {
      const notification = await createNotification({
        orgId,
        userId: m.id,
        type: "record_change",
        text,
        entityType: "patient",
        entityId: fileNumber,
        actorName: actor.name,
      });
      if (notification) emitToUser(m.id, "notification:new", notification);
    }
  } catch (err) {
    console.error("Failed to notify clinic:", err);
  }
}

// Every patient route requires a signed-in user with an active clinic.
patientsRouter.use(requireAuth, requireOrg);

patientsRouter.get(
  "/",
  requirePermission({ patient: ["read"] }),
  async (req, res, next) => {
    try {
      res.json(
        await service.listPatients(
          req.organizationId!,
          isReceptionOnly(req.memberRole),
          providerScope(req.memberRole, req.user!.id),
        ),
      );
    } catch (err) {
      next(err);
    }
  },
);

patientsRouter.get(
  "/:fileNumber",
  requirePermission({ patient: ["read"] }),
  async (req, res, next) => {
    try {
      const patient = await service.getPatient(
        req.organizationId!,
        req.params.fileNumber as string,
        isReceptionOnly(req.memberRole),
        providerScope(req.memberRole, req.user!.id),
      );
      if (!patient) throw new HttpError(404, "Patient not found.");
      res.json(patient);
    } catch (err) {
      next(err);
    }
  },
);

patientsRouter.post(
  "/",
  requirePermission({ patient: ["write"] }),
  async (req, res, next) => {
    try {
      const input = patientInputSchema.parse(req.body);
      const created = await service.createPatient(
        req.organizationId!,
        req.user!.id,
        input,
        isReceptionOnly(req.memberRole),
      );
      await recordActivity({
        orgId: req.organizationId!,
        actor: { id: req.user!.id, name: req.user!.name },
        action: `Created patient ${created.name}`,
        entityType: "patient",
        entityId: created.fileNumber,
        patientName: created.name,
        patientFileNumber: created.fileNumber,
      });
      await notifyClinic(
        req.organizationId!,
        { id: req.user!.id, name: req.user!.name },
        `${req.user!.name} added patient ${created.name}`,
        created.fileNumber,
      );
      res.status(201).json(created);
    } catch (err) {
      next(err);
    }
  },
);

patientsRouter.put(
  "/:fileNumber",
  requirePermission({ patient: ["write"] }),
  async (req, res, next) => {
    try {
      const input = patientInputSchema.parse(req.body);
      const updated = await service.updatePatient(
        req.organizationId!,
        req.params.fileNumber as string,
        input,
        isReceptionOnly(req.memberRole),
      );
      if (!updated) throw new HttpError(404, "Patient not found.");
      await recordActivity({
        orgId: req.organizationId!,
        actor: { id: req.user!.id, name: req.user!.name },
        action: `Updated patient ${updated.name}`,
        entityType: "patient",
        entityId: updated.fileNumber,
        patientName: updated.name,
        patientFileNumber: updated.fileNumber,
      });
      await notifyClinic(
        req.organizationId!,
        { id: req.user!.id, name: req.user!.name },
        `${req.user!.name} updated ${updated.name}'s record`,
        updated.fileNumber,
      );
      res.json(updated);
    } catch (err) {
      next(err);
    }
  },
);

// Reassign a patient to another clinician ("transfer"). Gated on patient:write
// like any edit; the new provider must be a member of the same clinic.
patientsRouter.post(
  "/:fileNumber/transfer",
  requirePermission({ patient: ["write"] }),
  async (req, res, next) => {
    try {
      const { providerId } = transferInputSchema.parse(req.body);
      const [provider] = await db
        .select({ name: user.name })
        .from(member)
        .innerJoin(user, eq(user.id, member.userId))
        .where(
          and(
            eq(member.organizationId, req.organizationId!),
            eq(member.userId, providerId),
          ),
        );
      if (!provider) {
        throw new HttpError(400, "Selected provider is not a member of this clinic.");
      }

      const updated = await service.transferPatient(
        req.organizationId!,
        req.params.fileNumber as string,
        providerId,
        provider.name,
        providerScope(req.memberRole, req.user!.id),
      );
      if (!updated) throw new HttpError(404, "Patient not found.");

      await recordActivity({
        orgId: req.organizationId!,
        actor: { id: req.user!.id, name: req.user!.name },
        action: `Transferred patient ${updated.name} to ${provider.name}`,
        entityType: "patient",
        entityId: updated.fileNumber,
        patientName: updated.name,
        patientFileNumber: updated.fileNumber,
      });
      await notifyClinic(
        req.organizationId!,
        { id: req.user!.id, name: req.user!.name },
        `${req.user!.name} transferred ${updated.name} to ${provider.name}`,
        updated.fileNumber,
      );
      res.json(updated);
    } catch (err) {
      next(err);
    }
  },
);

// Append lab results to a patient's record. Gated by the dedicated `lab`
// statement (not patient:write) so lab staff can submit analyses without
// being able to edit the rest of the record. Full clinicians also hold
// lab:write.
patientsRouter.post(
  "/:fileNumber/labs",
  requirePermission({ lab: ["write"] }),
  async (req, res, next) => {
    try {
      const { labs } = labsAppendSchema.parse(req.body);
      const updated = await service.appendLabs(
        req.organizationId!,
        req.params.fileNumber as string,
        labs,
      );
      if (!updated) throw new HttpError(404, "Patient not found.");
      await recordActivity({
        orgId: req.organizationId!,
        actor: { id: req.user!.id, name: req.user!.name },
        action: `Added ${labs.length} lab result${labs.length === 1 ? "" : "s"} for ${updated.name}`,
        entityType: "patient",
        entityId: updated.fileNumber,
        patientName: updated.name,
        patientFileNumber: updated.fileNumber,
      });
      await notifyClinic(
        req.organizationId!,
        { id: req.user!.id, name: req.user!.name },
        `${req.user!.name} added lab results for ${updated.name}`,
        updated.fileNumber,
      );
      res.status(201).json(updated);
    } catch (err) {
      next(err);
    }
  },
);

// Remove one lab result. Gated by `lab:write` like appending, so lab staff can
// correct their own submissions without patient-edit rights.
patientsRouter.delete(
  "/:fileNumber/labs",
  requirePermission({ lab: ["write"] }),
  async (req, res, next) => {
    try {
      const match = labDeleteSchema.parse(req.body);
      const updated = await service.deleteLab(
        req.organizationId!,
        req.params.fileNumber as string,
        match,
      );
      if (!updated) throw new HttpError(404, "Patient not found.");
      await recordActivity({
        orgId: req.organizationId!,
        actor: { id: req.user!.id, name: req.user!.name },
        action: `Removed lab result ${match.name} for ${updated.name}`,
        entityType: "patient",
        entityId: updated.fileNumber,
        patientName: updated.name,
        patientFileNumber: updated.fileNumber,
      });
      res.json(updated);
    } catch (err) {
      next(err);
    }
  },
);

patientsRouter.delete(
  "/:fileNumber",
  requirePermission({ patient: ["delete"] }),
  async (req, res, next) => {
    try {
      const ok = await service.deletePatient(
        req.organizationId!,
        req.params.fileNumber as string,
      );
      if (!ok) throw new HttpError(404, "Patient not found.");
      await recordActivity({
        orgId: req.organizationId!,
        actor: { id: req.user!.id, name: req.user!.name },
        action: `Deleted patient #${req.params.fileNumber}`,
        entityType: "patient",
        entityId: req.params.fileNumber as string,
        patientFileNumber: req.params.fileNumber as string,
      });
      res.status(204).end();
    } catch (err) {
      next(err);
    }
  },
);
