import { nanoid } from "nanoid";
import type {
  AssistantConversationItem,
  AssistantDebugPayload,
  FollowupStateUsageDebug,
  AssistantMessageRequestPayload,
  AssistantMessageResponsePayload,
  AssistantRequirement,
  AnswerGroundingCheck,
  RequirementCoverageReport,
  UnifiedRetrievalResult,
  AssistantSessionState
} from "../types/assistant";
import type { NoRouteReason, NormalizeRequestPayload, RouteDecisionV2, RouteHint, RouteHintSummary } from "../types/normalizer";
import type { AnswerStructureV11, InvestigationLastAnswerMode } from "../types/stage1Contracts";
import { ANSWER_STRUCTURE_SCHEMA_VERSION } from "../types/stage1Contracts";
import {
  FEATURE_ASSISTANT_ANSWER_POLICY_V11,
  FEATURE_ASSISTANT_CONTRACTS_V11,
  FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1,
  FEATURE_ASSISTANT_INVESTIGATION_STATE_V1,
  FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1
} from "../config";
import { logJson } from "../utils/log";
import { composeAssistantAnswer } from "./answerComposer";
import { AssistantDataLayer } from "./assistantDataLayer";
import { NormalizerService } from "./normalizerService";
import { AssistantSessionLogger } from "./assistantSessionLogger";
import { AssistantSessionStore } from "./assistantSessionStore";
import { updateInvestigationState } from "./investigationState";
import { normalizeRetrievalResult } from "./retrievalResultNormalizer";

function retrievalSummaryForRoute(route: string): string {
  if (route === "store_canonical") return "Canonical accounting data path selected.";
  if (route === "store_feature_risk") return "Risk/control profile path selected.";
  if (route === "hybrid_store_plus_live") return "Hybrid chain analysis path selected.";
  if (route === "live_mcp_drilldown") return "Live drilldown path selected.";
  if (route === "batch_refresh_then_store") return "Heavy analytical batch path selected.";
  return "Route selected.";
}

function mapNoRouteReason(reason: NoRouteReason | null | undefined): string {
  if (reason === "out_of_scope") return "Fragment out of scope.";
  if (reason === "insufficient_specificity") return "Needs clarification.";
  if (reason === "missing_mapping") return "Route mapping is missing.";
  if (reason === "unsupported_fragment_type") return "Fragment type unsupported.";
  return "No-route decision.";
}

function extractFragments(normalized: unknown): unknown[] {
  if (!normalized || typeof normalized !== "object") {
    return [];
  }
  const source = normalized as { fragments?: unknown[] };
  return Array.isArray(source.fragments) ? source.fragments : [];
}

function extractExecutionState(normalized: unknown): Array<Record<string, unknown>> {
  const fragments = extractFragments(normalized);
  return fragments.map((item) => {
    if (!item || typeof item !== "object") {
      return {};
    }
    const fragment = item as Record<string, unknown>;
    return {
      fragment_id: fragment.fragment_id ?? null,
      execution_readiness: fragment.execution_readiness ?? null,
      route_status: fragment.route_status ?? null,
      no_route_reason: fragment.no_route_reason ?? null
    };
  });
}

function fragmentTextById(normalized: unknown): Map<string, string> {
  const result = new Map<string, string>();
  for (const item of extractFragments(normalized)) {
    if (!item || typeof item !== "object") {
      continue;
    }
    const fragment = item as Record<string, unknown>;
    const fragmentId = typeof fragment.fragment_id === "string" ? fragment.fragment_id : "";
    if (!fragmentId) {
      continue;
    }
    const text =
      (typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
      (typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
      "";
    result.set(fragmentId, text);
  }
  return result;
}

function extractDiscardedIntentSegments(normalized: unknown): string[] {
  if (!normalized || typeof normalized !== "object") {
    return [];
  }
  const source = normalized as { discarded_fragments?: unknown[] };
  if (!Array.isArray(source.discarded_fragments)) {
    return [];
  }
  return source.discarded_fragments
    .map((item) => {
      if (!item || typeof item !== "object") {
        return null;
      }
      const value = item as Record<string, unknown>;
      const text = typeof value.raw_fragment_text === "string" ? value.raw_fragment_text.trim() : "";
      return text || null;
    })
    .filter((item): item is string => Boolean(item));
}

function collectDateSpans(text: string): Array<{ start: number; end: number }> {
  const spans: Array<{ start: number; end: number }> = [];
  const datePattern = /\b20\d{2}[-/.](?:0[1-9]|1[0-2])(?:[-/.](?:0[1-9]|[12]\d|3[01]))?\b/g;
  let match: RegExpExecArray | null = null;
  while ((match = datePattern.exec(text)) !== null) {
    spans.push({
      start: match.index,
      end: match.index + match[0].length
    });
  }
  return spans;
}

function intersectsAnySpan(start: number, end: number, spans: Array<{ start: number; end: number }>): boolean {
  return spans.some((span) => start < span.end && end > span.start);
}

function extractAccountTokens(text: string): string[] {
  const lower = String(text ?? "").toLowerCase();
  const explicitAccounts = new Set<string>();
  const contextualPattern =
    /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu;
  let contextual: RegExpExecArray | null = null;
  while ((contextual = contextualPattern.exec(lower)) !== null) {
    if (contextual[1]) {
      explicitAccounts.add(contextual[1]);
    }
  }
  if (explicitAccounts.size > 0) {
    return Array.from(explicitAccounts);
  }

  const spans = collectDateSpans(lower);
  const accountList: string[] = [];
  const genericPattern = /\b\d{2}(?:\.\d{2})?\b/g;
  let generic: RegExpExecArray | null = null;
  while ((generic = genericPattern.exec(lower)) !== null) {
    const value = generic[0];
    const start = generic.index;
    const end = start + value.length;
    if (intersectsAnySpan(start, end, spans)) {
      continue;
    }
    accountList.push(value);
  }
  return Array.from(new Set(accountList));
}

function extractSubjectTokens(text: string): string[] {
  const lower = text.toLowerCase();
  const tokens: string[] = [];
  const push = (token: string, match: boolean): void => {
    if (match) tokens.push(token);
  };

  push("nds", /\b(?:\u043d\u0434\u0441|vat)\b/iu.test(lower));
  push("os", /(?:\b\u043e\u0441\b|\u043e\u0441\u043d\u043e\u0432\u043d(?:\u044b\u0435|\u044b\u0445)\s+\u0441\u0440\u0435\u0434|osnovn(?:ye|yh)\s+sred)/iu.test(lower));
  push(
    "counterparty",
    /(?:\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|supplier|buyer|counterparty|kontragent|postavshch|pokupatel)/iu.test(
      lower
    )
  );
  push(
    "document",
    /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|\u0432\u044b\u043f\u0438\u0441\u043a|\u043f\u043b\u0430\u0442\u0435\u0436|document|invoice|posting|dokument|realiz|postuplen|vypisk|platezh)/iu.test(
      lower
    )
  );
  push("saldo", /(?:\u0441\u0430\u043b\u044c\u0434\u043e|\u043e\u0441\u0442\u0430\u0442\u043a|saldo|balance)/iu.test(lower));
  push("anomaly", /(?:\u0430\u043d\u043e\u043c\u0430\u043b|\u0440\u0438\u0441\u043a|\u043f\u043e\u0434\u043e\u0437\u0440|\u0445\u0432\u043e\u0441\u0442|anomaly|risk|anomali|hvost|tail)/iu.test(lower));
  push("chain", /(?:\u0446\u0435\u043f\u043e\u0447|\u0440\u0430\u0437\u043b\u043e\u0436|\u0441\u0432\u044f\u0437\u043a|\u0440\u0430\u0437\u0440\u044b\u0432|chain|razlozh|svyaz|razryv)/iu.test(lower));

  const accountMatches = extractAccountTokens(lower);
  for (const account of accountMatches) {
    tokens.push(`account_${account}`);
  }

  return Array.from(new Set(tokens));
}

interface RequirementExtractionResult {
  requirements: AssistantRequirement[];
  byFragment: Map<string, string[]>;
}

function extractRequirements(routeSummary: RouteHintSummary | null, normalized: unknown, userMessage: string): RequirementExtractionResult {
  const fragmentText = fragmentTextById(normalized);
  const byFragment = new Map<string, string[]>();
  const requirements: AssistantRequirement[] = [];

  const pushRequirement = (input: {
    requirement_id: string;
    source_fragment_id: string | null;
    requirement_text: string;
    status: AssistantRequirement["status"];
    route: string | null;
  }): void => {
    const subjectTokens = extractSubjectTokens(input.requirement_text);
    requirements.push({
      requirement_id: input.requirement_id,
      source_fragment_id: input.source_fragment_id,
      requirement_text: input.requirement_text,
      subject_tokens: subjectTokens,
      status: input.status,
      route: input.route
    });
    if (input.source_fragment_id) {
      const current = byFragment.get(input.source_fragment_id) ?? [];
      current.push(input.requirement_id);
      byFragment.set(input.source_fragment_id, current);
    }
  };

  if (!routeSummary) {
    pushRequirement({
      requirement_id: "R1",
      source_fragment_id: null,
      requirement_text: userMessage,
      status: "clarification_needed",
      route: null
    });
    return { requirements, byFragment };
  }

  if (routeSummary.mode === "legacy_v1") {
    pushRequirement({
      requirement_id: "R1",
      source_fragment_id: "F1",
      requirement_text: userMessage,
      status: "covered",
      route: routeSummary.route_hint
    });
    return { requirements, byFragment };
  }

  routeSummary.decisions.forEach((decision, index) => {
    const requirementId = `R${index + 1}`;
    const text = fragmentText.get(decision.fragment_id) ?? userMessage;
    let status: AssistantRequirement["status"] = "covered";
    if (decision.route === "no_route") {
      if (decision.no_route_reason === "out_of_scope") {
        status = "out_of_scope";
      } else if (decision.no_route_reason === "insufficient_specificity") {
        status = "clarification_needed";
      } else {
        status = "uncovered";
      }
    }

    pushRequirement({
      requirement_id: requirementId,
      source_fragment_id: decision.fragment_id,
      requirement_text: text,
      status,
      route: decision.route === "no_route" ? null : decision.route
    });
  });

  return { requirements, byFragment };
}

interface ExecutionPlanItem {
  fragment_id: string;
  requirement_ids: string[];
  route: string;
  should_execute: boolean;
  fragment_text: string;
  no_route_reason: NoRouteReason | null;
  clarification_reason: string | null;
}

function toExecutionPlan(
  routeSummary: RouteHintSummary | null,
  normalized: unknown,
  userMessage: string,
  requirementByFragment: Map<string, string[]>
): ExecutionPlanItem[] {
  if (!routeSummary) {
    return [];
  }
  const fragmentText = fragmentTextById(normalized);

  if (routeSummary.mode === "legacy_v1") {
    return [
      {
        fragment_id: "F1",
        requirement_ids: requirementByFragment.get("F1") ?? ["R1"],
        route: routeSummary.route_hint,
        should_execute: true,
        fragment_text: userMessage,
        no_route_reason: null,
        clarification_reason: null
      }
    ];
  }

  return routeSummary.decisions.map((decision) => {
    const text = fragmentText.get(decision.fragment_id) ?? userMessage;
    if (decision.route === "no_route") {
      return {
        fragment_id: decision.fragment_id,
        requirement_ids: requirementByFragment.get(decision.fragment_id) ?? [],
        route: "no_route",
        should_execute: false,
        fragment_text: text,
        no_route_reason: decision.no_route_reason ?? null,
        clarification_reason: decision.clarification_reason ?? null
      };
    }
    return {
      fragment_id: decision.fragment_id,
      requirement_ids: requirementByFragment.get(decision.fragment_id) ?? [],
      route: decision.route,
      should_execute: true,
      fragment_text: text,
      no_route_reason: null,
      clarification_reason: decision.clarification_reason ?? null
    };
  });
}

function toDebugRoutes(routeSummary: RouteHintSummary | null): Array<Record<string, unknown>> {
  if (!routeSummary) {
    return [];
  }
  if (routeSummary.mode === "legacy_v1") {
    return [
      {
        fragment_id: "F1",
        route: routeSummary.route_hint,
        reason: retrievalSummaryForRoute(routeSummary.route_hint),
        confidence: routeSummary.confidence,
        intent_class: routeSummary.intent_class
      }
    ];
  }
  return routeSummary.decisions.map((decision: RouteDecisionV2) => ({
    fragment_id: decision.fragment_id,
    route: decision.route,
    reason: decision.reason,
    route_status: decision.route_status ?? null,
    no_route_reason: decision.no_route_reason ?? null,
    clarification_reason: decision.clarification_reason ?? null,
    execution_readiness: decision.execution_readiness ?? null
  }));
}

interface RetrievalCallRecord {
  fragment_id: string;
  requirement_ids: string[];
  route: string;
  status: "executed" | "skipped" | "failed";
  query_text: string;
  reason: string | null;
}

interface RetrievalRawRecord {
  fragment_id: string;
  route: string;
  raw_result: Record<string, unknown>;
}

function buildSkippedResult(item: ExecutionPlanItem): UnifiedRetrievalResult {
  return normalizeRetrievalResult(item.fragment_id, item.requirement_ids, "no_route", {
    status: "empty",
    result_type: "summary",
    items: [],
    summary: {
      skipped: true,
      reason: mapNoRouteReason(item.no_route_reason),
      no_route_reason: item.no_route_reason,
      clarification_reason: item.clarification_reason
    },
    evidence: [],
    why_included: [],
    selection_reason: [mapNoRouteReason(item.no_route_reason)],
    risk_factors: [],
    business_interpretation: ["Р”Р°РЅРЅС‹Р№ С„СЂР°РіРјРµРЅС‚ РЅРµ Р±С‹Р» РІС‹РїРѕР»РЅРµРЅ РёР·-Р·Р° no-route СЂРµС€РµРЅРёСЏ."],
    confidence: "low",
    limitations: ["Р¤СЂР°РіРјРµРЅС‚ С‚СЂРµР±СѓРµС‚ СѓС‚РѕС‡РЅРµРЅРёСЏ РёР»Рё РѕС‚СЃСѓС‚СЃС‚РІСѓРµС‚ РїРѕРґРґРµСЂР¶РёРІР°РµРјС‹Р№ РјР°СЂС€СЂСѓС‚."],
    errors: []
  });
}

function summarizeUnique(values: string[], limit = 6): string[] {
  return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
}

interface SubjectTokenRule {
  critical: boolean;
  patterns: string[];
  routes?: string[];
}

const SUBJECT_TOKEN_RULES: Record<string, SubjectTokenRule> = {
  nds: {
    critical: true,
    patterns: [
      "vat",
      "accumulationregister",
      "\u043d\u0434\u0441",
      "\u043a\u043d\u0438\u0433\u0438\u043f\u043e\u043a\u0443\u043f\u043e\u043a",
      "\u043a\u043d\u0438\u0433\u0438\u043f\u0440\u043e\u0434\u0430\u0436",
      "\u043d\u0430\u043b\u043e\u0433\u043d\u0430\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043d\u0443\u044e\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c"
    ]
  },
  os: {
    critical: true,
    patterns: ["fixed_asset", "fixedasset", "\u043e\u0441\u043d\u043e\u0432\u043d", "\u0430\u043c\u043e\u0440\u0442\u0438\u0437"]
  },
  saldo: {
    critical: true,
    patterns: ["balance", "saldo", "\u0441\u0430\u043b\u044c\u0434\u043e", "\u043e\u0441\u0442\u0430\u0442"]
  },
  counterparty: {
    critical: false,
    patterns: [
      "counterparty",
      "supplier",
      "buyer",
      "counterparty_id",
      "journal_counterparty",
      "document_has_counterparty",
      "\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442",
      "\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a",
      "\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b"
    ],
    routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical"]
  },
  document: {
    critical: false,
    patterns: [
      "document",
      "recorder",
      "journal",
      "document_refs_count",
      "recorded_by_document",
      "journal_refers_to_document",
      "\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442"
    ],
    routes: ["hybrid_store_plus_live", "store_feature_risk", "store_canonical", "live_mcp_drilldown"]
  },
  anomaly: {
    critical: false,
    patterns: [
      "risk",
      "risk_score",
      "unknown_link_count",
      "zero_guid",
      "navigation_links",
      "missing_counterparty_link",
      "\u0430\u043d\u043e\u043c\u0430\u043b",
      "\u0440\u0438\u0441\u043a"
    ],
    routes: ["store_feature_risk", "batch_refresh_then_store"]
  },
  chain: {
    critical: false,
    patterns: ["chain", "cross_entity_chain", "relation_types", "operations_count", "matched_counterparties", "\u0446\u0435\u043f\u043e\u0447"],
    routes: ["hybrid_store_plus_live"]
  }
};

function hasRegexMatch(corpus: string, pattern: RegExp): boolean {
  try {
    return pattern.test(corpus);
  } catch {
    return false;
  }
}

function evaluateSubjectTokenMatch(token: string, corpus: string, executedRoutes: Set<string>): { matched: boolean; critical: boolean } {
  if (token.startsWith("account_")) {
    const account = token.slice("account_".length).trim();
    if (!account) {
      return { matched: false, critical: true };
    }
    const escaped = account.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    const accountPattern = new RegExp(`(^|[^0-9])${escaped}([^0-9]|$)`, "i");
    return { matched: hasRegexMatch(corpus, accountPattern), critical: true };
  }

  const rule = SUBJECT_TOKEN_RULES[token];
  if (rule) {
    const byPattern = rule.patterns.some((pattern) => corpus.includes(pattern));
    const byRoute = Array.isArray(rule.routes) ? rule.routes.some((route) => executedRoutes.has(route)) : false;
    return { matched: byPattern || byRoute, critical: rule.critical };
  }

  return { matched: corpus.includes(token), critical: false };
}

function evaluateCoverage(
  requirements: AssistantRequirement[],
  retrievalResults: UnifiedRetrievalResult[]
): { requirements: AssistantRequirement[]; coverage: RequirementCoverageReport } {
  const statusByRequirement = new Map<string, Array<UnifiedRetrievalResult["status"]>>();

  for (const result of retrievalResults) {
    for (const requirementId of result.requirement_ids) {
      const list = statusByRequirement.get(requirementId) ?? [];
      list.push(result.status);
      statusByRequirement.set(requirementId, list);
    }
  }

  const resolvedRequirements = requirements.map((requirement) => {
    if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
      return requirement;
    }
    const statuses = statusByRequirement.get(requirement.requirement_id) ?? [];
    if (statuses.length === 0) {
      return { ...requirement, status: "uncovered" as const };
    }
    if (statuses.includes("ok")) {
      return { ...requirement, status: "covered" as const };
    }
    if (statuses.includes("partial")) {
      return { ...requirement, status: "partially_covered" as const };
    }
    if (statuses.includes("empty") && !statuses.includes("error")) {
      return { ...requirement, status: "covered" as const };
    }
    return { ...requirement, status: "uncovered" as const };
  });

  const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
  const requirementsUncovered = resolvedRequirements
    .filter((item) => item.status === "uncovered")
    .map((item) => item.requirement_id);
  const requirementsPartiallyCovered = resolvedRequirements
    .filter((item) => item.status === "partially_covered")
    .map((item) => item.requirement_id);
  const clarificationNeededFor = resolvedRequirements
    .filter((item) => item.status === "clarification_needed")
    .map((item) => item.requirement_id);
  const outOfScopeRequirements = resolvedRequirements
    .filter((item) => item.status === "out_of_scope")
    .map((item) => item.requirement_id);

  return {
    requirements: resolvedRequirements,
    coverage: {
      requirements_total: resolvedRequirements.length,
      requirements_covered: requirementsCovered,
      requirements_uncovered: requirementsUncovered,
      requirements_partially_covered: requirementsPartiallyCovered,
      clarification_needed_for: clarificationNeededFor,
      out_of_scope_requirements: outOfScopeRequirements
    }
  };
}

function checkGrounding(
  userMessage: string,
  requirements: AssistantRequirement[],
  coverage: RequirementCoverageReport,
  retrievalResults: UnifiedRetrievalResult[]
): AnswerGroundingCheck {
  const whyIncludedSummary = summarizeUnique(retrievalResults.flatMap((item) => item.why_included));
  const selectionReasonSummary = summarizeUnique(retrievalResults.flatMap((item) => item.selection_reason));

  const hasMaterialResults = retrievalResults.some((item) => item.status === "ok" || item.status === "partial");
  const subjectTokens = extractSubjectTokens(userMessage);
  const executedRoutes = new Set(
    retrievalResults
      .filter((item) => item.status !== "error")
      .map((item) => item.route)
      .filter(Boolean)
  );
  const retrievalCorpus = JSON.stringify(
    retrievalResults.map((item) => ({
      route: item.route,
      result_type: item.result_type,
      summary: item.summary,
      items: item.items,
      evidence: item.evidence,
      why_included: item.why_included,
      selection_reason: item.selection_reason,
      risk_factors: item.risk_factors,
      business_interpretation: item.business_interpretation
    }))
  ).toLowerCase();

  const missingSubjectTokens: string[] = [];
  const missingCriticalTokens: string[] = [];

  for (const token of subjectTokens) {
    const match = evaluateSubjectTokenMatch(token, retrievalCorpus, executedRoutes);
    if (!match.matched) {
      missingSubjectTokens.push(token);
      if (match.critical) {
        missingCriticalTokens.push(token);
      }
    }
  }

  const onlyAccountCriticalMissing =
    missingCriticalTokens.length > 0 && missingCriticalTokens.every((token) => token.startsWith("account_"));
  const accountOnlyMismatchRecoverable =
    hasMaterialResults &&
    coverage.requirements_covered > 0 &&
    onlyAccountCriticalMissing &&
    (whyIncludedSummary.length > 0 || selectionReasonSummary.length > 0);

  const routeSubjectMatch = !hasMaterialResults || missingCriticalTokens.length === 0 || accountOnlyMismatchRecoverable;

  let status: AnswerGroundingCheck["status"] = "grounded";
  const reasons: string[] = [];

  if (!routeSubjectMatch) {
    status = "route_mismatch_blocked";
    reasons.push(`РќРµ РїРѕРґС‚РІРµСЂР¶РґРµРЅС‹ РєСЂРёС‚РёС‡РЅС‹Рµ РїСЂРµРґРјРµС‚РЅС‹Рµ С‚РѕРєРµРЅС‹ Р·Р°РїСЂРѕСЃР°: ${missingCriticalTokens.join(", ")}`);
  } else if (accountOnlyMismatchRecoverable) {
    status = "partial";
    reasons.push(`Р С‡РµС‚-С‚РѕРєРµРЅС‹ РЅРµ РїРѕРґС‚РІРµСЂР¶РґРµРЅС‹ РЅР°РїСЂСЏРјСѓСЋ (${missingCriticalTokens.join(", ")}), РЅРѕ РµСЃС‚СЊ СЂРµР»РµРІР°РЅС‚РЅР°СЏ РѕРїРѕСЂР° РґР»СЏ РѕРіСЂР°РЅРёС‡РµРЅРЅРѕРіРѕ РІС‹РІРѕРґР°.`);
  } else if (coverage.requirements_covered === 0) {
    status = "no_grounded_answer";
    reasons.push("РќРё РѕРґРЅРѕ С‚СЂРµР±РѕРІР°РЅРёРµ РЅРµ РїРѕР»СѓС‡РёР»Рѕ РїРѕРґС‚РІРµСЂР¶РґРµРЅРЅРѕРіРѕ РїРѕРєСЂС‹С‚РёСЏ.");
  } else if (
    coverage.requirements_uncovered.length > 0 ||
    coverage.requirements_partially_covered.length > 0 ||
    coverage.clarification_needed_for.length > 0 ||
    coverage.out_of_scope_requirements.length > 0
  ) {
    status = "partial";
    reasons.push("Р’РѕРїСЂРѕСЃ РїРѕРєСЂС‹С‚ С‡Р°СЃС‚РёС‡РЅРѕ: РµСЃС‚СЊ РЅРµРїРѕРєСЂС‹С‚С‹Рµ РёР»Рё С‚СЂРµР±СѓСЋС‰РёРµ СѓС‚РѕС‡РЅРµРЅРёСЏ С‚СЂРµР±РѕРІР°РЅРёСЏ.");
  }

  if (whyIncludedSummary.length === 0) {
    reasons.push("РќРµС‚ explainable-СЃРёРіРЅР°Р»РѕРІ why_included РІ СЂРµР·СѓР»СЊС‚Р°С‚Р°С… РІС‹Р±РѕСЂРєРё.");
  }
  if (missingSubjectTokens.length > 0 && missingCriticalTokens.length === 0) {
    reasons.push(`Р§Р°СЃС‚СЊ РєРѕРЅС‚РµРєСЃС‚РЅС‹С… С‚РѕРєРµРЅРѕРІ РЅРµ РїРѕРґС‚РІРµСЂР¶РґРµРЅР° РЅР°РїСЂСЏРјСѓСЋ: ${missingSubjectTokens.join(", ")}`);
  }

  const missingRequirements = [
    ...coverage.requirements_uncovered,
    ...coverage.requirements_partially_covered,
    ...coverage.clarification_needed_for,
    ...coverage.out_of_scope_requirements
  ];

  return {
    status,
    route_subject_match: routeSubjectMatch,
    missing_requirements: missingRequirements,
    reasons,
    why_included_summary: whyIncludedSummary,
    selection_reason_summary: selectionReasonSummary
  };
}

function firstNonEmptyLine(text: string): string {
  const line = text
    .split("\n")
    .map((item) => item.trim())
    .find((item) => item.length > 0);
  return (line ?? text).slice(0, 220);
}

function buildClaimEvidenceLinks(
  retrievalResults: UnifiedRetrievalResult[]
): NonNullable<AnswerStructureV11["evidence_block"]["claim_evidence_links"]> {
  const byClaim = new Map<string, string[]>();
  for (const result of retrievalResults) {
    for (const evidence of result.evidence) {
      const claimRef = String(evidence.claim_ref ?? "").trim();
      if (!claimRef) {
        continue;
      }
      const evidenceId = String(evidence.evidence_id ?? "").trim();
      if (!evidenceId) {
        continue;
      }
      const current = byClaim.get(claimRef) ?? [];
      current.push(evidenceId);
      byClaim.set(claimRef, current);
    }
  }

  return Array.from(byClaim.entries())
    .slice(0, 10)
    .map(([claimRef, evidenceIds]) => ({
      claim_ref: claimRef,
      evidence_ids: summarizeUnique(evidenceIds, 10)
    }));
}

function buildAnswerStructureV11(input: {
  assistantReply: string;
  coverageReport: RequirementCoverageReport;
  groundingCheck: AnswerGroundingCheck;
  retrievalResults: UnifiedRetrievalResult[];
}): AnswerStructureV11 {
  const evidenceIds = summarizeUnique(
    input.retrievalResults.flatMap((item) => item.evidence.map((evidence) => evidence.evidence_id)),
    10
  );
  const mechanismNotes = summarizeUnique(
    input.retrievalResults.flatMap((item) =>
      item.evidence
        .map((evidence) => evidence.mechanism_note)
        .filter((note): note is string => typeof note === "string" && note.trim().length > 0)
    ),
    6
  );
  const sourceRefs = summarizeUnique(
    input.retrievalResults.flatMap((item) =>
      item.evidence
        .map((evidence) => evidence.source_ref?.canonical_ref)
        .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
    ),
    8
  );
  const limitationReasonCodes = summarizeUnique(
    input.retrievalResults.flatMap((item) =>
      item.evidence
        .flatMap((evidence) => {
          const code = evidence.limitation?.reason_code;
          return typeof code === "string" && code.trim().length > 0 ? [code] : [];
        })
    ),
    8
  ) as AnswerStructureV11["mechanism_block"]["limitation_reason_codes"];
  const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
  const limitations = summarizeUnique(
    [...input.retrievalResults.flatMap((item) => item.limitations), ...input.groundingCheck.reasons],
    8
  );
  const clarificationQuestions = input.coverageReport.clarification_needed_for.map(
    (item) => `РЈС‚РѕС‡РЅРёС‚Рµ С‚СЂРµР±РѕРІР°РЅРёРµ ${item}.`
  );
  const recommendedActions = summarizeUnique(
    [
      ...input.coverageReport.requirements_uncovered.map((item) => `РџСЂРѕРІРµСЂРёС‚СЊ РЅРµРїРѕРєСЂС‹С‚РѕРµ С‚СЂРµР±РѕРІР°РЅРёРµ ${item}.`),
      ...input.coverageReport.requirements_partially_covered.map((item) => `Р”РѕСѓС‚РѕС‡РЅРёС‚СЊ С‡Р°СЃС‚РёС‡РЅРѕ РїРѕРєСЂС‹С‚РѕРµ С‚СЂРµР±РѕРІР°РЅРёРµ ${item}.`)
    ],
    6
  );
  const mechanismStatus: AnswerStructureV11["mechanism_block"]["status"] =
    mechanismNotes.length === 0
      ? "unresolved"
      : limitationReasonCodes.includes("missing_mechanism") || limitationReasonCodes.includes("heuristic_inference")
        ? "limited"
        : "grounded";

  return {
    schema_version: ANSWER_STRUCTURE_SCHEMA_VERSION,
    answer_summary: firstNonEmptyLine(input.assistantReply),
    direct_answer: input.assistantReply,
    mechanism_block: {
      status: mechanismStatus,
      mechanism_notes: mechanismNotes,
      limitation_reason_codes: limitationReasonCodes
    },
    evidence_block: {
      evidence_ids: evidenceIds,
      source_refs: sourceRefs,
      mechanism_notes: mechanismNotes,
      coverage_note:
        input.coverageReport.requirements_total === input.coverageReport.requirements_covered
          ? "coverage_full_or_near_full"
          : "coverage_partial_or_limited",
      ...(FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 && claimEvidenceLinks.length > 0
        ? {
            claim_evidence_links: claimEvidenceLinks
          }
        : {})
    },
    uncertainty_block: {
      open_uncertainties: input.groundingCheck.missing_requirements,
      limitations
    },
    next_step_block: {
      recommended_actions: recommendedActions,
      clarification_questions: clarificationQuestions
    }
  };
}

const FOLLOWUP_ROUTE_HINTS = new Set(["store_canonical", "store_feature_risk", "hybrid_store_plus_live", "live_mcp_drilldown", "batch_refresh_then_store"]);
const FOLLOWUP_BUSINESS_CONTEXT_MAX = 320;
const FOLLOWUP_SUBJECT_MAX = 160;
const FOLLOWUP_QUESTION_APPEND_MAX = 260;

function compactWhitespace(value: string): string {
  return value.replace(/\s+/g, " ").trim();
}

function hasAccountingSignal(text: string): boolean {
  const lower = text.toLowerCase();
  if (/(?:^|[\s,;:])\d{2}(?:\.\d{2})?(?=$|[\s,.;:])/i.test(lower)) {
    return true;
  }
  return /(РїСЂРѕРІРѕРґРє|РґРѕРєСѓРјРµРЅС‚|СЂРµР°Р»РёР·Р°С†|РїРѕСЃС‚СѓРїР»РµРЅ|РІР·Р°РёРјРѕСЂР°СЃС‡РµС‚|СЃР°Р»СЊРґРѕ|РѕСЃС‚Р°С‚Рє|СЃС‡РµС‚|РЅРґСЃ|Р°РјРѕСЂС‚РёР·|СЂР±Рї|РѕСЃ|РєРѕРЅС‚СЂР°РіРµРЅС‚|РїРѕСЃС‚Р°РІС‰РёРє|РїРѕРєСѓРїР°С‚РµР»|РѕРїР»Р°С‚|Р±Р°РЅРє|РІС‹РїРёСЃРє|СЃРєР»Р°Рґ|С‚РѕРІР°СЂ|РјР°С‚РµСЂРёР°Р»|counterparty|supplier|invoice|posting|ledger|account|anomaly|risk)/i.test(
    lower
  );
}

function hasFollowupMarker(text: string): boolean {
  const compact = compactWhitespace(text.toLowerCase());
  return /^(Рё|Р° РµС‰Рµ|Р° РµС‰С‘|РµС‰Рµ|РµС‰С‘|РґРѕР±Р°РІ|СѓС‚РѕС‡РЅ|РїСЂРѕРґРѕР»Р¶|С‚Р°РєР¶Рµ|plus|also|dobav|utochn|prodolzh)/i.test(compact);
}

function hasReferentialPointer(text: string): boolean {
  return /(РїРѕ СЌС‚РѕРјСѓ|РїРѕ С‚РѕРјСѓ|СЌС‚Рѕ Р¶Рµ|СЌС‚РѕР№|СЌС‚РёРј|С‚РѕРјСѓ|same thing|that one|po etomu|po tomu)/i.test(text.toLowerCase());
}

function hasSmallTalkSignal(text: string): boolean {
  return /(РїСЂРёРІРµС‚|РєР°Рє РґРµР»Р°|СЃРїР°СЃРёР±Рѕ|thanks|thank you|hello|hi)\b/i.test(text.toLowerCase());
}

function countTokens(text: string): number {
  return compactWhitespace(text)
    .split(" ")
    .filter(Boolean).length;
}

function hasPeriodLiteral(text: string): boolean {
  return /\b(20\d{2}(?:[-/.](?:0[1-9]|1[0-2]))?)\b/.test(text);
}

function routeFromInvestigationState(state: NonNullable<AssistantSessionState["investigation_state"]>): RouteHint | null {
  const rawDomain = compactWhitespace(state.focus.domain ?? "");
  if (!rawDomain) {
    return null;
  }
  if (FOLLOWUP_ROUTE_HINTS.has(rawDomain)) {
    return rawDomain as RouteHint;
  }
  for (const candidate of rawDomain.split(",").map((item) => compactWhitespace(item))) {
    if (FOLLOWUP_ROUTE_HINTS.has(candidate)) {
      return candidate as RouteHint;
    }
  }
  return null;
}

interface FollowupStateBindingResult {
  normalizedQuestion: string;
  mergedContext: NormalizeRequestPayload["context"];
  usage: FollowupStateUsageDebug | null;
}

function withCappedLength(value: string, max: number): string {
  return value.length <= max ? value : value.slice(0, max);
}

function mergeBusinessContext(existing: string | undefined, patchParts: string[]): string | undefined {
  const existingText = compactWhitespace(existing ?? "");
  const patchText = compactWhitespace(patchParts.filter(Boolean).join("; "));
  if (!existingText && !patchText) {
    return undefined;
  }
  const merged = existingText && patchText ? `${existingText}; ${patchText}` : existingText || patchText;
  return withCappedLength(merged, FOLLOWUP_BUSINESS_CONTEXT_MAX);
}

function buildFollowupStateBinding(input: {
  userMessage: string;
  payloadContext: NormalizeRequestPayload["context"];
  investigationState: NonNullable<AssistantSessionState["investigation_state"]>;
}): FollowupStateBindingResult {
  const userMessage = String(input.userMessage ?? "").trim();
  if (!userMessage || input.investigationState.status !== "active" || input.investigationState.turn_index <= 0) {
    return {
      normalizedQuestion: userMessage,
      mergedContext: input.payloadContext,
      usage: null
    };
  }

  const strongSignal = hasAccountingSignal(userMessage);
  const followupMarker = hasFollowupMarker(userMessage);
  const referentialPointer = hasReferentialPointer(userMessage);
  const shortPrompt = countTokens(userMessage) <= 10;
  const smallTalkSignal = hasSmallTalkSignal(userMessage);
  const shouldBind = !smallTalkSignal && (followupMarker || referentialPointer || (!strongSignal && shortPrompt));

  if (!shouldBind) {
    return {
      normalizedQuestion: userMessage,
      mergedContext: input.payloadContext,
      usage: null
    };
  }

  const context: NormalizeRequestPayload["context"] = {
    ...(input.payloadContext ?? {})
  };
  const expectedRouteFromState = !context?.expected_route ? routeFromInvestigationState(input.investigationState) : null;
  const periodHintFromState = !context?.period_hint ? input.investigationState.focus.period : null;

  if (expectedRouteFromState) {
    context.expected_route = expectedRouteFromState;
  }
  if (periodHintFromState) {
    context.period_hint = periodHintFromState;
  }

  const subject = withCappedLength(compactWhitespace(input.investigationState.focus.active_query_subject ?? ""), FOLLOWUP_SUBJECT_MAX);
  const businessContextPatch: string[] = ["followup_state_binding_v1"];
  if (input.investigationState.focus.period) {
    businessContextPatch.push("active_period");
  }
  if (subject) {
    businessContextPatch.push(`focus_subject:${subject}`);
  }
  if (input.investigationState.focus.primary_accounts.length > 0) {
    businessContextPatch.push(`focus_accounts:${input.investigationState.focus.primary_accounts.join(",")}`);
  }

  const mergedBusinessContext = mergeBusinessContext(context?.business_context, businessContextPatch);
  if (mergedBusinessContext) {
    context.business_context = mergedBusinessContext;
  }

  const shouldAugmentQuestion = Boolean(subject) && (followupMarker || referentialPointer || !strongSignal);
  let normalizedQuestion = userMessage;
  if (shouldAugmentQuestion) {
    const appendParts: string[] = [`Р¤РѕРєСѓСЃ С‚РµРєСѓС‰РµРіРѕ СЂР°Р·Р±РѕСЂР°: ${subject}`];
    if (input.investigationState.focus.primary_accounts.length > 0 && !/\b\d{2}(?:\.\d{2})?\b/.test(userMessage)) {
      appendParts.push(`РЎС‡РµС‚Р° С„РѕРєСѓСЃР°: ${input.investigationState.focus.primary_accounts.join(", ")}`);
    }
    if (periodHintFromState && !hasPeriodLiteral(userMessage)) {
      appendParts.push(`РџРµСЂРёРѕРґ С„РѕРєСѓСЃР°: ${periodHintFromState}`);
    }
    const appendBlock = withCappedLength(compactWhitespace(appendParts.join("; ")), FOLLOWUP_QUESTION_APPEND_MAX);
    normalizedQuestion = `${userMessage}\n${appendBlock}`.trim();
  }

  const reason = followupMarker
    ? "followup_marker"
    : referentialPointer
      ? "referential_pointer"
      : "underspecified_short_followup";

  return {
    normalizedQuestion,
    mergedContext: context,
    usage: {
      applied: true,
      reason,
      state_turn_index: input.investigationState.turn_index,
      context_patch: {
        period_hint_from_state: Boolean(periodHintFromState),
        expected_route_from_state: Boolean(expectedRouteFromState),
        business_context_from_state: Boolean(mergedBusinessContext),
        question_augmented: shouldAugmentQuestion
      }
    }
  };
}

function cloneItems(items: AssistantConversationItem[]): AssistantConversationItem[] {
  return items.map((item) => ({
    ...item,
    debug: item.debug ? { ...item.debug } : null
  }));
}

export class AssistantService {
  constructor(
    private readonly normalizerService: NormalizerService,
    private readonly sessions: AssistantSessionStore,
    private readonly dataLayer: AssistantDataLayer = new AssistantDataLayer(),
    private readonly sessionLogger: AssistantSessionLogger = new AssistantSessionLogger()
  ) {}

  public getSession(sessionId: string): AssistantSessionState | null {
    return this.sessions.getSession(sessionId);
  }

  public async handleMessage(payload: AssistantMessageRequestPayload): Promise<AssistantMessageResponsePayload> {
    const session = this.sessions.ensureSession(payload.session_id);
    const sessionId = session.session_id;
    const userMessage = String(payload.user_message ?? payload.message ?? "").trim();

    const userItem: AssistantConversationItem = {
      message_id: `msg-${nanoid(10)}`,
      session_id: sessionId,
      role: "user",
      text: userMessage,
      reply_type: null,
      created_at: new Date().toISOString(),
      trace_id: null,
      debug: null
    };
    this.sessions.appendItem(sessionId, userItem);

    const followupBinding =
      FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 &&
      FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 &&
      session.investigation_state
        ? buildFollowupStateBinding({
            userMessage,
            payloadContext: payload.context,
            investigationState: session.investigation_state
          })
        : {
            normalizedQuestion: userMessage,
            mergedContext: payload.context,
            usage: null as FollowupStateUsageDebug | null
          };

    const normalizePayload: NormalizeRequestPayload = {
      apiKey: payload.apiKey,
      model: payload.model,
      baseUrl: payload.baseUrl,
      temperature: payload.temperature,
      maxOutputTokens: payload.maxOutputTokens,
      promptVersion: payload.promptVersion ?? "normalizer_v2_0_2",
      systemPrompt: payload.systemPrompt,
      developerPrompt: payload.developerPrompt,
      domainPrompt: payload.domainPrompt,
      fewShotExamples: payload.fewShotExamples,
      userQuestion: followupBinding.normalizedQuestion,
      context: followupBinding.mergedContext,
      useMock: Boolean(payload.useMock)
    };

    const normalized = await this.normalizerService.normalize(normalizePayload);
    const requirementExtraction = extractRequirements(normalized.route_hint_summary, normalized.normalized, userMessage);
    const executionPlan = toExecutionPlan(
      normalized.route_hint_summary,
      normalized.normalized,
      userMessage,
      requirementExtraction.byFragment
    );
    const retrievalCalls: RetrievalCallRecord[] = [];
    const retrievalResultsRaw: RetrievalRawRecord[] = [];
    const retrievalResults: UnifiedRetrievalResult[] = [];

    for (const planItem of executionPlan) {
      if (!planItem.should_execute) {
        retrievalCalls.push({
          fragment_id: planItem.fragment_id,
          requirement_ids: planItem.requirement_ids,
          route: planItem.route,
          status: "skipped",
          query_text: planItem.fragment_text,
          reason: mapNoRouteReason(planItem.no_route_reason)
        });
        retrievalResults.push(buildSkippedResult(planItem));
        continue;
      }

      retrievalCalls.push({
        fragment_id: planItem.fragment_id,
        requirement_ids: planItem.requirement_ids,
        route: planItem.route,
        status: "executed",
        query_text: planItem.fragment_text,
        reason: null
      });

      try {
        const raw = this.dataLayer.executeRoute(planItem.route, planItem.fragment_text);
        retrievalResultsRaw.push({
          fragment_id: planItem.fragment_id,
          route: planItem.route,
          raw_result: raw as unknown as Record<string, unknown>
        });
        retrievalResults.push(normalizeRetrievalResult(planItem.fragment_id, planItem.requirement_ids, planItem.route, raw));
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        retrievalCalls[retrievalCalls.length - 1].status = "failed";
        retrievalCalls[retrievalCalls.length - 1].reason = message;
        const rawError = {
          status: "error",
          result_type: "summary",
          items: [],
          summary: {
            route: planItem.route
          },
          evidence: [],
          why_included: [],
          selection_reason: [],
          risk_factors: [],
          business_interpretation: [],
          confidence: "low",
          limitations: ["Route executor failed."],
          errors: [message]
        };
        retrievalResultsRaw.push({
          fragment_id: planItem.fragment_id,
          route: planItem.route,
          raw_result: rawError
        });
        retrievalResults.push(normalizeRetrievalResult(planItem.fragment_id, planItem.requirement_ids, planItem.route, rawError));
      }
    }

    const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
    const groundingCheck = checkGrounding(
      userMessage,
      coverageEvaluation.requirements,
      coverageEvaluation.coverage,
      retrievalResults
    );

    const composition = composeAssistantAnswer({
      userMessage,
      routeSummary: normalized.route_hint_summary,
      retrievalResults,
      requirements: coverageEvaluation.requirements,
      coverageReport: coverageEvaluation.coverage,
      groundingCheck,
      enableAnswerPolicyV11: FEATURE_ASSISTANT_ANSWER_POLICY_V11
    });

    const answerStructureV11 = FEATURE_ASSISTANT_CONTRACTS_V11
      ? FEATURE_ASSISTANT_ANSWER_POLICY_V11 && composition.answer_structure_v11
        ? composition.answer_structure_v11
        : buildAnswerStructureV11({
            assistantReply: composition.assistant_reply,
            coverageReport: coverageEvaluation.coverage,
            groundingCheck,
            retrievalResults
          })
      : null;

    const investigationStateSnapshot =
      FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && session.investigation_state
        ? updateInvestigationState({
            previous: session.investigation_state,
            timestamp: new Date().toISOString(),
            questionId: userItem.message_id,
            userMessage,
            routeSummary: normalized.route_hint_summary,
            requirements: coverageEvaluation.requirements,
            coverageReport: coverageEvaluation.coverage,
            retrievalResults,
            replyType: composition.reply_type as InvestigationLastAnswerMode
          })
        : null;

    if (FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 && investigationStateSnapshot) {
      this.sessions.setInvestigationState(sessionId, investigationStateSnapshot);
    }

    const debug: AssistantDebugPayload = {
      trace_id: normalized.trace_id,
      prompt_version: normalized.prompt_version,
      schema_version: normalized.schema_version,
      fallback_type: composition.fallback_type,
      route_summary: normalized.route_hint_summary,
      fragments: extractFragments(normalized.normalized),
      requirements_extracted: coverageEvaluation.requirements,
      coverage_report: coverageEvaluation.coverage,
      routes: toDebugRoutes(normalized.route_hint_summary),
      retrieval_status: retrievalResults.map((item) => ({
        fragment_id: item.fragment_id,
        requirement_ids: item.requirement_ids,
        route: item.route,
        status: item.status,
        result_type: item.result_type
      })),
      retrieval_results: retrievalResults,
      answer_grounding_check: groundingCheck,
      dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
      ...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
      answer_structure_v11: answerStructureV11,
      investigation_state_snapshot: investigationStateSnapshot,
      normalized: normalized.normalized
    };

    const assistantItem: AssistantConversationItem = {
      message_id: `msg-${nanoid(10)}`,
      session_id: sessionId,
      role: "assistant",
      text: composition.assistant_reply,
      reply_type: composition.reply_type,
      created_at: new Date().toISOString(),
      trace_id: normalized.trace_id,
      debug
    };
    this.sessions.appendItem(sessionId, assistantItem);

    const current = this.sessions.getSession(sessionId);
    if (current) {
      this.sessionLogger.persistSession(current);
    }
    const conversation = cloneItems(current?.items ?? []);

    logJson({
      timestamp: new Date().toISOString(),
      level: "info",
      service: "assistant_loop",
      message: "assistant_message_processed",
      sessionId,
      eventType: "assistant_message",
      details: {
        session_id: sessionId,
        message_id: assistantItem.message_id,
        user_message: userMessage,
        normalizer_output: normalized.normalized,
        execution_plan: executionPlan,
        resolved_execution_state: extractExecutionState(normalized.normalized),
        routes: toDebugRoutes(normalized.route_hint_summary),
        retrieval_calls: retrievalCalls,
        retrieval_results_raw: retrievalResultsRaw,
        retrieval_results_normalized: retrievalResults,
        requirements_extracted: coverageEvaluation.requirements,
        requirements_total: coverageEvaluation.coverage.requirements_total,
        requirements_covered: coverageEvaluation.coverage.requirements_covered,
        requirements_uncovered: coverageEvaluation.coverage.requirements_uncovered,
        coverage_status:
          coverageEvaluation.coverage.requirements_total === coverageEvaluation.coverage.requirements_covered &&
          coverageEvaluation.coverage.requirements_uncovered.length === 0 &&
          coverageEvaluation.coverage.requirements_partially_covered.length === 0
            ? "full"
            : "partial_or_limited",
        answer_grounding_status: groundingCheck.status,
        reply_semantic_type: composition.reply_type,
        why_included_summary: groundingCheck.why_included_summary,
        selection_reason_summary: groundingCheck.selection_reason_summary,
        route_subject_match: groundingCheck.route_subject_match,
        clarification_target: coverageEvaluation.coverage.clarification_needed_for,
        dropped_intent_segments: extractDiscardedIntentSegments(normalized.normalized),
        ...(followupBinding.usage ? { followup_state_usage: followupBinding.usage } : {}),
        answer_structure_v11: answerStructureV11,
        investigation_state_snapshot: investigationStateSnapshot,
        fallback_type: composition.fallback_type,
        assistant_reply: composition.assistant_reply,
        reply_type: composition.reply_type,
        trace_id: normalized.trace_id
      }
    });

    return {
      ok: true,
      session_id: sessionId,
      assistant_reply: composition.assistant_reply,
      reply_type: composition.reply_type,
      conversation_item: assistantItem,
      debug,
      conversation
    };
  }
}
