#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { closeSync, existsSync, openSync, realpathSync, rmSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
  proofUnits,
  semanticReadiness,
  validateCriterionRegistry,
  type ProofUnit,
} from "./criterion-registry.js";
import { loadJourneyManifestAtLeast, type JourneyManifest } from "./journey-manifest.js";
import {
  authoritativeRemoteHead,
  canonicalRepositoryParts,
  canonicalGitHubRepository,
  commandOutput,
} from "./repository-identity.js";
import {
  approveOwnerJourney,
  authorOwnerJourney,
  ensureRecoveredOwnerApprovalUi,
  ensureRecoveredOwnerAuthoringUi,
  ownerApprovalProof,
  ownerAuthoringProof,
  type OwnerEvidence,
} from "./04-owner-lifecycle.spec.js";
import {
  journeyScopedOwnerLabels,
  manifestFingerprint,
  reconcilePersistedClosure,
  transitionToApproved,
  transitionToOwnerCreated,
  type ApprovalIdentity,
  type OwnerCreatedIdentity,
  type PersistedClosureState,
} from "./journey-manifest.js";

export interface ExecutionStep {
  readonly unitId: string;
  readonly startBarrier: string;
  readonly awaitBarrier: string;
  readonly criterionIds: readonly string[];
  readonly requiredManifestStage: ProofUnit["requiredManifestStage"];
  readonly testFile: string;
}
export function validateStructure(root = process.cwd()): void {
  validateCriterionRegistry();
  for (const unit of proofUnits)
    if (!existsSync(resolve(root, unit.file)))
      throw new Error(`${unit.unitId} proof does not exist: ${unit.file}`);
}
export function validateSemanticReadiness(): void {
  const status = semanticReadiness();
  if (status.pending.length)
    throw new Error(
      `semantic readiness refused: pending migrations: ${status.pending.flatMap((u) => u.criterionIds).join(", ")}`,
    );
}
export function validateExecutionPlan(root = process.cwd()): void {
  validateStructure(root);
  validateSemanticReadiness();
  for (const u of proofUnits)
    if (!u.scope.trim() || !u.inputSelectors)
      throw new Error(`${u.unitId} is unanchored or missing declared inputs`);
}
export function statusSummary(): string {
  const r = semanticReadiness();
  return `registry structure: 30/30\nsemantic readiness: ${r.ready}/30\nowner acceptance: 0/30`;
}
export function executionSchedule(): readonly ExecutionStep[] {
  return Object.freeze(
    proofUnits.map((u) =>
      Object.freeze({
        unitId: u.unitId,
        startBarrier: u.startBarrier,
        awaitBarrier: u.awaitBarrier,
        criterionIds: u.criterionIds,
        requiredManifestStage: u.requiredManifestStage,
        testFile: u.file,
      }),
    ),
  );
}
export function executionFiles(): readonly string[] {
  return executionSchedule().map((s) => s.testFile);
}
const necessities = ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL"] as const;
const selectorEnv: Record<string, string> = {
  "repository.remoteUrl": "AWP_REPOSITORY_REMOTE_URL",
  "repository.checkoutPath": "AWP_REPOSITORY_CHECKOUT_PATH",
  "repository.owner": "AWP_REPOSITORY_OWNER",
  "repository.name": "AWP_REPOSITORY_NAME",
  "repository.defaultBranch": "AWP_REPOSITORY_DEFAULT_BRANCH",
  agentAccountId: "AWP_AGENT_ACCOUNT_ID",
  agentModel: "AWP_AGENT_MODEL",
  projectId: "AWP_PROJECT_ID",
  planId: "AWP_PLAN_ID",
  planRevisionId: "AWP_PLAN_REVISION_ID",
  taskIds: "AWP_TASK_IDS",
  factoryRunId: "AWP_FACTORY_RUN_ID",
  firstTaskId: "AWP_FIRST_TASK_ID",
};
function select(manifest: JourneyManifest, selector: string): string | undefined {
  if (selector.startsWith("repository."))
    return manifest.repository[selector.slice(11) as keyof typeof manifest.repository];
  const value = (manifest as unknown as Record<string, unknown>)[selector];
  return Array.isArray(value) ? value.join(",") : typeof value === "string" ? value : undefined;
}
export function proofEnvironment(
  unit: ProofUnit,
  manifest: JourneyManifest,
  runtime: NodeJS.ProcessEnv = {},
  live = false,
): NodeJS.ProcessEnv {
  const env: NodeJS.ProcessEnv = {};
  for (const key of necessities) if (process.env[key]) env[key] = process.env[key];
  for (const selector of unit.inputSelectors) {
    const value = select(manifest, selector);
    if (!value) throw new Error(`${unit.unitId} missing declared input ${selector}`);
    env[selectorEnv[selector] ?? selector] = value;
  }
  for (const key of unit.runtimeInputs) {
    const value = runtime[key];
    if (!value) throw new Error(`${unit.unitId} missing runtime input ${key}`);
    env[key] = value;
  }
  if (live) env.AWP_GOLIVE_EXECUTION = "1";
  return env;
}
export function manifestEnvironment(manifest: JourneyManifest): NodeJS.ProcessEnv {
  const synthetic: ProofUnit = { ...proofUnits[0]!, inputSelectors: Object.keys(selectorEnv) };
  return proofEnvironment(synthetic, manifest);
}

export type LifecycleHandler = (
  manifestPath: string,
  runtime: NodeJS.ProcessEnv,
) => Promise<readonly OwnerEvidence[]>;

export async function withJourneyMutationLock<T>(
  manifestPath: string,
  callback: () => Promise<T>,
): Promise<T> {
  const lockPath = `${manifestPath}.owner-lifecycle.lock`;
  let fd: number;
  try {
    fd = openSync(lockPath, "wx", 0o600);
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "EEXIST")
      throw new Error("owner lifecycle mutation is already in progress for this journey", {
        cause: error,
      });
    throw error;
  }
  try {
    return await callback();
  } finally {
    closeSync(fd);
    rmSync(lockPath, { force: true });
  }
}

function sql(value: string): string {
  return `'${value.replaceAll("'", "''")}'`;
}

export function hasExactDependencyChain(
  taskIds: readonly string[],
  edges: readonly (readonly [string, string])[],
): boolean {
  return (
    taskIds.length === 3 &&
    edges.length === 2 &&
    edges[0]?.[0] === taskIds[1] &&
    edges[0]?.[1] === taskIds[0] &&
    edges[1]?.[0] === taskIds[2] &&
    edges[1]?.[1] === taskIds[1]
  );
}

export function isPristinePreApproval(
  planStatus: string,
  taskStates: readonly (readonly [string, string])[],
  taskIds: readonly string[],
  edges: readonly (readonly [string, string])[],
): boolean {
  return (
    planStatus === "draft" &&
    taskStates.length === 3 &&
    taskStates.every(
      ([id, status], index) =>
        id === taskIds[index] && status === (index === 0 ? "planned" : "blocked"),
    ) &&
    hasExactDependencyChain(taskIds, edges)
  );
}
export function isCausallyAdvancedApproval(
  planStatus: string,
  runStatus: string,
  runReason: string,
  taskStates: readonly (readonly [string, string])[],
  taskIds: readonly string[],
): boolean {
  const exactIds =
    taskStates.length === 3 && taskStates.every(([id], index) => id === taskIds[index]);
  if (!exactIds) return false;
  if (runStatus === "queued") {
    return (
      planStatus === "approved" &&
      runReason === "Automatic dependency-legal dispatch after Plan approval" &&
      taskStates[0]?.[1] === "dispatched" &&
      taskStates[1]?.[1] === "blocked" &&
      taskStates[2]?.[1] === "blocked"
    );
  }
  const advancedRuns = new Set([
    "starting",
    "running",
    "waiting",
    "blocked",
    "cancelling",
    "cancelled",
    "failed",
    "retryable",
    "resolving",
    "completed",
  ]);
  const firstTaskStates = new Set([
    "dispatched",
    "queued",
    "executing",
    "waiting",
    "review",
    "correction",
    "completed",
    "cancelled",
    "failed",
  ]);
  const dependentTaskStates = new Set(["blocked", "ready", ...firstTaskStates]);
  if (!advancedRuns.has(runStatus)) return false;
  if (!firstTaskStates.has(taskStates[0]![1])) return false;
  if (!dependentTaskStates.has(taskStates[1]![1]) || !dependentTaskStates.has(taskStates[2]![1]))
    return false;
  if (runStatus === "completed" || planStatus === "completed")
    return (
      runStatus === "completed" &&
      planStatus === "completed" &&
      taskStates.every(([, status]) => status === "completed")
    );
  return planStatus === "approved";
}

function postgresRows(runtime: NodeJS.ProcessEnv, statement: string): string[][] {
  const databaseUrl = runtime.AWP_TEST_POSTGRES_URL;
  if (!databaseUrl) throw new Error("AWP_TEST_POSTGRES_URL is required");
  const output = commandOutput(
    execFileSync("psql", [databaseUrl, "-At", "-F", "\t", "-c", statement], {
      encoding: "utf8",
    }),
  );
  return output ? output.split("\n").map((row) => row.split("\t")) : [];
}

/** Crash-recovery query contract shared with the owner lifecycle proof. */
export const persistedOwnerClosure = Object.freeze({
  authoring(
    manifest: Extract<JourneyManifest, { stage: "seed" }>,
    runtime: NodeJS.ProcessEnv,
  ): readonly PersistedClosureState<OwnerCreatedIdentity>[] {
    const labels = journeyScopedOwnerLabels(manifest.journeyKey);
    const projects = postgresRows(
      runtime,
      `select id, repository_url from projects where name=${sql(labels.projectName)} order by id`,
    );
    if (!projects.length) return [];
    if (projects.length !== 1)
      return projects.map(() => ({ status: "mismatched", reason: "duplicate journey project" }));
    const [projectId, repositoryUrl] = projects[0]!;
    if (repositoryUrl !== manifest.repository.remoteUrl)
      return [{ status: "mismatched", reason: "repository URL differs from seed" }];
    const rows = postgresRows(
      runtime,
      `select p.id, r.id, v.id, v.summary, r.sequence, (select count(*) from goals g where g.project_id=p.project_id), (select min(g.title) from goals g where g.project_id=p.project_id), (select min(g.success_criteria->>0) from goals g where g.project_id=p.project_id), coalesce(string_agg(t.id, ',' order by t.position), ''), coalesce(string_agg(t.title, '|' order by t.position), ''), coalesce(string_agg(t.status, '|' order by t.position), '') from plans p join plan_revisions r on r.plan_id=p.id and r.project_id=p.project_id join project_vision_versions v on v.id=r.project_vision_version_id and v.project_id=p.project_id left join tasks t on t.plan_revision_id=r.id and t.project_id=p.project_id where p.project_id=${sql(projectId!)} and p.title=${sql(labels.planTitle)} group by p.id,r.id,v.id,v.summary,r.sequence order by p.id,r.id`,
    );
    if (rows.length !== 1)
      return [
        { status: "partial", reason: `expected one Plan/Revision closure, found ${rows.length}` },
      ];
    const [
      planId,
      planRevisionId,
      visionVersionId,
      visionSummary,
      revisionSequence,
      goalCount,
      persistedGoalTitle,
      persistedCriterion,
      taskList,
      titles,
      taskStatuses,
    ] = rows[0]!;
    const taskIds = taskList!.split(",").filter(Boolean);
    const dependencyEdges = postgresRows(
      runtime,
      `select d.task_id, d.prerequisite_task_id from task_dependencies d join tasks t on t.id=d.task_id and t.project_id=${sql(projectId!)} and t.plan_revision_id=${sql(planRevisionId!)} order by t.position, d.prerequisite_task_id`,
    ) as [string, string][];
    const expectedVision = "Deliver one trustworthy Project-to-Merge journey.";
    const expectedGoalTitle = "Launch the first complete journey";
    const expectedCriterion = "Project, execution, review, and trusted merge are proven.";
    const expectedTitles = [
      "Document the canonical owner journey",
      "Add owner journey verification notes",
      "Review owner journey documentation",
    ];
    if (
      visionSummary !== expectedVision ||
      revisionSequence !== "1" ||
      goalCount !== "1" ||
      persistedGoalTitle !== expectedGoalTitle ||
      persistedCriterion !== expectedCriterion ||
      taskIds.length !== 3 ||
      titles!.split("|").join("|") !== expectedTitles.join("|") ||
      taskStatuses !== "planned|blocked|blocked" ||
      !hasExactDependencyChain(taskIds, dependencyEdges)
    )
      return [{ status: "partial", reason: "tasks or dependencies are incomplete" }];
    return [
      {
        status: "complete",
        value: {
          projectId: projectId!,
          planId: planId!,
          planRevisionId: planRevisionId!,
          taskIds,
          authoringReceipt: manifestFingerprint({
            projectId,
            projectName: labels.projectName,
            repositoryUrl,
            visionVersionId,
            goalTitle: persistedGoalTitle,
            criterion: persistedCriterion,
            planId,
            planRevisionId,
            taskIds,
            taskTitles: expectedTitles,
            dependencyCount: 2,
          }),
        },
      },
    ];
  },
  approval(
    manifest: Extract<JourneyManifest, { stage: "owner-created" }>,
    runtime: NodeJS.ProcessEnv,
  ): readonly PersistedClosureState<ApprovalIdentity>[] {
    const planRows = postgresRows(
      runtime,
      `select status from plans where id=${sql(manifest.planId)} and project_id=${sql(manifest.projectId)}`,
    );
    const taskStates = postgresRows(
      runtime,
      `select id, status from tasks where project_id=${sql(manifest.projectId)} and plan_revision_id=${sql(manifest.planRevisionId)} order by position`,
    ) as [string, string][];
    const dependencyEdges = postgresRows(
      runtime,
      `select d.task_id, d.prerequisite_task_id from task_dependencies d join tasks t on t.id=d.task_id and t.project_id=${sql(manifest.projectId)} and t.plan_revision_id=${sql(manifest.planRevisionId)} order by t.position, d.prerequisite_task_id`,
    ) as [string, string][];
    if (planRows.length !== 1 || taskStates.length !== 3)
      return [{ status: "mismatched", reason: "owner-created Plan/Task identity is missing" }];
    const planStatus = planRows[0]![0]!;
    const rows = postgresRows(
      runtime,
      `select f.id, f.account_id, f.model, f.status, f.reason, p.status, t.status, f.task_id from factory_runs f join plans p on p.id=${sql(manifest.planId)} and p.project_id=f.project_id join tasks t on t.id=${sql(manifest.taskIds[0]!)} and t.project_id=f.project_id and t.plan_revision_id=f.plan_revision_id where f.project_id=${sql(manifest.projectId)} and f.plan_revision_id=${sql(manifest.planRevisionId)} order by f.id`,
    );
    if (!rows.length) {
      if (!isPristinePreApproval(planStatus, taskStates, manifest.taskIds, dependencyEdges))
        return [{ status: "partial", reason: "approval closure mutated without its FactoryRun" }];
      return [];
    }
    if (rows.length !== 1)
      return rows.map(() => ({ status: "mismatched", reason: "multiple FactoryRuns" }));
    const [
      factoryRunId,
      accountId,
      model,
      runStatus,
      runReason,
      persistedPlanStatus,
      firstTaskStatus,
      factoryTaskId,
    ] = rows[0]!;
    if (
      accountId !== manifest.agentAccountId ||
      model !== manifest.agentModel ||
      firstTaskStatus !== taskStates[0]?.[1] ||
      factoryTaskId !== manifest.taskIds[0] ||
      !isCausallyAdvancedApproval(
        persistedPlanStatus!,
        runStatus!,
        runReason ?? "",
        taskStates,
        manifest.taskIds,
      ) ||
      !hasExactDependencyChain(manifest.taskIds, dependencyEdges)
    )
      return [
        { status: "mismatched", reason: "FactoryRun or approval state differs from journey" },
      ];
    return [
      {
        status: "complete",
        value: {
          factoryRunId: factoryRunId!,
          firstTaskId: manifest.taskIds[0]!,
          approvalReceipt: manifestFingerprint({
            projectId: manifest.projectId,
            planId: manifest.planId,
            planRevisionId: manifest.planRevisionId,
            planStatus: persistedPlanStatus,
            firstTaskId: manifest.taskIds[0],
            firstTaskStatus,
            factoryRunId,
            factoryTaskId,
            accountId,
            model,
            runStatus,
            runReason,
          }),
        },
      },
    ];
  },
});

function recoveredEvidence(
  manifest: JourneyManifest,
  proof: typeof ownerAuthoringProof | typeof ownerApprovalProof,
): readonly OwnerEvidence[] {
  return proof.criterionIds.map((criterionId) => ({
    criterionId,
    proofId: proof.proofId,
    outcome: "passed",
    generation: manifest.generation,
    seedFingerprint: manifest.seedFingerprint,
    entityIds: {
      ...(manifest.stage === "seed"
        ? {}
        : {
            projectId: manifest.projectId,
            planId: manifest.planId,
            planRevisionId: manifest.planRevisionId,
            taskIds: manifest.taskIds,
          }),
      ...(manifest.stage === "approved" ? { factoryRunId: manifest.factoryRunId } : {}),
    },
    evidenceSource: "ui+postgres",
    timestamp: new Date().toISOString(),
  }));
}

export const ownerLifecycleHandlers: ReadonlyMap<
  "owner-authoring" | "owner-approval",
  LifecycleHandler
> = new Map([
  [
    "owner-authoring",
    async (manifestPath, runtime) =>
      withJourneyMutationLock(manifestPath, async () => {
        const manifest = loadJourneyManifestAtLeast(manifestPath, "seed");
        if (manifest.stage !== "seed")
          throw new Error("owner-authoring requires exact seed identity");
        const unit = proofUnits.find((candidate) => candidate.unitId === "owner-authoring")!;
        const environment = proofEnvironment(
          unit,
          manifest,
          { ...runtime, AWP_JOURNEY_MANIFEST_PATH: manifestPath },
          true,
        );
        const recovered = reconcilePersistedClosure(
          "owner authoring",
          persistedOwnerClosure.authoring(manifest, environment),
        );
        if (recovered) {
          await ensureRecoveredOwnerAuthoringUi(manifest, recovered, environment);
          const checkpoint = transitionToOwnerCreated(manifestPath, manifest, recovered);
          return recoveredEvidence(checkpoint, ownerAuthoringProof);
        }
        return (await authorOwnerJourney(manifest, environment)).evidence;
      }),
  ],
  [
    "owner-approval",
    async (manifestPath, runtime) =>
      withJourneyMutationLock(manifestPath, async () => {
        const manifest = loadJourneyManifestAtLeast(manifestPath, "owner-created");
        if (manifest.stage !== "owner-created")
          throw new Error("owner-approval requires exact owner-created identity");
        const unit = proofUnits.find((candidate) => candidate.unitId === "owner-approval")!;
        const environment = proofEnvironment(
          unit,
          manifest,
          { ...runtime, AWP_JOURNEY_MANIFEST_PATH: manifestPath },
          true,
        );
        const recovered = reconcilePersistedClosure(
          "owner approval",
          persistedOwnerClosure.approval(manifest, environment),
        );
        if (recovered) {
          await ensureRecoveredOwnerApprovalUi(manifest, recovered, environment);
          const checkpoint = transitionToApproved(manifestPath, manifest, recovered);
          return recoveredEvidence(checkpoint, ownerApprovalProof);
        }
        return (await approveOwnerJourney(manifest, environment)).evidence;
      }),
  ],
]);

export function lifecycleHandlerFor(unitId: string): LifecycleHandler | undefined {
  return ownerLifecycleHandlers.get(unitId as "owner-authoring" | "owner-approval");
}
export interface RepositoryPreflight {
  checkoutPath: string;
  originUrl: string;
  defaultBranchRef: string;
  preRunTip: string;
}
export function validateRepository(manifest: JourneyManifest): RepositoryPreflight {
  const checkout = realpathSync(manifest.repository.checkoutPath);
  const git = (args: string[]) =>
    commandOutput(execFileSync("git", ["-C", checkout, ...args], { encoding: "utf8" }));
  const top = realpathSync(git(["rev-parse", "--show-toplevel"]));
  if (top !== checkout) throw new Error("repository checkoutPath does not equal Git toplevel");
  const origin = git(["remote", "get-url", "origin"]);
  const seededRemote = canonicalRepositoryParts(manifest.repository.remoteUrl);
  if (
    seededRemote.owner !== manifest.repository.owner.toLowerCase() ||
    seededRemote.repository !== manifest.repository.name.toLowerCase() ||
    canonicalGitHubRepository(origin) !== canonicalGitHubRepository(manifest.repository.remoteUrl)
  ) {
    throw new Error("repository origin/seed host, owner, and name identity mismatch");
  }
  const remoteHead = authoritativeRemoteHead(git(["ls-remote", "--symref", "origin", "HEAD"]));
  const authoritativeBranch = remoteHead.branchRef.slice("refs/heads/".length);
  if (authoritativeBranch !== manifest.repository.defaultBranch) {
    throw new Error(
      `repository default branch mismatch: origin=${authoritativeBranch}, seed=${manifest.repository.defaultBranch}`,
    );
  }
  const ref = `refs/remotes/origin/${authoritativeBranch}`;
  git(["show-ref", "--verify", ref]);
  return {
    checkoutPath: checkout,
    originUrl: origin,
    defaultBranchRef: ref,
    preRunTip: remoteHead.oid,
  };
}
export function executeJourney(manifestPath: string, root = process.cwd()): never {
  validateStructure(root);
  validateExecutionPlan(root);
  const manifest = loadJourneyManifestAtLeast(manifestPath, "approved");
  validateRepository(manifest);
  throw new Error("live controller unavailable until all proof migrations are ready");
}
export function main(argv = process.argv.slice(2)): void {
  const validation = argv.includes("--validate") || argv.includes("--validate-bindings");
  const list = argv.includes("--list");
  const execute = argv.includes("--execute");
  if (execute && (validation || list))
    throw new Error("validation/listing and live execution are separate commands");
  validateStructure();
  console.log(statusSummary());
  if (validation || list) {
    if (list)
      for (const u of proofUnits)
        console.log(
          `${u.unitId} ${u.migrationStatus} ${u.criterionIds.join(",")} ${u.startBarrier}->${u.awaitBarrier} ${u.file}`,
        );
    return;
  }
  if (!execute) throw new Error("refusing to start live journey without explicit --execute");
  const i = argv.indexOf("--manifest");
  if (i < 0 || !argv[i + 1]) throw new Error("--execute requires --manifest <persisted-json-path>");
  executeJourney(resolve(argv[i + 1]!));
}
const invoked = process.argv[1] ? resolve(process.argv[1]) : undefined;
if (invoked === resolve(fileURLToPath(import.meta.url))) {
  try {
    main();
  } catch (error) {
    console.error(error instanceof Error ? error.message : error);
    process.exitCode = 1;
  }
}
