import type { FastifyInstance, FastifyReply } from "fastify";
import type { MultipartFile } from "@fastify/multipart";
import { z } from "zod";
import {
  DocumentReviewServiceError,
  documentReviewService,
} from "../../lib/document-reviews.js";
import { requireSession } from "../../lib/auth.js";
import { getSessionProfile } from "../../lib/session.js";

const itemIdParamSchema = z.object({
  itemId: z.string().uuid(),
});

const versionParamsSchema = z.object({
  itemId: z.string().uuid(),
  versionId: z.string().uuid(),
});

const decisionSchema = z.object({
  decisionCode: z.enum(["approved", "rejected"]),
  justification: z.string().min(1).max(4000),
  rejectionReason: z.string().max(4000).optional().or(z.literal("")),
  guidanceForNewVersion: z.string().max(4000).optional().or(z.literal("")),
  finalValidationAnalysisId: z.string().uuid().optional().nullable(),
});

const anchoredCommentSchema = z.object({
  selectionStart: z.coerce.number().int().min(0).optional().nullable(),
  selectionEnd: z.coerce.number().int().min(1).optional().nullable(),
  selectedText: z.string().min(1).max(1500),
  selectionContextBefore: z.string().max(1200).optional().nullable(),
  selectionContextAfter: z.string().max(1200).optional().nullable(),
  pageNumber: z.coerce.number().int().min(1).optional().nullable(),
  anchorX: z.coerce.number().min(0).max(1).optional().nullable(),
  anchorY: z.coerce.number().min(0).max(1).optional().nullable(),
  selectionRects: z
    .array(
      z.object({
        x: z.coerce.number().min(0).max(1),
        y: z.coerce.number().min(0).max(1),
        width: z.coerce.number().positive().max(1),
        height: z.coerce.number().positive().max(1),
      }),
    )
    .max(24)
    .optional()
    .nullable(),
  commentText: z.string().min(1).max(4000),
});

const transferReviewSchema = z.object({
  reviewerUserId: z.string().uuid(),
  note: z.string().max(4000).optional().or(z.literal("")),
});

const listQuerySchema = z.object({
  status: z.string().optional(),
  clientId: z.string().uuid().optional(),
  caseId: z.string().uuid().optional(),
  teamId: z.string().uuid().optional(),
  from: z
    .string()
    .optional()
    .transform((value) => (value ? new Date(value) : undefined)),
  to: z
    .string()
    .optional()
    .transform((value) => (value ? new Date(value) : undefined)),
  hasAiAlert: z
    .string()
    .optional()
    .transform((value) => {
      if (value === undefined) {
        return undefined;
      }

      return value === "1" || value === "true";
    }),
  decisionCode: z.enum(["approved", "rejected"]).optional(),
});

function getMultipartFieldValue(
  fields: Record<string, MultipartFile["fields"][string]>,
  key: string,
) {
  const field = fields[key];

  if (!field || Array.isArray(field) || !("value" in field)) {
    return "";
  }

  return String(field.value ?? "").trim();
}

function parseBoolean(value: string) {
  return value === "1" || value === "true" || value === "yes" || value === "sim";
}

function parseDueAt(value: string) {
  const normalized = String(value).trim();
  const dateOnlyMatch = normalized.match(/^(\d{4})-(\d{2})-(\d{2})$/);

  if (dateOnlyMatch) {
    const [, year, month, day] = dateOnlyMatch;
    return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 12, 0, 0));
  }

  return new Date(normalized);
}

function handleServiceError(reply: FastifyReply, error: unknown): never {
  if (error instanceof DocumentReviewServiceError) {
    if (error.statusCode === 404) {
      throw reply.notFound(error.message);
    }

    if (error.statusCode === 400) {
      throw reply.badRequest(error.message);
    }

    throw reply.code(error.statusCode).send({
      message: error.message,
    });
  }

  throw error;
}

export async function registerDocumentReviewRoutes(app: FastifyInstance) {
  app.get("/items", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const query = listQuerySchema.parse(request.query);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      return documentReviewService.listItems({
        lawFirmId: profile.lawFirm.id,
        filters: {
          status: query.status ?? null,
          clientId: query.clientId ?? null,
          caseId: query.caseId ?? null,
          teamId: query.teamId ?? null,
          from: query.from ?? null,
          to: query.to ?? null,
          hasAiAlert: query.hasAiAlert ?? null,
          decisionCode: query.decisionCode ?? null,
        },
      });
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.get("/metrics", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const query = z
      .object({
        clientId: z.string().uuid().optional(),
        caseId: z.string().uuid().optional(),
        teamId: z.string().uuid().optional(),
      })
      .parse(request.query);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      return documentReviewService.getMetrics({
        lawFirmId: profile.lawFirm.id,
        filters: {
          clientId: query.clientId ?? null,
          caseId: query.caseId ?? null,
          teamId: query.teamId ?? null,
        },
      });
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.get("/items/:itemId", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { itemId } = itemIdParamSchema.parse(request.params);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      return documentReviewService.getItemDetail({
        lawFirmId: profile.lawFirm.id,
        itemId,
      });
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/precheck/upload", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const multipartRequest = request as typeof request & {
      file: () => Promise<MultipartFile | undefined>;
    };
    const file = await multipartRequest.file();

    if (!file) {
      throw reply.badRequest("A file upload is required");
    }

    const clientId = getMultipartFieldValue(file.fields, "clientId");
    const caseId = getMultipartFieldValue(file.fields, "caseId");
    const teamId = getMultipartFieldValue(file.fields, "teamId");
    const documentName = getMultipartFieldValue(file.fields, "documentName") || file.filename;
    const objective = getMultipartFieldValue(file.fields, "objective");
    const dueAtRaw = getMultipartFieldValue(file.fields, "dueAt");
    const providedText = getMultipartFieldValue(file.fields, "textContent");
    const itemId = getMultipartFieldValue(file.fields, "itemId");
    const reviewerUserId = getMultipartFieldValue(file.fields, "reviewerUserId");

    if (!clientId || !caseId || !teamId || !dueAtRaw || !reviewerUserId) {
      throw reply.badRequest("clientId, caseId, teamId, dueAt and reviewerUserId are required");
    }

    const dueAt = parseDueAt(dueAtRaw);
    if (Number.isNaN(dueAt.getTime())) {
      throw reply.badRequest("dueAt must be a valid date");
    }

    try {
      const analysis = await documentReviewService.runPreSubmissionAnalysis({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        clientId,
        caseId,
        teamId,
        reviewerUserId,
        documentName,
        objective,
        dueAt,
        originalFileName: file.filename,
        mimeType: file.mimetype,
        fileBuffer: await file.toBuffer(),
        providedText: providedText || null,
        itemId: itemId || null,
      });

      return reply.code(201).send(analysis);
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/items", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const multipartRequest = request as typeof request & {
      file: () => Promise<MultipartFile | undefined>;
    };
    const file = await multipartRequest.file();

    if (!file) {
      throw reply.badRequest("A file upload is required");
    }

    const clientId = getMultipartFieldValue(file.fields, "clientId");
    const caseId = getMultipartFieldValue(file.fields, "caseId");
    const teamId = getMultipartFieldValue(file.fields, "teamId");
    const documentName = getMultipartFieldValue(file.fields, "documentName") || file.filename;
    const objective = getMultipartFieldValue(file.fields, "objective");
    const dueAtRaw = getMultipartFieldValue(file.fields, "dueAt");
    const providedText = getMultipartFieldValue(file.fields, "textContent");
    const reuseAnalysisId = getMultipartFieldValue(file.fields, "reuseAnalysisId");
    const forceSubmit = parseBoolean(getMultipartFieldValue(file.fields, "forceSubmit"));
    const reviewerUserId = getMultipartFieldValue(file.fields, "reviewerUserId");

    if (!clientId || !caseId || !teamId || !dueAtRaw || !reviewerUserId) {
      throw reply.badRequest("clientId, caseId, teamId, dueAt and reviewerUserId are required");
    }

    const dueAt = parseDueAt(dueAtRaw);
    if (Number.isNaN(dueAt.getTime())) {
      throw reply.badRequest("dueAt must be a valid date");
    }

    try {
      const result = await documentReviewService.submitNewDocument({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        clientId,
        caseId,
        teamId,
        reviewerUserId,
        documentName,
        objective,
        dueAt,
        originalFileName: file.filename,
        mimeType: file.mimetype,
        fileBuffer: await file.toBuffer(),
        providedText: providedText || null,
        forceSubmit,
        reuseAnalysisId: reuseAnalysisId || null,
      });

      return reply.code(result.created ? 201 : 200).send(result);
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/items/:itemId/versions", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { itemId } = itemIdParamSchema.parse(request.params);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    const multipartRequest = request as typeof request & {
      file: () => Promise<MultipartFile | undefined>;
    };
    const file = await multipartRequest.file();

    if (!file) {
      throw reply.badRequest("A file upload is required");
    }

    const submissionNote = getMultipartFieldValue(file.fields, "submissionNote");
    const providedText = getMultipartFieldValue(file.fields, "textContent");
    const reuseAnalysisId = getMultipartFieldValue(file.fields, "reuseAnalysisId");
    const forceSubmit = parseBoolean(getMultipartFieldValue(file.fields, "forceSubmit"));
    const reviewerUserId = getMultipartFieldValue(file.fields, "reviewerUserId");

    try {
      const result = await documentReviewService.submitNewVersion({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        itemId,
        originalFileName: file.filename,
        mimeType: file.mimetype,
        fileBuffer: await file.toBuffer(),
        submissionNote: submissionNote || null,
        providedText: providedText || null,
        reviewerUserId: reviewerUserId || null,
        forceSubmit,
        reuseAnalysisId: reuseAnalysisId || null,
      });

      return reply.code(result.created ? 201 : 200).send(result);
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/items/:itemId/versions/:versionId/analysis", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { itemId, versionId } = versionParamsSchema.parse(request.params);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      const analysis = await documentReviewService.runReviewSupportAnalysis({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        itemId,
        versionId,
      });

      return reply.code(201).send(analysis);
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/items/:itemId/versions/:versionId/final-validation", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { itemId, versionId } = versionParamsSchema.parse(request.params);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      const analysis = await documentReviewService.runFinalValidation({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        itemId,
        versionId,
      });

      return reply.code(201).send(analysis);
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/items/:itemId/versions/:versionId/comments", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { itemId, versionId } = versionParamsSchema.parse(request.params);
    const payload = anchoredCommentSchema.parse(request.body);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      const comment = await documentReviewService.createAnchoredComment({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        itemId,
        versionId,
        selectionStart: payload.selectionStart ?? null,
        selectionEnd: payload.selectionEnd ?? null,
        selectedText: payload.selectedText,
        selectionContextBefore: payload.selectionContextBefore ?? null,
        selectionContextAfter: payload.selectionContextAfter ?? null,
        pageNumber: payload.pageNumber ?? null,
        anchorX: payload.anchorX ?? null,
        anchorY: payload.anchorY ?? null,
        selectionRects: payload.selectionRects ?? [],
        commentText: payload.commentText,
      });

      return reply.code(201).send({
        ...comment,
        createdBy:
          profile.user.displayName ||
          `${profile.user.firstName} ${profile.user.lastName}`.trim() ||
          profile.user.email,
      });
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/items/:itemId/versions/:versionId/decisions", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { itemId, versionId } = versionParamsSchema.parse(request.params);
    const payload = decisionSchema.parse(request.body);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      const decision = await documentReviewService.recordDecision({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        itemId,
        versionId,
        decisionCode: payload.decisionCode,
        justification: payload.justification,
        rejectionReason: payload.rejectionReason || null,
        guidanceForNewVersion: payload.guidanceForNewVersion || null,
        finalValidationAnalysisId: payload.finalValidationAnalysisId ?? null,
      });

      return reply.code(201).send(decision);
    } catch (error) {
      handleServiceError(reply, error);
    }
  });

  app.post("/items/:itemId/transfer", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);
    const { itemId } = itemIdParamSchema.parse(request.params);
    const payload = transferReviewSchema.parse(request.body);

    if (!profile) {
      throw reply.unauthorized("Session is no longer valid");
    }

    try {
      const result = await documentReviewService.transferReview({
        lawFirmId: profile.lawFirm.id,
        actor: {
          actorUserId: profile.user.id,
          officeId: profile.user.primaryOfficeId ?? null,
          request,
        },
        itemId,
        reviewerUserId: payload.reviewerUserId,
        note: payload.note || null,
      });

      return reply.code(201).send(result);
    } catch (error) {
      handleServiceError(reply, error);
    }
  });
}
