import type { FastifyInstance, FastifyReply } 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 { prisma } from "../../lib/prisma.js";
import { getSessionProfile } from "../../lib/session.js";

const CLIENT_STRUCTURE_AI_INSTRUCTIONS_MAX_LENGTH = 12000;
const CLIENT_STRUCTURE_CONDITIONAL_PROMPT_MAX_LENGTH = 500;
const CLIENT_STRUCTURE_DOCUMENT_TYPE_NAME_MAX_LENGTH = 100;
const CLIENT_STRUCTURE_DOCUMENT_TYPE_DESCRIPTION_MAX_LENGTH = 255;

const clientInputSchema = z.object({
  firstName: z.string().min(1).max(100),
  middleName: z.string().max(100).optional().or(z.literal("")),
  lastName: z.string().min(1).max(100),
  email: z.string().email().optional().or(z.literal("")),
  phone: z.string().max(50).optional().or(z.literal("")),
  preferredLanguage: z.string().max(30).optional().or(z.literal("")),
});

const clientCreateSchema = clientInputSchema;

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

const clientImportSchema = z.object({
  clients: z.array(clientInputSchema).min(1).max(500),
});

const clientSponsorUpdateSchema = z
  .object({
    clear: z.boolean().optional().default(false),
    entityType: z.enum(["person", "company"]).optional().default("company"),
    firstName: z.string().max(100).optional().or(z.literal("")),
    lastName: z.string().max(100).optional().or(z.literal("")),
    companyName: z.string().max(255).optional().or(z.literal("")),
    email: z.string().email().optional().or(z.literal("")),
    phone: z.string().max(50).optional().or(z.literal("")),
  })
  .superRefine((value, ctx) => {
    if (value.clear) {
      return;
    }

    if (value.entityType === "company" && !normalizeOptionalValue(value.companyName)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "companyName is required when entityType is company",
        path: ["companyName"],
      });
    }

    if (
      value.entityType === "person" &&
      !normalizeOptionalValue(value.firstName) &&
      !normalizeOptionalValue(value.lastName)
    ) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "firstName or lastName is required when entityType is person",
        path: ["firstName"],
      });
    }
  });

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

const clientStructureNodeTypeSchema = z.enum(["group", "field", "document"]);
const clientStructureDataTypeSchema = z.enum([
  "text",
  "long_text",
  "number",
  "boolean",
  "date",
  "email",
  "phone",
]);

const clientStructureNodeParamSchema = z.object({
  nodeId: z.string().uuid(),
});

const clientStructureDocumentTypeCreateSchema = z.object({
  name: z.string().trim().min(1).max(CLIENT_STRUCTURE_DOCUMENT_TYPE_NAME_MAX_LENGTH),
  description: z
    .string()
    .trim()
    .max(CLIENT_STRUCTURE_DOCUMENT_TYPE_DESCRIPTION_MAX_LENGTH)
    .optional()
    .or(z.literal("")),
});

const clientStructureCreateSchema = z
  .object({
    parentId: z.string().uuid().nullable().optional(),
    nodeType: clientStructureNodeTypeSchema,
    label: z.string().trim().min(1).max(255),
    fieldKey: z.string().trim().max(120).optional().or(z.literal("")),
    description: z.string().trim().max(2000).optional().or(z.literal("")),
    aiInstructions: z
      .string()
      .trim()
      .max(CLIENT_STRUCTURE_AI_INSTRUCTIONS_MAX_LENGTH)
      .optional()
      .or(z.literal("")),
    dataType: clientStructureDataTypeSchema.optional(),
    documentTypeCode: z.string().trim().max(50).optional().or(z.literal("")),
    repeatable: z.boolean().optional().default(false),
    required: z.boolean().optional().default(false),
    conditional: z.boolean().optional().default(false),
    conditionalPrompt: z
      .string()
      .trim()
      .max(CLIENT_STRUCTURE_CONDITIONAL_PROMPT_MAX_LENGTH)
      .optional()
      .or(z.literal("")),
  })
  .superRefine((value, ctx) => {
    if (value.nodeType === "field" && !value.dataType) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "dataType is required when nodeType is field",
        path: ["dataType"],
      });
    }

    if (value.nodeType === "field" && value.repeatable) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "repeatable is only allowed when nodeType is group",
        path: ["repeatable"],
      });
    }

    if (value.nodeType === "field" && normalizeOptionalValue(value.documentTypeCode)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "documentTypeCode is only allowed when nodeType is document",
        path: ["documentTypeCode"],
      });
    }

    if (value.nodeType === "document" && !normalizeOptionalValue(value.documentTypeCode)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "documentTypeCode is required when nodeType is document",
        path: ["documentTypeCode"],
      });
    }

    if (value.nodeType === "document" && value.dataType) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "dataType is only allowed when nodeType is field",
        path: ["dataType"],
      });
    }

    if (value.nodeType === "document" && value.repeatable) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "repeatable is only allowed when nodeType is group",
        path: ["repeatable"],
      });
    }

    if (value.conditional && !normalizeOptionalValue(value.conditionalPrompt)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "conditionalPrompt is required when conditional is true",
        path: ["conditionalPrompt"],
      });
    }

    if (!value.conditional && normalizeOptionalValue(value.conditionalPrompt)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "conditionalPrompt is only allowed when conditional is true",
        path: ["conditionalPrompt"],
      });
    }
  });

const clientStructureUpdateSchema = z
  .object({
    label: z.string().trim().min(1).max(255),
    fieldKey: z.string().trim().max(120).optional().or(z.literal("")),
    description: z.string().trim().max(2000).optional().or(z.literal("")),
    aiInstructions: z
      .string()
      .trim()
      .max(CLIENT_STRUCTURE_AI_INSTRUCTIONS_MAX_LENGTH)
      .optional()
      .or(z.literal("")),
    dataType: clientStructureDataTypeSchema.optional(),
    documentTypeCode: z.string().trim().max(50).optional().or(z.literal("")),
    repeatable: z.boolean().optional().default(false),
    required: z.boolean().optional().default(false),
    conditional: z.boolean().optional().default(false),
    conditionalPrompt: z
      .string()
      .trim()
      .max(CLIENT_STRUCTURE_CONDITIONAL_PROMPT_MAX_LENGTH)
      .optional()
      .or(z.literal("")),
  })
  .superRefine((value, ctx) => {
    if (value.conditional && !normalizeOptionalValue(value.conditionalPrompt)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "conditionalPrompt is required when conditional is true",
        path: ["conditionalPrompt"],
      });
    }

    if (!value.conditional && normalizeOptionalValue(value.conditionalPrompt)) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "conditionalPrompt is only allowed when conditional is true",
        path: ["conditionalPrompt"],
      });
    }
  });

const clientStructureReorderSchema = z.object({
  nodeId: z.string().uuid(),
  targetNodeId: z.string().uuid(),
  position: z.enum(["before", "after"]),
});

type ClientInput = z.infer<typeof clientInputSchema>;

type ClientRecord = {
  id: string;
  client_number: string | null;
  first_name: string;
  middle_name: string | null;
  last_name: string;
  email: string | null;
  phone: string | null;
  preferred_language: string | null;
  created_at: Date;
};

type ClientDetailRecord = ClientRecord & {
  law_firm_id: string;
  primary_office_id: string | null;
  preferred_name: string | null;
  date_of_birth: Date | null;
  gender: string | null;
  country_of_birth: string | null;
  country_of_citizenship: string | null;
  immigration_status: string | null;
  intake_channel_code: string | null;
  created_by_user_id: string | null;
  updated_at: Date;
  deleted_at: Date | null;
};

type ClientStructureNodeRecord = {
  id: string;
  law_firm_id: string;
  parent_id: string | null;
  node_type: string;
  field_key: string;
  label: string;
  description: string | null;
  ai_instructions: string | null;
  data_type: string | null;
  document_type_code: string | null;
  is_required: number;
  is_repeatable: number;
  is_conditional: number;
  conditional_prompt: string | null;
  sort_order: number;
  created_by_user_id: string | null;
  created_at: Date;
  updated_at: Date;
};

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

type ClientDetailDocumentMatchRecord = {
  id: string;
  client_structure_node_id: string | null;
  document_type_code: string;
  title: string;
  validation_status: string | null;
  validation_confidence: number | null;
  validation_reason: string | null;
  created_at: Date;
};

type DefaultClientStructureNodeSeed = {
  fieldKey: string;
  label: string;
  nodeType: "group" | "field" | "document";
  parentFieldKey?: string;
  description?: string | null;
  dataType?: z.infer<typeof clientStructureDataTypeSchema>;
  documentTypeCode?: string | null;
  aiInstructions?: string | null;
  repeatable?: boolean;
  required?: boolean;
  conditional?: boolean;
  conditionalPrompt?: string | null;
  sortOrder: number;
};

type SerializedClient = ReturnType<typeof serializeClient>;

const defaultClientStructureDocumentTypeSeeds = [
  {
    code: "passport",
    name: "Passport",
    description: "Passport identity document",
    categoryCode: "identity",
    isIdentityDocument: 1,
    isExpirable: 1,
  },
  {
    code: "birth_certificate",
    name: "Birth Certificate",
    description: "Civil birth certificate",
    categoryCode: "civil_record",
    isIdentityDocument: 1,
    isExpirable: 0,
  },
  {
    code: "marriage_certificate",
    name: "Marriage Certificate",
    description: "Civil marriage certificate",
    categoryCode: "civil_record",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "resume",
    name: "Resume",
    description: "Professional resume or CV",
    categoryCode: "professional",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "employment_letter",
    name: "Employment Letter",
    description: "Offer letter or employment verification",
    categoryCode: "employment",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "pay_stub",
    name: "Pay Stub",
    description: "Proof of current compensation",
    categoryCode: "financial",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "tax_return",
    name: "Tax Return",
    description: "Income tax filing evidence",
    categoryCode: "financial",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "visa",
    name: "Visa",
    description: "Visa page or approval evidence",
    categoryCode: "immigration",
    isIdentityDocument: 1,
    isExpirable: 1,
  },
  {
    code: "i94",
    name: "I-94",
    description: "Admission record",
    categoryCode: "immigration",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "photo",
    name: "Photo",
    description: "Passport-style or evidence photo",
    categoryCode: "media",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "translation",
    name: "Translation",
    description: "Certified translation document",
    categoryCode: "supporting",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
  {
    code: "other_supporting",
    name: "Other Supporting",
    description: "Any additional supporting evidence",
    categoryCode: "supporting",
    isIdentityDocument: 0,
    isExpirable: 0,
  },
] as const satisfies Array<{
  code: string;
  name: string;
  description: string;
  categoryCode: string;
  isIdentityDocument: 0 | 1;
  isExpirable: 0 | 1;
}>;

const defaultClientStructureNodeSeeds: DefaultClientStructureNodeSeed[] = [
  {
    fieldKey: "system.identity",
    label: "Identity",
    nodeType: "group",
    description: "Core personal identification fields for the client profile.",
    sortOrder: 0,
  },
  {
    fieldKey: "system.identity.client_number",
    label: "Client number",
    nodeType: "field",
    parentFieldKey: "system.identity",
    dataType: "text",
    sortOrder: 0,
  },
  {
    fieldKey: "system.identity.first_name",
    label: "First name",
    nodeType: "field",
    parentFieldKey: "system.identity",
    dataType: "text",
    required: true,
    sortOrder: 10,
  },
  {
    fieldKey: "system.identity.middle_name",
    label: "Middle name",
    nodeType: "field",
    parentFieldKey: "system.identity",
    dataType: "text",
    sortOrder: 20,
  },
  {
    fieldKey: "system.identity.last_name",
    label: "Surname",
    nodeType: "field",
    parentFieldKey: "system.identity",
    dataType: "text",
    required: true,
    sortOrder: 30,
  },
  {
    fieldKey: "system.identity.preferred_name",
    label: "Preferred name",
    nodeType: "field",
    parentFieldKey: "system.identity",
    dataType: "text",
    sortOrder: 40,
  },
  {
    fieldKey: "system.identity.date_of_birth",
    label: "Date of birth",
    nodeType: "field",
    parentFieldKey: "system.identity",
    dataType: "date",
    sortOrder: 50,
  },
  {
    fieldKey: "system.identity.gender",
    label: "Gender",
    nodeType: "field",
    parentFieldKey: "system.identity",
    dataType: "text",
    sortOrder: 60,
  },
  {
    fieldKey: "system.contact",
    label: "Contact",
    nodeType: "group",
    description: "Primary contact channels used to communicate with the client.",
    sortOrder: 10,
  },
  {
    fieldKey: "system.contact.email",
    label: "E-mail",
    nodeType: "field",
    parentFieldKey: "system.contact",
    dataType: "email",
    sortOrder: 0,
  },
  {
    fieldKey: "system.contact.phone",
    label: "Telephone",
    nodeType: "field",
    parentFieldKey: "system.contact",
    dataType: "phone",
    sortOrder: 10,
  },
  {
    fieldKey: "system.contact.preferred_language",
    label: "Preferred language",
    nodeType: "field",
    parentFieldKey: "system.contact",
    dataType: "text",
    sortOrder: 20,
  },
  {
    fieldKey: "system.immigration",
    label: "Immigration",
    nodeType: "group",
    description: "Immigration profile data used across cases and forms.",
    sortOrder: 20,
  },
  {
    fieldKey: "system.immigration.country_of_birth",
    label: "Country of birth",
    nodeType: "field",
    parentFieldKey: "system.immigration",
    dataType: "text",
    sortOrder: 0,
  },
  {
    fieldKey: "system.immigration.country_of_citizenship",
    label: "Country of citizenship",
    nodeType: "field",
    parentFieldKey: "system.immigration",
    dataType: "text",
    sortOrder: 10,
  },
  {
    fieldKey: "system.immigration.immigration_status",
    label: "Immigration status",
    nodeType: "field",
    parentFieldKey: "system.immigration",
    dataType: "text",
    sortOrder: 20,
  },
  {
    fieldKey: "system.immigration.intake_channel_code",
    label: "Intake channel",
    nodeType: "field",
    parentFieldKey: "system.immigration",
    dataType: "text",
    sortOrder: 30,
  },
  {
    fieldKey: "system.relationships",
    label: "Relationships",
    nodeType: "group",
    description: "Relationships linked to the client record.",
    sortOrder: 30,
  },
  {
    fieldKey: "system.relationships.sponsor",
    label: "Sponsor",
    nodeType: "group",
    parentFieldKey: "system.relationships",
    sortOrder: 0,
  },
  {
    fieldKey: "system.relationships.sponsor.id",
    label: "Record ID",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "text",
    sortOrder: 0,
  },
  {
    fieldKey: "system.relationships.sponsor.entity_type",
    label: "Entity type",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "text",
    sortOrder: 10,
  },
  {
    fieldKey: "system.relationships.sponsor.name",
    label: "Name",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "text",
    sortOrder: 20,
  },
  {
    fieldKey: "system.relationships.sponsor.first_name",
    label: "First name",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "text",
    sortOrder: 30,
  },
  {
    fieldKey: "system.relationships.sponsor.last_name",
    label: "Last name",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "text",
    sortOrder: 40,
  },
  {
    fieldKey: "system.relationships.sponsor.company_name",
    label: "Company name",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "text",
    sortOrder: 50,
  },
  {
    fieldKey: "system.relationships.sponsor.email",
    label: "E-mail",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "email",
    sortOrder: 60,
  },
  {
    fieldKey: "system.relationships.sponsor.phone",
    label: "Telephone",
    nodeType: "field",
    parentFieldKey: "system.relationships.sponsor",
    dataType: "phone",
    sortOrder: 70,
  },
  {
    fieldKey: "system.workspace_metadata",
    label: "Workspace metadata",
    nodeType: "group",
    description: "Internal workspace identifiers linked to the client record.",
    sortOrder: 40,
  },
  {
    fieldKey: "system.workspace_metadata.id",
    label: "Client ID",
    nodeType: "field",
    parentFieldKey: "system.workspace_metadata",
    dataType: "text",
    sortOrder: 0,
  },
  {
    fieldKey: "system.workspace_metadata.law_firm_id",
    label: "Law firm ID",
    nodeType: "field",
    parentFieldKey: "system.workspace_metadata",
    dataType: "text",
    sortOrder: 10,
  },
  {
    fieldKey: "system.workspace_metadata.primary_office_id",
    label: "Primary office ID",
    nodeType: "field",
    parentFieldKey: "system.workspace_metadata",
    dataType: "text",
    sortOrder: 20,
  },
  {
    fieldKey: "system.workspace_metadata.created_by_user_id",
    label: "Created by user ID",
    nodeType: "field",
    parentFieldKey: "system.workspace_metadata",
    dataType: "text",
    sortOrder: 30,
  },
  {
    fieldKey: "system.audit",
    label: "Audit",
    nodeType: "group",
    description: "Audit timestamps for the client lifecycle.",
    sortOrder: 50,
  },
  {
    fieldKey: "system.audit.created_at",
    label: "Created at",
    nodeType: "field",
    parentFieldKey: "system.audit",
    dataType: "date",
    sortOrder: 0,
  },
  {
    fieldKey: "system.audit.updated_at",
    label: "Updated at",
    nodeType: "field",
    parentFieldKey: "system.audit",
    dataType: "date",
    sortOrder: 10,
  },
  {
    fieldKey: "system.audit.deleted_at",
    label: "Deleted at",
    nodeType: "field",
    parentFieldKey: "system.audit",
    dataType: "date",
    sortOrder: 20,
  },
];

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 normalizeOptionalValue(value: string | undefined) {
  const normalized = value?.trim();
  return normalized ? normalized : null;
}

function normalizeClientStructureFieldKey(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .replace(/_+/g, "_");
}

function isMissingClientDataStructureTableError(error: unknown) {
  if (!(error instanceof Error)) {
    return false;
  }

  const message = error.message.toLowerCase();
  return (
    message.includes("client_data_structure_nodes") &&
    (message.includes("doesn't exist") ||
      message.includes("does not exist") ||
      message.includes("unknown table"))
  );
}

function isMissingClientDataStructureRepeatableColumnError(error: unknown) {
  if (!(error instanceof Error)) {
    return false;
  }

  const message = error.message.toLowerCase();
  return (
    message.includes("is_repeatable") &&
    (message.includes("unknown column") ||
      message.includes("doesn't exist") ||
      message.includes("does not exist"))
  );
}

function isMissingClientDataStructureDocumentColumnError(error: unknown) {
  if (!(error instanceof Error)) {
    return false;
  }

  const message = error.message.toLowerCase();
  return (
    (message.includes("document_type_code") || message.includes("ai_instructions")) &&
    (message.includes("unknown column") ||
      message.includes("doesn't exist") ||
      message.includes("does not exist"))
  );
}

function isMissingClientDataStructureConditionalColumnError(error: unknown) {
  if (!(error instanceof Error)) {
    return false;
  }

  const message = error.message.toLowerCase();
  return (
    (message.includes("is_conditional") || message.includes("conditional_prompt")) &&
    (message.includes("unknown column") ||
      message.includes("doesn't exist") ||
      message.includes("does not exist"))
  );
}

function isMissingClientDocumentValidationColumnError(error: unknown) {
  if (!(error instanceof Error)) {
    return false;
  }

  const message = error.message.toLowerCase();
  return (
    (message.includes("client_structure_node_id") ||
      message.includes("validation_status") ||
      message.includes("validation_confidence") ||
      message.includes("validation_reason")) &&
    (message.includes("unknown column") ||
      message.includes("doesn't exist") ||
      message.includes("does not exist"))
  );
}

function replyClientDataStructureSchemaError(reply: FastifyReply, error: unknown) {
  if (isMissingClientDataStructureTableError(error)) {
    throw reply.internalServerError(
      "Client data structure table is missing. Run database/mysql/038_client_data_structure_nodes.sql, database/mysql/039_client_data_structure_repeatable.sql, database/mysql/040_client_structure_document_nodes.sql, and database/mysql/041_client_structure_conditional_nodes.sql.",
    );
  }

  if (isMissingClientDataStructureRepeatableColumnError(error)) {
    throw reply.internalServerError(
      "Client data structure schema is outdated. Run database/mysql/039_client_data_structure_repeatable.sql.",
    );
  }

  if (isMissingClientDataStructureDocumentColumnError(error) || isMissingClientDocumentValidationColumnError(error)) {
    throw reply.internalServerError(
      "Client data structure document schema is outdated. Run database/mysql/040_client_structure_document_nodes.sql.",
    );
  }

  if (isMissingClientDataStructureConditionalColumnError(error)) {
    throw reply.internalServerError(
      "Client data structure conditional schema is outdated. Run database/mysql/041_client_structure_conditional_nodes.sql.",
    );
  }
}

function serializeClient(client: ClientRecord, sponsor: ClientSponsorSummary | null = null) {
  return {
    id: client.id,
    clientNumber: client.client_number,
    firstName: client.first_name,
    middleName: client.middle_name,
    lastName: client.last_name,
    name: formatClientName(client),
    email: client.email,
    phone: client.phone,
    preferredLanguage: client.preferred_language,
    createdAt: client.created_at,
    sponsor,
  };
}

function serializeClientStructureDocumentType(documentType: ClientStructureDocumentTypeRecord) {
  return {
    code: documentType.code,
    name: documentType.name,
    description: documentType.description,
  };
}

function serializeClientStructureNode(node: ClientStructureNodeRecord) {
  const isSystemNode = !node.created_by_user_id;

  return {
    id: node.id,
    lawFirmId: node.law_firm_id,
    parentId: node.parent_id,
    nodeType:
      node.node_type === "group" ? "group" : node.node_type === "document" ? "document" : "field",
    fieldKey: node.field_key,
    label: node.label,
    description: node.description,
    aiInstructions: node.ai_instructions,
    dataType: node.data_type,
    documentTypeCode: node.document_type_code,
    required: Boolean(node.is_required),
    repeatable: Boolean(node.is_repeatable),
    conditional: Boolean(node.is_conditional),
    conditionalPrompt: node.conditional_prompt,
    sortOrder: Number(node.sort_order),
    createdByUserId: node.created_by_user_id,
    createdAt: node.created_at,
    updatedAt: node.updated_at,
    source: isSystemNode ? "system" : "custom",
    locked: isSystemNode,
  };
}

function serializeClientDetailDocumentMatch(match: ClientDetailDocumentMatchRecord) {
  return {
    id: match.id,
    nodeId: match.client_structure_node_id,
    documentTypeCode: match.document_type_code,
    title: match.title,
    validationStatus: match.validation_status,
    validationConfidence:
      match.validation_confidence == null ? null : Number(match.validation_confidence),
    validationReason: match.validation_reason,
    createdAt: match.created_at,
  };
}

function serializeClientDetail(
  client: ClientDetailRecord,
  structureNodes: ClientStructureNodeRecord[],
  documentMatches: ClientDetailDocumentMatchRecord[],
  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,
    intakeChannelCode: client.intake_channel_code,
    createdByUserId: client.created_by_user_id,
    createdAt: client.created_at,
    updatedAt: client.updated_at,
    deletedAt: client.deleted_at,
    sponsor,
    structureNodes: structureNodes.map((node) => serializeClientStructureNode(node)),
    documentMatches: documentMatches.map((match) => serializeClientDetailDocumentMatch(match)),
  };
}

async function serializeClientsWithSponsors(
  lawFirmId: string,
  clients: ClientRecord[],
): Promise<SerializedClient[]> {
  const sponsorMap = await listClientSponsorsByClientIds({
    lawFirmId,
    clientIds: clients.map((client) => client.id),
  });

  return clients.map((client) => serializeClient(client, sponsorMap.get(client.id) ?? null));
}

async function listClientStructureNodes(lawFirmId: string) {
  return prisma.$queryRaw<ClientStructureNodeRecord[]>`
    SELECT
      id,
      law_firm_id,
      parent_id,
      node_type,
      field_key,
      label,
      description,
      ai_instructions,
      data_type,
      document_type_code,
      is_required,
      is_repeatable,
      is_conditional,
      conditional_prompt,
      sort_order,
      created_by_user_id,
      created_at,
      updated_at
    FROM client_data_structure_nodes
    WHERE law_firm_id = ${lawFirmId}
    ORDER BY COALESCE(parent_id, ''), sort_order ASC, label ASC
  `;
}

async function listClientStructureDocumentTypes() {
  await ensureDefaultClientStructureDocumentTypes();

  return prisma.$queryRaw<ClientStructureDocumentTypeRecord[]>`
    SELECT code, name, description
    FROM document_types
    ORDER BY name ASC, code ASC
  `;
}

async function resolveClientStructureDocumentTypeCode(documentTypeCode: string) {
  await ensureDefaultClientStructureDocumentTypes();

  const normalizedCode = documentTypeCode.trim();

  if (!normalizedCode) {
    return null;
  }

  const [row] = await prisma.$queryRaw<Array<{ code: string }>>`
    SELECT code
    FROM document_types
    WHERE code = ${normalizedCode}
    LIMIT 1
  `;

  return row?.code ?? null;
}

async function ensureDefaultClientStructureDocumentTypes() {
  for (const documentType of defaultClientStructureDocumentTypeSeeds) {
    await prisma.$executeRaw`
      INSERT IGNORE INTO document_types (
        code,
        name,
        description,
        category_code,
        is_identity_document,
        is_expirable
      ) VALUES (
        ${documentType.code},
        ${documentType.name},
        ${documentType.description},
        ${documentType.categoryCode},
        ${documentType.isIdentityDocument},
        ${documentType.isExpirable}
      )
    `;
  }
}

async function buildUniqueClientStructureDocumentTypeCode(baseName: string) {
  const normalizedBaseCode =
    normalizeClientStructureFieldKey(baseName).slice(0, 50) || "custom_document";
  const rows = await prisma.$queryRaw<Array<{ code: string }>>`
    SELECT code
    FROM document_types
  `;
  const existingCodes = new Set(rows.map((row) => row.code.toLowerCase()));

  if (!existingCodes.has(normalizedBaseCode.toLowerCase())) {
    return normalizedBaseCode;
  }

  let sequence = 2;

  while (true) {
    const suffix = `_${sequence}`;
    const candidateCode = `${normalizedBaseCode.slice(0, Math.max(1, 50 - suffix.length))}${suffix}`;

    if (!existingCodes.has(candidateCode.toLowerCase())) {
      return candidateCode;
    }

    sequence += 1;
  }
}

async function listClientDetailDocumentMatches(lawFirmId: string, clientId: string) {
  return prisma.$queryRaw<ClientDetailDocumentMatchRecord[]>`
    SELECT
      id,
      client_structure_node_id,
      document_type_code,
      title,
      validation_status,
      validation_confidence,
      validation_reason,
      created_at
    FROM document_records
    WHERE law_firm_id = ${lawFirmId}
      AND client_id = ${clientId}
      AND client_structure_node_id IS NOT NULL
    ORDER BY created_at DESC, title ASC
  `;
}

async function ensureDefaultClientStructureNodes(lawFirmId: string) {
  const existingNodes = await listClientStructureNodes(lawFirmId);
  const existingByFieldKey = new Map(
    existingNodes.map((node) => [node.field_key, node] as const),
  );
  let insertedAny = false;

  for (const seedNode of defaultClientStructureNodeSeeds) {
    if (existingByFieldKey.has(seedNode.fieldKey)) {
      continue;
    }

    const parentId = seedNode.parentFieldKey
      ? existingByFieldKey.get(seedNode.parentFieldKey)?.id ?? null
      : null;

    if (seedNode.parentFieldKey && !parentId) {
      throw new Error(
        `Client structure parent "${seedNode.parentFieldKey}" was not found while seeding "${seedNode.fieldKey}".`,
      );
    }

    const now = new Date();
    const nodeRecord: ClientStructureNodeRecord = {
      id: createId(),
      law_firm_id: lawFirmId,
      parent_id: parentId,
      node_type: seedNode.nodeType,
      field_key: seedNode.fieldKey,
      label: seedNode.label,
      description: seedNode.description ?? null,
      ai_instructions: seedNode.aiInstructions ?? null,
      data_type: seedNode.nodeType === "field" ? seedNode.dataType ?? null : null,
      document_type_code:
        seedNode.nodeType === "document" ? seedNode.documentTypeCode ?? null : null,
      is_required: seedNode.nodeType !== "group" && seedNode.required ? 1 : 0,
      is_repeatable: seedNode.nodeType === "group" && seedNode.repeatable ? 1 : 0,
      is_conditional: seedNode.conditional ? 1 : 0,
      conditional_prompt: seedNode.conditional ? seedNode.conditionalPrompt ?? null : null,
      sort_order: seedNode.sortOrder,
      created_by_user_id: null,
      created_at: now,
      updated_at: now,
    };

    await prisma.$executeRaw`
      INSERT INTO client_data_structure_nodes (
        id,
        law_firm_id,
        parent_id,
        node_type,
        field_key,
        label,
        description,
        ai_instructions,
        data_type,
        document_type_code,
        is_required,
        is_repeatable,
        is_conditional,
        conditional_prompt,
        sort_order,
        created_by_user_id
      ) VALUES (
        ${nodeRecord.id},
        ${nodeRecord.law_firm_id},
        ${nodeRecord.parent_id},
        ${nodeRecord.node_type},
        ${nodeRecord.field_key},
        ${nodeRecord.label},
        ${nodeRecord.description},
        ${nodeRecord.ai_instructions},
        ${nodeRecord.data_type},
        ${nodeRecord.document_type_code},
        ${nodeRecord.is_required},
        ${nodeRecord.is_repeatable},
        ${nodeRecord.is_conditional},
        ${nodeRecord.conditional_prompt},
        ${nodeRecord.sort_order},
        ${nodeRecord.created_by_user_id}
      )
    `;

    existingByFieldKey.set(nodeRecord.field_key, nodeRecord);
    insertedAny = true;
  }

  return insertedAny ? listClientStructureNodes(lawFirmId) : existingNodes;
}

async function getClientStructureNode(lawFirmId: string, nodeId: string) {
  const [node] = await prisma.$queryRaw<ClientStructureNodeRecord[]>`
    SELECT
      id,
      law_firm_id,
      parent_id,
      node_type,
      field_key,
      label,
      description,
      ai_instructions,
      data_type,
      document_type_code,
      is_required,
      is_repeatable,
      is_conditional,
      conditional_prompt,
      sort_order,
      created_by_user_id,
      created_at,
      updated_at
    FROM client_data_structure_nodes
    WHERE law_firm_id = ${lawFirmId}
      AND id = ${nodeId}
    LIMIT 1
  `;

  return node ?? null;
}

async function buildUniqueClientStructureFieldKey(
  lawFirmId: string,
  baseKey: string,
  excludeNodeId: string | null = null,
) {
  const normalizedBaseKey = normalizeClientStructureFieldKey(baseKey) || "field";
  const nodes = await prisma.$queryRaw<Array<{ id: string; field_key: string }>>`
    SELECT id, field_key
    FROM client_data_structure_nodes
    WHERE law_firm_id = ${lawFirmId}
  `;
  const existingKeys = new Set(
    nodes
      .filter((node) => node.id !== excludeNodeId)
      .map((node) => node.field_key.toLowerCase()),
  );

  if (!existingKeys.has(normalizedBaseKey.toLowerCase())) {
    return normalizedBaseKey;
  }

  let sequence = 2;
  while (existingKeys.has(`${normalizedBaseKey}_${sequence}`.toLowerCase())) {
    sequence += 1;
  }

  return `${normalizedBaseKey}_${sequence}`;
}

async function nextClientStructureSortOrder(lawFirmId: string, parentId: string | null) {
  const [row] = await prisma.$queryRaw<Array<{ next_sort_order: number }>>`
    SELECT COALESCE(MAX(sort_order), 0) + 1 AS next_sort_order
    FROM client_data_structure_nodes
    WHERE law_firm_id = ${lawFirmId}
      AND parent_id <=> ${parentId}
  `;

  return Number(row?.next_sort_order ?? 1);
}

async function currentClientCount(lawFirmId: string) {
  const count = await prisma.client.count({
    where: { law_firm_id: lawFirmId },
  });

  return count;
}

async function createClients(
  lawFirmId: string,
  officeId: string | null,
  userId: string,
  clients: ClientInput[],
) {
  const baseCount = await currentClientCount(lawFirmId);

  return prisma.$transaction(
    clients.map((client, index) =>
      prisma.client.create({
        data: {
          id: createId(),
          law_firm_id: lawFirmId,
          primary_office_id: officeId,
          client_number: formatClientNumber(baseCount + index + 1),
          first_name: client.firstName.trim(),
          middle_name: normalizeOptionalValue(client.middleName),
          last_name: client.lastName.trim(),
          email: normalizeOptionalValue(client.email),
          phone: normalizeOptionalValue(client.phone),
          preferred_language: normalizeOptionalValue(client.preferredLanguage),
          created_by_user_id: userId,
        },
      }),
    ),
  );
}

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

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

    const terms = (query.q ?? "")
      .trim()
      .split(/\s+/)
      .filter(Boolean)
      .slice(0, 5);

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

    return serializeClientsWithSponsors(profile.lawFirm.id, clients);
  });

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

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

    try {
      const nodes = await ensureDefaultClientStructureNodes(profile.lawFirm.id);
      return nodes.map((node) => serializeClientStructureNode(node));
    } catch (error) {
      replyClientDataStructureSchemaError(reply, error);
      throw error;
    }
  });

  app.get("/structure/document-types", 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 documentTypes = await listClientStructureDocumentTypes();
    return documentTypes.map((item) => serializeClientStructureDocumentType(item));
  });

  app.post("/structure/document-types", 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 = clientStructureDocumentTypeCreateSchema.parse(request.body);
    await ensureDefaultClientStructureDocumentTypes();

    const documentTypeCode = await buildUniqueClientStructureDocumentTypeCode(payload.name);

    await prisma.$executeRaw`
      INSERT INTO document_types (
        code,
        name,
        description,
        category_code,
        is_identity_document,
        is_expirable
      ) VALUES (
        ${documentTypeCode},
        ${payload.name.trim()},
        ${normalizeOptionalValue(payload.description)},
        'supporting',
        0,
        0
      )
    `;

    const [createdDocumentType] = await prisma.$queryRaw<ClientStructureDocumentTypeRecord[]>`
      SELECT code, name, description
      FROM document_types
      WHERE code = ${documentTypeCode}
      LIMIT 1
    `;

    if (!createdDocumentType) {
      throw reply.internalServerError("Failed to load created document type");
    }

    const serializedDocumentType = serializeClientStructureDocumentType(createdDocumentType);

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "document_type",
      entityId: createdDocumentType.code,
      action: "client.structure.document_type.create",
      afterJson: serializedDocumentType,
      request,
    });

    return reply.code(201).send(serializedDocumentType);
  });

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

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

    try {
      await ensureDefaultClientStructureNodes(profile.lawFirm.id);
      const payload = clientStructureCreateSchema.parse(request.body);
      const parentId = payload.parentId ?? null;

      if (parentId) {
        const parent = await getClientStructureNode(profile.lawFirm.id, parentId);

        if (!parent) {
          throw reply.notFound("Parent structure node not found");
        }

        if (parent.node_type !== "group") {
          throw reply.badRequest("Only group nodes can receive children");
        }
      }

      const resolvedDocumentTypeCode =
        payload.nodeType === "document" ?
          await resolveClientStructureDocumentTypeCode(payload.documentTypeCode ?? "") :
          null;

      if (payload.nodeType === "document" && !resolvedDocumentTypeCode) {
        throw reply.badRequest("documentTypeCode is invalid");
      }

      const fieldKey = await buildUniqueClientStructureFieldKey(
        profile.lawFirm.id,
        payload.fieldKey || payload.label,
      );
      const sortOrder = await nextClientStructureSortOrder(profile.lawFirm.id, parentId);
      const nodeId = createId();

      await prisma.$executeRaw`
        INSERT INTO client_data_structure_nodes (
          id,
          law_firm_id,
          parent_id,
          node_type,
          field_key,
          label,
          description,
          ai_instructions,
          data_type,
          document_type_code,
          is_required,
          is_repeatable,
          is_conditional,
          conditional_prompt,
          sort_order,
          created_by_user_id
        ) VALUES (
          ${nodeId},
          ${profile.lawFirm.id},
          ${parentId},
          ${payload.nodeType},
          ${fieldKey},
          ${payload.label.trim()},
          ${normalizeOptionalValue(payload.description)},
          ${normalizeOptionalValue(payload.aiInstructions)},
          ${payload.nodeType === "field" ? payload.dataType ?? null : null},
          ${payload.nodeType === "document" ? resolvedDocumentTypeCode : null},
          ${payload.nodeType !== "group" && payload.required ? 1 : 0},
          ${payload.nodeType === "group" && payload.repeatable ? 1 : 0},
          ${payload.conditional ? 1 : 0},
          ${payload.conditional ? normalizeOptionalValue(payload.conditionalPrompt) : null},
          ${sortOrder},
          ${profile.user.id}
        )
      `;

      const createdNode = await getClientStructureNode(profile.lawFirm.id, nodeId);

      if (!createdNode) {
        throw reply.internalServerError("Failed to load created client structure node");
      }

      const serializedNode = serializeClientStructureNode(createdNode);

      await writeAuditLog({
        lawFirmId: profile.lawFirm.id,
        officeId: profile.user.primaryOfficeId ?? null,
        actorUserId: profile.user.id,
        entityType: "client_structure_node",
        entityId: createdNode.id,
        action: "client.structure.create",
        afterJson: serializedNode,
        request,
      });

      return reply.code(201).send(serializedNode);
    } catch (error) {
      replyClientDataStructureSchemaError(reply, error);
      throw error;
    }
  });

  app.patch("/structure/:nodeId", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

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

    try {
      await ensureDefaultClientStructureNodes(profile.lawFirm.id);
      const { nodeId } = clientStructureNodeParamSchema.parse(request.params);
      const payload = clientStructureUpdateSchema.parse(request.body);
      const currentNode = await getClientStructureNode(profile.lawFirm.id, nodeId);

      if (!currentNode) {
        throw reply.notFound("Client structure node not found");
      }

      const beforeNode = serializeClientStructureNode(currentNode);

      if (!currentNode.created_by_user_id) {
        await prisma.$executeRaw`
          UPDATE client_data_structure_nodes
          SET
            ai_instructions = ${normalizeOptionalValue(payload.aiInstructions)},
            updated_at = CURRENT_TIMESTAMP
          WHERE id = ${currentNode.id}
        `;

        const updatedSystemNode = await getClientStructureNode(profile.lawFirm.id, currentNode.id);

        if (!updatedSystemNode) {
          throw reply.internalServerError("Failed to load updated client structure node");
        }

        const serializedSystemNode = serializeClientStructureNode(updatedSystemNode);

        await writeAuditLog({
          lawFirmId: profile.lawFirm.id,
          officeId: profile.user.primaryOfficeId ?? null,
          actorUserId: profile.user.id,
          entityType: "client_structure_node",
          entityId: updatedSystemNode.id,
          action: "client.structure.update",
          beforeJson: beforeNode,
          afterJson: serializedSystemNode,
          request,
        });

        return serializedSystemNode;
      }

      if (currentNode.node_type === "field" && !payload.dataType) {
        throw reply.badRequest("dataType is required when nodeType is field");
      }

      if (currentNode.node_type === "field" && payload.repeatable) {
        throw reply.badRequest("repeatable is only allowed when nodeType is group");
      }

      if (currentNode.node_type === "document" && !normalizeOptionalValue(payload.documentTypeCode)) {
        throw reply.badRequest("documentTypeCode is required when nodeType is document");
      }

      if (currentNode.node_type === "document" && payload.repeatable) {
        throw reply.badRequest("repeatable is only allowed when nodeType is group");
      }

      const resolvedDocumentTypeCode =
        currentNode.node_type === "document" ?
          await resolveClientStructureDocumentTypeCode(payload.documentTypeCode ?? "") :
          null;

      if (currentNode.node_type === "document" && !resolvedDocumentTypeCode) {
        throw reply.badRequest("documentTypeCode is invalid");
      }

      const fieldKey = await buildUniqueClientStructureFieldKey(
        profile.lawFirm.id,
        payload.fieldKey || currentNode.field_key,
        currentNode.id,
      );

      await prisma.$executeRaw`
        UPDATE client_data_structure_nodes
        SET
          field_key = ${fieldKey},
          label = ${payload.label.trim()},
          description = ${normalizeOptionalValue(payload.description)},
          ai_instructions = ${normalizeOptionalValue(payload.aiInstructions)},
          data_type = ${currentNode.node_type === "field" ? payload.dataType ?? null : null},
          document_type_code = ${currentNode.node_type === "document" ? resolvedDocumentTypeCode : null},
          is_required = ${currentNode.node_type !== "group" && payload.required ? 1 : 0},
          is_repeatable = ${currentNode.node_type === "group" && payload.repeatable ? 1 : 0},
          is_conditional = ${payload.conditional ? 1 : 0},
          conditional_prompt = ${payload.conditional ? normalizeOptionalValue(payload.conditionalPrompt) : null},
          updated_at = CURRENT_TIMESTAMP
        WHERE id = ${currentNode.id}
      `;

      const updatedNode = await getClientStructureNode(profile.lawFirm.id, currentNode.id);

      if (!updatedNode) {
        throw reply.internalServerError("Failed to load updated client structure node");
      }

      const serializedNode = serializeClientStructureNode(updatedNode);

      await writeAuditLog({
        lawFirmId: profile.lawFirm.id,
        officeId: profile.user.primaryOfficeId ?? null,
        actorUserId: profile.user.id,
        entityType: "client_structure_node",
        entityId: updatedNode.id,
        action: "client.structure.update",
        beforeJson: beforeNode,
        afterJson: serializedNode,
        request,
      });

      return serializedNode;
    } catch (error) {
      replyClientDataStructureSchemaError(reply, error);
      throw error;
    }
  });

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

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

    try {
      await ensureDefaultClientStructureNodes(profile.lawFirm.id);
      const payload = clientStructureReorderSchema.parse(request.body);

      if (payload.nodeId === payload.targetNodeId) {
        const nodes = await listClientStructureNodes(profile.lawFirm.id);
        return nodes.map((node) => serializeClientStructureNode(node));
      }

      const [currentNode, targetNode] = await Promise.all([
        getClientStructureNode(profile.lawFirm.id, payload.nodeId),
        getClientStructureNode(profile.lawFirm.id, payload.targetNodeId),
      ]);

      if (!currentNode || !targetNode) {
        throw reply.notFound("Client structure node not found");
      }

      if (currentNode.node_type !== "group" || targetNode.node_type !== "group") {
        throw reply.badRequest("Only group nodes can be reordered.");
      }

      if (currentNode.parent_id !== targetNode.parent_id) {
        throw reply.badRequest("Groups can only be reordered within the same parent.");
      }

      const beforeNode = serializeClientStructureNode(currentNode);

      await prisma.$transaction(async (tx) => {
        const siblings = await tx.$queryRaw<ClientStructureNodeRecord[]>`
          SELECT
            id,
            law_firm_id,
            parent_id,
            node_type,
            field_key,
            label,
            description,
            ai_instructions,
            data_type,
            document_type_code,
            is_required,
            is_repeatable,
            is_conditional,
            conditional_prompt,
            sort_order,
            created_by_user_id,
            created_at,
            updated_at
          FROM client_data_structure_nodes
          WHERE law_firm_id = ${profile.lawFirm.id}
            AND parent_id <=> ${currentNode.parent_id}
          ORDER BY sort_order ASC, label ASC
        `;

        const fromIndex = siblings.findIndex((node) => node.id === currentNode.id);
        const targetIndex = siblings.findIndex((node) => node.id === targetNode.id);

        if (fromIndex < 0 || targetIndex < 0) {
          throw reply.notFound("Client structure node not found");
        }

        const [movedNode] = siblings.splice(fromIndex, 1);
        let insertIndex = targetIndex + (payload.position === "after" ? 1 : 0);

        if (fromIndex < insertIndex) {
          insertIndex -= 1;
        }

        siblings.splice(insertIndex, 0, movedNode);

        await Promise.all(
          siblings.map((node, index) =>
            tx.$executeRaw`
              UPDATE client_data_structure_nodes
              SET
                sort_order = ${(index + 1) * 10},
                updated_at = CURRENT_TIMESTAMP
              WHERE id = ${node.id}
            `,
          ),
        );
      });

      const updatedNode = await getClientStructureNode(profile.lawFirm.id, currentNode.id);

      if (!updatedNode) {
        throw reply.internalServerError("Failed to load reordered client structure node");
      }

      await writeAuditLog({
        lawFirmId: profile.lawFirm.id,
        officeId: profile.user.primaryOfficeId ?? null,
        actorUserId: profile.user.id,
        entityType: "client_structure_node",
        entityId: updatedNode.id,
        action: "client.structure.reorder",
        beforeJson: beforeNode,
        afterJson: serializeClientStructureNode(updatedNode),
        request,
      });

      const nodes = await listClientStructureNodes(profile.lawFirm.id);
      return nodes.map((node) => serializeClientStructureNode(node));
    } catch (error) {
      replyClientDataStructureSchemaError(reply, error);
      throw error;
    }
  });

  app.delete("/structure/:nodeId", async (request, reply) => {
    const session = await requireSession(request, reply);
    const profile = await getSessionProfile(session);

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

    try {
      await ensureDefaultClientStructureNodes(profile.lawFirm.id);
      const { nodeId } = clientStructureNodeParamSchema.parse(request.params);
      const currentNode = await getClientStructureNode(profile.lawFirm.id, nodeId);

      if (!currentNode) {
        throw reply.notFound("Client structure node not found");
      }

      if (!currentNode.created_by_user_id) {
        throw reply.badRequest("System client structure nodes cannot be deleted.");
      }

      const beforeNode = serializeClientStructureNode(currentNode);

      await prisma.$executeRaw`
        DELETE FROM client_data_structure_nodes
        WHERE id = ${currentNode.id}
      `;

      await writeAuditLog({
        lawFirmId: profile.lawFirm.id,
        officeId: profile.user.primaryOfficeId ?? null,
        actorUserId: profile.user.id,
        entityType: "client_structure_node",
        entityId: currentNode.id,
        action: "client.structure.delete",
        beforeJson: beforeNode,
        request,
      });

      return reply.code(204).send();
    } catch (error) {
      replyClientDataStructureSchemaError(reply, error);
      throw error;
    }
  });

  app.get("/:clientId", 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 { clientId } = clientIdParamSchema.parse(request.params);
    const client = await prisma.client.findFirst({
      where: {
        id: clientId,
        law_firm_id: profile.lawFirm.id,
      },
    });

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

    try {
      const structureNodes = await ensureDefaultClientStructureNodes(profile.lawFirm.id);
      const [sponsorMap, documentMatches] = await Promise.all([
        listClientSponsorsByClientIds({
          lawFirmId: profile.lawFirm.id,
          clientIds: [client.id],
        }),
        listClientDetailDocumentMatches(profile.lawFirm.id, client.id),
      ]);

      return serializeClientDetail(
        client,
        structureNodes,
        documentMatches,
        sponsorMap.get(client.id) ?? null,
      );
    } catch (error) {
      replyClientDataStructureSchemaError(reply, error);
      throw error;
    }
  });

  app.post("/", 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 = clientCreateSchema.parse(request.body);
    const [client] = await createClients(
      profile.lawFirm.id,
      profile.user.primaryOfficeId ?? null,
      profile.user.id,
      [payload],
    );

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "client",
      entityId: client.id,
      action: "client.create",
      afterJson: {
        clientNumber: client.client_number,
        firstName: client.first_name,
        middleName: client.middle_name,
        lastName: client.last_name,
        email: client.email,
      },
      request,
    });

    return reply.code(201).send(serializeClient(client));
  });

  app.post("/import", 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 = clientImportSchema.parse(request.body);
    const clients = await createClients(
      profile.lawFirm.id,
      profile.user.primaryOfficeId ?? null,
      profile.user.id,
      payload.clients,
    );

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "client_import",
      entityId: null,
      action: "client.import",
      afterJson: {
        importedCount: clients.length,
        clientIds: clients.map((client) => client.id),
      },
      request,
    });

    return reply.code(201).send({
      importedCount: clients.length,
      clients: clients.map((client) => serializeClient(client)),
    });
  });

  app.patch("/:clientId/sponsor", 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 { clientId } = clientIdParamSchema.parse(request.params);
    const payload = clientSponsorUpdateSchema.parse(request.body);

    const client = await prisma.client.findFirst({
      where: {
        id: clientId,
        law_firm_id: profile.lawFirm.id,
        deleted_at: null,
      },
    });

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

    const existingSponsors = await prisma.relatedParty.findMany({
      where: {
        law_firm_id: profile.lawFirm.id,
        client_id: clientId,
        relation_type: "sponsor",
        deleted_at: null,
      },
      orderBy: [{ updated_at: "desc" }, { created_at: "desc" }],
    });

    const primarySponsor = existingSponsors[0] ?? null;
    const duplicateSponsorIds = existingSponsors.slice(1).map((item) => item.id);
    const now = new Date();

    if (payload.clear) {
      if (existingSponsors.length > 0) {
        await prisma.relatedParty.updateMany({
          where: {
            id: {
              in: existingSponsors.map((item) => item.id),
            },
          },
          data: {
            deleted_at: now,
          },
        });
      }

      await writeAuditLog({
        lawFirmId: profile.lawFirm.id,
        officeId: profile.user.primaryOfficeId ?? null,
        actorUserId: profile.user.id,
        entityType: "client_sponsor",
        entityId: clientId,
        action: "client.sponsor.clear",
        request,
      });

      return serializeClient(client, null);
    }

    const sponsorData = {
      entity_type: payload.entityType,
      first_name: normalizeOptionalValue(payload.firstName),
      last_name: normalizeOptionalValue(payload.lastName),
      company_name: normalizeOptionalValue(payload.companyName),
      email: normalizeOptionalValue(payload.email),
      phone: normalizeOptionalValue(payload.phone),
    };

    const sponsor =
      primarySponsor ?
        await prisma.relatedParty.update({
          where: {
            id: primarySponsor.id,
          },
          data: {
            ...sponsorData,
            deleted_at: null,
          },
        }) :
        await prisma.relatedParty.create({
          data: {
            id: createId(),
            law_firm_id: profile.lawFirm.id,
            client_id: clientId,
            case_id: null,
            relation_type: "sponsor",
            entity_type: sponsorData.entity_type,
            first_name: sponsorData.first_name,
            last_name: sponsorData.last_name,
            company_name: sponsorData.company_name,
            email: sponsorData.email,
            phone: sponsorData.phone,
          },
        });

    if (duplicateSponsorIds.length > 0) {
      await prisma.relatedParty.updateMany({
        where: {
          id: {
            in: duplicateSponsorIds,
          },
        },
        data: {
          deleted_at: now,
        },
      });
    }

    const serializedSponsor: ClientSponsorSummary = {
      id: sponsor.id,
      entityType: sponsor.entity_type === "company" ? "company" : "person",
      name:
        sponsor.entity_type === "company" ?
          sponsor.company_name?.trim() || sponsor.preferred_name?.trim() || "Unnamed sponsor" :
          [sponsor.first_name?.trim(), sponsor.last_name?.trim()].filter(Boolean).join(" ") ||
          sponsor.preferred_name?.trim() ||
          sponsor.company_name?.trim() ||
          "Unnamed sponsor",
      firstName: sponsor.first_name,
      lastName: sponsor.last_name,
      companyName: sponsor.company_name,
      email: sponsor.email,
      phone: sponsor.phone,
    };

    await writeAuditLog({
      lawFirmId: profile.lawFirm.id,
      officeId: profile.user.primaryOfficeId ?? null,
      actorUserId: profile.user.id,
      entityType: "client_sponsor",
      entityId: sponsor.id,
      action: primarySponsor ? "client.sponsor.update" : "client.sponsor.create",
      afterJson: {
        clientId,
        entityType: serializedSponsor.entityType,
        name: serializedSponsor.name,
        email: serializedSponsor.email,
        phone: serializedSponsor.phone,
      },
      request,
    });

    return serializeClient(client, serializedSponsor);
  });
}
