/**
 * ReportResolver — ADR-0002 post-write_report settlement.
 *
 * After a worker REPORT lands:
 * 1. Rebuild ledger (pending_pm_review / waiting_pm_consolidation projection)
 * 2. Lifecycle `active` tasks: physical submit_review
 * 3. Hot_path `fcop/tasks/` tasks: projection only (no silent early return)
 */

import { basename, join } from "node:path";
import { promises as fs } from "node:fs";

import { shouldIgnoreCoordinationWatchPath } from "../_internal/report-ephemeral.ts";
import { LedgerBuilder } from "../ledger/LedgerBuilder.ts";
import {
  parseMarkdownFrontmatter,
  resolveReportTaskIdFromContent,
  strField,
} from "../ledger/frontmatter.ts";
import { resolveDriver } from "../lifecycle/authorityDefaults.ts";
import { resolveLedgerLayout } from "../ledger/paths.ts";
import { areAllChildrenSettledForRoot } from "../ledger/lifecycleProjection.ts";
import { resolveReviewLinkedTaskId } from "../review/resolveReviewLinkedTaskId.ts";
import { isGovernanceReportToPm } from "../fcop/governance.ts";
import { TaskFrontmatterStore } from "../lifecycle/TaskFrontmatterStore.ts";
import {
  findTaskLocationById,
  findTaskPathById,
} from "../lifecycle/taskPathUtils.ts";
import type { TaskFm } from "../lifecycle/types.ts";
import { LifecycleKernel } from "../lifecycle/LifecycleKernel.ts";
import type { PanelEventBridge } from "../panel/PanelEventBridge.ts";
import {
  formatSkipSummaryLines,
  isAutoGeneratedReciprocalReport,
  recordReconcileSkip,
  reportStatusDone,
  taskStageSkipReason,
  type ReconcileSummaryCounters,
  type SubmitReviewSkipReason,
} from "./reportSubmitEligibility.ts";
import { evaluateReworkSettlement } from "./reworkSettlement.ts";
import { isTaskReopenedForReworkFromLedger } from "./taskReworkSemantics.ts";
import {
  isPmAdminFinalSummaryReport,
  resolveSettlementRootId,
} from "./reportParenting.ts";
import { selectCanonicalPmFinalReport } from "./selectCanonicalPmFinalReport.ts";
import { tryApplyLateReportIntake } from "../pm/lateReportIntake.ts";
import { evaluateProductDeliveryGate } from "../pm/ProductDeliveryGovernance.ts";
import { evaluatePmSummaryGate } from "../pm/PmSummaryGate.ts";

export interface ReportResolverOpts {
  projectRoot: string;
  lifecycleRoot: string;
  moveTimeoutMs?: number;
  logger?: {
    info?(msg: string): void;
    warn?(msg: string): void;
    debug?(msg: string): void;
  };
  panelEvents?: PanelEventBridge;
  /** When set, skip reasons increment counters instead of per-line info logs. */
  reconcileSummary?: ReconcileSummaryCounters;
  /** Verbose per-skip lines (default: env CODEFLOWMU_REPORT_RESOLVER_DEBUG=1). */
  debug?: boolean;
  /** When true, automatic reconcile must not write task frontmatter. */
  yamlFallbackMode?: boolean;
}

function reportSenderMaySubmitReview(reportSender: string, fm: TaskFm): boolean {
  const s = reportSender.trim().toUpperCase();
  const executor = resolveDriver(fm);
  return s === executor && executor.length > 0;
}

function normalizeId(id: string): string {
  return id.replace(/\.md$/i, "").trim();
}

export type ReportResolveOutcome = SubmitReviewSkipReason | "reconciled" | "noop";

export class ReportResolver {
  readonly #projectRoot: string;
  readonly #lifecycleRoot: string;
  readonly #kernel: LifecycleKernel;
  readonly #store: TaskFrontmatterStore;
  readonly #ledger: LedgerBuilder;
  readonly #timeoutMs: number;
  readonly #log: NonNullable<ReportResolverOpts["logger"]>;
  readonly #panelEvents: PanelEventBridge | undefined;
  readonly #reconcileSummary: ReconcileSummaryCounters | undefined;
  readonly #debug: boolean;
  readonly #yamlFallbackMode: boolean;
  readonly #localSkipCounts = new Map<string, number>();

  constructor(opts: ReportResolverOpts) {
    this.#projectRoot = opts.projectRoot;
    this.#lifecycleRoot = opts.lifecycleRoot;
    this.#yamlFallbackMode = opts.yamlFallbackMode === true;
    this.#kernel = new LifecycleKernel({
      lifecycleRoot: opts.lifecycleRoot,
      yamlFallbackMode: this.#yamlFallbackMode,
    });
    this.#store = new TaskFrontmatterStore();
    this.#ledger = new LedgerBuilder({ projectRoot: opts.projectRoot });
    this.#timeoutMs = opts.moveTimeoutMs ?? 5_000;
    this.#log = opts.logger ?? {};
    this.#panelEvents = opts.panelEvents;
    this.#reconcileSummary = opts.reconcileSummary;
    this.#debug =
      opts.debug ??
      process.env.CODEFLOWMU_REPORT_RESOLVER_DEBUG === "1";
  }

  /** Emit aggregated skip summary (batch reconcile / debug). */
  flushSkipSummary(): void {
    if (this.#localSkipCounts.size === 0) return;
    const line = formatSkipSummaryLines(this.#localSkipCounts);
    if (line) this.#log.info?.(line);
    this.#localSkipCounts.clear();
  }

  #recordSkip(reason: SubmitReviewSkipReason, detail?: string): void {
    if (this.#reconcileSummary) {
      recordReconcileSkip(this.#reconcileSummary, reason);
    } else {
      this.#localSkipCounts.set(
        reason,
        (this.#localSkipCounts.get(reason) ?? 0) + 1,
      );
    }
    if (this.#debug && detail) {
      this.#log.debug?.(`[ReportResolver] ${detail}`);
      if (!this.#log.debug) this.#log.info?.(`[ReportResolver] ${detail}`);
    }
  }

  /** Fire-and-forget hook — call after write_report or ReportWatcher `add`. */
  scheduleResolve(reportFilePath: string): void {
    if (shouldIgnoreCoordinationWatchPath(reportFilePath)) return;
    void this.resolve(reportFilePath).catch((err) => {
      this.#log.warn?.(
        `[ReportResolver] failed for ${basename(reportFilePath)}: ${
          err instanceof Error ? err.message : String(err)
        }`,
      );
    });
  }

  async resolve(reportFilePath: string): Promise<ReportResolveOutcome> {
    if (shouldIgnoreCoordinationWatchPath(reportFilePath)) return "noop";
    const reportName = basename(reportFilePath);
    if (/OBSERVATION/i.test(reportName)) return "noop";

    const reportRaw = await fs.readFile(reportFilePath, "utf-8");
    const reportFm = parseMarkdownFrontmatter(reportRaw);
    const reportSender = strField(reportFm, "sender");
    if (!reportSender) return "noop";
    if (isGovernanceReportToPm(reportName, reportSender)) return "noop";

    if (isAutoGeneratedReciprocalReport(reportRaw)) {
      this.#recordSkip(
        "skipped_auto",
        `skip submit_review: auto-generated reciprocal report ${reportName}`,
      );
      return "skipped_auto";
    }

    await this.#ledger.rebuild();

    const taskId = normalizeId(resolveReportTaskIdFromContent(reportFm, reportRaw, reportName));
    if (!taskId) {
      this.#recordSkip("skipped_no_task", `skip: no task_id in ${reportName}`);
      return "skipped_no_task";
    }

    const reportId = normalizeId(strField(reportFm, "report_id") || reportName.replace(/\.md$/i, ""));
    const lateIntake = await tryApplyLateReportIntake({
      projectRoot: this.#projectRoot,
      reportId,
      reportFilePath,
      filename: reportName,
      taskId,
      reportFm,
      sender: reportSender,
      recipient: strField(reportFm, "recipient"),
      logger: this.#log,
    });
    if (lateIntake) {
      this.#recordSkip(
        "skipped_late_intake",
        `late report intake noted_only for ${reportName} (related=${lateIntake.related_task_id})`,
      );
      return "skipped_late_intake";
    }

    const childOutcome = await this.#maybeSubmitChildTaskReview(
      reportFilePath,
      reportName,
      reportSender,
      taskId,
      reportFm,
    );
    await this.#maybeSubmitRootThreadReview(taskId);
    return childOutcome;
  }

  /** After hot_path PM approve — refresh thread consolidation flags. */
  async reconcileThreadSettlement(triggerTaskId: string): Promise<void> {
    await this.#ledger.rebuild();
    await this.#maybeSubmitRootThreadReview(triggerTaskId);
  }

  #yamlFallbackLifecycleBlocked(detail: string): ReportResolveOutcome {
    this.#recordSkip("skipped_yaml_fallback", detail);
    return "skipped_yaml_fallback";
  }

  async #maybeSubmitChildTaskReview(
    reportFilePath: string,
    reportName: string,
    reportSender: string,
    taskId: string,
    reportFm: Record<string, unknown>,
  ): Promise<ReportResolveOutcome> {
    const layout = resolveLedgerLayout(this.#projectRoot);
    const located = await findTaskLocationById(this.#lifecycleRoot, taskId, {
      hotTasksDir: layout.tasksDir,
    });
    if (!located) {
      this.#recordSkip(
        "skipped_no_task",
        `task ${taskId} not found under _lifecycle or fcop/tasks`,
      );
      this.#log.warn?.(
        `[ReportResolver] task ${taskId} not found under _lifecycle or fcop/tasks`,
      );
      return "skipped_no_task";
    }

    const { fm: taskFm, body } = await this.#store.read(located.path);
    if (!reportSenderMaySubmitReview(reportSender, taskFm)) {
      this.#recordSkip(
        "skipped_sender",
        `skip submit_review: sender ${reportSender} != executor`,
      );
      return "skipped_sender";
    }

    const parentId = strField(taskFm, "parent").trim();
    if (parentId) {
      await this.#projectChildPendingPmReview(
        located,
        taskFm,
        body,
        reportFilePath,
        reportName,
        reportSender,
        taskId,
        reportFm,
      );
      return reportStatusDone(reportFm) ? "reconciled" : "skipped_not_done";
    }

    if (located.storage === "hot_path") {
      await this.#projectChildPendingPmReview(
        located,
        taskFm,
        body,
        reportFilePath,
        reportName,
        reportSender,
        taskId,
        reportFm,
      );
      return reportStatusDone(reportFm) ? "reconciled" : "skipped_not_done";
    }

    let lifecycleLocated = located;
    if (lifecycleLocated.stage === "inbox") {
      await this.#kernel.runtimeDispatchInboxToActive(lifecycleLocated.path);
      const again = await findTaskPathById(this.#lifecycleRoot, taskId);
      if (!again) return "skipped_no_task";
      lifecycleLocated = { ...again, storage: "lifecycle" as const };
    }
    const stageSkip = taskStageSkipReason(lifecycleLocated.stage);
    if (stageSkip) {
      this.#recordSkip(
        stageSkip,
        `skip submit_review: task in ${lifecycleLocated.stage}, need active`,
      );
      return stageSkip;
    }

    if (!reportStatusDone(reportFm)) {
      this.#recordSkip(
        "skipped_not_done",
        `skip submit_review: report status not done`,
      );
      return "skipped_not_done";
    }

    if (isTaskReopenedForReworkFromLedger(taskFm)) {
      const fcopRoot = join(this.#projectRoot, "fcop");
      const allTasks = await this.#ledger.listTasks(undefined, { pendingOnly: false });
      const settlement = await evaluateReworkSettlement({
        taskFm,
        taskId,
        reportId: reportName,
        fcopRoot,
        allTasks: allTasks.map((t) => ({
          task_id: t.task_id,
          thread_key: t.thread_key,
          recipient: t.recipient,
          bucket: t.bucket,
          display_status: t.yaml?.display_status as string | undefined,
          lifecycle_projection: t.yaml?.lifecycle_projection as string | undefined,
          review_status: t.yaml?.review_status as string | undefined,
          parent: t.parent,
          references: t.yaml?.references as string | string[] | undefined,
          created_at: t.created_at,
        })),
      });
      if (!settlement.settled) {
        this.#recordSkip(
          "skipped_rework",
          `skip submit_review: task ${taskId} in ADMIN rework; complete rework before resubmit`,
        );
        return "skipped_rework";
      }
      if (Object.keys(settlement.patch).length > 0) {
        await this.#store.patch(lifecycleLocated.path, settlement.patch);
        Object.assign(taskFm, settlement.patch);
      }
    }

    const tasks = await this.#ledger.listTasks();
    const hasUnsettledChildren = tasks.some(
      (t) => normalizeId(t.parent ?? "") === taskId,
    );
    if (hasUnsettledChildren) {
      await this.#maybeSubmitRootThreadReview(taskId);
      return "noop";
    }

    const result = await this.withTimeout("submit_review", () =>
      this.#kernel.submitReview({
        taskId,
        actor: reportSender,
        reportId: reportName,
      }),
    );
    this.#log.info?.(
      `[ReportResolver] ${result.task_id}: active → review (report ${reportName})`,
    );
    if (this.#reconcileSummary) {
      recordReconcileSkip(this.#reconcileSummary, "reconciled");
    }
    this.#panelEvents?.emit("codeflowmu.lifecycle.task_to_review", {
      event: "lifecycle_task_to_review",
      task_id: result.task_id,
      report_file: reportFilePath,
      report_id: reportName,
      actor: reportSender,
      from_stage: "active",
      to_stage: "review",
    });

    await this.#ledger.rebuild();

    this.#log.info?.(
      `[ReportResolver] ${result.task_id}: awaiting PM business decision (no auto approve_review)`,
    );

    return "reconciled";
  }

  async #projectChildPendingPmReview(
    located: NonNullable<Awaited<ReturnType<typeof findTaskLocationById>>>,
    taskFm: TaskFm,
    body: string,
    reportFilePath: string,
    reportName: string,
    reportSender: string,
    taskId: string,
    reportFm: Record<string, unknown>,
  ): Promise<void> {
    if (located.storage === "lifecycle" && located.stage === "inbox") {
      await this.#kernel.runtimeDispatchInboxToActive(located.path);
      const again = await findTaskPathById(this.#lifecycleRoot, taskId);
      if (again) {
        located = { ...again, storage: "lifecycle" as const };
        const reread = await this.#store.read(again.path);
        Object.assign(taskFm, reread.fm);
      }
    }
    if (reportStatusDone(reportFm)) {
      taskFm.lifecycle_projection = "review";
      taskFm.display_status = "waiting_pm_review";
      await this.#store.write(located.path, taskFm, body);
    }
    await this.#ledger.rebuild();
    this.#log.info?.(
      `[ReportResolver] ${taskId}: pending_pm_review (${located.storage}, report ${reportName})`,
    );
    this.#panelEvents?.emit("codeflowmu.lifecycle.pending_pm_review", {
      event: "lifecycle_pending_pm_review",
      task_id: taskId,
      report_file: reportFilePath,
      report_id: reportName,
      actor: reportSender,
      storage: located.storage,
    });

    if (reportStatusDone(reportFm) && located.storage === "hot_path") {
      this.#log.info?.(
        `[ReportResolver] ${taskId}: report ready; awaiting explicit PM review (no auto approve/archive)`,
      );
    }
  }

  #emitRootReviewBlocked(rootTaskId: string): void {
    this.#panelEvents?.emit("codeflowmu.lifecycle.root_review_blocked", {
      event: "root_review_blocked",
      root_task_id: rootTaskId,
      reason: "waiting_pm_to_admin_done_report",
      child_tasks_settled: true,
    });
    this.#panelEvents?.emit("codeflowmu.pm.waiting_pm_summary", {
      event: "waiting_pm_summary",
      root_task_id: rootTaskId,
    });
  }

  async #markRootWaitingPmSummary(rootId: string): Promise<void> {
    const layout = resolveLedgerLayout(this.#projectRoot);
    const located = await findTaskLocationById(this.#lifecycleRoot, rootId, {
      hotTasksDir: layout.tasksDir,
    });
    if (!located || located.storage !== "lifecycle") return;
    const { fm, body } = await this.#store.read(located.path);
    fm.display_status = "waiting_pm_summary";
    await this.#store.write(located.path, fm, body);
  }

  #findPmToAdminDoneReportForRoot(
    rootId: string,
    reports: import("./types.ts").LedgerReportRecord[],
    threadKey?: string,
  ) {
    const { canonical } = selectCanonicalPmFinalReport(reports, {
      rootTaskId: rootId,
      threadKey,
    });
    if (!canonical) return undefined;
    if (canonical.status !== "done" && canonical.status !== "completed") {
      return undefined;
    }
    if (!isPmAdminFinalSummaryReport(rootId, canonical)) return undefined;
    return canonical;
  }

  async #maybeSubmitRootThreadReview(triggerTaskId: string): Promise<void> {
    const layout = resolveLedgerLayout(this.#projectRoot);
    const tasks = await this.#ledger.listTasks();
    const trigger = tasks.find((t) => t.task_id === triggerTaskId);
    if (!trigger) return;

    const rootId = resolveSettlementRootId(trigger, tasks);
    if (!rootId) return;

    const threadsPath = join(layout.ledgerDir, "threads.jsonl");
    let threads: import("./types.ts").LedgerThreadRecord[] = [];
    try {
      const raw = await fs.readFile(threadsPath, "utf-8");
      for (const line of raw.split("\n")) {
        if (!line.trim()) continue;
        try {
          threads.push(JSON.parse(line) as import("./types.ts").LedgerThreadRecord);
        } catch {
          /* skip */
        }
      }
    } catch {
      return;
    }

    const thread = threads.find((th) => th.root_task_id === rootId);
    if (!thread) return;

    const reports = await this.#loadAllReports(layout.ledgerDir);
    const reviewApproved = await this.#ledgerReviewApprovedMap(tasks);

    if (
      !areAllChildrenSettledForRoot(rootId, tasks, thread, reports, reviewApproved)
    ) {
      return;
    }

    const rootLocated = await findTaskLocationById(this.#lifecycleRoot, rootId, {
      hotTasksDir: layout.tasksDir,
    });

    const pmReport = this.#findPmToAdminDoneReportForRoot(
      rootId,
      reports,
      thread.thread_key,
    );

    if (rootLocated?.storage === "hot_path") {
      if (!pmReport) {
        this.#emitRootReviewBlocked(rootId);
      }
      this.#log.info?.(
        `[ReportResolver] root ${rootId}: waiting_pm_consolidation (hot_path, ledger projected)`,
      );
      await this.#ledger.rebuild();
      return;
    }

    if (!rootLocated || rootLocated.stage !== "active") return;

    if (!pmReport) {
      this.#emitRootReviewBlocked(rootId);
      await this.#markRootWaitingPmSummary(rootId);
      this.#log.info?.(
        `[ReportResolver] root ${rootId} children settled; awaiting PM→ADMIN summary before submit_review`,
      );
      return;
    }

    const { fm: rootFm, body: rootBody } = await this.#store.read(rootLocated.path);
    const productGate = await evaluateProductDeliveryGate({
      projectRoot: this.#projectRoot,
      taskId: rootId,
      taskBody: rootBody,
      taskFrontmatter: rootFm,
    });
    const summaryGate = evaluatePmSummaryGate({
      thread,
      tasks,
      reports,
      root_task_id: rootId,
      root_body: rootBody,
      allow_existing_final: true,
    });
    if (!productGate.allowed || !summaryGate.ok) {
      const findings = [
        ...productGate.findings,
        ...(!summaryGate.ok ? [summaryGate.skipped_reason] : []),
      ];
      this.#panelEvents?.emit("codeflowmu.lifecycle.root_review_blocked", {
        event: "root_review_blocked",
        root_task_id: rootId,
        reason: "close_gate_failed",
        findings,
      });
      await this.#markRootWaitingPmSummary(rootId);
      this.#log.info?.(
        `[ReportResolver] root ${rootId} close_gate_failed: ${findings.join(",")}`,
      );
      return;
    }
    if (isTaskReopenedForReworkFromLedger(rootFm)) {
      const fcopRoot = join(this.#projectRoot, "fcop");
      const settlement = await evaluateReworkSettlement({
        taskFm: rootFm,
        taskId: rootId,
        reportId: pmReport.report_id,
        fcopRoot,
        allTasks: tasks.map((t) => ({
          task_id: t.task_id,
          thread_key: t.thread_key,
          recipient: t.recipient,
          bucket: t.bucket,
          display_status: t.yaml?.display_status as string | undefined,
          lifecycle_projection: t.yaml?.lifecycle_projection as string | undefined,
          review_status: t.yaml?.review_status as string | undefined,
          parent: t.parent,
          references: t.yaml?.references as string | string[] | undefined,
          created_at: t.created_at,
        })),
      });
      if (!settlement.settled) {
        this.#emitRootReviewBlocked(rootId);
        await this.#markRootWaitingPmSummary(rootId);
        this.#log.info?.(
          `[ReportResolver] root ${rootId} in rework; ignoring stale PM final for submit_review`,
        );
        await this.#ledger.rebuild();
        return;
      }
      if (Object.keys(settlement.patch).length > 0) {
        await this.#store.patch(rootLocated.path, settlement.patch);
      }
    }

    const result = await this.withTimeout("root submit_review", () =>
      this.#kernel.submitReview({
        taskId: rootId,
        actor: "PM",
        reportId: pmReport.report_id,
        reason: "ReportResolver: all child tasks settled",
      }),
    );
    this.#log.info?.(
      `[ReportResolver] root ${result.task_id}: active → review (PM settlement)`,
    );
    await this.#ledger.rebuild();
  }

  async #loadAllReports(ledgerDir: string): Promise<
    import("./types.ts").LedgerReportRecord[]
  > {
    const path = join(ledgerDir, "reports.jsonl");
    const out: import("./types.ts").LedgerReportRecord[] = [];
    try {
      const raw = await fs.readFile(path, "utf-8");
      for (const line of raw.split("\n")) {
        if (!line.trim()) continue;
        try {
          out.push(JSON.parse(line) as import("./types.ts").LedgerReportRecord);
        } catch {
          /* skip */
        }
      }
    } catch {
      /* empty */
    }
    return out;
  }

  async #ledgerReviewApprovedMap(
    tasks: import("./types.ts").LedgerTaskRecord[],
  ): Promise<Map<string, boolean>> {
    const layout = resolveLedgerLayout(this.#projectRoot);
    return this.#loadReviewApprovedMap(tasks, layout.reviewsDir);
  }

  async #loadReviewApprovedMap(
    tasks: import("./types.ts").LedgerTaskRecord[],
    reviewsDir: string,
  ): Promise<Map<string, boolean>> {
    const map = new Map<string, boolean>();
    for (const t of tasks) {
      if (t.bucket !== "tasks" && t.bucket !== "active") continue;
      try {
        const { fm } = await this.#store.read(t.path);
        if (String(fm.review_status ?? "").toLowerCase() === "approved") {
          map.set(t.task_id, true);
        }
      } catch {
        /* skip */
      }
    }
    try {
      const names = await fs.readdir(reviewsDir);
      for (const name of names) {
        if (!/^REVIEW-/i.test(name) || !name.endsWith(".md")) continue;
        const raw = await fs.readFile(join(reviewsDir, name), "utf-8");
        const fm = parseMarkdownFrontmatter(raw);
        if (String(fm.decision ?? "").toLowerCase() !== "approved") continue;
        const linkedTaskId = resolveReviewLinkedTaskId(fm, { filename: name });
        if (linkedTaskId) map.set(linkedTaskId, true);
      }
    } catch {
      /* no reviews */
    }
    return map;
  }

  private async withTimeout<T>(
    label: string,
    work: () => Promise<T>,
  ): Promise<T> {
    let timer: ReturnType<typeof setTimeout> | undefined;
    const timeout = new Promise<never>((_, reject) => {
      timer = setTimeout(
        () => reject(new Error(`${label} timed out after ${this.#timeoutMs}ms`)),
        this.#timeoutMs,
      );
    });
    try {
      return await Promise.race([work(), timeout]);
    } finally {
      if (timer !== undefined) clearTimeout(timer);
    }
  }
}
