/**
 * LifecycleGovernor — async, non-blocking FCoP v3 lifecycle enforcement.
 *
 * CodeFlowMu 2.0 task-report hot path: agents start from TASK content already
 * injected by TaskDispatcher; Python `claim_task` must NOT sit on the critical
 * path. This module performs governance moves via `LifecycleStateMachine` with
 * a hard timeout. Failures are logged only — they never block dispatch
 * or agent execution.
 */

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

import {
  LifecycleKernel,
  resolveTaskFileForMutation,
  stageFromPath,
} from "../lifecycle/index.ts";
import { scheduleLedgerRebuild } from "../ledger/scheduleLedgerRebuild.ts";
import { ReportResolver } from "../ledger/ReportResolver.ts";
import { isCanonicalReportMarkdownFilename } from "../_internal/report-ephemeral.ts";
import {
  emptyReconcileSummary,
  formatPmToAdminReconcileSummary,
  isAutoGeneratedReciprocalReport,
  parseReportFile,
  recordReconcileSkip,
  reportNeedsAdminAttention,
  reportStatusDone,
  selectCanonicalPmToAdminReports,
  taskStageSkipReason,
} from "../ledger/reportSubmitEligibility.ts";
import { findTaskLocationById } from "../lifecycle/taskPathUtils.ts";
import { resolveLedgerLayout } from "../ledger/paths.ts";
import type { PanelEventBridge } from "../panel/PanelEventBridge.ts";
import { taskLifecycleMutex } from "../_internal/KeyedMutex.ts";
import { YamlFallbackWriteBlockedError } from "../lifecycle/yamlFallbackGuard.ts";
import type { TaskExecutionDecision } from "./DispatchAttemptStore.ts";

export interface LifecycleGovernorLogger {
  info?: (msg: string) => void;
  warn?: (msg: string) => void;
}

export interface LifecycleGovernorOpts {
  /** Absolute path to `fcop/_lifecycle/` (parent of inbox/active/review/done/archive). */
  lifecycleRoot: string;
  /** Per-move timeout (ms). Default 5000. */
  moveTimeoutMs?: number;
  logger?: LifecycleGovernorLogger;
  /** Optional sink for panel / SSE / JSONL (log center). */
  panelEvents?: PanelEventBridge;
  /** When true, watcher/reconcile automatic lifecycle writes are read-only. */
  yamlFallbackMode?: boolean;
}

const DEFAULT_MOVE_TIMEOUT_MS = 5_000;

/** PM hot-path reports that should trigger submit_review on linked active tasks. */
const PM_TO_ADMIN_REPORT_RE =
  /^REPORT-\d{8}-\d{3,}-PM-to-ADMIN(-[a-z][a-z0-9-]*)?\.md$/i;

async function withTimeout<T>(
  label: string,
  timeoutMs: number,
  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 ${timeoutMs}ms`)),
      timeoutMs,
    );
  });
  try {
    return await Promise.race([work(), timeout]);
  } finally {
    if (timer !== undefined) clearTimeout(timer);
  }
}

export class LifecycleGovernor {
  private readonly _lifecycleRoot: string;
  private readonly _kernel: LifecycleKernel;
  private readonly _resolver: ReportResolver;
  private readonly _timeoutMs: number;
  private readonly _log: LifecycleGovernorLogger;
  private readonly _projectRoot: string;
  private readonly _panelEvents: PanelEventBridge | undefined;
  private readonly _yamlFallbackMode: boolean;

  constructor(opts: LifecycleGovernorOpts & { projectRoot?: string }) {
    this._lifecycleRoot = opts.lifecycleRoot;
    this._yamlFallbackMode = opts.yamlFallbackMode === true;
    this._kernel = new LifecycleKernel({
      lifecycleRoot: opts.lifecycleRoot,
      yamlFallbackMode: this._yamlFallbackMode,
    });
    this._projectRoot =
      opts.projectRoot ??
      join(opts.lifecycleRoot, "..", "..").replace(/\\/g, "/");
    this._panelEvents = opts.panelEvents;
    this._resolver = new ReportResolver({
      projectRoot: this._projectRoot,
      lifecycleRoot: opts.lifecycleRoot,
      moveTimeoutMs: opts.moveTimeoutMs,
      logger: opts.logger,
      yamlFallbackMode: this._yamlFallbackMode,
      ...(opts.panelEvents ? { panelEvents: opts.panelEvents } : {}),
    });
    this._timeoutMs = opts.moveTimeoutMs ?? DEFAULT_MOVE_TIMEOUT_MS;
    this._log = opts.logger ?? {};
  }

  /** ADR-0002: exclusive lifecycle syscall surface for runtime. */
  get lifecycleKernel(): LifecycleKernel {
    return this._kernel;
  }

  /**
   * Fire-and-forget: inbox → active after runtime dispatch (not before agent work).
   */
  scheduleInboxToActive(taskFilePath: string): void {
    void this._moveInboxToActive(taskFilePath).catch((err) => {
      if (err instanceof YamlFallbackWriteBlockedError) return;
      this._log.warn?.(
        `[LifecycleGovernor] inbox→active skipped for ${basename(taskFilePath)}: ${
          err instanceof Error ? err.message : String(err)
        }`,
      );
    });
  }

  /** Await explicit dispatch: inbox → active before session start. */
  async awaitDispatchInboxToActive(
    taskFilePath: string,
    claim?: {
      attemptId?: string;
      leaseId?: string;
      agentId?: string;
      decision?: TaskExecutionDecision;
    },
  ): Promise<string> {
    if (claim?.decision && claim.decision.decision !== "ALLOW") {
      throw new Error(
        `LIFECYCLE_DECISION_REJECTED: ${claim.decision.decision}/${claim.decision.reason}`,
      );
    }
    await this._moveInboxToActive(taskFilePath, claim);
    const resolved = await resolveTaskFileForMutation(
      taskFilePath,
      this._lifecycleRoot,
    );
    return resolved;
  }

  /**
   * Fire-and-forget: when a worker REPORT lands, submit_review if sender matches executor.
   */
  scheduleTaskToReviewOnReport(reportFilePath: string): void {
    this._resolver.scheduleResolve(reportFilePath);
  }

  /** Await report settlement (active→review / pending_pm_review projection). */
  async resolveReportSettlement(
    reportFilePath: string,
    executionDecision?: import("./DispatchAttemptStore.ts").TaskExecutionDecision,
  ): Promise<import("../ledger/ReportResolver.ts").ReportResolveOutcome> {
    if (executionDecision && executionDecision.decision !== "ALLOW") {
      throw new Error(
        `LIFECYCLE_DECISION_NOT_ALLOWED: ${executionDecision.operation_id}=${executionDecision.decision}`,
      );
    }
    return this._resolver.resolve(reportFilePath);
  }

  /**
   * Startup reconcile: scan existing PM→ADMIN reports and submit_review for stuck
   * active tasks (ReportWatcher uses ignoreInitial and misses pre-existing files).
   */
  reconcileStuckPmOutboundReports(reportsDir: string): void {
    void this._reconcileStuckPmOutboundReports(reportsDir).catch((err) => {
      this._log.warn?.(
        `[LifecycleGovernor] reconcileStuckPmOutboundReports: ${
          err instanceof Error ? err.message : String(err)
        }`,
      );
    });
  }

  private async _reconcileStuckPmOutboundReports(
    reportsDir: string,
  ): Promise<void> {
    let names: string[];
    try {
      names = await fs.readdir(reportsDir);
    } catch {
      return;
    }

    const counters = emptyReconcileSummary();
    const parsed = [];
    for (const name of names) {
      if (!isCanonicalReportMarkdownFilename(name)) continue;
      if (!PM_TO_ADMIN_REPORT_RE.test(name)) continue;
      counters.scanned += 1;
      const path = join(reportsDir, name);
      try {
        const raw = await fs.readFile(path, "utf-8");
        parsed.push(parseReportFile(path, name, raw));
      } catch {
        recordReconcileSkip(counters, "skipped_other");
      }
    }

    const { canonical, duplicatePaths } = selectCanonicalPmToAdminReports(parsed);
    const canonicalPaths = new Set(canonical.map((r) => r.path));
    const layout = resolveLedgerLayout(this._projectRoot);

    for (const r of parsed) {
      if (duplicatePaths.has(r.path)) {
        recordReconcileSkip(counters, "skipped_duplicate");
        continue;
      }
      if (isAutoGeneratedReciprocalReport(r.raw)) {
        recordReconcileSkip(counters, "skipped_auto");
        continue;
      }
      if (reportNeedsAdminAttention(r.fm)) {
        recordReconcileSkip(counters, "admin_attention");
        this._panelEvents?.emit("codeflowmu.lifecycle.admin_attention", {
          event: "admin_attention",
          report_id: r.filename,
          task_id: r.taskId,
          status: String(r.fm.status ?? "blocked"),
        });
        continue;
      }
      if (!reportStatusDone(r.fm)) {
        recordReconcileSkip(counters, "skipped_not_done");
        continue;
      }
      if (!canonicalPaths.has(r.path)) continue;

      if (!r.taskId) {
        recordReconcileSkip(counters, "skipped_no_task");
        continue;
      }

      const located = await findTaskLocationById(this._lifecycleRoot, r.taskId, {
        hotTasksDir: layout.tasksDir,
      });
      const stageSkip = taskStageSkipReason(located?.stage);
      if (stageSkip) {
        recordReconcileSkip(counters, stageSkip);
        continue;
      }

      counters.eligible += 1;
      const resolver = new ReportResolver({
        projectRoot: this._projectRoot,
        lifecycleRoot: this._lifecycleRoot,
        moveTimeoutMs: this._timeoutMs,
        logger: this._log,
        yamlFallbackMode: this._yamlFallbackMode,
        reconcileSummary: counters,
        ...(this._panelEvents ? { panelEvents: this._panelEvents } : {}),
      });
      const outcome = await resolver.resolve(r.path);
      if (outcome !== "reconciled" && outcome !== "noop") {
        /* counted inside resolver */
      }
    }

    if (counters.scanned > 0) {
      this._log.info?.(formatPmToAdminReconcileSummary(counters));
    }
  }

  /**
   * After a dispatch session ends failed/timeout without REPORT: move active → inbox
   * (when runtime already promoted the file) or patch frontmatter when still in inbox.
   */
  async restoreToInboxAfterDispatchFailure(
    taskFilePath: string,
    reason: string,
  ): Promise<void> {
    const taskId = this._taskIdFromPath(taskFilePath);
    await taskLifecycleMutex.run(taskId, async () => {
      await this._restoreToInboxLocked(taskFilePath, reason);
    });
  }

  async repairInboxLifecycleSplit(
    taskFilePath: string,
    reason: string,
  ): Promise<string> {
    const taskId = this._taskIdFromPath(taskFilePath);
    await taskLifecycleMutex.run(taskId, async () => {
      const resolved = await resolveTaskFileForMutation(
        taskFilePath,
        this._lifecycleRoot,
      );
      if (stageFromPath(resolved, this._lifecycleRoot) !== "inbox") return;
      await this._kernel.runtimeRepairInboxSplit(resolved, reason);
      scheduleLedgerRebuild(this._projectRoot);
    });
    return resolveTaskFileForMutation(taskFilePath, this._lifecycleRoot);
  }

  private _taskIdFromPath(taskFilePath: string): string {
    return basename(taskFilePath).replace(/\.md$/i, "");
  }

  private async _restoreToInboxLocked(
    taskFilePath: string,
    reason: string,
  ): Promise<void> {
    let resolved: string;
    try {
      resolved = await resolveTaskFileForMutation(
        taskFilePath,
        this._lifecycleRoot,
      );
    } catch {
      return;
    }
    const stage = stageFromPath(resolved, this._lifecycleRoot);
    if (stage === "inbox") {
      try {
        const raw = await fs.readFile(resolved, "utf-8");
        const stateMatch = raw.match(/^state:\s*(\S+)/m);
        const state = stateMatch?.[1];
        if (
          state === "dispatched" ||
          state === "active" ||
          state === "running"
        ) {
          await this._kernel.runtimeRepairInboxSplit(resolved, reason);
          this._log.info?.(
            `[LifecycleGovernor] ${basename(resolved)}: inbox frontmatter ${state} → inbox (${reason})`,
          );
          scheduleLedgerRebuild(this._projectRoot);
        }
      } catch (err) {
        this._log.warn?.(
          `[LifecycleGovernor] inbox state patch skipped for ${basename(taskFilePath)}: ${
            err instanceof Error ? err.message : String(err)
          }`,
        );
      }
      return;
    }
    if (stage === "active") {
      await withTimeout("active→inbox restore", this._timeoutMs, async () => {
        await this._kernel.runtimeRestoreActiveToInbox(resolved, reason);
      });
      this._log.info?.(
        `[LifecycleGovernor] ${basename(resolved)}: active → inbox (${reason})`,
      );
      scheduleLedgerRebuild(this._projectRoot);
      return;
    }

    try {
      const raw = await fs.readFile(resolved, "utf-8");
      const stateMatch = raw.match(/^state:\s*(\S+)/m);
      const state = stateMatch?.[1];
      if (state !== "dispatched" && state !== "active") return;
      await this._kernel.runtimeRepairInboxSplit(resolved, reason);
      this._log.info?.(
        `[LifecycleGovernor] ${basename(resolved)}: frontmatter → inbox (${reason})`,
      );
    } catch (err) {
      this._log.warn?.(
        `[LifecycleGovernor] inbox restore skipped for ${basename(taskFilePath)}: ${
          err instanceof Error ? err.message : String(err)
        }`,
      );
    }
  }

  private async _moveInboxToActive(
    taskFilePath: string,
    claim?: { attemptId?: string; leaseId?: string; agentId?: string },
  ): Promise<void> {
    const taskId = this._taskIdFromPath(taskFilePath);
    await taskLifecycleMutex.run(taskId, async () => {
      let resolved: string;
      try {
        resolved = await resolveTaskFileForMutation(
          taskFilePath,
          this._lifecycleRoot,
        );
      } catch {
        return;
      }
      const stage = stageFromPath(resolved, this._lifecycleRoot);
      if (stage !== "inbox") return;

      await withTimeout("inbox→active", this._timeoutMs, async () => {
        await this._kernel.runtimeDispatchInboxToActive(resolved, claim);
      });
      this._log.info?.(
        `[LifecycleGovernor] ${basename(resolved)}: inbox → active (runtime_dispatch)`,
      );
      this._panelEvents?.emit("codeflowmu.lifecycle.inbox_to_active", {
        event: "lifecycle_inbox_to_active",
        task_file: resolved,
        task_id: taskId,
        lifecycle_root: this._lifecycleRoot,
        from_stage: "inbox",
        to_stage: "active",
      });
      scheduleLedgerRebuild(this._projectRoot);
    });
  }

}

/** Resolve `fcop/_lifecycle` from an inbox directory path. */
export function lifecycleRootFromInboxDir(inboxDir: string): string {
  const norm = inboxDir.replace(/\\/g, "/");
  if (norm.endsWith("/inbox")) {
    return join(inboxDir, "..");
  }
  return join(inboxDir, "_lifecycle");
}
