import { createHash } from "node:crypto";
import { Prisma } from "@prisma/client";
import { ensureClientStructureValueSchema, listClientStructureFieldValues } from "./client-structure-values.js";
import { listClientSponsorsByClientIds } from "./client-sponsors.js";
import { buildTextDocx } from "./docx-documents.js";
import { extractDocumentText } from "./document-reviews.js";
import { createId } from "./id.js";
import { buildTextPdf, type TextSection } from "./pdf-documents.js";
import { prisma } from "./prisma.js";
import { createRepositoryItem } from "./repository.js";
import { saveBinaryFile } from "./storage.js";
import { createAiRun, finishAiRun, runJsonChatCompletion } from "./tenant-ai.js";

type PetitionTypeRow = {
  id: string;
  name: string;
  description: string | null;
  generation_instructions: string | null;
  created_at: Date;
  updated_at: Date;
};

type PetitionExampleRow = {
  id: string;
  petition_type_id: string;
  title: string;
  original_file_name: string;
  mime_type: string;
  extracted_text: string | null;
  notes: string | null;
  ai_structure_json: string | null;
  ai_structure_summary: string | null;
  ai_structure_model: string | null;
  ai_structure_ai_run_id: string | null;
  ai_structured_at: Date | null;
  created_at: Date;
  updated_at: Date;
  file_id: string;
};

type PetitionOutputMode = "generation" | "rectification";

export type PetitionExampleSummary = {
  id: string;
  petitionTypeId: string;
  title: string;
  fileId: string;
  fileName: string;
  mimeType: string;
  notes: string | null;
  extractedTextLength: number;
  createdAt: string;
  updatedAt: string;
};

export type PetitionTypeSummary = {
  id: string;
  name: string;
  description: string | null;
  generationInstructions: string | null;
  exampleCount: number;
  createdAt: string;
  updatedAt: string;
  examples: PetitionExampleSummary[];
};

export type PetitionOutputAsset = {
  format: "pdf" | "docx";
  fileId: string;
  fileName: string;
  repositoryItemId: string;
};

export type PetitionGenerationResult = {
  title: string;
  summary: string;
  petitionText: string;
  missingInformation: string[];
  cautions: string[];
  examplesConsultedCount: number;
  assets: PetitionOutputAsset[];
};

export type PetitionRectificationResult = {
  title: string;
  summary: string;
  petitionText: string;
  changesApplied: string[];
  cautions: string[];
  examplesConsultedCount: number;
  assets: PetitionOutputAsset[];
};

export type PetitionTypeFieldSuggestionTarget = "description" | "generation_instructions";

export type PetitionTypeFieldSuggestion = {
  aiRunId: string;
  model: string;
  target: PetitionTypeFieldSuggestionTarget;
  value: string;
  reason: string;
};

type PetitionExampleStructuredSection = {
  heading: string;
  purpose: string;
  keyElements: string[];
};

type PetitionExampleAiStructure = {
  summary: string;
  documentObjective: string;
  tone: string;
  audience: string;
  sections: PetitionExampleStructuredSection[];
  argumentPatterns: string[];
  requestPatterns: string[];
  factualVariables: string[];
  evidencePatterns: string[];
  draftingInstructions: string[];
  cautionNotes: string[];
};

const maxExamplesForPrompt = 5;
const maxExampleCharacters = 3_500;
const maxExamplesCombinedCharacters = 14_000;
const maxClientSummaryCharacters = 10_000;
const maxExampleAnalysisCharacters = 22_000;
const maxExampleStructuredExcerptCharacters = 1_200;
const petitionTypeFieldSuggestionMaxLengthByTarget: Record<PetitionTypeFieldSuggestionTarget, number> =
  {
    description: 4_000,
    generation_instructions: 12_000,
  };

let ensurePetitionsSchemaPromise: Promise<void> | null = null;

export class PetitionServiceError extends Error {
  statusCode: number;

  constructor(statusCode: number, message: string) {
    super(message);
    this.statusCode = statusCode;
  }
}

function normalizeText(value: string) {
  return String(value ?? "").replace(/\u0000/g, "").replace(/\r\n/g, "\n").trim();
}

function normalizeOptionalText(value: string | null | undefined) {
  const normalized = normalizeText(String(value ?? ""));
  return normalized || null;
}

function truncateText(value: string | null | undefined, maxLength: number) {
  const normalized = normalizeText(String(value ?? ""));

  if (!normalized) {
    return "";
  }

  return normalized.length > maxLength ? normalized.slice(0, maxLength).trim() : normalized;
}

function parseJsonObject(value: string | null | undefined) {
  if (!value) {
    return null;
  }

  try {
    const parsed = JSON.parse(value);
    return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
  } catch {
    return null;
  }
}

function normalizeStringArray(value: unknown, limit = 12) {
  if (!Array.isArray(value)) {
    return [];
  }

  return Array.from(
    new Set(
      value
        .map((item) => normalizeText(String(item ?? "")))
        .filter(Boolean)
        .slice(0, limit),
    ),
  );
}

function sanitizePetitionExampleAiStructure(value: unknown): PetitionExampleAiStructure {
  const record = value && typeof value === "object" ? (value as Record<string, unknown>) : {};
  const rawSections = Array.isArray(record.sections) ? record.sections : [];
  const sections = rawSections
    .map((item) => {
      const section = item && typeof item === "object" ? (item as Record<string, unknown>) : {};

      return {
        heading: truncateText(String(section.heading ?? ""), 160),
        purpose: truncateText(String(section.purpose ?? ""), 320),
        keyElements: normalizeStringArray(section.keyElements, 6).map((entry) =>
          truncateText(entry, 220),
        ),
      } satisfies PetitionExampleStructuredSection;
    })
    .filter(
      (section) => section.heading || section.purpose || section.keyElements.some(Boolean),
    )
    .slice(0, 10);

  const structure = {
    summary: truncateText(String(record.summary ?? ""), 900),
    documentObjective: truncateText(String(record.documentObjective ?? ""), 320),
    tone: truncateText(String(record.tone ?? ""), 220),
    audience: truncateText(String(record.audience ?? ""), 220),
    sections,
    argumentPatterns: normalizeStringArray(record.argumentPatterns, 10).map((entry) =>
      truncateText(entry, 260),
    ),
    requestPatterns: normalizeStringArray(record.requestPatterns, 10).map((entry) =>
      truncateText(entry, 260),
    ),
    factualVariables: normalizeStringArray(record.factualVariables, 12).map((entry) =>
      truncateText(entry, 220),
    ),
    evidencePatterns: normalizeStringArray(record.evidencePatterns, 10).map((entry) =>
      truncateText(entry, 220),
    ),
    draftingInstructions: normalizeStringArray(record.draftingInstructions, 12).map((entry) =>
      truncateText(entry, 260),
    ),
    cautionNotes: normalizeStringArray(record.cautionNotes, 10).map((entry) =>
      truncateText(entry, 260),
    ),
  } satisfies PetitionExampleAiStructure;

  if (
    !structure.summary &&
    !structure.documentObjective &&
    structure.sections.length === 0 &&
    structure.argumentPatterns.length === 0 &&
    structure.requestPatterns.length === 0 &&
    structure.draftingInstructions.length === 0
  ) {
    throw new PetitionServiceError(
      502,
      "The AI did not return a usable structured representation for the petition example.",
    );
  }

  return structure;
}

function parsePetitionExampleAiStructure(value: string | null | undefined) {
  const parsed = parseJsonObject(value);

  if (!parsed) {
    return null;
  }

  try {
    return sanitizePetitionExampleAiStructure(parsed);
  } catch {
    return null;
  }
}

function slugifyFileToken(value: string) {
  return String(value ?? "")
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^a-zA-Z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "")
    .replace(/_+/g, "_");
}

async function ensurePetitionsSchema() {
  if (!ensurePetitionsSchemaPromise) {
    ensurePetitionsSchemaPromise = (async () => {
      await prisma.$executeRawUnsafe(`
        CREATE TABLE IF NOT EXISTS petition_types (
          id CHAR(36) NOT NULL PRIMARY KEY,
          law_firm_id CHAR(36) NOT NULL,
          name VARCHAR(191) NOT NULL,
          description TEXT NULL,
          generation_instructions LONGTEXT NULL,
          created_by_user_id CHAR(36) NULL,
          created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
          deleted_at TIMESTAMP NULL DEFAULT NULL,
          UNIQUE KEY uq_petition_types_law_firm_name (law_firm_id, name),
          KEY idx_petition_types_law_firm (law_firm_id, created_at),
          KEY idx_petition_types_deleted_at (law_firm_id, deleted_at, name),
          CONSTRAINT fk_petition_types_law_firm
            FOREIGN KEY (law_firm_id) REFERENCES law_firms (id),
          CONSTRAINT fk_petition_types_created_by
            FOREIGN KEY (created_by_user_id) REFERENCES users (id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
      `);

      const deletedAtColumn = await prisma.$queryRaw<Array<{ column_name: string }>>`
        SELECT COLUMN_NAME AS column_name
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = 'petition_types'
          AND COLUMN_NAME = 'deleted_at'
        LIMIT 1
      `;

      if (deletedAtColumn.length === 0) {
        await prisma.$executeRawUnsafe(`
          ALTER TABLE petition_types
          ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at
        `);
      }

      await prisma.$executeRawUnsafe(`
        CREATE TABLE IF NOT EXISTS petition_type_examples (
          id CHAR(36) NOT NULL PRIMARY KEY,
          law_firm_id CHAR(36) NOT NULL,
          petition_type_id CHAR(36) NOT NULL,
          repository_item_id CHAR(36) NOT NULL,
          file_id CHAR(36) NOT NULL,
          title VARCHAR(255) NOT NULL,
          original_file_name VARCHAR(255) NOT NULL,
          mime_type VARCHAR(100) NOT NULL,
          extracted_text LONGTEXT NULL,
          notes TEXT NULL,
          ai_structure_json LONGTEXT NULL,
          ai_structure_summary TEXT NULL,
          ai_structure_model VARCHAR(191) NULL,
          ai_structure_ai_run_id CHAR(36) NULL,
          ai_structured_at TIMESTAMP NULL DEFAULT NULL,
          created_by_user_id CHAR(36) NULL,
          created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
          updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
          deleted_at TIMESTAMP NULL DEFAULT NULL,
          UNIQUE KEY uq_petition_type_examples_file (file_id),
          KEY idx_petition_type_examples_type (petition_type_id, created_at),
          KEY idx_petition_type_examples_law_firm (law_firm_id, created_at),
          KEY idx_petition_type_examples_deleted (law_firm_id, petition_type_id, deleted_at, created_at),
          CONSTRAINT fk_petition_type_examples_law_firm
            FOREIGN KEY (law_firm_id) REFERENCES law_firms (id),
          CONSTRAINT fk_petition_type_examples_type
            FOREIGN KEY (petition_type_id) REFERENCES petition_types (id),
          CONSTRAINT fk_petition_type_examples_repository_item
            FOREIGN KEY (repository_item_id) REFERENCES repository_items (id),
          CONSTRAINT fk_petition_type_examples_file
            FOREIGN KEY (file_id) REFERENCES files (id),
          CONSTRAINT fk_petition_type_examples_created_by
            FOREIGN KEY (created_by_user_id) REFERENCES users (id)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
      `);

      const updatedAtExampleColumn = await prisma.$queryRaw<Array<{ column_name: string }>>`
        SELECT COLUMN_NAME AS column_name
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = 'petition_type_examples'
          AND COLUMN_NAME = 'updated_at'
        LIMIT 1
      `;

      if (updatedAtExampleColumn.length === 0) {
        await prisma.$executeRawUnsafe(`
          ALTER TABLE petition_type_examples
          ADD COLUMN updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
          ON UPDATE CURRENT_TIMESTAMP AFTER created_at
        `);
      }

      const deletedAtExampleColumn = await prisma.$queryRaw<Array<{ column_name: string }>>`
        SELECT COLUMN_NAME AS column_name
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = 'petition_type_examples'
          AND COLUMN_NAME = 'deleted_at'
        LIMIT 1
      `;

      if (deletedAtExampleColumn.length === 0) {
        await prisma.$executeRawUnsafe(`
          ALTER TABLE petition_type_examples
          ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at
        `);
      }

      const petitionExampleOptionalColumns: Array<{ name: string; definition: string }> = [
        {
          name: "ai_structure_json",
          definition: "LONGTEXT NULL AFTER notes",
        },
        {
          name: "ai_structure_summary",
          definition: "TEXT NULL AFTER ai_structure_json",
        },
        {
          name: "ai_structure_model",
          definition: "VARCHAR(191) NULL AFTER ai_structure_summary",
        },
        {
          name: "ai_structure_ai_run_id",
          definition: "CHAR(36) NULL AFTER ai_structure_model",
        },
        {
          name: "ai_structured_at",
          definition: "TIMESTAMP NULL DEFAULT NULL AFTER ai_structure_ai_run_id",
        },
      ];

      for (const column of petitionExampleOptionalColumns) {
        const existingColumn = await prisma.$queryRaw<Array<{ column_name: string }>>`
          SELECT COLUMN_NAME AS column_name
          FROM INFORMATION_SCHEMA.COLUMNS
          WHERE TABLE_SCHEMA = DATABASE()
            AND TABLE_NAME = 'petition_type_examples'
            AND COLUMN_NAME = ${column.name}
          LIMIT 1
        `;

        if (existingColumn.length === 0) {
          await prisma.$executeRawUnsafe(`
            ALTER TABLE petition_type_examples
            ADD COLUMN ${column.name} ${column.definition}
          `);
        }
      }
    })().catch((error) => {
      ensurePetitionsSchemaPromise = null;
      throw error;
    });
  }

  await ensurePetitionsSchemaPromise;
}

async function getPetitionTypeRow(input: {
  lawFirmId: string;
  petitionTypeId: string;
}) {
  await ensurePetitionsSchema();

  const [row] = await prisma.$queryRaw<PetitionTypeRow[]>`
    SELECT id, name, description, generation_instructions, created_at, updated_at
    FROM petition_types
    WHERE id = ${input.petitionTypeId}
      AND law_firm_id = ${input.lawFirmId}
      AND deleted_at IS NULL
    LIMIT 1
  `;

  return row ?? null;
}

async function getPetitionTypeOrThrow(input: {
  lawFirmId: string;
  petitionTypeId: string;
}) {
  const petitionType = await getPetitionTypeRow(input);

  if (!petitionType) {
    throw new PetitionServiceError(404, "Petition type not found.");
  }

  return petitionType;
}

async function storeFileWithRepositoryItem(input: {
  lawFirmId: string;
  clientId?: string | null;
  caseId?: string | null;
  actorUserId: string;
  itemTypeCode: "document" | "generated_form";
  sourceEntityType: string;
  sourceEntityId?: string | null;
  subject: string;
  bodyText?: string | null;
  summaryText?: string | null;
  metadataJson?: unknown;
  fileName: string;
  mimeType: string;
  bytes: Buffer;
  kind: "uploads" | "generated";
}) {
  const stored = await saveBinaryFile({
    lawFirmId: input.lawFirmId,
    caseId: input.caseId ?? null,
    fileName: input.fileName,
    bytes: input.bytes,
    kind: input.kind,
  });

  const repositoryItemId = await createRepositoryItem({
    lawFirmId: input.lawFirmId,
    clientId: input.clientId ?? null,
    caseId: input.caseId ?? null,
    itemTypeCode: input.itemTypeCode,
    channelCode: "system",
    sourceEntityType: input.sourceEntityType,
    sourceEntityId: input.sourceEntityId ?? null,
    subject: input.subject,
    bodyText: input.bodyText ?? null,
    summaryText: input.summaryText ?? null,
    metadataJson: input.metadataJson,
    createdByUserId: input.actorUserId,
    authoredByUserId: input.actorUserId,
  });

  const fileId = createId();
  await prisma.$executeRaw`
    INSERT INTO files (
      id, law_firm_id, client_id, case_id, repository_item_id, storage_provider, storage_bucket,
      object_key, storage_region, original_file_name, stored_file_name, mime_type, size_bytes,
      checksum_sha256, is_encrypted, uploaded_by_user_id, uploaded_at, created_at
    ) VALUES (
      ${fileId},
      ${input.lawFirmId},
      ${input.clientId ?? null},
      ${input.caseId ?? null},
      ${repositoryItemId},
      'local_dev',
      'workspace',
      ${stored.relativeObjectKey},
      'local',
      ${input.fileName},
      ${stored.storedFileName},
      ${input.mimeType},
      ${input.bytes.length},
      ${stored.checksumSha256},
      0,
      ${input.actorUserId},
      NOW(),
      CURRENT_TIMESTAMP
    )
  `;

  return {
    repositoryItemId,
    fileId,
  };
}

async function loadPetitionExamples(input: {
  lawFirmId: string;
  petitionTypeId: string;
}) {
  await ensurePetitionsSchema();

  return prisma.$queryRaw<PetitionExampleRow[]>`
    SELECT
      id,
      petition_type_id,
      title,
      original_file_name,
      mime_type,
      extracted_text,
      notes,
      ai_structure_json,
      ai_structure_summary,
      ai_structure_model,
      ai_structure_ai_run_id,
      ai_structured_at,
      created_at,
      updated_at,
      file_id
    FROM petition_type_examples
    WHERE law_firm_id = ${input.lawFirmId}
      AND petition_type_id = ${input.petitionTypeId}
      AND deleted_at IS NULL
    ORDER BY created_at DESC
  `;
}

async function getPetitionExampleRow(input: {
  lawFirmId: string;
  exampleId: string;
}) {
  await ensurePetitionsSchema();

  const [row] = await prisma.$queryRaw<PetitionExampleRow[]>`
    SELECT
      id,
      petition_type_id,
      title,
      original_file_name,
      mime_type,
      extracted_text,
      notes,
      ai_structure_json,
      ai_structure_summary,
      ai_structure_model,
      ai_structure_ai_run_id,
      ai_structured_at,
      created_at,
      updated_at,
      file_id
    FROM petition_type_examples
    WHERE id = ${input.exampleId}
      AND law_firm_id = ${input.lawFirmId}
      AND deleted_at IS NULL
    LIMIT 1
  `;

  return row ?? null;
}

async function getPetitionExampleOrThrow(input: {
  lawFirmId: string;
  exampleId: string;
}) {
  const example = await getPetitionExampleRow(input);

  if (!example) {
    throw new PetitionServiceError(404, "Petition example not found.");
  }

  return example;
}

async function loadClientDraftingContext(input: {
  lawFirmId: string;
  clientId: string;
}) {
  await ensureClientStructureValueSchema();

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

  if (!client) {
    throw new PetitionServiceError(404, "Client not found.");
  }

  const sponsorMap = await listClientSponsorsByClientIds({
    lawFirmId: input.lawFirmId,
    clientIds: [input.clientId],
  });
  const sponsor = sponsorMap.get(input.clientId) ?? null;

  const [structureValues, canonicalSummaryRows] = await Promise.all([
    listClientStructureFieldValues({
      lawFirmId: input.lawFirmId,
      clientId: input.clientId,
    }),
    prisma.$queryRaw<Array<{ subject: string | null; summary_text: string | null; body_text: string | null }>>`
      SELECT subject, summary_text, body_text
      FROM repository_items
      WHERE law_firm_id = ${input.lawFirmId}
        AND client_id = ${input.clientId}
        AND item_type_code = 'client_profile_summary'
      ORDER BY occurred_at DESC, created_at DESC
      LIMIT 1
    `,
  ]);

  const nodeIds = Array.from(
    new Set(structureValues.map((item) => item.client_structure_node_id).filter(Boolean)),
  );

  const nodes = nodeIds.length
    ? await prisma.$queryRaw<Array<{ id: string; label: string; field_key: string }>>`
        SELECT id, label, field_key
        FROM client_data_structure_nodes
        WHERE law_firm_id = ${input.lawFirmId}
          AND id IN (${Prisma.join(nodeIds)})
      `
    : [];

  const nodeMap = new Map(nodes.map((node) => [node.id, node]));
  const structuredFields = structureValues
    .map((value) => {
      const node = nodeMap.get(value.client_structure_node_id);
      const textValue = normalizeOptionalText(value.value_text);

      if (!node || !textValue) {
        return null;
      }

      return {
        label: node.label,
        fieldKey: node.field_key,
        value: textValue,
        confidence:
          typeof value.confidence_score === "number" && Number.isFinite(value.confidence_score)
            ? Number(value.confidence_score)
            : null,
      };
    })
    .filter((item): item is NonNullable<typeof item> => Boolean(item));

  const canonicalSummary = normalizeOptionalText(
    canonicalSummaryRows[0]?.summary_text ?? canonicalSummaryRows[0]?.body_text ?? "",
  );

  return {
    client,
    sponsor,
    structuredFields,
    canonicalSummary,
  };
}

function buildClientPromptSnapshot(input: Awaited<ReturnType<typeof loadClientDraftingContext>>) {
  const sponsor =
    input.sponsor ?
      {
        name: input.sponsor.name,
        entityType: input.sponsor.entityType,
        email: input.sponsor.email,
        phone: input.sponsor.phone,
      } :
      null;

  const snapshot = {
    client: {
      id: input.client.id,
      name: [input.client.first_name, input.client.middle_name, input.client.last_name]
        .map((value) => String(value ?? "").trim())
        .filter(Boolean)
        .join(" "),
      preferredName: input.client.preferred_name,
      clientNumber: input.client.client_number,
      dateOfBirth: input.client.date_of_birth?.toISOString?.() ?? null,
      email: input.client.email,
      phone: input.client.phone,
      preferredLanguage: input.client.preferred_language,
      countryOfBirth: input.client.country_of_birth,
      countryOfCitizenship: input.client.country_of_citizenship,
      immigrationStatus: input.client.immigration_status,
      sponsor,
    },
    structuredFields: input.structuredFields.map((field) => ({
      label: field.label,
      fieldKey: field.fieldKey,
      value: field.value,
      confidence: field.confidence,
    })),
    canonicalSummary: input.canonicalSummary,
  };

  const serialized = JSON.stringify(snapshot, null, 2);
  return serialized.length > maxClientSummaryCharacters
    ? `${serialized.slice(0, maxClientSummaryCharacters)}\n...`
    : serialized;
}

function buildPromptReadyExampleBlock(example: PetitionExampleRow) {
  const structured = parsePetitionExampleAiStructure(example.ai_structure_json);
  const excerpt = truncateText(
    example.extracted_text ?? "",
    structured ? maxExampleStructuredExcerptCharacters : maxExampleCharacters,
  );
  const lines = [`Titulo do exemplo: ${example.title}`];

  if (structured) {
    if (structured.summary) {
      lines.push(`Resumo estruturado: ${structured.summary}`);
    }

    if (structured.documentObjective) {
      lines.push(`Objetivo da peca: ${structured.documentObjective}`);
    }

    if (structured.tone) {
      lines.push(`Tom e estilo: ${structured.tone}`);
    }

    if (structured.audience) {
      lines.push(`Destinatario principal: ${structured.audience}`);
    }

    if (structured.sections.length > 0) {
      lines.push(
        [
          "Estrutura recomendada:",
          ...structured.sections.map((section, index) =>
            [
              `${index + 1}. ${section.heading || "Secao sem titulo"}`,
              section.purpose ? `Proposito: ${section.purpose}` : null,
              section.keyElements.length > 0
                ? `Elementos-chave: ${section.keyElements.join(" • ")}`
                : null,
            ]
              .filter(Boolean)
              .join(" | "),
          ),
        ].join("\n"),
      );
    }

    if (structured.argumentPatterns.length > 0) {
      lines.push(`Padroes argumentativos: ${structured.argumentPatterns.join(" • ")}`);
    }

    if (structured.requestPatterns.length > 0) {
      lines.push(`Padroes de pedidos: ${structured.requestPatterns.join(" • ")}`);
    }

    if (structured.factualVariables.length > 0) {
      lines.push(`Variaveis factuais a personalizar: ${structured.factualVariables.join(" • ")}`);
    }

    if (structured.evidencePatterns.length > 0) {
      lines.push(`Padroes de prova/documentos: ${structured.evidencePatterns.join(" • ")}`);
    }

    if (structured.draftingInstructions.length > 0) {
      lines.push(`Instrucoes reaproveitaveis: ${structured.draftingInstructions.join(" • ")}`);
    }

    if (structured.cautionNotes.length > 0) {
      lines.push(`Cuidados do modelo: ${structured.cautionNotes.join(" • ")}`);
    }
  }

  if (example.notes?.trim()) {
    lines.push(`Observacoes do escritorio: ${normalizeText(example.notes)}`);
  }

  if (excerpt) {
    lines.push(`Trecho-base do exemplo:\n${excerpt}`);
  }

  return truncateText(lines.filter(Boolean).join("\n\n"), maxExampleCharacters);
}

function limitExamplesForPrompt(examples: PetitionExampleRow[]) {
  const picked: Array<{ title: string; text: string }> = [];
  let totalCharacters = 0;

  for (const example of examples) {
    if (picked.length >= maxExamplesForPrompt) {
      break;
    }

    const promptReadyText = buildPromptReadyExampleBlock(example);

    if (!promptReadyText) {
      continue;
    }

    if (totalCharacters + promptReadyText.length > maxExamplesCombinedCharacters && picked.length > 0) {
      break;
    }

    picked.push({
      title: example.title,
      text: promptReadyText,
    });
    totalCharacters += promptReadyText.length;
  }

  return picked;
}

function buildPetitionSections(input: {
  summary: string;
  petitionText: string;
  missingInformation: string[];
  cautions: string[];
}) {
  const sections: TextSection[] = [];

  if (input.summary.trim()) {
    sections.push({
      heading: "Resumo",
      lines: [input.summary.trim()],
    });
  }

  const petitionParagraphs = normalizeText(input.petitionText)
    .split(/\n{2,}/)
    .map((paragraph) => paragraph.replace(/\s+/g, " ").trim())
    .filter(Boolean);

  sections.push({
    heading: "Texto da peticao",
    lines: petitionParagraphs.length ? petitionParagraphs : [normalizeText(input.petitionText)],
  });

  if (input.missingInformation.length > 0) {
    sections.push({
      heading: "Informacoes pendentes",
      lines: input.missingInformation.map((item, index) => `${index + 1}. ${item}`),
    });
  }

  if (input.cautions.length > 0) {
    sections.push({
      heading: "Cuidados",
      lines: input.cautions.map((item, index) => `${index + 1}. ${item}`),
    });
  }

  return sections;
}

async function buildOutputAssets(input: {
  lawFirmId: string;
  actorUserId: string;
  clientId?: string | null;
  title: string;
  subtitle: string;
  petitionText: string;
  summary: string;
  missingInformation: string[];
  cautions: string[];
  metadataJson: Record<string, unknown>;
  sourceEntityType: string;
  sourceEntityId?: string | null;
}) {
  const sections = buildPetitionSections({
    summary: input.summary,
    petitionText: input.petitionText,
    missingInformation: input.missingInformation,
    cautions: input.cautions,
  });
  const baseName =
    slugifyFileToken(input.title).slice(0, 120) ||
    `peticao_${createHash("sha1").update(input.title).digest("hex").slice(0, 10)}`;

  const pdf = await buildTextPdf({
    title: input.title,
    subtitle: input.subtitle,
    sections,
  });

  const assets: PetitionOutputAsset[] = [];

  const pdfRecord = await storeFileWithRepositoryItem({
    lawFirmId: input.lawFirmId,
    clientId: input.clientId ?? null,
    actorUserId: input.actorUserId,
    itemTypeCode: "generated_form",
    sourceEntityType: input.sourceEntityType,
    sourceEntityId: input.sourceEntityId ?? null,
    subject: input.title,
    bodyText: input.petitionText,
    summaryText: input.summary,
    metadataJson: {
      ...input.metadataJson,
      outputFormat: "pdf",
    },
    fileName: `${baseName}.pdf`,
    mimeType: "application/pdf",
    bytes: pdf.bytes,
    kind: "generated",
  });

  assets.push({
    format: "pdf",
    fileId: pdfRecord.fileId,
    fileName: `${baseName}.pdf`,
    repositoryItemId: pdfRecord.repositoryItemId,
  });

  try {
    const docx = await buildTextDocx({
      title: input.title,
      subtitle: input.subtitle,
      sections,
    });

    const docxRecord = await storeFileWithRepositoryItem({
      lawFirmId: input.lawFirmId,
      clientId: input.clientId ?? null,
      actorUserId: input.actorUserId,
      itemTypeCode: "generated_form",
      sourceEntityType: input.sourceEntityType,
      sourceEntityId: input.sourceEntityId ?? null,
      subject: input.title,
      bodyText: input.petitionText,
      summaryText: input.summary,
      metadataJson: {
        ...input.metadataJson,
        outputFormat: "docx",
      },
      fileName: `${baseName}.docx`,
      mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
      bytes: docx.bytes,
      kind: "generated",
    });

    assets.push({
      format: "docx",
      fileId: docxRecord.fileId,
      fileName: `${baseName}.docx`,
      repositoryItemId: docxRecord.repositoryItemId,
    });
  } catch {
    // Keep the PDF available even when the host environment cannot package DOCX files.
  }

  return assets;
}

export async function listPetitionTypes(input: { lawFirmId: string }) {
  await ensurePetitionsSchema();

  const [petitionTypes, exampleRows] = await Promise.all([
    prisma.$queryRaw<PetitionTypeRow[]>`
      SELECT id, name, description, generation_instructions, created_at, updated_at
      FROM petition_types
      WHERE law_firm_id = ${input.lawFirmId}
        AND deleted_at IS NULL
      ORDER BY name ASC, created_at DESC
    `,
    prisma.$queryRaw<PetitionExampleRow[]>`
      SELECT
        id,
        petition_type_id,
        title,
        original_file_name,
        mime_type,
        extracted_text,
        notes,
        ai_structure_json,
        ai_structure_summary,
        ai_structure_model,
        ai_structure_ai_run_id,
        ai_structured_at,
        created_at,
        updated_at,
        file_id
      FROM petition_type_examples
      WHERE law_firm_id = ${input.lawFirmId}
        AND deleted_at IS NULL
      ORDER BY created_at DESC
    `,
  ]);

  const examplesByTypeId = new Map<string, PetitionExampleSummary[]>();

  for (const row of exampleRows) {
    const currentExamples = examplesByTypeId.get(row.petition_type_id) ?? [];
    currentExamples.push({
      id: row.id,
      petitionTypeId: row.petition_type_id,
      title: row.title,
      fileId: row.file_id,
      fileName: row.original_file_name,
      mimeType: row.mime_type,
      notes: row.notes,
      extractedTextLength: normalizeText(row.extracted_text ?? "").length,
      createdAt: row.created_at.toISOString(),
      updatedAt: row.updated_at.toISOString(),
    });
    examplesByTypeId.set(row.petition_type_id, currentExamples);
  }

  return petitionTypes.map((row) => {
    const examples = examplesByTypeId.get(row.id) ?? [];

    return {
      id: row.id,
      name: row.name,
      description: row.description,
      generationInstructions: row.generation_instructions,
      exampleCount: examples.length,
      createdAt: row.created_at.toISOString(),
      updatedAt: row.updated_at.toISOString(),
      examples,
    } satisfies PetitionTypeSummary;
  });
}

export async function createPetitionType(input: {
  lawFirmId: string;
  actorUserId: string;
  name: string;
  description?: string | null;
  generationInstructions?: string | null;
}) {
  await ensurePetitionsSchema();

  const id = createId();

  try {
    await prisma.$executeRaw`
      INSERT INTO petition_types (
        id, law_firm_id, name, description, generation_instructions, created_by_user_id, created_at, updated_at
      ) VALUES (
        ${id},
        ${input.lawFirmId},
        ${normalizeText(input.name)},
        ${normalizeOptionalText(input.description)},
        ${normalizeOptionalText(input.generationInstructions)},
        ${input.actorUserId},
        CURRENT_TIMESTAMP,
        CURRENT_TIMESTAMP
      )
    `;
  } catch (error) {
    if (
      error instanceof Error &&
      /uq_petition_types_law_firm_name|duplicate/i.test(error.message)
    ) {
      throw new PetitionServiceError(409, "A petition type with this name already exists.");
    }

    throw error;
  }

  const created = await getPetitionTypeOrThrow({
    lawFirmId: input.lawFirmId,
    petitionTypeId: id,
  });

  return {
    id: created.id,
    name: created.name,
    description: created.description,
    generationInstructions: created.generation_instructions,
    exampleCount: 0,
    createdAt: created.created_at.toISOString(),
    updatedAt: created.updated_at.toISOString(),
    examples: [],
  } satisfies PetitionTypeSummary;
}

export async function updatePetitionType(input: {
  lawFirmId: string;
  petitionTypeId: string;
  name: string;
  description?: string | null;
  generationInstructions?: string | null;
}) {
  await ensurePetitionsSchema();
  await getPetitionTypeOrThrow({
    lawFirmId: input.lawFirmId,
    petitionTypeId: input.petitionTypeId,
  });

  try {
    await prisma.$executeRaw`
      UPDATE petition_types
      SET
        name = ${normalizeText(input.name)},
        description = ${normalizeOptionalText(input.description)},
        generation_instructions = ${normalizeOptionalText(input.generationInstructions)},
        updated_at = CURRENT_TIMESTAMP
      WHERE id = ${input.petitionTypeId}
        AND law_firm_id = ${input.lawFirmId}
        AND deleted_at IS NULL
    `;
  } catch (error) {
    if (
      error instanceof Error &&
      /uq_petition_types_law_firm_name|duplicate/i.test(error.message)
    ) {
      throw new PetitionServiceError(409, "A petition type with this name already exists.");
    }

    throw error;
  }

  const [updated, exampleRows] = await Promise.all([
    getPetitionTypeOrThrow({
      lawFirmId: input.lawFirmId,
      petitionTypeId: input.petitionTypeId,
    }),
    loadPetitionExamples({
      lawFirmId: input.lawFirmId,
      petitionTypeId: input.petitionTypeId,
    }),
  ]);

  const examples = exampleRows.map((row) => ({
    id: row.id,
    petitionTypeId: row.petition_type_id,
    title: row.title,
    fileId: row.file_id,
    fileName: row.original_file_name,
    mimeType: row.mime_type,
    notes: row.notes,
    extractedTextLength: normalizeText(row.extracted_text ?? "").length,
    createdAt: row.created_at.toISOString(),
    updatedAt: row.updated_at.toISOString(),
  }));

  return {
    id: updated.id,
    name: updated.name,
    description: updated.description,
    generationInstructions: updated.generation_instructions,
    exampleCount: examples.length,
    createdAt: updated.created_at.toISOString(),
    updatedAt: updated.updated_at.toISOString(),
    examples,
  } satisfies PetitionTypeSummary;
}

export async function softDeletePetitionType(input: {
  lawFirmId: string;
  petitionTypeId: string;
}) {
  await ensurePetitionsSchema();

  const petitionType = await getPetitionTypeOrThrow({
    lawFirmId: input.lawFirmId,
    petitionTypeId: input.petitionTypeId,
  });

  const deletionSuffix = `__deleted__${input.petitionTypeId.slice(0, 8)}`;
  const preservedName = normalizeText(petitionType.name);
  const truncatedName = preservedName.slice(0, Math.max(1, 191 - deletionSuffix.length));
  const archivedName = `${truncatedName}${deletionSuffix}`;

  await prisma.$executeRaw`
    UPDATE petition_types
    SET
      name = ${archivedName},
      deleted_at = CURRENT_TIMESTAMP,
      updated_at = CURRENT_TIMESTAMP
    WHERE id = ${input.petitionTypeId}
      AND law_firm_id = ${input.lawFirmId}
      AND deleted_at IS NULL
  `;

  return {
    id: input.petitionTypeId,
    deletedAt: new Date().toISOString(),
  };
}

async function structurePetitionExampleForAi(input: {
  lawFirmId: string;
  petitionType: PetitionTypeRow;
  title: string;
  notes?: string | null;
  extractedText: string;
}) {
  const aiRun = await createAiRun({
    lawFirmId: input.lawFirmId,
    runType: "petition_example_structuring",
  });

  try {
    const completion = await runJsonChatCompletion({
      lawFirmId: input.lawFirmId,
      maxCompletionTokens: 1_800,
      systemPrompt: [
        "You analyze a Brazilian Portuguese legal petition example and convert it into reusable structured drafting guidance for future AI petition generation.",
        'Return JSON only with the exact schema {"summary":"string","documentObjective":"string","tone":"string","audience":"string","sections":[{"heading":"string","purpose":"string","keyElements":["string"]}],"argumentPatterns":["string"],"requestPatterns":["string"],"factualVariables":["string"],"evidencePatterns":["string"],"draftingInstructions":["string"],"cautionNotes":["string"]}.',
        "Focus on reusable structure, style, argument patterns, pleading flow, and placeholders that should be customized later.",
        "Do not copy client-specific facts as if they were reusable truths.",
        "When facts appear specific to one client, convert them into generic placeholders or caution notes.",
        "Write every field in Brazilian Portuguese.",
        "Do not mention OpenAI, prompts, or internal implementation details.",
      ].join(" "),
      userPrompt: JSON.stringify(
        {
          petitionType: {
            name: input.petitionType.name,
            description: input.petitionType.description,
            generationInstructions: input.petitionType.generation_instructions,
          },
          exampleTitle: input.title,
          uploaderNotes: normalizeOptionalText(input.notes),
          sourceText: truncateText(input.extractedText, maxExampleAnalysisCharacters),
          outputGoal:
            "Produce a structured representation that another AI can read later to imitate the example's organization and drafting approach without copying client-specific facts.",
        },
        null,
        2,
      ),
    });

    const structure = sanitizePetitionExampleAiStructure(completion.json);
    const structuredSummary =
      structure.summary ||
      structure.documentObjective ||
      truncateText(input.extractedText, 700);

    await finishAiRun({
      aiRunId: aiRun.id,
      status: "completed",
      inputTokens: completion.usage.inputTokens,
      outputTokens: completion.usage.outputTokens,
    });

    return {
      aiRunId: aiRun.id,
      structuredAt: new Date().toISOString(),
      model: completion.model,
      structure,
      summary: structuredSummary,
    };
  } catch (error) {
    await finishAiRun({
      aiRunId: aiRun.id,
      status: "failed",
      errorMessage:
        error instanceof Error
          ? error.message
          : "Failed to structure the petition example for later AI reuse.",
    });
    throw error instanceof PetitionServiceError
      ? error
      : new PetitionServiceError(
          502,
          "A AI nao conseguiu estruturar o exemplo juridico para reutilizacao posterior.",
        );
  }
}

export async function uploadPetitionExample(input: {
  lawFirmId: string;
  actorUserId: string;
  petitionTypeId: string;
  title?: string | null;
  notes?: string | null;
  fileName: string;
  mimeType: string;
  bytes: Buffer;
}) {
  const petitionType = await getPetitionTypeOrThrow({
    lawFirmId: input.lawFirmId,
    petitionTypeId: input.petitionTypeId,
  });

  const extractedText = await extractDocumentText({
    fileName: input.fileName,
    mimeType: input.mimeType,
    bytes: input.bytes,
  });

  if (!normalizeText(extractedText)) {
    throw new PetitionServiceError(
      400,
      "Unable to extract readable text from the uploaded example. Use PDF, DOCX, text, or image with visible text.",
    );
  }

  const title = normalizeOptionalText(input.title) ?? petitionType.name;
  const structuredExample = await structurePetitionExampleForAi({
    lawFirmId: input.lawFirmId,
    petitionType,
    title,
    notes: input.notes,
    extractedText,
  });
  const stored = await storeFileWithRepositoryItem({
    lawFirmId: input.lawFirmId,
    actorUserId: input.actorUserId,
    itemTypeCode: "document",
    sourceEntityType: "petition_example",
    sourceEntityId: petitionType.id,
    subject: `${petitionType.name} • exemplo`,
    bodyText: extractedText,
    summaryText: title,
    metadataJson: {
      petitionTypeId: petitionType.id,
      petitionTypeName: petitionType.name,
      notes: normalizeOptionalText(input.notes),
      structuredForAi: true,
      aiStructureSummary: structuredExample.summary,
      aiStructure: structuredExample.structure,
      aiStructureModel: structuredExample.model,
    },
    fileName: input.fileName,
    mimeType: input.mimeType || "application/octet-stream",
    bytes: input.bytes,
    kind: "uploads",
  });

  const exampleId = createId();
  await prisma.$executeRaw`
    INSERT INTO petition_type_examples (
      id, law_firm_id, petition_type_id, repository_item_id, file_id, title, original_file_name,
      mime_type, extracted_text, notes, ai_structure_json, ai_structure_summary, ai_structure_model,
      ai_structure_ai_run_id, ai_structured_at, created_by_user_id, created_at, updated_at, deleted_at
    ) VALUES (
      ${exampleId},
      ${input.lawFirmId},
      ${petitionType.id},
      ${stored.repositoryItemId},
      ${stored.fileId},
      ${title},
      ${input.fileName},
      ${input.mimeType || "application/octet-stream"},
      ${extractedText},
      ${normalizeOptionalText(input.notes)},
      ${JSON.stringify(structuredExample.structure)},
      ${structuredExample.summary},
      ${structuredExample.model},
      ${structuredExample.aiRunId},
      ${new Date(structuredExample.structuredAt)},
      ${input.actorUserId},
      CURRENT_TIMESTAMP,
      CURRENT_TIMESTAMP,
      NULL
    )
  `;

  return {
    id: exampleId,
    petitionTypeId: petitionType.id,
    title,
    fileId: stored.fileId,
    fileName: input.fileName,
    mimeType: input.mimeType || "application/octet-stream",
    notes: normalizeOptionalText(input.notes),
    extractedTextLength: normalizeText(extractedText).length,
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString(),
  } satisfies PetitionExampleSummary;
}

export async function updatePetitionExample(input: {
  lawFirmId: string;
  exampleId: string;
  petitionTypeId: string;
  title?: string | null;
  notes?: string | null;
}) {
  await ensurePetitionsSchema();

  const existingExample = await getPetitionExampleOrThrow({
    lawFirmId: input.lawFirmId,
    exampleId: input.exampleId,
  });
  const petitionType = await getPetitionTypeOrThrow({
    lawFirmId: input.lawFirmId,
    petitionTypeId: input.petitionTypeId,
  });
  const normalizedTitle = normalizeOptionalText(input.title) ?? existingExample.title;
  const normalizedNotes = normalizeOptionalText(input.notes);
  const normalizedExtractedText = normalizeOptionalText(existingExample.extracted_text);

  if (!normalizedExtractedText) {
    throw new PetitionServiceError(
      400,
      "This petition example no longer has readable extracted text to structure.",
    );
  }

  const structuredExample = await structurePetitionExampleForAi({
    lawFirmId: input.lawFirmId,
    petitionType,
    title: normalizedTitle,
    notes: normalizedNotes,
    extractedText: normalizedExtractedText,
  });

  await prisma.$executeRaw`
    UPDATE petition_type_examples
    SET
      petition_type_id = ${petitionType.id},
      title = ${normalizedTitle},
      notes = ${normalizedNotes},
      ai_structure_json = ${JSON.stringify(structuredExample.structure)},
      ai_structure_summary = ${structuredExample.summary},
      ai_structure_model = ${structuredExample.model},
      ai_structure_ai_run_id = ${structuredExample.aiRunId},
      ai_structured_at = ${new Date(structuredExample.structuredAt)},
      updated_at = CURRENT_TIMESTAMP
    WHERE id = ${input.exampleId}
      AND law_firm_id = ${input.lawFirmId}
      AND deleted_at IS NULL
  `;

  const updatedExample = await getPetitionExampleOrThrow({
    lawFirmId: input.lawFirmId,
    exampleId: input.exampleId,
  });

  return {
    id: updatedExample.id,
    petitionTypeId: updatedExample.petition_type_id,
    title: updatedExample.title,
    fileId: updatedExample.file_id,
    fileName: updatedExample.original_file_name,
    mimeType: updatedExample.mime_type,
    notes: updatedExample.notes,
    extractedTextLength: normalizeText(updatedExample.extracted_text ?? "").length,
    createdAt: updatedExample.created_at.toISOString(),
    updatedAt: updatedExample.updated_at.toISOString(),
  } satisfies PetitionExampleSummary;
}

export async function softDeletePetitionExample(input: {
  lawFirmId: string;
  exampleId: string;
}) {
  await ensurePetitionsSchema();
  await getPetitionExampleOrThrow({
    lawFirmId: input.lawFirmId,
    exampleId: input.exampleId,
  });

  await prisma.$executeRaw`
    UPDATE petition_type_examples
    SET
      deleted_at = CURRENT_TIMESTAMP,
      updated_at = CURRENT_TIMESTAMP
    WHERE id = ${input.exampleId}
      AND law_firm_id = ${input.lawFirmId}
      AND deleted_at IS NULL
  `;

  return {
    id: input.exampleId,
    deletedAt: new Date().toISOString(),
  };
}

function buildExamplesPrompt(examples: PetitionExampleRow[]) {
  const selectedExamples = limitExamplesForPrompt(examples);

  if (selectedExamples.length === 0) {
    return {
      examplesConsultedCount: 0,
      promptBlock: "No petition examples were uploaded for this type.",
    };
  }

  const blocks = selectedExamples.map(
    (example, index) =>
      `Example ${index + 1} - ${example.title}\n${example.text}`,
  );

  return {
    examplesConsultedCount: selectedExamples.length,
    promptBlock: blocks.join("\n\n---\n\n"),
  };
}

export async function suggestPetitionTypeFieldValue(input: {
  lawFirmId: string;
  name: string;
  description?: string | null;
  generationInstructions?: string | null;
  target: PetitionTypeFieldSuggestionTarget;
}) {
  const normalizedName = normalizeText(input.name);

  if (normalizedName.length < 2) {
    throw new PetitionServiceError(
      400,
      "Informe o nome do tipo de petição antes de pedir uma sugestão da AI.",
    );
  }

  const aiRun = await createAiRun({
    lawFirmId: input.lawFirmId,
    runType: "petition_type_field_suggestion",
  });

  try {
    const completion = await runJsonChatCompletion({
      lawFirmId: input.lawFirmId,
      maxCompletionTokens: 1_000,
      systemPrompt: [
        "You suggest one value for a Brazilian legal petition type configuration field.",
        'Return JSON only in the format {"value":"...","reason":"..."}.',
        'If target is "description", write a concise plain-language description of what this petition type is for, ideally 1-3 sentences in Brazilian Portuguese.',
        'If target is "generation_instructions", write drafting guidance in Brazilian Portuguese for the AI that will generate petitions of this type.',
        "The drafting guidance should be directly usable by another AI and mention tone, expected structure, mandatory sections, fact-handling, and any relevant care points when helpful.",
        "Do not invent client facts, case numbers, or tribunal details.",
        "Do not mention OpenAI, prompts, or internal implementation details.",
        "Keep the result practical and ready to paste into a workspace form field.",
      ].join(" "),
      userPrompt: JSON.stringify(
        {
          target: input.target,
          petitionTypeName: normalizedName,
          currentDescription: normalizeOptionalText(input.description),
          currentGenerationInstructions: normalizeOptionalText(input.generationInstructions),
          outputRules:
            input.target === "description"
              ? "Return a compact description suitable for a visible form field."
              : "Return operational drafting instructions suitable for an internal AI configuration field.",
        },
        null,
        2,
      ),
    });

    const value = truncateText(
      String(completion.json.value ?? ""),
      petitionTypeFieldSuggestionMaxLengthByTarget[input.target],
    );
    const reason = truncateText(String(completion.json.reason ?? ""), 600);

    if (!value) {
      throw new PetitionServiceError(502, "The AI did not return a usable petition type suggestion.");
    }

    await finishAiRun({
      aiRunId: aiRun.id,
      status: "completed",
      inputTokens: completion.usage.inputTokens,
      outputTokens: completion.usage.outputTokens,
    });

    return {
      aiRunId: aiRun.id,
      model: completion.model,
      target: input.target,
      value,
      reason,
    } satisfies PetitionTypeFieldSuggestion;
  } catch (error) {
    await finishAiRun({
      aiRunId: aiRun.id,
      status: "failed",
      errorMessage:
        error instanceof Error ? error.message : "Failed to generate petition type field suggestion.",
    });
    throw error;
  }
}

export async function generatePetition(input: {
  lawFirmId: string;
  actorUserId: string;
  petitionTypeId: string;
  clientId: string;
  instructions?: string | null;
}) {
  const [petitionType, clientContext, examples] = await Promise.all([
    getPetitionTypeOrThrow({
      lawFirmId: input.lawFirmId,
      petitionTypeId: input.petitionTypeId,
    }),
    loadClientDraftingContext({
      lawFirmId: input.lawFirmId,
      clientId: input.clientId,
    }),
    loadPetitionExamples({
      lawFirmId: input.lawFirmId,
      petitionTypeId: input.petitionTypeId,
    }),
  ]);

  const aiRun = await createAiRun({
    lawFirmId: input.lawFirmId,
    clientId: input.clientId,
    runType: "petition_generation",
  });

  const clientName = [clientContext.client.first_name, clientContext.client.middle_name, clientContext.client.last_name]
    .map((value) => String(value ?? "").trim())
    .filter(Boolean)
    .join(" ");
  const examplePrompt = buildExamplesPrompt(examples);

  try {
    const completion = await runJsonChatCompletion({
      lawFirmId: input.lawFirmId,
      systemPrompt:
        "You draft Brazilian Portuguese legal petitions for a law office. Use the uploaded examples as style references only. Never copy prior client facts. Use only the supplied client data and clearly list any missing information instead of inventing facts. Return valid JSON only.",
      userPrompt: [
        `Petition type: ${petitionType.name}`,
        petitionType.description ? `Type description: ${petitionType.description}` : null,
        petitionType.generation_instructions
          ? `Base drafting instructions: ${petitionType.generation_instructions}`
          : null,
        normalizeOptionalText(input.instructions)
          ? `Additional user instructions: ${normalizeOptionalText(input.instructions)}`
          : null,
        `Client data snapshot:\n${buildClientPromptSnapshot(clientContext)}`,
        `Reference examples:\n${examplePrompt.promptBlock}`,
        [
          "Return a JSON object with this exact schema:",
          '{',
          '  "title": "string",',
          '  "summary": "string",',
          '  "petitionText": "string",',
          '  "missingInformation": ["string"],',
          '  "cautions": ["string"]',
          '}',
          "petitionText must contain the full petition body in Portuguese, ready for attorney review.",
        ].join("\n"),
      ]
        .filter(Boolean)
        .join("\n\n"),
      maxCompletionTokens: 6_000,
    });

    const title =
      normalizeOptionalText(String(completion.json.title ?? "")) ??
      `${petitionType.name} • ${clientName}`;
    const summary =
      normalizeOptionalText(String(completion.json.summary ?? "")) ??
      `Petição gerada para ${clientName || "cliente"}.`;
    const petitionText =
      normalizeOptionalText(
        String(completion.json.petitionText ?? completion.json.documentText ?? ""),
      ) ?? "";

    if (!petitionText) {
      throw new PetitionServiceError(502, "The AI returned an empty petition.");
    }

    const missingInformation = normalizeStringArray(completion.json.missingInformation, 16);
    const cautions = normalizeStringArray(completion.json.cautions, 12);
    const assets = await buildOutputAssets({
      lawFirmId: input.lawFirmId,
      actorUserId: input.actorUserId,
      clientId: input.clientId,
      title,
      subtitle: `${petitionType.name} • ${clientName}`,
      petitionText,
      summary,
      missingInformation,
      cautions,
      metadataJson: {
        mode: "generation" satisfies PetitionOutputMode,
        petitionTypeId: petitionType.id,
        petitionTypeName: petitionType.name,
        clientId: input.clientId,
        clientName,
        missingInformation,
        cautions,
      },
      sourceEntityType: "petition_generation",
      sourceEntityId: petitionType.id,
    });

    await finishAiRun({
      aiRunId: aiRun.id,
      status: "completed",
      inputTokens: completion.usage.inputTokens,
      outputTokens: completion.usage.outputTokens,
    });

    return {
      title,
      summary,
      petitionText,
      missingInformation,
      cautions,
      examplesConsultedCount: examplePrompt.examplesConsultedCount,
      assets,
    } satisfies PetitionGenerationResult;
  } catch (error) {
    await finishAiRun({
      aiRunId: aiRun.id,
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "Petition generation failed.",
    });
    throw error;
  }
}

export async function rectifyPetition(input: {
  lawFirmId: string;
  actorUserId: string;
  fileName: string;
  mimeType: string;
  bytes: Buffer;
  instructions: string;
  petitionTypeId?: string | null;
  clientId?: string | null;
}) {
  const sourceText = await extractDocumentText({
    fileName: input.fileName,
    mimeType: input.mimeType,
    bytes: input.bytes,
  });

  const normalizedSourceText = normalizeText(sourceText);

  if (!normalizedSourceText) {
    throw new PetitionServiceError(
      400,
      "Unable to extract readable text from the petition to rectify.",
    );
  }

  const [petitionType, clientContext, examples] = await Promise.all([
    input.petitionTypeId ?
      getPetitionTypeOrThrow({
        lawFirmId: input.lawFirmId,
        petitionTypeId: input.petitionTypeId,
      }) :
      Promise.resolve(null),
    input.clientId ?
      loadClientDraftingContext({
        lawFirmId: input.lawFirmId,
        clientId: input.clientId,
      }) :
      Promise.resolve(null),
    input.petitionTypeId ?
      loadPetitionExamples({
        lawFirmId: input.lawFirmId,
        petitionTypeId: input.petitionTypeId,
      }) :
      Promise.resolve([]),
  ]);

  const aiRun = await createAiRun({
    lawFirmId: input.lawFirmId,
    clientId: input.clientId ?? null,
    runType: "petition_rectification",
  });
  const clientName =
    clientContext ?
      [clientContext.client.first_name, clientContext.client.middle_name, clientContext.client.last_name]
        .map((value) => String(value ?? "").trim())
        .filter(Boolean)
        .join(" ") :
      "";
  const examplePrompt = buildExamplesPrompt(examples);

  try {
    const completion = await runJsonChatCompletion({
      lawFirmId: input.lawFirmId,
      systemPrompt:
        "You revise Brazilian Portuguese legal petitions. Apply the requested corrections while preserving legal tone, coherence, formatting, and client facts. Never invent facts. Return valid JSON only.",
      userPrompt: [
        petitionType ? `Petition type: ${petitionType.name}` : null,
        petitionType?.description ? `Type description: ${petitionType.description}` : null,
        petitionType?.generation_instructions
          ? `Style and drafting instructions: ${petitionType.generation_instructions}`
          : null,
        clientContext ? `Client data snapshot:\n${buildClientPromptSnapshot(clientContext)}` : null,
        `Correction instructions:\n${normalizeText(input.instructions)}`,
        `Original petition text:\n${normalizedSourceText}`,
        petitionType ? `Reference examples:\n${examplePrompt.promptBlock}` : null,
        [
          "Return a JSON object with this exact schema:",
          '{',
          '  "title": "string",',
          '  "summary": "string",',
          '  "correctedText": "string",',
          '  "changesApplied": ["string"],',
          '  "cautions": ["string"]',
          '}',
          "correctedText must contain the full revised petition body in Portuguese.",
        ].join("\n"),
      ]
        .filter(Boolean)
        .join("\n\n"),
      maxCompletionTokens: 6_000,
    });

    const title =
      normalizeOptionalText(String(completion.json.title ?? "")) ??
      `Peticao retificada • ${input.fileName.replace(/\.[^.]+$/, "")}`;
    const summary =
      normalizeOptionalText(String(completion.json.summary ?? "")) ??
      "Petição retificada conforme instruções.";
    const petitionText =
      normalizeOptionalText(
        String(completion.json.correctedText ?? completion.json.petitionText ?? ""),
      ) ?? "";

    if (!petitionText) {
      throw new PetitionServiceError(502, "The AI returned an empty rectified petition.");
    }

    const changesApplied = normalizeStringArray(completion.json.changesApplied, 16);
    const cautions = normalizeStringArray(completion.json.cautions, 12);
    const assets = await buildOutputAssets({
      lawFirmId: input.lawFirmId,
      actorUserId: input.actorUserId,
      clientId: input.clientId ?? null,
      title,
      subtitle: petitionType ?
        `${petitionType.name} • ${clientName || "retificacao"}` :
        `Retificacao • ${clientName || input.fileName}`,
      petitionText,
      summary,
      missingInformation: [],
      cautions,
      metadataJson: {
        mode: "rectification" satisfies PetitionOutputMode,
        petitionTypeId: petitionType?.id ?? null,
        petitionTypeName: petitionType?.name ?? null,
        clientId: input.clientId ?? null,
        clientName: clientName || null,
        sourceFileName: input.fileName,
        changesApplied,
        cautions,
      },
      sourceEntityType: "petition_rectification",
      sourceEntityId: petitionType?.id ?? null,
    });

    await finishAiRun({
      aiRunId: aiRun.id,
      status: "completed",
      inputTokens: completion.usage.inputTokens,
      outputTokens: completion.usage.outputTokens,
    });

    return {
      title,
      summary,
      petitionText,
      changesApplied,
      cautions,
      examplesConsultedCount: examplePrompt.examplesConsultedCount,
      assets,
    } satisfies PetitionRectificationResult;
  } catch (error) {
    await finishAiRun({
      aiRunId: aiRun.id,
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "Petition rectification failed.",
    });
    throw error;
  }
}
