/**
 * Cron driver genérico do event_log — a peça prometida em dispatcher.ts.
 *
 * Seleciona SÓ event_types com handler registrado: tipos drenados por crons
 * dedicados (ex. ai_agent.dispatch_requested → agent-dispatcher) não têm
 * handler no registry e ficam intocados.
 */
import type { SupabaseClient } from "@supabase/supabase-js";
import {
  avisoDeEventoMorto,
  IA_QUE_NAO_RESPONDEU,
  MENSAGEM_DO_NUMERO_OFICIAL_QUE_NAO_ENTROU,
  MENSAGEM_DO_NUMERO_OFICIAL_SEM_ATENDIMENTO,
  MENSAGEM_QUE_NAO_ENTROU,
} from "@/lib/event-log/aviso-de-evento-morto";
import { dispatchEvent, getRegisteredHandlers, type EventRow } from "@/lib/event-log/dispatcher";
import { logger } from "@/lib/logger";
import { ehOperante } from "@/lib/organizacao/operante";

const MAX_ATTEMPTS = 5;

/** Depois disto, um evento em `processing` é considerado órfão e volta à fila. */
const PROCESSING_STALE_MS = 10 * 60 * 1000;

export interface DrainSummary {
  /**
   * Os `skipped` COM motivo, para o resumo poder ser lido de fora do banco.
   *
   * Opcional de propósito: quem monta um `DrainSummary` literal (por exemplo
   * `tests/unit/event-log-drain-loop.test.ts`) não precisa mudar.
   */
  pulados?: string[];
  /**
   * Dreno escopado: eventos em que o handler do escopo concluiu e que voltaram
   * a `pending` só porque os outros consumidores são do worker. Não é
   * `retried` — ninguém falhou.
   */
  deixados_ao_worker?: number;
  scanned: number;
  done: number;
  retried: number;
  failed: number;
  dead: number;
}

function backoffAt(attempts: number): string {
  // 1min, 2min, 4min, 8min... (2^n minutos)
  const minutes = Math.pow(2, attempts);
  return new Date(Date.now() + minutes * 60_000).toISOString();
}

/**
 * O EVENTO QUE MORREU PRECISA DIZER A ALGUÉM QUE MORREU.
 *
 * O kind `event_dead` existia na constraint de `agent_inbox_items` desde a
 * migration 0050, na cópia (`lib/ai/agent-inbox-copy.ts`) e na política de
 * destino (`lib/ai/inbox-destino.ts`) — e **não tinha um único produtor no
 * repositório**. Um evento que esgotava as 5 tentativas virava `status='dead'`
 * e sumia: nenhum aviso, nenhuma tela, nenhum Sentry.
 *
 * Medido numa VPS em produção: quatro `media.derive_requested` mortos, com o
 * cliente ouvindo "não consigo ouvir áudio" e ninguém do lado de cá sabendo.
 * O evento morto é o fim da linha de um efeito colateral que o produto
 * prometeu — mídia que nunca foi derivada, mensagem que nunca saiu. Silêncio
 * aqui é a promessa quebrada sem recibo.
 *
 * ⚠️ NASCE SEM REFERÊNCIA, e isso é a política, não esquecimento:
 * `POLITICAS_DE_AVISO.event_dead` declara `refs: []` porque não existe tela de
 * `event_log` para onde mandar quem lê. Preencher `ref_kind` com uma entidade
 * sem destino faria o aviso oferecer um botão que não leva a lugar nenhum.
 *
 * Dedupe por `kind`, MENOS o aviso da IA que deixou de responder: um aviso
 * aberto por organização enquanto o problema durar, como `midia_nao_lida` e
 * `budget_exceeded` já fazem. Uma linha por evento inundaria a Central numa pane
 * de handler — e Central inundada é Central que ninguém abre, que é como o
 * alerta morre pela segunda vez. O aviso da IA (dreno do agent-engine) é outra
 * família e fica fora da consulta: sem isso, ele aberto calaria a mídia, e a
 * mídia aberta já não o cala (`aviso-de-evento-morto.ts`, "as duas famílias").
 *
 * Fire-and-forget: falhar ao avisar não pode derrubar o dreno.
 *
 * Exportada (além de `drainEventLog`) porque é ELA que a corrida da issue #880
 * atravessa: o cron `event-log-drain` e o drain-loop do worker chamam este
 * dreno ao mesmo tempo, e os dois passam por aqui. O teste da corrida entra por
 * esta função — `tests/unit/aviso-event-dead-concorrente-abre-uma-vez.test.ts`.
 */
export async function avisarEventoMorto(
  admin: SupabaseClient,
  row: Pick<EventRow, "id" | "organization_id" | "event_type" | "attempts">,
  motivo: string,
): Promise<void> {
  try {
    const { data: jaAberto } = await admin
      .from("agent_inbox_items")
      .select("id")
      .eq("organization_id", row.organization_id)
      .eq("kind", "event_dead")
      .eq("status", "open")
      .neq("title", IA_QUE_NAO_RESPONDEU.titulo)
      .neq("title", MENSAGEM_QUE_NAO_ENTROU.titulo)
      .neq("title", MENSAGEM_DO_NUMERO_OFICIAL_QUE_NAO_ENTROU.titulo)
      .neq("title", MENSAGEM_DO_NUMERO_OFICIAL_SEM_ATENDIMENTO.titulo)
      .limit(1)
      .maybeSingle();
    if (jaAberto) return;

    // `critical` e não `warn`: é a mesma classe de `job_dead` — algo que o
    // produto prometeu fazer parou de tentar. O CHECK de
    // `agent_inbox_items.severity` aceita info|warn|critical, e valor fora
    // disso é recusado com 23514: o aviso nunca abriria, exatamente no caso
    // que esta função existe para tornar visível.
    // O texto é o MESMO do dreno do agent-engine (`edge/crm/drain.ts`), que
    // desiste do `ai_agent.dispatch_requested` — ver `aviso-de-evento-morto.ts`.
    const { title, body } = avisoDeEventoMorto({
      eventType: row.event_type,
      tentativas: row.attempts + 1,
      motivo,
    });
    const { error } = await admin.from("agent_inbox_items").insert({
      organization_id: row.organization_id,
      kind: "event_dead",
      severity: "critical",
      title,
      body,
    });
    if (error) {
      // `23505` é o outro dreno chegando primeiro: o índice único parcial
      // `agent_inbox_event_dead_aberto_unico` (migration 0491) recusou a segunda
      // linha, e recusar é o que este aviso PROMETE — um por organização e por
      // família. Antes do índice os dois passavam pelo "não existe" e os dois
      // inseriam (issue #880); agora quem chega segundo recebe `23505`, que é o
      // mesmo desfecho de ter encontrado o aviso aberto na consulta de cima, e
      // não uma falha do dreno.
      if (error.code === "23505") return;
      logger.error("[event-log.drain] aviso de evento morto recusado", {
        event_id: row.id,
        error: error.message,
      });
    }
  } catch (err) {
    logger.error("[event-log.drain] aviso de evento morto falhou", {
      event_id: row.id,
      error: err instanceof Error ? err.message : String(err),
    });
  }
}

/**
 * Dreno de UMA organização, rodando só alguns handlers — o do webhook de
 * mensagem quando há worker drenando (`lib/dev/kick-local-pipeline.ts`).
 *
 * Ele não é dono do evento: os outros handlers do mesmo evento ficam para o
 * laço do worker. Por isso o evento volta a `pending` com o `consumed_by`
 * acrescido, e falha de um handler escopado NÃO conta tentativa nem aplica
 * backoff — o worker roda o que faltou já no próximo tique, e é ele quem conta.
 */
export interface EscopoDoDreno {
  organizationId: string;
  handlers: readonly string[];
}

export async function drainEventLog(
  admin: SupabaseClient,
  opts: {
    limit?: number;
    escopo?: EscopoDoDreno;
    /**
     * Se `false`, este tick NÃO procura evento preso em `processing`.
     *
     * Existe para o laço do worker (#3693): ele varre a cada 2 s quando há
     * trabalho, e a varredura procura linhas de MINUTOS atrás
     * (`PROCESSING_STALE_MS`). Repetir a consulta vinte vezes por minuto era
     * tráfego ao PostgREST sem chance de achar nada novo. O default é `true` —
     * cron, webhook escopado e qualquer chamador que não pense no assunto
     * continuam recuperando presos a cada passada, e é o laço quem se
     * encarrega de não deixar passar mais de um minuto.
     */
    varrerPresos?: boolean;
  } = {},
): Promise<DrainSummary> {
  const limit = opts.limit ?? 50;
  const varrerPresos = opts.varrerPresos ?? true;
  const summary: DrainSummary = {
    scanned: 0,
    done: 0,
    retried: 0,
    failed: 0,
    dead: 0,
    pulados: [],
  };

  const escopo = opts.escopo;
  const handledTypes = [
    ...new Set(
      getRegisteredHandlers()
        .filter((h) => !escopo || escopo.handlers.includes(h.key))
        .flatMap((h) => h.events),
    ),
  ];
  if (!handledTypes.length) return summary;

  const nowIso = new Date().toISOString();

  // ─── EVENTO PRESO EM `processing` VOLTA PARA A FILA ────────────────────────
  //
  // A linha é marcada `processing` ANTES de o handler rodar, e NADA no produto
  // a devolvia: um handler que não retorna — processo derrubado no meio, OOM,
  // ida a um serviço externo sem timeout — deixava o evento preso para SEMPRE.
  // Não é hipótese: foi medido nesta frente com o Redis do debounce apontando
  // para uma porta sem ninguém escutando. O evento ficou `processing`,
  // `attempts=0`, `consumed_by` vazio, e o material que a pessoa cadastrou
  // nunca foi preparado — sem erro em lugar nenhum, e sem uma segunda chance.
  //
  // `job_queue` tem reaper desde sempre; o `event_log` não tinha. É o
  // invariante 4 do Sistema Vivo (nenhuma demanda sem próximo passo) aplicado à
  // fila de eventos.
  //
  // A janela é generosa de propósito: o handler mais lento do registry é um
  // turno de agente, e reclamar cedo demais faria DOIS workers agirem sobre o
  // mesmo evento — trocar um evento parado por um efeito em dobro.
  //
  // `updated_at` é confiável como "quando alguém tocou esta linha": o trigger
  // `trg_event_log_touch` (BEFORE UPDATE) o reescreve em toda atualização, então
  // a linha carrega o instante do CLAIM enquanto o handler não volta.
  //
  // O dreno escopado não reclama: a varredura é de TODAS as organizações, e é
  // do laço do worker e do cron.
  const limiteDePresos = new Date(Date.now() - PROCESSING_STALE_MS).toISOString();
  const { data: presos } =
    escopo || !varrerPresos
      ? { data: [] }
      : await admin
          .from("event_log")
          .select("id, organization_id, event_type, attempts")
          .eq("status", "processing")
          .lt("updated_at", limiteDePresos);

  // ─── E A VOLTA CONTA COMO TENTATIVA ────────────────────────────────────────
  //
  // Devolver o evento à fila com `attempts` intacto era o laço que derrubava o
  // worker de produção 313 vezes em 2026-09-15: um PDF de 18 KB estourava o
  // heap (`lib/ai/rag/extractors/pdf.ts` explica o custo), o processo morria
  // ANTES de o handler devolver erro — e só handler que devolve erro
  // incrementava `attempts`. O evento voltava a `pending` com `attempts=0`,
  // era reclamado de novo, matava de novo. Um evento envenenado tinha
  // tentativas infinitas, e o `MAX_ATTEMPTS` só valia para quem falhava
  // educadamente.
  //
  // Um evento que ficou `processing` além da janela é uma tentativa que não
  // voltou. Conta como as outras: mesmo `attempts + 1`, mesmo `dead` no limite,
  // mesmo aviso na Central. O backoff importa tanto quanto a contagem — sem ele,
  // o evento reclamado é o primeiro da fila do próximo tique, e o worker
  // recém-reiniciado morre no mesmo minuto.
  //
  // MAS ele entra a partir da SEGUNDA volta, e a exceção tem dono: o invariante
  // `tests/invariants/event-log-drain.test.ts`, caso 9, afirma que o órfão volta
  // para a fila E É PROCESSADO NO MESMO TIQUE, com a razão escrita lá — o órfão
  // legítimo (um deploy que reiniciou o worker no meio de um evento sadio) não
  // deve pagar espera nenhuma, porque a espera é o defeito que aquele caso veio
  // impedir. Cobrar backoff já na primeira volta trocaria o laço do evento
  // envenenado por uma lentidão em todo deploy.
  //
  // O laço quebra igual: o envenenado volta UMA vez de graça, derruba o processo
  // de novo, e da segunda em diante paga 2, 4, 8… minutos até `MAX_ATTEMPTS`.
  // Uma tentativa a mais por evento envenenado é o preço de não tirar do órfão
  // legítimo a volta imediata que ele sempre teve.
  //
  // Um por um, com o guarda `status = 'processing'`: duas instâncias do dreno
  // (o laço do worker e o cron do app) podem ler a mesma linha presa, e só a
  // primeira a tocar incrementa — a segunda encontra `pending` e não faz nada.
  let reclamados = 0;
  for (const preso of presos ?? []) {
    const attempts = preso.attempts + 1;
    const dead = attempts >= MAX_ATTEMPTS;
    // `preso.attempts === 0` é a PRIMEIRA volta deste evento — ninguém o
    // reclamou antes. Ele volta pronto para o mesmo tique (ver acima).
    const primeiraVolta = preso.attempts === 0;
    const motivo = `tentativa não voltou em ${PROCESSING_STALE_MS / 60_000} min (processo derrubado?)`;
    const { data: tocado } = await admin
      .from("event_log")
      .update({
        status: dead ? "dead" : "pending",
        attempts,
        last_error: motivo,
        next_attempt_at: dead || primeiraVolta ? null : backoffAt(attempts),
        updated_at: nowIso,
      })
      .eq("id", preso.id)
      .eq("status", "processing")
      .select("id");
    if (!tocado?.length) continue;
    reclamados += 1;
    if (dead) {
      summary.dead += 1;
      await avisarEventoMorto(admin, preso, motivo);
    }
  }
  if (reclamados) {
    logger.warn("[event-log.drain] eventos presos em processing devolvidos à fila", {
      quantidade: reclamados,
    });
  }

  let pendentes = admin
    .from("event_log")
    // ─── A VARREDURA NÃO LEVA O CORPO DO EVENTO ────────────────────────────────
    //
    // `payload` e `metadata` são as colunas gordas desta tabela (JSONB com o
    // dossiê inteiro do que disparou o evento) e o dreno carrega 50 linhas por
    // tique para, na maioria delas, só decidir se a linha ainda interessa. Quem
    // chega ao handler — e é quem precisa do corpo — é UMA linha por vez, e o
    // claim abaixo (`update ... select`) já devolve a representação da linha
    // reclamada: o corpo vem de lá, no mesmo round-trip que já existia, sem uma
    // consulta a mais (#3878, item 3).
    //
    // `created_at` também vai no claim: quem monta o `EventRow` do handler
    // continua recebendo a idade do evento (`dispatcher.ts`, "quando o evento
    // foi emitido"), só que lida uma vez por linha processada em vez de 50.
    .select("id, organization_id, event_type, entity_kind, entity_id, consumed_by, attempts")
    .eq("status", "pending")
    .or(`next_attempt_at.is.null,next_attempt_at.lte.${nowIso}`)
    .in("event_type", handledTypes);
  if (escopo) pendentes = pendentes.eq("organization_id", escopo.organizationId);
  // O escopado lê do mais NOVO: a mensagem desta requisição acabou de entrar, e
  // do mais antigo um acúmulo da organização (rajada de respostas a uma
  // campanha) ocupava o limite inteiro e ela nunca entrava no lote.
  const { data: rows, error } = await pendentes
    .order("created_at", { ascending: !escopo })
    .limit(limit);

  if (error) {
    logger.error("[event-log.drain] select failed", { error: error.message });

    return summary;
  }

  // ─── ORGANIZAÇÃO PARADA ────────────────────────────────────────────────────
  //
  // Uma consulta por lote, `in (...)`, ANTES de reclamar qualquer linha. Org
  // que não volta da leitura conta como parada (falha fechada). Se a LEITURA
  // falha, o lote inteiro espera o próximo tique: consumir às cegas marcaria
  // `skipped` para sempre o handler "pula" de uma org operante.
  const orgIds = [...new Set((rows ?? []).map((r) => (r as { organization_id: string }).organization_id))];
  const parados = new Set<string>();
  if (orgIds.length) {
    const { data: orgs, error: orgErr } = await admin
      .from("organizations")
      .select("id, status")
      .in("id", orgIds);
    if (orgErr) {
      logger.error("[event-log.drain] status das organizações indisponível — lote adiado", {
        error: orgErr.message,
      });
      return summary;
    }
    const operantes = new Set(
      ((orgs ?? []) as Array<{ id: string; status: string | null }>)
        .filter((o) => ehOperante(o.status))
        .map((o) => o.id),
    );
    for (const id of orgIds) if (!operantes.has(id)) parados.add(id);
  }

  for (const raw of rows ?? []) {
    const row = raw as unknown as EventRow;
    summary.scanned += 1;

    // Linha em que o escopo já fez a sua parte e só espera o worker: reclamá-la
    // tiraria a linha do laço do worker por um instante sem efeito nenhum.
    if (
      escopo &&
      !getRegisteredHandlers().some(
        (h) =>
          escopo.handlers.includes(h.key) &&
          h.events.includes(row.event_type) &&
          !row.consumed_by.includes(h.key),
      )
    )
      continue;

    // Claim otimista — outra instância pode ter pego a mesma linha.
    //
    // O `consumed_by` e o `attempts` que valem são os que o CLAIM devolve, não
    // os da leitura do lote. Entre a leitura e o claim, outro dreno (o escopado
    // do webhook, ou um segundo global) pode ter reclamado a linha, rodado
    // handlers e devolvido a `pending` com o `consumed_by` acrescido — e o claim
    // passa, porque a linha está `pending` de novo. Filtrar pela leitura velha
    // rodava esses handlers outra vez: o "cliente voltou" mandava a mensagem
    // proativa em dobro. O claim é exclusivo (pending → processing), então o
    // que ele devolve não muda até esta instância soltar a linha.
    // Revalide também o prazo: outro dreno pode ter devolvido a linha a
    // `pending` com retry/backoff futuro depois da leitura do lote.
    //
    // E é AQUI que o corpo do evento entra (#3878, item 3): o `select` do lote
    // não traz `payload`/`metadata`/`created_at`, e este `update ... select` —
    // que já existia para revalidar o claim — devolve a representação da linha
    // reclamada, ou seja, de UMA linha, a que vai ser processada agora. Zero
    // consulta a mais, e as 49 linhas do lote que não chegam ao handler nunca
    // pagam o JSONB delas.
    const { data: claimed } = await admin
      .from("event_log")
      .update({ status: "processing", updated_at: new Date().toISOString() })
      .eq("id", row.id)
      .eq("status", "pending")
      .or(`next_attempt_at.is.null,next_attempt_at.lte.${nowIso}`)
      .select("id, consumed_by, attempts, payload, metadata, created_at");
    const reclamada = claimed?.[0] as Partial<
      Pick<EventRow, "consumed_by" | "attempts" | "payload" | "metadata" | "created_at">
    > | undefined;
    if (!reclamada) continue;
    // ponytail: as colunas são NOT NULL e o `select` as pede, então no banco
    // sempre voltam; o `??` só cobre dublês de teste que devolvem `{ id }`.
    row.consumed_by = reclamada.consumed_by ?? row.consumed_by;
    row.attempts = reclamada.attempts ?? row.attempts;
    row.payload = (reclamada.payload ?? row.payload) as Record<string, unknown>;
    row.metadata = (reclamada.metadata ?? row.metadata) as Record<string, unknown>;
    if (reclamada.created_at) row.created_at = reclamada.created_at;

    const results = await dispatchEvent(row, {
      orgParada: parados.has(row.organization_id),
      soHandlers: escopo?.handlers,
    });

    const okKeys = results
      .filter((r) => r.status === "ok" || r.status === "skipped")
      .map((r) => r.consumer_key);
    const consumedBy = [...new Set([...row.consumed_by, ...okKeys])];

    if (escopo) {
      const falhas = results.filter((r) => r.status === "retry" || r.status === "error");
      if (falhas.length) {
        logger.warn("[event-log.drain] handler do dreno escopado não concluiu — fica para o worker", {
          event_id: row.id,
          falhas: falhas.map((r) => `${r.consumer_key}: ${r.detail ?? r.status}`).join("; "),
        });
      }
      const faltaAlguem = getRegisteredHandlers().some(
        (h) => h.events.includes(row.event_type) && !consumedBy.includes(h.key),
      );
      // O motivo do `skipped` sobrevive aqui também (ver o ramo `done` abaixo).
      const pulados = results.filter((r) => r.status === "skipped" && r.detail);
      if (pulados.length)
        summary.pulados?.push(
          ...pulados.map((r) => `${row.event_type}/${r.consumer_key}: ${r.detail}`),
        );
      await admin
        .from("event_log")
        .update({
          status: faltaAlguem ? "pending" : "done",
          consumed_by: consumedBy,
          updated_at: new Date().toISOString(),
          ...(pulados.length
            ? { last_error: pulados.map((r) => `${r.consumer_key}: ${r.detail}`).join("; ") }
            : {}),
        })
        .eq("id", row.id);
      if (!faltaAlguem) summary.done += 1;
      else if (falhas.length) summary.retried += 1;
      else summary.deixados_ao_worker = (summary.deixados_ao_worker ?? 0) + 1;
      continue;
    }
    const retry = results.find((r) => r.status === "retry");
    const errors = results.filter((r) => r.status === "error");

    if (retry) {
      // Reagendamento benigno (ex. janela anti-ban): NÃO conta attempt — mesmo
      // que outro handler do mesmo tick tenha retornado erro (esse handler
      // nunca entrou em consumed_by, então ele reroda no próximo tick; aqui só
      // preservamos o last_error dele pra visibilidade/observabilidade).
      // retry_at é opcional no HandlerResult — sem ele, aplica o mesmo backoff
      // do branch de erro pra não busy-loop reprocessando a cada tick.
      const retryAt = retry.retry_at ?? backoffAt(row.attempts + 1);
      await admin
        .from("event_log")
        .update({
          status: "pending",
          consumed_by: consumedBy,
          next_attempt_at: retryAt,
          updated_at: new Date().toISOString(),
          ...(errors.length
            ? {
                last_error: errors
                  .map((e) => `${e.consumer_key}: ${e.detail ?? "error"}`)
                  .join("; "),
              }
            : {}),
        })
        .eq("id", row.id);
      summary.retried += 1;
    } else if (errors.length) {
      const attempts = row.attempts + 1;
      const dead = attempts >= MAX_ATTEMPTS;
      const motivo = errors.map((e) => `${e.consumer_key}: ${e.detail ?? "error"}`).join("; ");
      await admin
        .from("event_log")
        .update({
          status: dead ? "dead" : "pending",
          attempts,
          consumed_by: consumedBy,
          last_error: motivo,
          next_attempt_at: dead ? null : backoffAt(attempts),
          updated_at: new Date().toISOString(),
        })
        .eq("id", row.id);
      if (dead) await avisarEventoMorto(admin, row, motivo);
      summary[dead ? "dead" : "failed"] += 1;
    } else {
      // O MOTIVO DE UM `skipped` SOBREVIVE À LINHA.
      //
      // `skipped` conta como sucesso — e deve mesmo: o handler decidiu que não
      // era caso dele. Mas o `detail` era DESCARTADO por construção, e com ele
      // a única evidência de por que um evento não fez nada. Quem investigasse
      // "subi o material e não aconteceu nada" encontrava uma linha `done` sem
      // uma palavra de explicação.
      //
      // Não muda o desfecho do evento; só deixa de jogar fora a resposta.
      const pulados = results.filter((r) => r.status === "skipped" && r.detail);
      // E o motivo sai também no RESUMO, não só na linha.
      //
      // A linha basta para quem tem psql; não basta para o CI, onde o único
      // artefato que sobrevive ao job é o trace — e o trace guarda o CORPO da
      // resposta HTTP. Um e2e que morre porque o gatilho pulou ficava sem poder
      // dizer QUAL pulo foi: travou por horas o diagnóstico de
      // `gatilho-de-etapa.spec.ts`, com `failed=0` e nenhuma pista.
      if (pulados.length)
        summary.pulados?.push(
          ...pulados.map((r) => `${row.event_type}/${r.consumer_key}: ${r.detail}`),
        );
      await admin
        .from("event_log")
        .update({
          status: "done",
          consumed_by: consumedBy,
          updated_at: new Date().toISOString(),
          ...(pulados.length
            ? { last_error: pulados.map((r) => `${r.consumer_key}: ${r.detail}`).join("; ") }
            : {}),
        })
        .eq("id", row.id);
      summary.done += 1;
    }
  }
  return summary;
}
