import { existsSync } from "node:fs";
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import OpenAI from "openai";
import * as XLSX from "xlsx";
import { SCORING_MODEL, type Ampelfarbe } from "./scoring-model";
import {
  calculateScoringResult,
  deriveTrafficLight,
  type LlmScoringAssessment,
  type ScoringDimension,
} from "./scoring";
import {
  buildLlmDossier,
  createPseudonymizer,
  type LlmDossier,
  type PrivacyAudit,
  type Pseudonymizer,
} from "./privacy";
import { AI_DISCLAIMER_TEXT, AI_DISCLAIMER_TITLE, writeJuryReport } from "./report";
import { isSafeStem } from "../../security";
import { dataPath } from "../../deployPaths";

export interface FrageMitAntwort {
  id?: string;
  text: string;
  antwort?: string;
  confidence?: number;
  abgedeckt?: boolean;
  quelle?: "pdf_direkt" | "llm_segmentierung";
}

export interface SegmentierungsQualitaet {
  durchschnitt_confidence: number;
  abgedeckte_fragen: number;
  gesamt_fragen: number;
  niedriges_vertrauen: boolean;
}

export interface AntwortEintrag {
  fieldName?: string;
  label?: string;
  antwortFormat?: "einzelfrage" | "mehrfachfrage_ein_antwortfeld";
  zuordnungsmodus?: "einzelfrage" | "gemeinsame_antwort" | "llm_segmentiert";
  fragen?: FrageMitAntwort[];
  antwort?: string;
  zusammenfassung?: string;
  segmentierung?: SegmentierungsQualitaet;
}

export interface ExtractedData {
  kontakt?: Record<string, unknown>;
  unternehmen?: Record<string, unknown>;
  fragen?: Record<string, AntwortEintrag>;
  kriterium?: Record<string, AntwortEintrag>;
  [key: string]: unknown;
}

export interface Ampelbewertung {
  farbe: Ampelfarbe;
  gesamtScore?: number;
  begruendung: string;
  ausschlussgruende?: string[];
  unbewertbareKriterien?: number;
  missingDataWarnings?: string[];
  dimensionen?: ScoringDimension[];
}

export interface SWOTAnalyse {
  staerken: string[];
  schwaechen: string[];
  chancen: string[];
  risiken: string[];
}

export interface SummaryData extends ExtractedData {
  fragen?: Record<string, AntwortEintrag>;
  kriterium?: Record<string, AntwortEintrag>;
  ampelbewertung?: Ampelbewertung;
  swot_analyse?: SWOTAnalyse;
  _privacy?: PrivacyAudit;
  _schemaVersion?: number;
  _summarizedAt?: string;
}

export type LlmCallStatus = "running" | "done" | "error";

export interface LlmCallTrace {
  id: string;
  operation: "segmentierung" | "zusammenfassung" | "scoring" | "swot";
  label: string;
  model: string;
  status: LlmCallStatus;
  startedAt: string;
  finishedAt?: string;
  durationMs?: number;
  request: Record<string, unknown>;
  response?: Record<string, unknown>;
  error?: string;
}

export interface SummarizeCompanyOptions {
  onLlmCall?: (trace: LlmCallTrace) => void;
}

const OUTPUTS_DIR =
  process.env.BMP_EXTRACTOR_OUTPUTS_DIR ??
  dataPath(join(import.meta.dir, "../extractor/outputs"), "extractor", "outputs");
const XLSX_TEMPLATE_PATH = join(import.meta.dir, "template.xlsx");
const OPENROUTER_PRIVACY_PROVIDER = {
  data_collection: "deny",
  zdr: true,
} as const;

function createClient(): OpenAI {
  const apiKey = process.env.OPENROUTER_API_KEY;
  if (!apiKey) throw new Error("OPENROUTER_API_KEY is not set in environment");

  return new OpenAI({
    baseURL: "https://openrouter.ai/api/v1",
    apiKey,
    defaultHeaders: {
      "HTTP-Referer": "https://github.com/bmp-rewrite",
      "X-Title": "bmp-rewrite summarizer",
    },
  });
}

function buildQuestionContext(entry: AntwortEintrag, fallbackLabel: string): string {
  const frageText = (entry.fragen ?? []).map((frage, index) => `${index + 1}. ${frage.text}`).join("\n");
  const label = entry.label?.trim() || fallbackLabel;
  return frageText ? `${label}\nZugehörige Fragen:\n${frageText}` : label;
}

function jsonSchemaFormat(name: string, schema: Record<string, unknown>) {
  return {
    type: "json_schema",
    json_schema: {
      name,
      strict: true,
      schema,
    },
  } as never;
}

function serializeCompletion(completion: unknown): Record<string, unknown> {
  const value = completion as {
    id?: unknown;
    model?: unknown;
    created?: unknown;
    usage?: unknown;
    choices?: Array<{ finish_reason?: unknown; message?: unknown }>;
  };
  return {
    id: value.id,
    model: value.model,
    created: value.created,
    usage: value.usage,
    choices: value.choices?.map((choice) => ({
      finish_reason: choice.finish_reason,
      message: choice.message,
    })),
  };
}

function beginLlmTrace(
  onLlmCall: SummarizeCompanyOptions["onLlmCall"] | undefined,
  operation: LlmCallTrace["operation"],
  label: string,
  model: string,
  request: Record<string, unknown>,
): LlmCallTrace {
  const trace: LlmCallTrace = {
    id: crypto.randomUUID(),
    operation,
    label,
    model,
    status: "running",
    startedAt: new Date().toISOString(),
    request,
  };
  onLlmCall?.(trace);
  return trace;
}

function finishLlmTrace(
  onLlmCall: SummarizeCompanyOptions["onLlmCall"] | undefined,
  trace: LlmCallTrace,
  result: { response?: unknown; error?: unknown },
): void {
  const finishedAt = new Date();
  const startedAt = new Date(trace.startedAt);
  onLlmCall?.({
    ...trace,
    status: result.error ? "error" : "done",
    finishedAt: finishedAt.toISOString(),
    durationMs: finishedAt.getTime() - startedAt.getTime(),
    response: result.response ? serializeCompletion(result.response) : undefined,
    error: result.error ? String(result.error) : undefined,
  });
}

async function createStructuredCompletion(
  client: OpenAI,
  model: string,
  messages: Array<{ role: "system" | "user"; content: string }>,
  schemaName: string,
  schema: Record<string, unknown>,
  sessionId?: string,
  trace?: {
    operation: LlmCallTrace["operation"];
    label: string;
    onLlmCall?: SummarizeCompanyOptions["onLlmCall"];
  },
) {
  const structuredRequest = {
    model,
    response_format: jsonSchemaFormat(schemaName, schema),
    messages,
    provider: OPENROUTER_PRIVACY_PROVIDER,
    usage: { include: true },
    session_id: sessionId,
  };
  const primaryTrace = trace
    ? beginLlmTrace(trace.onLlmCall, trace.operation, trace.label, model, structuredRequest as Record<string, unknown>)
    : undefined;
  try {
    const completion = await client.chat.completions.create(structuredRequest as never);
    if (primaryTrace) finishLlmTrace(trace?.onLlmCall, primaryTrace, { response: completion });
    return completion;
  } catch (error) {
    const message = String(error);
    if (!message.includes("400") && !message.toLowerCase().includes("provider returned error")) {
      if (primaryTrace) finishLlmTrace(trace?.onLlmCall, primaryTrace, { error });
      throw error;
    }
    if (primaryTrace) finishLlmTrace(trace?.onLlmCall, primaryTrace, { error });

    const fallbackRequest = {
      model,
      response_format: { type: "json_object" } as never,
      messages,
      provider: OPENROUTER_PRIVACY_PROVIDER,
      usage: { include: true },
      session_id: sessionId,
    };
    const fallbackTrace = trace
      ? beginLlmTrace(trace.onLlmCall, trace.operation, `${trace.label} (JSON fallback)`, model, fallbackRequest as Record<string, unknown>)
      : undefined;
    try {
      const completion = await client.chat.completions.create(fallbackRequest as never);
      if (fallbackTrace) finishLlmTrace(trace?.onLlmCall, fallbackTrace, { response: completion });
      return completion;
    } catch (fallbackError) {
      if (fallbackTrace) finishLlmTrace(trace?.onLlmCall, fallbackTrace, { error: fallbackError });
      throw fallbackError;
    }
  }
}

async function summarizeText(
  client: OpenAI,
  model: string,
  text: string,
  context: string,
  sessionId?: string,
  trace?: {
    label: string;
    onLlmCall?: SummarizeCompanyOptions["onLlmCall"];
  },
): Promise<string> {
  const request = {
    model,
    messages: [
      {
        role: "system",
        content:
          "Du bist ein präziser Assistent, der Unternehmensantworten aus Bewerbungsunterlagen auf Deutsch verdichtet. Erstelle eine kurze, prägnante Zusammenfassung, die zwingend kürzer als die Originalantwort ist. Nenne nur Informationen, die in der Originalantwort ausdrücklich enthalten sind. Füge keine Details, Kennzahlen, Bewertungen, Schlussfolgerungen oder Interpretationen hinzu, die nicht explizit genannt werden. Behalte die wichtigsten Aussagen bei, streiche Redundanzen und bleibe sachlich nah am Original.",
      },
      {
        role: "user",
        content: `Kontext:\n${context}\n\nFasse die folgende Antwort zusammen:\n\n${text}`,
      },
    ],
    provider: OPENROUTER_PRIVACY_PROVIDER,
    usage: { include: true },
    session_id: sessionId,
  };
  const llmTrace = trace
    ? beginLlmTrace(trace.onLlmCall, "zusammenfassung", trace.label, model, request as Record<string, unknown>)
    : undefined;
  let completion: Awaited<ReturnType<typeof client.chat.completions.create>>;
  try {
    completion = await client.chat.completions.create(request as never);
    if (llmTrace) finishLlmTrace(trace?.onLlmCall, llmTrace, { response: completion });
  } catch (error) {
    if (llmTrace) finishLlmTrace(trace?.onLlmCall, llmTrace, { error });
    throw error;
  }

  const msg = completion.choices[0]?.message as Record<string, unknown> | undefined;
  const content = (msg?.content ?? msg?.reasoning) as string | null | undefined;

  if (!content?.trim()) {
    throw new Error(
      `Model returned no content (finish_reason: ${completion.choices[0]?.finish_reason}). Try a non-reasoning model such as openai/gpt-4o-mini.`,
    );
  }

  return content.trim();
}

function extractJsonObject(text: string): string {
  const trimmed = text.trim();
  if (trimmed.startsWith("```")) {
    const match = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
    if (match?.[1]) return match[1].trim();
  }
  return trimmed;
}

function summarizeSegmentierungsQualitaet(fragen: FrageMitAntwort[] | undefined): SegmentierungsQualitaet | undefined {
  const items = fragen ?? [];
  if (!items.length) return undefined;

  const covered = items.filter((frage) => frage.abgedeckt).length;
  const confidences = items
    .map((frage) => frage.confidence)
    .filter((value): value is number => typeof value === "number");
  const average = confidences.length
    ? confidences.reduce((sum, value) => sum + value, 0) / confidences.length
    : 0;
  const lowConfidenceCount = items.filter((frage) => (frage.confidence ?? 0) < 0.5).length;

  return {
    durchschnitt_confidence: Number(average.toFixed(2)),
    abgedeckte_fragen: covered,
    gesamt_fragen: items.length,
    niedriges_vertrauen: average < 0.6 || lowConfidenceCount > Math.floor(items.length / 2),
  };
}

function formatQuestionAnswers(fragen: FrageMitAntwort[] | undefined): string {
  return (fragen ?? [])
    .map((frage, index) => {
      const titel = frage.id || `frage_${index + 1}`;
      const antwort = frage.antwort?.trim() || "";
      const confidence = typeof frage.confidence === "number"
        ? ` (Confidence: ${frage.confidence.toFixed(2)})`
        : "";
      const status = frage.abgedeckt === false ? " [nicht abgedeckt]" : "";
      return antwort ? `${titel}${confidence}${status}: ${antwort}` : status ? `${titel}${confidence}${status}` : "";
    })
    .filter(Boolean)
    .join("\n\n");
}

function formatValue(value: unknown): string {
  if (value == null) return "";
  if (typeof value === "string") return value;
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  if (Array.isArray(value)) return value.map((item) => formatValue(item)).join("; ");
  return JSON.stringify(value);
}

function prettifyKey(key: string): string {
  return key
    .replaceAll("_", " ")
    .replace(/([a-zäöüß])([A-Z])/g, "$1 $2")
    .replace(/^./, (char) => char.toUpperCase());
}

function appendSectionRows(
  rows: Array<Array<string>>,
  titel: string,
  data: Record<string, unknown> | undefined,
): void {
  rows.push([titel, "", ""]);
  if (!data) {
    rows.push(["", "Keine Daten", ""]);
    rows.push(["", "", ""]);
    return;
  }

  for (const [key, value] of Object.entries(data)) {
    rows.push(["", prettifyKey(key), formatValue(value)]);
  }
  rows.push(["", "", ""]);
}

interface UnterfrageLayout {
  bereich: "fragen" | "kriterium";
  key: string;
  frageId: string;
  kategorie: string;
  unterkategorie: string;
}

const UNTERFRAGE_LAYOUT: UnterfrageLayout[] = [
  {
    bereich: "fragen",
    key: "frage1",
    frageId: "frage1_1",
    kategorie: "ALLGEMEIN:K1",
    unterkategorie: "Organisationale Kernkompetenz: K1a",
  },
  {
    bereich: "fragen",
    key: "frage2",
    frageId: "frage2_1",
    kategorie: "ALLGEMEIN",
    unterkategorie: "Leistungsorientierung & Erfolgsmessung: K1B",
  },
  {
    bereich: "fragen",
    key: "frage3",
    frageId: "frage3_1",
    kategorie: "ALLGEMEIN",
    unterkategorie: "Geschäftsmodell-Transformation: K1C",
  },
  {
    bereich: "kriterium",
    key: "robustheit_resilienz",
    frageId: "robustheit_resilienz_1",
    kategorie: "RESILIENZ",
    unterkategorie: "Krisenmanagement & Reaktionsfähigkeit: K2A",
  },
  {
    bereich: "kriterium",
    key: "robustheit_resilienz",
    frageId: "robustheit_resilienz_2",
    kategorie: "RESILIENZ",
    unterkategorie: "Risikomanagement & Frühwarnsystem: K2B",
  },
  {
    bereich: "kriterium",
    key: "robustheit_resilienz",
    frageId: "robustheit_resilienz_3",
    kategorie: "RESILIENZ",
    unterkategorie: "Stakeholder-Management & Kommunikation: K3B",
  },
  {
    bereich: "kriterium",
    key: "robustheit_resilienz",
    frageId: "robustheit_resilienz_4",
    kategorie: "RESILIENZ",
    unterkategorie: "Finanzielle Stabilität & Liquiditätsmanagement: K4B",
  },
  {
    bereich: "kriterium",
    key: "robustheit_resilienz",
    frageId: "robustheit_resilienz_5",
    kategorie: "RESILIENZ",
    unterkategorie: "Regionale Vernetzung & Standortsicherung: K5B",
  },
  {
    bereich: "kriterium",
    key: "zukunftsfaehigkeit_innovation",
    frageId: "zukunftsfaehigkeit_innovation_1",
    kategorie: "INNOVATION & ZUKUNFTSFÄHIGKEIT",
    unterkategorie: "Kundenorientierte Innovationsziele: K1C",
  },
  {
    bereich: "kriterium",
    key: "zukunftsfaehigkeit_innovation",
    frageId: "zukunftsfaehigkeit_innovation_2",
    kategorie: "INNOVATION & ZUKUNFTSFÄHIGKEIT",
    unterkategorie: "Trendmonitoring & Experimentierkultur: K2C",
  },
  {
    bereich: "kriterium",
    key: "zukunftsfaehigkeit_innovation",
    frageId: "zukunftsfaehigkeit_innovation_3",
    kategorie: "INNOVATION & ZUKUNFTSFÄHIGKEIT",
    unterkategorie: "Produkt- & Prozessinnovation: K3C",
  },
  {
    bereich: "kriterium",
    key: "zukunftsfaehigkeit_innovation",
    frageId: "zukunftsfaehigkeit_innovation_4",
    kategorie: "INNOVATION & ZUKUNFTSFÄHIGKEIT",
    unterkategorie: "Innovationsnetzwerke & Partnerintegration: K4C",
  },
  {
    bereich: "kriterium",
    key: "zukunftsfaehigkeit_innovation",
    frageId: "zukunftsfaehigkeit_innovation_5",
    kategorie: "INNOVATION & ZUKUNFTSFÄHIGKEIT",
    unterkategorie: "Wissensmanagement & Wissensinfrastruktur: K5C",
  },
  {
    bereich: "kriterium",
    key: "nachhaltigkeit_verantwortung",
    frageId: "nachhaltigkeit_verantwortung_1",
    kategorie: "NACHHALTIGKEIT & VERANTWORTUNG",
    unterkategorie: "Ökologische Ressourceneffizienz: K6A",
  },
  {
    bereich: "kriterium",
    key: "nachhaltigkeit_verantwortung",
    frageId: "nachhaltigkeit_verantwortung_2",
    kategorie: "NACHHALTIGKEIT & VERANTWORTUNG",
    unterkategorie: "Regionale Lieferkette & Corporate Social Responsibility:K6B",
  },
  {
    bereich: "kriterium",
    key: "nachhaltigkeit_verantwortung",
    frageId: "nachhaltigkeit_verantwortung_3",
    kategorie: "NACHHALTIGKEIT & VERANTWORTUNG",
    unterkategorie: "Standortbindung & Regionalentwicklung: K6C",
  },
  {
    bereich: "kriterium",
    key: "attraktivitaet",
    frageId: "attraktivitaet_1",
    kategorie: "ARBEITGEBERATTRAKTIVITÄT",
    unterkategorie: "Mitarbeitermotivation & Durchhaltevermögen: K7A",
  },
  {
    bereich: "kriterium",
    key: "attraktivitaet",
    frageId: "attraktivitaet_2",
    kategorie: "ARBEITGEBERATTRAKTIVITÄT",
    unterkategorie: "Sinn & Purpose-Orientierung: K7B",
  },
  {
    bereich: "kriterium",
    key: "attraktivitaet",
    frageId: "attraktivitaet_3",
    kategorie: "ARBEITGEBERATTRAKTIVITÄT",
    unterkategorie: "Personalentwicklung & Weiterbildung: K7C",
  },
  {
    bereich: "kriterium",
    key: "attraktivitaet",
    frageId: "attraktivitaet_4",
    kategorie: "ARBEITGEBERATTRAKTIVITÄT",
    unterkategorie: "Employer Branding & Recruiting: K7D",
  },
  {
    bereich: "kriterium",
    key: "attraktivitaet",
    frageId: "attraktivitaet_5",
    kategorie: "ARBEITGEBERATTRAKTIVITÄT",
    unterkategorie: "Unternehmenskultur & Kulturmessung: K7E",
  },
];

function getAntwortEintrag(
  data: SummaryData,
  bereich: "fragen" | "kriterium",
  key: string,
): AntwortEintrag | undefined {
  return bereich === "fragen" ? data.fragen?.[key] : data.kriterium?.[key];
}

function buildStammdatenRows(data: SummaryData): string[][] {
  const kontakt = (data.kontakt as Record<string, unknown> | undefined) ?? {};
  const kontaktUnternehmen = (kontakt.unternehmen as Record<string, unknown> | undefined) ?? {};
  const ansprechpartner = (kontakt.ansprechpartner as Record<string, unknown> | undefined) ?? {};
  const unternehmensdaten = (data.unternehmen as Record<string, unknown> | undefined) ?? {};

  const rows: string[][] = [["Bereich", "Feld", "Wert"]];
  appendSectionRows(rows, "Kontakt", kontaktUnternehmen);
  appendSectionRows(rows, "Ansprechpartner", ansprechpartner);
  appendSectionRows(rows, "Unternehmensdaten", unternehmensdaten);
  return rows;
}

function buildKategorienRows(data: SummaryData): string[][] {
  const rows: string[][] = [["Label", "Fragen", "Originale Antwort", "KI - Zusammenfassung"]];

  for (const value of Object.values(data.fragen ?? {})) {
    rows.push([
      value?.label ?? "",
      (value?.fragen ?? []).map((frage) => frage.text).join("\n"),
      value?.antwort ?? "",
      value?.zusammenfassung ?? "",
    ]);
  }

  for (const value of Object.values(data.kriterium ?? {})) {
    rows.push([
      value?.label ?? "",
      (value?.fragen ?? []).map((frage) => frage.text).join("\n"),
      value?.antwort ?? "",
      value?.zusammenfassung ?? "",
    ]);
  }

  return rows;
}

function buildFragenUndAntwortenRows(data: SummaryData): string[][] {
  const rows: string[][] = [[
    "Kategorie",
    "Unterkategorie",
    "Frage",
    "Zugeordnete Orginale Antworten (Mehrfachfragen KI Segmentiert)",
  ]];

  for (const layout of UNTERFRAGE_LAYOUT) {
    const entry = getAntwortEintrag(data, layout.bereich, layout.key);
    const frage = entry?.fragen?.find((item) => item.id === layout.frageId);
    rows.push([
      layout.kategorie,
      layout.unterkategorie,
      frage?.text ?? "",
      frage?.antwort ?? "",
    ]);
  }

  return rows;
}

function buildAnalysenRows(data: SummaryData): string[][] {
  const ampel = data.ampelbewertung;
  return [
    ["Bereich", "Wert"],
    ["Ampelfarbe", ampel?.farbe ?? ""],
    ["Begruendung", ampel?.begruendung ?? ""],
    ["Ausschlussgruende", (ampel?.ausschlussgruende ?? []).join("; ")],
    ["Unbewertbare Kriterien", ampel?.unbewertbareKriterien == null ? "" : String(ampel.unbewertbareKriterien)],
    ["Fehlende Daten", (ampel?.missingDataWarnings ?? []).join("; ")],
    ["", ""],
    ["SWOT - Staerken", (data.swot_analyse?.staerken ?? []).join("; ")],
    ["SWOT - Schwaechen", (data.swot_analyse?.schwaechen ?? []).join("; ")],
    ["SWOT - Chancen", (data.swot_analyse?.chancen ?? []).join("; ")],
    ["SWOT - Risiken", (data.swot_analyse?.risiken ?? []).join("; ")],
  ];
}

function buildScoringRows(data: SummaryData): string[][] {
  const rows: string[][] = [[
    "Dimension",
    "Dimension Gewicht (%)",
    "Dimension Score",
    "Subkriterium",
    "Indikator",
    "Subkriterium Gewicht (%)",
    "Farbe",
    "Score",
    "Gewichteter Score",
    "Confidence",
    "Evidenz",
    "Begruendung",
    "Fehlende Daten",
  ]];

  for (const dimension of data.ampelbewertung?.dimensionen ?? []) {
    for (const subcriterion of dimension.subcriteria) {
      rows.push([
        dimension.name,
        String(dimension.weight),
        String(dimension.score),
        subcriterion.name,
        subcriterion.indikator,
        String(subcriterion.weight),
        subcriterion.farbe,
        subcriterion.score == null ? "" : String(subcriterion.score),
        String(subcriterion.weightedScore),
        String(subcriterion.confidence),
        subcriterion.evidence,
        subcriterion.begruendung,
        subcriterion.missingReason,
      ]);
    }
  }

  return rows;
}

function buildDisclaimerRows(): string[][] {
  return [
    [AI_DISCLAIMER_TITLE],
    [AI_DISCLAIMER_TEXT],
  ];
}

async function writeTemplateWorkbook(data: SummaryData, xlsxPath: string): Promise<boolean> {
  if (!existsSync(XLSX_TEMPLATE_PATH)) {
    return false;
  }

  const ExcelJS = await import("exceljs");
  const workbook = new ExcelJS.Workbook();
  await workbook.xlsx.readFile(XLSX_TEMPLATE_PATH);

  const populateWorksheet = (sheetName: string, rows: string[][]) => {
    const worksheet = workbook.getWorksheet(sheetName) ?? workbook.addWorksheet(sheetName);

    const templateRowCount = worksheet.rowCount;
    const templateColumnCount = worksheet.columnCount;
    const rowCount = Math.max(templateRowCount, rows.length);
    const colCount = Math.max(
      templateColumnCount,
      rows.reduce((max, row) => Math.max(max, row.length), 0),
    );

    for (let rowIndex = 1; rowIndex <= rowCount; rowIndex += 1) {
      if (rowIndex > templateRowCount && templateRowCount > 0) {
        const sourceRow = worksheet.getRow(templateRowCount);
        const targetRow = worksheet.getRow(rowIndex);
        if (sourceRow.height != null) {
          targetRow.height = sourceRow.height;
        }
      }

      for (let colIndex = 1; colIndex <= colCount; colIndex += 1) {
        const cell = worksheet.getCell(rowIndex, colIndex);

        if ((rowIndex > templateRowCount || colIndex > templateColumnCount) && templateRowCount > 0 && templateColumnCount > 0) {
          const sourceCell = worksheet.getCell(
            Math.min(rowIndex, templateRowCount),
            Math.min(colIndex, templateColumnCount),
          );
          cell.style = structuredClone(sourceCell.style);
        }

        cell.value = rows[rowIndex - 1]?.[colIndex - 1] ?? "";
      }
    }
  };

  populateWorksheet("Stammdaten", buildStammdatenRows(data));
  populateWorksheet("Kategorien", buildKategorienRows(data));
  populateWorksheet("Fragen und Antworten", buildFragenUndAntwortenRows(data));
  populateWorksheet("Analysen", buildAnalysenRows(data));
  populateWorksheet("Scoring", buildScoringRows(data));
  populateWorksheet("Disclaimer", buildDisclaimerRows());

  await workbook.xlsx.writeFile(xlsxPath);
  return true;
}

function createWorkbook(data: SummaryData): XLSX.WorkBook {
  const workbook = XLSX.utils.book_new();

  const stammdatenSheet = XLSX.utils.aoa_to_sheet(buildStammdatenRows(data));
  stammdatenSheet["!cols"] = [{ wch: 22 }, { wch: 28 }, { wch: 177.33 }];
  XLSX.utils.book_append_sheet(workbook, stammdatenSheet, "Stammdaten");

  const kategorienSheet = XLSX.utils.aoa_to_sheet(buildKategorienRows(data));
  kategorienSheet["!cols"] = [{ wch: 25.93 }, { wch: 255.29 }, { wch: 255.29 }, { wch: 255.29 }];
  XLSX.utils.book_append_sheet(workbook, kategorienSheet, "Kategorien");

  const fragenSheet = XLSX.utils.aoa_to_sheet(buildFragenUndAntwortenRows(data));
  fragenSheet["!cols"] = [{ wch: 28 }, { wch: 28 }, { wch: 255.29 }, { wch: 255.29 }];
  XLSX.utils.book_append_sheet(workbook, fragenSheet, "Fragen und Antworten");

  const analysenSheet = XLSX.utils.aoa_to_sheet(buildAnalysenRows(data));
  analysenSheet["!cols"] = [{ wch: 24 }, { wch: 254.83 }];
  XLSX.utils.book_append_sheet(workbook, analysenSheet, "Analysen");

  const scoringSheet = XLSX.utils.aoa_to_sheet(buildScoringRows(data));
  scoringSheet["!cols"] = [
    { wch: 24 },
    { wch: 18 },
    { wch: 16 },
    { wch: 30 },
    { wch: 30 },
    { wch: 18 },
    { wch: 14 },
    { wch: 12 },
    { wch: 18 },
    { wch: 12 },
    { wch: 80 },
    { wch: 80 },
    { wch: 50 },
  ];
  XLSX.utils.book_append_sheet(workbook, scoringSheet, "Scoring");

  const disclaimerSheet = XLSX.utils.aoa_to_sheet(buildDisclaimerRows());
  disclaimerSheet["!cols"] = [{ wch: 140 }];
  XLSX.utils.book_append_sheet(workbook, disclaimerSheet, "Disclaimer");

  return workbook;
}

function buildScoringFallbackBegruendung(
  farbe: Ampelfarbe,
  scoring: ReturnType<typeof calculateScoringResult>,
  ausschlussgruende: string[],
): string {
  if (ausschlussgruende.length) {
    return `Rot wegen automatischem Ausschluss: ${ausschlussgruende.join("; ")}.`;
  }

  const strongestDimensions = scoring.dimensionen
    .toSorted((a, b) => b.score - a.score)
    .slice(0, 2)
    .map((dimension) => `${dimension.name} (${dimension.score})`)
    .join(", ");
  const missing = scoring.unbewertbareKriterien
    ? ` ${scoring.unbewertbareKriterien} Subkriterien waren mangels belastbarer Angaben unbewertbar.`
    : "";

  const meaning = farbe === "gruen"
    ? "fuer den weiteren Vergleich geeignet"
    : "mit begruendeten Zweifeln fuer die weitere Betrachtung";
  return `${farbe} (${meaning}). Staerkste Bereiche: ${strongestDimensions || "keine"}.${missing}`;
}

async function assessTrafficLight(
  client: OpenAI,
  model: string,
  data: LlmDossier,
  sessionId?: string,
  onLlmCall?: SummarizeCompanyOptions["onLlmCall"],
): Promise<Ampelbewertung> {
  const scoringInstructions = {
    ausschlussgruende: [
      "Inkubator / Grosskonzern-Tochter: Nicht Inhaber-gefuehrt bzw. staatlich",
      "Stiftung als Bewerber: Stiftungen duerfen sich nicht bewerben",
      "Zu kleines Unternehmen: Arbeitsbedingungen nicht Bayern-praegend bei sehr kleinen Betrieben",
    ],
    scoringModel: SCORING_MODEL,
  };

  const completion = await createStructuredCompletion(
    client,
    model,
    [
      {
        role: "system",
        content:
          "Du bewertest Bewerbungen fuer einen Unternehmenspreis anhand eines gewichteten Kriterienmodells. Antworte ausschliesslich auf Deutsch und ausschliesslich als JSON.",
      },
      {
        role: "user",
        content: `Bewerte die folgende Bewerbung nach dem bereitgestellten Ampel- und Gewichtungsmodell.

Modell:
${JSON.stringify(scoringInstructions, null, 2)}

Vorgehen:
- Pruefe zuerst die automatischen Ausschlussgruende. Nenne nur Ausschlussgruende, die aus den Daten belastbar erkennbar sind.
- Bewerte danach jedes Subkriterium des scoringModel mit genau einer Farbe: gruen, gelb, rot oder unbewertbar.
- Verwende unbewertbar, wenn die Daten fuer ein Subkriterium fehlen oder zu uneindeutig sind. Erfinde keine Kennzahlen.
- Nutze die Schwellenwerte als Bewertungsrahmen. Wenn qualitative Angaben klar einem Schwellenwert entsprechen, darfst du qualitativ bewerten.
- Wichtig: rot auf Ebene der Gesamtbewertung bedeutet ausschliesslich "Bewerbung verwerfen" und wird nachgelagert nur bei automatischen Ausschlussgruenden vergeben. Niedrige oder fehlende Scores ohne Ausschlussgrund fuehren zu gelb, nicht rot.
- evidence muss eine kurze, konkrete Fundstelle oder Zusammenfassung aus den Bewerbungsdaten sein. Bei unbewertbar bleibt evidence leer.
- confidence muss zwischen 0 und 1 liegen.
- missingReason ist bei unbewertbar Pflicht, sonst leer.
- Die Gesamtfarbe und Gewichtung werden nachgelagert berechnet; liefere nur die Faktorbewertungen und eine kurze Gesamtbegruendung.

Wichtige Vorgaben:
- Antworte ausschliesslich als JSON.
- Verwende genau die Felder begruendung, ausschlussgruende und dimensionen.
- Gib jede Dimension und jedes Subkriterium aus dem Modell mit der jeweiligen id zurueck.
- Die begruendung soll kurz, konkret und auf Deutsch sein.
- Pruefe nur auf Basis der vorliegenden Daten.

Bewerbungsdaten:
${JSON.stringify(data, null, 2)}`,
      },
    ],
    "ampelbewertung",
    {
      type: "object",
      additionalProperties: false,
      properties: {
        begruendung: { type: "string" },
        ausschlussgruende: {
          type: "array",
          items: { type: "string" },
        },
        dimensionen: {
          type: "array",
          items: {
            type: "object",
            additionalProperties: false,
            properties: {
              id: { type: "string" },
              subcriteria: {
                type: "array",
                items: {
                  type: "object",
                  additionalProperties: false,
                  properties: {
                    id: { type: "string" },
                    farbe: { type: "string", enum: ["gruen", "gelb", "rot", "unbewertbar"] },
                    evidence: { type: "string" },
                    begruendung: { type: "string" },
                    confidence: { type: "number" },
                    missingReason: { type: "string" },
                  },
                  required: ["id", "farbe", "evidence", "begruendung", "confidence", "missingReason"],
                },
              },
            },
            required: ["id", "subcriteria"],
          },
        },
      },
      required: ["begruendung", "ausschlussgruende", "dimensionen"],
    },
    sessionId,
    { operation: "scoring", label: "Ampelbewertung", onLlmCall },
  );

  const msg = completion.choices[0]?.message as Record<string, unknown> | undefined;
  const content = (msg?.content ?? msg?.reasoning) as string | null | undefined;

  if (!content?.trim()) {
    throw new Error(
      `Model returned no content for traffic light assessment (finish_reason: ${completion.choices[0]?.finish_reason}).`,
    );
  }

  const parsed = JSON.parse(extractJsonObject(content)) as Partial<LlmScoringAssessment> & {
    begruendung?: string;
  };
  const ausschlussgruende = Array.isArray(parsed.ausschlussgruende)
    ? parsed.ausschlussgruende.map((flag) => String(flag).trim()).filter(Boolean)
    : [];
  const scoring = calculateScoringResult(SCORING_MODEL, {
    ausschlussgruende,
    dimensionen: Array.isArray(parsed.dimensionen) ? parsed.dimensionen : [],
  });
  const farbe = deriveTrafficLight(scoring, ausschlussgruende);

  return {
    farbe,
    gesamtScore: scoring.gesamtScore,
    begruendung: String(parsed.begruendung ?? "").trim() || buildScoringFallbackBegruendung(farbe, scoring, ausschlussgruende),
    ausschlussgruende,
    unbewertbareKriterien: scoring.unbewertbareKriterien,
    missingDataWarnings: scoring.missingDataWarnings,
    dimensionen: scoring.dimensionen,
  };
}

async function segmentMultiQuestionAnswer(
  client: OpenAI,
  model: string,
  entry: AntwortEintrag,
  fallbackLabel: string,
  sessionId?: string,
  onLlmCall?: SummarizeCompanyOptions["onLlmCall"],
): Promise<FrageMitAntwort[]> {
  const fragen = entry.fragen ?? [];
  if (!fragen.length || entry.antwortFormat !== "mehrfachfrage_ein_antwortfeld") {
    return fragen;
  }

  const completion = await createStructuredCompletion(
    client,
    model,
    [
      {
        role: "system",
        content:
          "Du ordnest eine gemeinsame Freitextantwort mehreren Unterfragen zu. Antworte ausschließlich auf Deutsch und ausschließlich als JSON mit dem Feld fragen. fragen muss ein Array mit Objekten der Form { id, text, antwort, confidence } sein. confidence muss zwischen 0 und 1 liegen.",
      },
      {
        role: "user",
        content: `Ordne die folgende gemeinsame Antwort den Unterfragen zu.

Bereich: ${entry.label?.trim() || fallbackLabel}

Unterfragen:
${fragen.map((frage, index) => `${index + 1}. [${frage.id ?? `frage_${index + 1}`}] ${frage.text}`).join("\n")}

Gemeinsame Antwort:
${entry.antwort ?? ""}

Vorgaben:
- Antworte ausschließlich als JSON mit dem Feld fragen.
- Gib für jede Unterfrage genau ein Objekt mit id, text, antwort und confidence zurück.
- antwort soll möglichst aus wörtlichen oder sehr nahen Textpassagen der Gesamtantwort bestehen.
- Füge keine Auslassungspunkte wie "..." oder „…“ ein, außer sie stehen exakt so im Original.
- confidence muss zwischen 0 und 1 liegen und angeben, wie sicher die Zuordnung ist.
- Wenn die Antwort eine Unterfrage nicht belastbar abdeckt, setze antwort auf einen leeren String und confidence auf 0.
- Erfinde nichts und paraphrasiere nicht unnötig. Bleibe möglichst nah am Originalinhalt.
- Mehrfachnennungen sind erlaubt, wenn ein Abschnitt mehrere Unterfragen klar beantwortet.
- Lasse keine Unterfrage aus.`,
      },
    ],
    "segmentierung_unterfragen",
    {
      type: "object",
      additionalProperties: false,
      properties: {
        fragen: {
          type: "array",
          items: {
            type: "object",
            additionalProperties: false,
            properties: {
              id: { type: "string" },
              text: { type: "string" },
              antwort: { type: "string" },
              confidence: { type: "number" },
            },
            required: ["id", "text", "antwort", "confidence"],
          },
        },
      },
      required: ["fragen"],
    },
    sessionId,
    { operation: "segmentierung", label: fallbackLabel, onLlmCall },
  );

  const msg = completion.choices[0]?.message as Record<string, unknown> | undefined;
  const content = (msg?.content ?? msg?.reasoning) as string | null | undefined;

  if (!content?.trim()) {
    throw new Error(
      `Model returned no content for answer segmentation (finish_reason: ${completion.choices[0]?.finish_reason}).`,
    );
  }

  const parsed = JSON.parse(extractJsonObject(content)) as { fragen?: Array<Partial<FrageMitAntwort>> };
  const byId = new Map((parsed.fragen ?? []).map((frage) => [String(frage.id ?? ""), frage]));

  return fragen.map((frage) => {
    const mapped = byId.get(String(frage.id ?? ""));
    const rawConfidence = mapped?.confidence;
    const confidence =
      typeof rawConfidence === "number"
        ? Math.max(0, Math.min(1, rawConfidence))
        : Number.isFinite(Number(rawConfidence))
          ? Math.max(0, Math.min(1, Number(rawConfidence)))
          : undefined;
    const antwort = String(mapped?.antwort ?? "").trim();

    return {
      id: frage.id,
      text: frage.text,
      antwort,
      confidence,
      abgedeckt: Boolean(antwort),
      quelle: "llm_segmentierung",
    };
  });
}

async function analyzeSWOT(
  client: OpenAI,
  model: string,
  data: LlmDossier,
  sessionId?: string,
  onLlmCall?: SummarizeCompanyOptions["onLlmCall"],
): Promise<SWOTAnalyse> {
  const completion = await createStructuredCompletion(
    client,
    model,
    [
      {
        role: "system",
        content:
          "Du analysierst Unternehmensbewerbungen und erstellst eine knappe SWOT-Analyse. Antworte ausschließlich auf Deutsch und ausschließlich als JSON mit den Feldern staerken, schwaechen, chancen und risiken. Jedes Feld muss ein Array aus kurzen Stichpunkten sein.",
      },
      {
        role: "user",
        content: `Erstelle auf Basis der folgenden Bewerbungsdaten eine SWOT-Analyse.

Vorgaben:
- Antworte ausschließlich als JSON.
- Verwende genau die Felder staerken, schwaechen, chancen und risiken.
- Jedes Feld muss ein Array aus kurzen, konkreten deutschen Stichpunkten sein.
- Nenne nur Punkte, die aus den vorliegenden Daten klar ableitbar sind.
- Erfinde nichts und ergänze keine externen Annahmen.
- Vermeide schwache oder spekulative Schwächen und Risiken.
- Wenn zu einem Bereich wenig belastbare Hinweise vorliegen, liefere lieber wenige Punkte als vage Aussagen.
- Die Stichpunkte sollen prägnant und entscheidungsrelevant sein.

Bewerbungsdaten:
${JSON.stringify(data, null, 2)}`,
      },
    ],
    "swot_analyse",
    {
      type: "object",
      additionalProperties: false,
      properties: {
        staerken: { type: "array", items: { type: "string" } },
        schwaechen: { type: "array", items: { type: "string" } },
        chancen: { type: "array", items: { type: "string" } },
        risiken: { type: "array", items: { type: "string" } },
      },
      required: ["staerken", "schwaechen", "chancen", "risiken"],
    },
    sessionId,
    { operation: "swot", label: "SWOT-Analyse", onLlmCall },
  );

  const msg = completion.choices[0]?.message as Record<string, unknown> | undefined;
  const content = (msg?.content ?? msg?.reasoning) as string | null | undefined;

  if (!content?.trim()) {
    throw new Error(
      `Model returned no content for SWOT analysis (finish_reason: ${completion.choices[0]?.finish_reason}).`,
    );
  }

  const parsed = JSON.parse(extractJsonObject(content)) as Partial<SWOTAnalyse>;
  const toStringArray = (value: unknown): string[] =>
    Array.isArray(value) ? value.map((item) => String(item).trim()).filter(Boolean) : [];

  return {
    staerken: toStringArray(parsed.staerken),
    schwaechen: toStringArray(parsed.schwaechen),
    chancen: toStringArray(parsed.chancen),
    risiken: toStringArray(parsed.risiken),
  };
}

export async function listCompanies(): Promise<string[]> {
  let entries: string[];
  try {
    entries = await readdir(OUTPUTS_DIR);
  } catch {
    return [];
  }

  const companies: string[] = [];
  for (const entry of entries) {
    if (!isSafeStem(entry)) continue;
    const jsonPath = join(OUTPUTS_DIR, entry, `${entry}.json`);
    if (await Bun.file(jsonPath).exists()) {
      companies.push(entry);
    }
  }
  return companies;
}

export async function summarizeCompany(
  stem: string,
  model: string,
  options: SummarizeCompanyOptions = {},
): Promise<SummaryData> {
  if (!isSafeStem(stem)) {
    throw new Error(`Invalid company id "${stem}"`);
  }
  const jsonPath = join(OUTPUTS_DIR, stem, `${stem}.json`);
  const file = Bun.file(jsonPath);
  if (!(await file.exists())) {
    throw new Error(`No extracted JSON found for "${stem}"`);
  }

  const data: ExtractedData = await file.json();
  const client = createClient();
  const output: SummaryData = structuredClone(data);
  const privacy: Pseudonymizer = createPseudonymizer(data);
  const sessionId = `summarizer-${stem}-${crypto.randomUUID()}`;

  // Summarize fragen answers
  if (data.fragen) {
    output.fragen = {};
    for (const [key, value] of Object.entries(data.fragen)) {
      const antwort = value?.antwort ?? "";
      const safeValue = privacy.pseudonymizeEntry(value);
      const safeAntwort = safeValue?.antwort ?? "";
      const enrichedFragen =
        antwort.trim() && value?.antwortFormat === "mehrfachfrage_ein_antwortfeld"
          ? await segmentMultiQuestionAnswer(client, model, safeValue, `Frage ${key}`, sessionId, options.onLlmCall)
          : safeValue?.fragen;
      const zusammenfassung = safeAntwort.trim()
        ? await summarizeText(
          client,
          model,
          safeAntwort,
          buildQuestionContext({ ...safeValue, fragen: enrichedFragen }, `Frage ${key}`),
          sessionId,
          { label: `Frage ${key}`, onLlmCall: options.onLlmCall },
        )
        : "";
      const segmentierung = summarizeSegmentierungsQualitaet(enrichedFragen);
      output.fragen[key] = {
        ...value,
        fragen: enrichedFragen,
        zuordnungsmodus:
          value?.antwortFormat === "mehrfachfrage_ein_antwortfeld"
            ? segmentierung?.niedriges_vertrauen
              ? "gemeinsame_antwort"
              : "llm_segmentiert"
            : value?.zuordnungsmodus,
        antwort,
        zusammenfassung,
        segmentierung,
      };
    }
  }

  // Summarize kriterium answers
  if (data.kriterium) {
    output.kriterium = {};
    const kriteriumLabels: Record<string, string> = {
      robustheit_resilienz: "Robustheit & Resilienz",
      zukunftsfaehigkeit_innovation: "Zukunftsfähigkeit & Innovation",
      nachhaltigkeit_verantwortung: "Nachhaltigkeit & Verantwortung",
      attraktivitaet: "Attraktivität",
    };
    for (const [key, value] of Object.entries(data.kriterium)) {
      const antwort = value?.antwort ?? "";
      const label = value?.label?.trim() || kriteriumLabels[key] || key;
      const safeValue = privacy.pseudonymizeEntry(value);
      const safeAntwort = safeValue?.antwort ?? "";
      const safeLabel = safeValue?.label?.trim() || label;
      const enrichedFragen =
        antwort.trim() && value?.antwortFormat === "mehrfachfrage_ein_antwortfeld"
          ? await segmentMultiQuestionAnswer(client, model, safeValue, safeLabel, sessionId, options.onLlmCall)
          : safeValue?.fragen;
      const zusammenfassung = safeAntwort.trim()
        ? await summarizeText(
          client,
          model,
          safeAntwort,
          buildQuestionContext({ ...safeValue, fragen: enrichedFragen }, safeLabel),
          sessionId,
          { label: safeLabel, onLlmCall: options.onLlmCall },
        )
        : "";
      const segmentierung = summarizeSegmentierungsQualitaet(enrichedFragen);
      output.kriterium[key] = {
        ...value,
        fragen: enrichedFragen,
        zuordnungsmodus:
          value?.antwortFormat === "mehrfachfrage_ein_antwortfeld"
            ? segmentierung?.niedriges_vertrauen
              ? "gemeinsame_antwort"
              : "llm_segmentiert"
            : value?.zuordnungsmodus,
        antwort,
        zusammenfassung,
        segmentierung,
      };
    }
  }

  const preparedScoringInput = privacy.pseudonymizeDossier(buildLlmDossier(output));
  output.ampelbewertung = await assessTrafficLight(client, model, preparedScoringInput.safe, sessionId, options.onLlmCall);

  const preparedSwotInput = privacy.pseudonymizeDossier(buildLlmDossier(output));
  output.swot_analyse = await analyzeSWOT(client, model, preparedSwotInput.safe, sessionId, options.onLlmCall);

  // Reverse pseudonyms in LLM-generated content
  const reverse = privacy.reversePseudonyms;
  for (const [key, frage] of Object.entries(output.fragen ?? {})) {
    if (frage.zusammenfassung) {
      frage.zusammenfassung = reverse(frage.zusammenfassung);
    }
    if (frage.fragen) {
      for (const f of frage.fragen) {
        if (f.antwort) {
          f.antwort = reverse(f.antwort);
        }
        if (f.text) {
          f.text = reverse(f.text);
        }
      }
    }
  }
  for (const [key, kriterium] of Object.entries(output.kriterium ?? {})) {
    if (kriterium.zusammenfassung) {
      kriterium.zusammenfassung = reverse(kriterium.zusammenfassung);
    }
    if (kriterium.fragen) {
      for (const f of kriterium.fragen) {
        if (f.antwort) {
          f.antwort = reverse(f.antwort);
        }
        if (f.text) {
          f.text = reverse(f.text);
        }
      }
    }
  }
  const ampel = output.ampelbewertung;
  if (ampel) {
    ampel.begruendung = reverse(ampel.begruendung);
    ampel.ausschlussgruende = (ampel.ausschlussgruende ?? []).map(reverse);
    for (const dimension of ampel.dimensionen ?? []) {
      for (const sub of dimension.subcriteria) {
        sub.evidence = reverse(sub.evidence);
        sub.begruendung = reverse(sub.begruendung);
        sub.missingReason = reverse(sub.missingReason);
      }
    }
  }
  const swot = output.swot_analyse;
  if (swot) {
    swot.staerken = swot.staerken.map(reverse);
    swot.schwaechen = swot.schwaechen.map(reverse);
    swot.chancen = swot.chancen.map(reverse);
    swot.risiken = swot.risiken.map(reverse);
  }

  output._privacy = privacy.audit();
  output._schemaVersion = 3;
  output._summarizedAt = new Date().toISOString();
  return output;
}

export async function writeSummary(
  stem: string,
  data: SummaryData,
  outputsDir: string,
): Promise<string[]> {
  if (!isSafeStem(stem)) {
    throw new Error(`Invalid company id "${stem}"`);
  }
  const { mkdir } = await import("node:fs/promises");
  const outDir = join(outputsDir, stem);
  await mkdir(outDir, { recursive: true });
  const outputData = structuredClone(data) as SummaryData;
  if (outputData.ampelbewertung) {
    delete outputData.ampelbewertung.gesamtScore;
  }

  const jsonPath = join(outDir, `${stem}.json`);
  await Bun.write(jsonPath, JSON.stringify(outputData, null, 2));

  const xlsxPath = join(outDir, `${stem}.xlsx`);
  if (!(await writeTemplateWorkbook(outputData, xlsxPath))) {
    const workbook = createWorkbook(outputData);
    const xlsxBuffer = XLSX.write(workbook, {
      bookType: "xlsx",
      type: "buffer",
      cellStyles: true,
    });
    await Bun.write(xlsxPath, xlsxBuffer);
  }

  const reportPaths = await writeJuryReport(stem, outputData, outDir);

  return [jsonPath, xlsxPath, ...reportPaths];
}
