import type { FastifyInstance } from "fastify";
import { z } from "zod";
import { requireSession } from "../../lib/auth.js";
import { writeAuditLog } from "../../lib/audit.js";
import {
  listClientSponsorsByClientIds,
  type ClientSponsorSummary,
} from "../../lib/client-sponsors.js";
import { createId } from "../../lib/id.js";
import { createDocumentAndExtraction } from "../../lib/intelligence.js";
import { prisma } from "../../lib/prisma.js";
import { getSessionProfile } from "../../lib/session.js";
import { readBinaryFile } from "../../lib/storage.js";
import {
  createWorkspaceApiToken,
  listWorkspaceApiTokens,
  requireWorkspaceApiAccess,
  revokeWorkspaceApiToken,
} from "../../lib/workspace-api.js";

const tokenNameSchema = z.object({
  name: z.string().trim().min(1).max(120),
});

const tokenIdParamSchema = z.object({
  tokenId: z.string().uuid(),
});

const clientIdParamSchema = z.object({
  clientId: z.string().uuid(),
});

const fileIdParamSchema = z.object({
  fileId: z.string().uuid(),
});

const documentListQuerySchema = z.object({
  caseId: z.string().uuid().optional(),
  limit: z.coerce.number().int().min(1).max(200).optional().default(100),
});

const clientListQuerySchema = z.object({
  q: z.string().trim().max(100).optional(),
  limit: z.coerce.number().int().min(1).max(200).optional().default(100),
});

const workspaceApiClientCreateSchema = z.object({
  firstName: z.string().trim().min(1).max(100),
  middleName: z.string().trim().max(100).optional().or(z.literal("")),
  lastName: z.string().trim().min(1).max(100),
  preferredName: z.string().trim().max(100).optional().or(z.literal("")),
  email: z.string().email().optional().or(z.literal("")),
  phone: z.string().trim().max(50).optional().or(z.literal("")),
  preferredLanguage: z.string().trim().max(30).optional().or(z.literal("")),
  dateOfBirth: z.string().trim().max(30).optional().or(z.literal("")),
  gender: z.string().trim().max(30).optional().or(z.literal("")),
  countryOfBirth: z.string().trim().max(2).optional().or(z.literal("")),
  countryOfCitizenship: z.string().trim().max(2).optional().or(z.literal("")),
  immigrationStatus: z.string().trim().max(100).optional().or(z.literal("")),
});

const workspaceApiClientUpdateSchema = z
  .object({
    firstName: z.string().trim().min(1).max(100).optional(),
    middleName: z.string().trim().max(100).optional().or(z.literal("")),
    lastName: z.string().trim().min(1).max(100).optional(),
    preferredName: z.string().trim().max(100).optional().or(z.literal("")),
    email: z.string().email().optional().or(z.literal("")),
    phone: z.string().trim().max(50).optional().or(z.literal("")),
    preferredLanguage: z.string().trim().max(30).optional().or(z.literal("")),
    dateOfBirth: z.string().trim().max(30).optional().or(z.literal("")),
    gender: z.string().trim().max(30).optional().or(z.literal("")),
    countryOfBirth: z.string().trim().max(2).optional().or(z.literal("")),
    countryOfCitizenship: z.string().trim().max(2).optional().or(z.literal("")),
    immigrationStatus: z.string().trim().max(100).optional().or(z.literal("")),
  })
  .refine((payload) => Object.keys(payload).length > 0, {
    message: "At least one field is required",
  });

const workspaceApiDocumentTypesQuerySchema = z.object({
  q: z.string().trim().max(100).optional(),
  limit: z.coerce.number().int().min(1).max(200).optional().default(100),
});

const fileQuerySchema = z.object({
  download: z
    .string()
    .optional()
    .transform((value) => value === "1" || value === "true"),
});

type WorkspaceApiClientRecord = {
  id: string;
  law_firm_id: string;
  primary_office_id: string | null;
  client_number: string | null;
  first_name: string;
  middle_name: string | null;
  last_name: string;
  preferred_name: string | null;
  date_of_birth: Date | null;
  gender: string | null;
  email: string | null;
  phone: string | null;
  preferred_language: string | null;
  country_of_birth: string | null;
  country_of_citizenship: string | null;
  immigration_status: string | null;
  created_at: Date;
  updated_at: Date;
};

type WorkspaceApiDocumentRow = {
  id: string;
  client_id: string;
  case_id: string | null;
  file_id: string;
  title: string;
  document_type_code: string;
  document_status: string;
  extracted_text: string | null;
  created_at: Date;
  updated_at: Date;
  original_file_name: string;
  mime_type: string;
  size_bytes: number;
};

type WorkspaceApiDocumentTypeRow = {
  code: string;
  name: string;
  description: string | null;
  category_code: string | null;
};

function normalizeOptionalValue(value: string | undefined) {
  const normalized = value?.trim();
  return normalized ? normalized : null;
}

function formatClientNumber(sequence: number) {
  return `CL-${String(sequence).padStart(6, "0")}`;
}

function formatClientName(client: {
  first_name: string;
  middle_name?: string | null;
  last_name: string;
}) {
  return [client.first_name, client.middle_name, client.last_name]
    .map((value) => value?.trim())
    .filter(Boolean)
    .join(" ");
}

function normalizeDateValue(value: string | undefined) {
  const normalized = String(value ?? "").trim();

  if (!normalized) {
    return null;
  }

  const date =
    /^\d{4}-\d{2}-\d{2}$/.test(normalized) ?
      new Date(`${normalized}T00:00:00.000Z`) :
      new Date(normalized);

  if (Number.isNaN(date.getTime())) {
    throw new Error("dateOfBirth must be a valid date");
  }

  return date;
}

function sanitizeContentDispositionFileName(fileName: string) {
  return fileName.replace(/["\\\r\n]+/g, "_");
}

function serializeWorkspaceApiClient(
  client: WorkspaceApiClientRecord,
  sponsor: ClientSponsorSummary | null = null,
) {
  return {
    id: client.id,
    lawFirmId: client.law_firm_id,
    primaryOfficeId: client.primary_office_id,
    clientNumber: client.client_number,
    firstName: client.first_name,
    middleName: client.middle_name,
    lastName: client.last_name,
    preferredName: client.preferred_name,
    name: formatClientName(client),
    dateOfBirth: client.date_of_birth,
    gender: client.gender,
    email: client.email,
    phone: client.phone,
    preferredLanguage: client.preferred_language,
    countryOfBirth: client.country_of_birth,
    countryOfCitizenship: client.country_of_citizenship,
    immigrationStatus: client.immigration_status,
    createdAt: client.created_at,
    updatedAt: client.updated_at,
    sponsor,
  };
}

function serializeWorkspaceApiDocument(row: WorkspaceApiDocumentRow) {
  return {
    id: row.id,
    clientId: row.client_id,
    caseId: row.case_id,
    fileId: row.file_id,
    title: row.title,
    documentTypeCode: row.document_type_code,
    documentStatus: row.document_status,
    extractedText: row.extracted_text,
    createdAt: row.created_at,
    updatedAt: row.updated_at,
    file: {
      id: row.file_id,
      fileName: row.original_file_name,
      mimeType: row.mime_type,
      sizeBytes: Number(row.size_bytes ?? 0),
      contentPath: `/workspace-api/v1/files/${row.file_id}/content`,
    },
  };
}

async function getWorkspaceApiClient(
  lawFirmId: string,
  clientId: string,
) {
  return prisma.client.findFirst({
    where: {
      id: clientId,
      law_firm_id: lawFirmId,
      deleted_at: null,
    },
  });
}

async function listWorkspaceApiDocumentRows(input: {
  lawFirmId: string;
  clientId: string;
  caseId?: string | null;
  limit: number;
}) {
  return prisma.$queryRaw<WorkspaceApiDocumentRow[]>`
    SELECT
      dr.id,
      dr.client_id,
      dr.case_id,
      dr.file_id,
      dr.title,
      dr.document_type_code,
      dr.document_status,
      dr.extracted_text,
      dr.created_at,
      dr.updated_at,
      f.original_file_name,
      f.mime_type,
      f.size_bytes
    FROM document_records dr
    INNER JOIN files f ON f.id = dr.file_id
    WHERE dr.law_firm_id = ${input.lawFirmId}
      AND dr.client_id = ${input.clientId}
      AND (${input.caseId ?? null} IS NULL OR dr.case_id = ${input.caseId ?? null})
    ORDER BY dr.created_at DESC
    LIMIT ${input.limit}
  `;
}

export async function registerWorkspaceApiRoutes(app: FastifyInstance) {
  app.get("/tokens", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

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

    return listWorkspaceApiTokens(profile.lawFirm.id);
  });

  app.post("/tokens", 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 payload = tokenNameSchema.parse(request.body);
    const created = await createWorkspaceApiToken({
      lawFirmId: profile.lawFirm.id,
      name: payload.name,
      createdByUserId: profile.user.id,
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "workspace_api_token",
      entityId: created.token.id,
      action: "workspace.api_token.create",
      afterJson: {
        tokenId: created.token.id,
        name: created.token.name,
        tokenPrefix: created.token.tokenPrefix,
        tokenLast4: created.token.tokenLast4,
      },
      request,
    });

    return reply.code(201).send({
      token: created.token,
      plainTextToken: created.plainTextToken,
      authorizationHeader: `Bearer ${created.plainTextToken}`,
    });
  });

  app.delete("/tokens/:tokenId", 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 { tokenId } = tokenIdParamSchema.parse(request.params);
    await revokeWorkspaceApiToken({
      lawFirmId: profile.lawFirm.id,
      tokenId,
    });

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "workspace_api_token",
      entityId: tokenId,
      action: "workspace.api_token.revoke",
      request,
    });

    return reply.code(204).send();
  });

  app.get("/v1/workspace", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);

    return {
      id: access.lawFirm.id,
      name: access.lawFirm.name,
      slug: access.lawFirm.slug,
      token: {
        id: access.tokenId,
        name: access.tokenName,
      },
    };
  });

  app.get("/v1/document-types", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const query = workspaceApiDocumentTypesQuerySchema.parse(request.query);
    const terms = (query.q ?? "")
      .trim()
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 5);

    const rows = await prisma.$queryRaw<WorkspaceApiDocumentTypeRow[]>`
      SELECT code, name, description, category_code
      FROM document_types
      ORDER BY name ASC, code ASC
    `;
    const filteredRows =
      terms.length > 0 ?
        rows.filter((row) =>
          terms.every((term) => {
            const normalizedTerm = term.toLowerCase();
            return [row.code, row.name, row.description]
              .filter((value): value is string => Boolean(value))
              .some((value) => value.toLowerCase().includes(normalizedTerm));
          }),
        ) :
        rows;

    return {
      workspaceId: access.lawFirm.id,
      items: filteredRows.slice(0, query.limit).map((row) => ({
        code: row.code,
        name: row.name,
        description: row.description,
        categoryCode: row.category_code,
      })),
    };
  });

  app.get("/v1/clients", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const query = clientListQuerySchema.parse(request.query);
    const terms = (query.q ?? "")
      .trim()
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 5);

    const clients = await prisma.client.findMany({
      where: {
        law_firm_id: access.lawFirm.id,
        deleted_at: null,
        ...(terms.length > 0 ?
          {
            AND: terms.map((term) => ({
              OR: [
                { first_name: { contains: term } },
                { middle_name: { contains: term } },
                { last_name: { contains: term } },
                { email: { contains: term } },
                { phone: { contains: term } },
                { client_number: { contains: term } },
              ],
            })),
          } :
          {}),
      },
      orderBy: {
        created_at: "desc",
      },
      take: query.limit,
    });
    const sponsorMap = await listClientSponsorsByClientIds({
      lawFirmId: access.lawFirm.id,
      clientIds: clients.map((client) => client.id),
    });

    return {
      workspaceId: access.lawFirm.id,
      items: clients.map((client) =>
        serializeWorkspaceApiClient(
          client as unknown as WorkspaceApiClientRecord,
          sponsorMap.get(client.id) ?? null,
        ),
      ),
    };
  });

  app.post("/v1/clients", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const payload = workspaceApiClientCreateSchema.parse(request.body);
    const count = await prisma.client.count({
      where: {
        law_firm_id: access.lawFirm.id,
      },
    });

    const client = await prisma.client.create({
      data: {
        id: createId(),
        law_firm_id: access.lawFirm.id,
        primary_office_id: null,
        client_number: formatClientNumber(count + 1),
        first_name: payload.firstName.trim(),
        middle_name: normalizeOptionalValue(payload.middleName),
        last_name: payload.lastName.trim(),
        preferred_name: normalizeOptionalValue(payload.preferredName),
        email: normalizeOptionalValue(payload.email),
        phone: normalizeOptionalValue(payload.phone),
        preferred_language: normalizeOptionalValue(payload.preferredLanguage),
        date_of_birth: normalizeDateValue(payload.dateOfBirth),
        gender: normalizeOptionalValue(payload.gender),
        country_of_birth: normalizeOptionalValue(payload.countryOfBirth),
        country_of_citizenship: normalizeOptionalValue(payload.countryOfCitizenship),
        immigration_status: normalizeOptionalValue(payload.immigrationStatus),
        created_by_user_id: null,
      },
    });

    await writeAuditLog({
      lawFirmId: access.lawFirm.id,
      actorUserId: null,
      entityType: "client",
      entityId: client.id,
      action: "workspace_api.client.create",
      afterJson: {
        tokenId: access.tokenId,
        tokenName: access.tokenName,
        clientNumber: client.client_number,
        firstName: client.first_name,
        lastName: client.last_name,
        email: client.email,
      },
      request,
    });

    return reply.code(201).send(
      serializeWorkspaceApiClient(client as unknown as WorkspaceApiClientRecord),
    );
  });

  app.get("/v1/clients/:clientId", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const { clientId } = clientIdParamSchema.parse(request.params);
    const client = await getWorkspaceApiClient(access.lawFirm.id, clientId);

    if (!client) {
      throw reply.notFound("Client not found");
    }

    const sponsorMap = await listClientSponsorsByClientIds({
      lawFirmId: access.lawFirm.id,
      clientIds: [client.id],
    });

    return serializeWorkspaceApiClient(
      client as unknown as WorkspaceApiClientRecord,
      sponsorMap.get(client.id) ?? null,
    );
  });

  app.patch("/v1/clients/:clientId", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const { clientId } = clientIdParamSchema.parse(request.params);
    const payload = workspaceApiClientUpdateSchema.parse(request.body);
    const client = await getWorkspaceApiClient(access.lawFirm.id, clientId);

    if (!client) {
      throw reply.notFound("Client not found");
    }

    const updateData: Record<string, unknown> = {};

    if (payload.firstName !== undefined) updateData.first_name = payload.firstName.trim();
    if (payload.middleName !== undefined) updateData.middle_name = normalizeOptionalValue(payload.middleName);
    if (payload.lastName !== undefined) updateData.last_name = payload.lastName.trim();
    if (payload.preferredName !== undefined) updateData.preferred_name = normalizeOptionalValue(payload.preferredName);
    if (payload.email !== undefined) updateData.email = normalizeOptionalValue(payload.email);
    if (payload.phone !== undefined) updateData.phone = normalizeOptionalValue(payload.phone);
    if (payload.preferredLanguage !== undefined) {
      updateData.preferred_language = normalizeOptionalValue(payload.preferredLanguage);
    }
    if (payload.dateOfBirth !== undefined) {
      updateData.date_of_birth = normalizeDateValue(payload.dateOfBirth);
    }
    if (payload.gender !== undefined) updateData.gender = normalizeOptionalValue(payload.gender);
    if (payload.countryOfBirth !== undefined) {
      updateData.country_of_birth = normalizeOptionalValue(payload.countryOfBirth);
    }
    if (payload.countryOfCitizenship !== undefined) {
      updateData.country_of_citizenship = normalizeOptionalValue(payload.countryOfCitizenship);
    }
    if (payload.immigrationStatus !== undefined) {
      updateData.immigration_status = normalizeOptionalValue(payload.immigrationStatus);
    }

    const updatedClient = await prisma.client.update({
      where: {
        id: client.id,
      },
      data: updateData,
    });

    const sponsorMap = await listClientSponsorsByClientIds({
      lawFirmId: access.lawFirm.id,
      clientIds: [updatedClient.id],
    });

    await writeAuditLog({
      lawFirmId: access.lawFirm.id,
      actorUserId: null,
      entityType: "client",
      entityId: updatedClient.id,
      action: "workspace_api.client.update",
      beforeJson: serializeWorkspaceApiClient(client as unknown as WorkspaceApiClientRecord),
      afterJson: serializeWorkspaceApiClient(
        updatedClient as unknown as WorkspaceApiClientRecord,
        sponsorMap.get(updatedClient.id) ?? null,
      ),
      request,
    });

    return serializeWorkspaceApiClient(
      updatedClient as unknown as WorkspaceApiClientRecord,
      sponsorMap.get(updatedClient.id) ?? null,
    );
  });

  app.get("/v1/clients/:clientId/documents", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const { clientId } = clientIdParamSchema.parse(request.params);
    const query = documentListQuerySchema.parse(request.query);
    const client = await getWorkspaceApiClient(access.lawFirm.id, clientId);

    if (!client) {
      throw reply.notFound("Client not found");
    }

    const rows = await listWorkspaceApiDocumentRows({
      lawFirmId: access.lawFirm.id,
      clientId,
      caseId: query.caseId ?? null,
      limit: query.limit,
    });

    return {
      workspaceId: access.lawFirm.id,
      clientId,
      items: rows.map((row) => serializeWorkspaceApiDocument(row)),
    };
  });

  app.post("/v1/clients/:clientId/documents", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const { clientId } = clientIdParamSchema.parse(request.params);
    const client = await getWorkspaceApiClient(access.lawFirm.id, clientId);

    if (!client) {
      throw reply.notFound("Client not found");
    }

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

    let caseId = "";
    let documentTypeCode = "";
    let title = "";
    let providedText = "";
    let createdDocument: Awaited<ReturnType<typeof createDocumentAndExtraction>> | null = null;
    let createdFileMeta: {
      originalFileName: string;
      mimeType: string;
      title: string;
    } | null = null;

    for await (const part of request.parts()) {
      if (part.type === "field") {
        const value = String(part.value ?? "").trim();

        if (part.fieldname === "caseId") {
          caseId = value;
        } else if (part.fieldname === "documentTypeCode") {
          documentTypeCode = value;
        } else if (part.fieldname === "title") {
          title = value;
        } else if (part.fieldname === "textContent") {
          providedText = value;
        }

        continue;
      }

      const resolvedCaseId = caseId || "";
      const resolvedDocumentTypeCode = documentTypeCode || "other_supporting";
      const resolvedTitle = title || part.filename;

      if (resolvedCaseId) {
        const caseRow = await prisma.caseRecord.findFirst({
          where: {
            id: resolvedCaseId,
            law_firm_id: access.lawFirm.id,
            client_id: clientId,
            deleted_at: null,
          },
          select: {
            id: true,
          },
        });

        if (!caseRow) {
          throw reply.badRequest("caseId does not belong to this client in the current workspace");
        }
      }

      const buffer = await part.toBuffer();
      createdDocument = await createDocumentAndExtraction({
        lawFirmId: access.lawFirm.id,
        clientId,
        caseId: resolvedCaseId || null,
        actorUserId: null,
        title: resolvedTitle,
        documentTypeCode: resolvedDocumentTypeCode,
        documentTypeMode: "auto",
        originalFileName: part.filename,
        mimeType: part.mimetype,
        fileBuffer: buffer,
        textContent: providedText || null,
      });
      createdFileMeta = {
        originalFileName: part.filename,
        mimeType: part.mimetype,
        title: resolvedTitle,
      };
      break;
    }

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

    const [documentRow] = await prisma.$queryRaw<WorkspaceApiDocumentRow[]>`
      SELECT
        dr.id,
        dr.client_id,
        dr.case_id,
        dr.file_id,
        dr.title,
        dr.document_type_code,
        dr.document_status,
        dr.extracted_text,
        dr.created_at,
        dr.updated_at,
        f.original_file_name,
        f.mime_type,
        f.size_bytes
      FROM document_records dr
      INNER JOIN files f ON f.id = dr.file_id
      WHERE dr.id = ${createdDocument.documentRecordId}
      LIMIT 1
    `;

    await writeAuditLog({
      lawFirmId: access.lawFirm.id,
      actorUserId: null,
      entityType: "document_record",
      entityId: createdDocument.documentRecordId,
      action: "workspace_api.document.upload",
      afterJson: {
        tokenId: access.tokenId,
        tokenName: access.tokenName,
        clientId,
        title: createdFileMeta.title,
        originalFileName: createdFileMeta.originalFileName,
        documentTypeCode: createdDocument.documentTypeCode,
        fileId: createdDocument.fileId,
      },
      request,
    });

    return reply.code(201).send({
      document: documentRow ? serializeWorkspaceApiDocument(documentRow) : null,
      classification: {
        documentTypeCode: createdDocument.documentTypeCode,
        source: createdDocument.classificationSource,
        confidence: createdDocument.classificationConfidence,
        reason: createdDocument.classificationReason,
      },
    });
  });

  app.get("/v1/files/:fileId/content", async (request, reply) => {
    const access = await requireWorkspaceApiAccess(request, reply);
    const { fileId } = fileIdParamSchema.parse(request.params);
    const query = fileQuerySchema.parse(request.query);

    const [file] = await prisma.$queryRaw<
      Array<{
        id: string;
        law_firm_id: string;
        storage_provider: string;
        object_key: string;
        mime_type: string;
        original_file_name: string;
      }>
    >`
      SELECT id, law_firm_id, storage_provider, object_key, mime_type, original_file_name
      FROM files
      WHERE id = ${fileId}
        AND law_firm_id = ${access.lawFirm.id}
      LIMIT 1
    `;

    if (!file) {
      throw reply.notFound("File not found");
    }

    const bytes = await readBinaryFile({
      storageProvider: file.storage_provider,
      objectKey: file.object_key,
    });
    const disposition = query.download ? "attachment" : "inline";
    const fileName = sanitizeContentDispositionFileName(file.original_file_name);

    reply.header("Content-Type", file.mime_type || "application/octet-stream");
    reply.header("Content-Length", bytes.length);
    reply.header("Content-Disposition", `${disposition}; filename="${fileName}"`);

    return reply.send(bytes);
  });
}
