import { accessSync, constants, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
import { resolve } from "node:path";
import { spawnSync } from "node:child_process";
import type { JsonValue } from "./types";

export interface SkillAvailabilityOptions {
  target: string;
  source?: string;
  requiredSkills?: string[];
}

export interface SkillAvailabilityReport {
  ok: boolean;
  runnerUsable: boolean;
  contractOk: boolean;
  available: boolean;
  degraded: boolean;
  blocker: string | null;
  degradedReason: string | null;
  checkedAt: string;
  source: string;
  target: string;
  path: string;
  resolvedPath: string;
  resolvedPathSource: "target" | "target-symlink" | "source-fallback" | "missing";
  resolution: {
    env: "UNIDESK_SKILLS_PATH";
    configuredTarget: string;
    configuredSource: string;
    resolvedPath: string;
    resolvedPathSource: "target" | "target-symlink" | "source-fallback" | "missing";
    runnerEnvValue: string;
    passesToRunnerEnv: true;
    targetBlocker: string | null;
    degradedReason: string | null;
    hostRolloutRequired: boolean;
  };
  mountPoint: string | null;
  exists: boolean;
  directory: boolean;
  readable: boolean;
  readonly: boolean;
  skillCount: number;
  targetExists: boolean;
  targetDirectory: boolean;
  targetReadable: boolean;
  targetReadonly: boolean;
  targetSkillCount: number;
  targetMissingSkills: string[];
  targetSymlink: boolean;
  targetRealPath: string | null;
  sourceExists: boolean;
  sourceDirectory: boolean;
  sourceReadable: boolean;
  sourceReadonly: boolean;
  sourceSkillCount: number;
  sourceMissingSkills: string[];
  sourceMountPoint: string | null;
  sourceSymlink: boolean;
  sourceRealPath: string | null;
  requiredSkills: string[];
  missingSkills: string[];
  skills: Array<{ name: string; present: boolean; skillMdPresent: boolean; path: string }>;
  pathSpelling: {
    expectedTarget: string;
    forbiddenPathChecked: true;
    forbiddenPathExists: boolean;
    forbiddenPathConfigured: boolean;
    forbiddenPathRoles: Array<"source" | "target">;
    forbiddenPathMustNotBeUsed: true;
  };
  expectedMount: string;
  version: {
    selectedFingerprint: string | null;
    selectedLatestMtime: string | null;
    sourceFingerprint: string | null;
    targetFingerprint: string | null;
    sourceLatestMtime: string | null;
    targetLatestMtime: string | null;
    sourceSampledSkillNames: string[];
    targetSampledSkillNames: string[];
  };
  capabilities: {
    mdtodoSafeTitleStdin: MdtodoSafeTitleStdinCapability;
  };
  warnings: string[];
  next: {
    inspect: string;
    safeTitleExample: string;
  };
  repairHint: string | null;
  error: string | null;
  valuesPrinted: false;
}

export interface SkillSyncPathReport {
  path: string;
  approved: boolean;
  exists: boolean;
  directory: boolean;
  readable: boolean;
  writable: boolean;
  readonly: boolean;
  mountPoint: string | null;
  symlink: boolean;
  realPath: string | null;
  skillCount: number;
  version: {
    fingerprint: string | null;
    latestMtime: string | null;
    latestMtimeMs: number | null;
    sampledSkillNames: string[];
  };
  requiredSkills: string[];
  missingSkills: string[];
  skills: Array<{ name: string; present: boolean; skillMdPresent: boolean; path: string }>;
  error: string | null;
}

export interface SkillSyncPreflightOptions {
  source?: string;
  target?: string;
  requiredSkills?: string[];
}

export interface SkillSyncPreflightReport {
  ok: boolean;
  degraded: boolean;
  blocker:
    | "unapproved-source"
    | "unapproved-target"
    | "skills-source-missing"
    | "skills-source-not-directory"
    | "skills-source-permission-failed"
    | "skills-target-missing"
    | "skills-target-not-directory"
    | "skills-target-permission-failed"
    | "skills-target-not-readonly"
    | "required-source-skills-missing"
    | "required-target-skills-missing"
    | "forbidden-skills-path-configured"
    | "forbidden-skills-path-present"
    | null;
  checkedAt: string;
  mode: "dry-run";
  dryRun: true;
  mutation: false;
  syncMode: "hostPath-read-only-projection";
  source: SkillSyncPathReport;
  target: SkillSyncPathReport;
  expected: {
    source: string;
    target: string;
    env: "UNIDESK_SKILLS_PATH";
    envValue: string;
    mount: string;
    requiredSkills: string[];
  };
  counts: {
    sourceSkills: number;
    targetSkills: number;
    requiredSkills: number;
    missingSourceSkills: number;
    missingTargetSkills: number;
  };
  version: {
    sourceFingerprint: string | null;
    targetFingerprint: string | null;
    sourceLatestMtime: string | null;
    targetLatestMtime: string | null;
    sourceSampledSkillNames: string[];
    targetSampledSkillNames: string[];
  };
  capabilities: {
    mdtodoSafeTitleStdin: MdtodoSafeTitleStdinCapability;
  };
  warnings: string[];
  missing: {
    sourceSkills: string[];
    targetSkills: string[];
  };
  permissionFailures: Array<{ path: string; operation: string; error: string }>;
  pathSpelling: SkillAvailabilityReport["pathSpelling"];
  plannedActions: {
    copy: false;
    writesSource: false;
    writesTarget: false;
    restartRequired: false;
    readsSecrets: false;
    copyFromArbitraryPath: false;
  };
  instructions: string[];
  commands: {
    dryRun: string;
    full: string;
    health: string;
    runtimePreflight: string;
    contractTest: string;
    safeTitleExample: string;
  };
  valuesPrinted: false;
}

export interface MdtodoSafeTitleStdinProbe {
  path: string;
  exists: boolean;
  commands: Record<"add" | "add-sub" | "update", boolean>;
  ready: boolean;
  identity: string | null;
}

export interface MdtodoSafeTitleStdinCapability {
  name: "mdtodo-safe-title-stdin";
  nonBlocking: true;
  ready: boolean;
  freshness: "ready" | "source-stale" | "target-stale" | "missing";
  source: MdtodoSafeTitleStdinProbe;
  target: MdtodoSafeTitleStdinProbe;
  warning: string | null;
  next: string;
}

export const defaultRequiredSkills = ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"];
export const defaultSource = "/home/ubuntu/.agents/skills";
export const expectedTarget = "/root/.agents/skills";
const forbiddenSkillsDirName = [".ag", "nets"].join("");
const forbiddenRelativeSkillsPath = `${forbiddenSkillsDirName}/skills`;
const forbiddenTargets = [`/root/${forbiddenSkillsDirName}/skills`, `/home/ubuntu/${forbiddenSkillsDirName}/skills`];
const skillDirectoryAliases: Record<string, string[]> = {
  "playwright-cli": ["playwright-cli", "playwright"],
};

function shellQuote(value: string): string {
  return `'${value.replace(/'/gu, "'\\''")}'`;
}

function commandOk(command: string, args: string[], timeoutMs = 2000): boolean {
  const result = spawnSync(command, args, {
    encoding: "utf8",
    timeout: timeoutMs,
    maxBuffer: 64 * 1024,
    shell: false,
  });
  return result.error === undefined && result.status === 0;
}

function decodeMountInfoPath(value: string): string {
  return value.replace(/\\([0-7]{3})/gu, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
}

function normalizedPathText(path: string): string {
  return path.replace(/\\/gu, "/").replace(/\/+$/u, "");
}

function normalizedResolvedPath(path: string | null): string | null {
  return path === null ? null : normalizedPathText(path);
}

function isForbiddenSkillsPath(path: string): boolean {
  const normalized = normalizedPathText(path);
  return normalized === forbiddenRelativeSkillsPath
    || normalized === `~/${forbiddenRelativeSkillsPath}`
    || normalized.endsWith(`/${forbiddenRelativeSkillsPath}`)
    || forbiddenTargets.includes(resolve(path));
}

function mountInfoForPath(path: string): { mountPoint: string | null; readonly: boolean | null } {
  try {
    const target = resolve(path);
    let best: { mountPoint: string; readonly: boolean } | null = null;
    for (const line of readFileSync("/proc/self/mountinfo", "utf8").split(/\r?\n/u)) {
      if (line.trim().length === 0) continue;
      const fields = line.split(" ");
      const mountPoint = decodeMountInfoPath(fields[4] ?? "");
      const options = (fields[5] ?? "").split(",");
      if (mountPoint.length === 0) continue;
      const matches = target === mountPoint || target.startsWith(mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`);
      if (!matches) continue;
      if (best === null || mountPoint.length > best.mountPoint.length) best = { mountPoint, readonly: options.includes("ro") };
    }
    return best ?? { mountPoint: null, readonly: null };
  } catch {
    return { mountPoint: null, readonly: null };
  }
}

function candidateDirs(name: string): string[] {
  return skillDirectoryAliases[name] ?? [name];
}

function skillStatus(target: string, requiredSkills: string[]): SkillAvailabilityReport["skills"] {
  return requiredSkills.map((name) => {
    const candidates = candidateDirs(name).map((candidate) => resolve(target, candidate));
    const existingPath = candidates.find((candidate) => existsSync(candidate) && statSync(candidate).isDirectory());
    const path = existingPath ?? candidates[0] ?? resolve(target, name);
    return {
      name,
      present: existingPath !== undefined,
      skillMdPresent: existsSync(resolve(path, "SKILL.md")),
      path,
    };
  });
}

function skillSetVersion(path: string, readable: boolean): SkillSyncPathReport["version"] {
  if (!readable) return { fingerprint: null, latestMtime: null, latestMtimeMs: null, sampledSkillNames: [] };
  try {
    const entries = readdirSync(path, { withFileTypes: true })
      .filter((entry) => entry.isDirectory())
      .map((entry) => {
        const skillPath = resolve(path, entry.name);
        const skillMdPath = resolve(skillPath, "SKILL.md");
        const skillStat = existsSync(skillMdPath) ? statSync(skillMdPath) : statSync(skillPath);
        return {
          name: entry.name,
          mtimeMs: Math.floor(skillStat.mtimeMs),
          skillMd: existsSync(skillMdPath),
        };
      })
      .sort((left, right) => left.name.localeCompare(right.name));
    const latestMtimeMs = entries.reduce<number | null>((latest, entry) => latest === null || entry.mtimeMs > latest ? entry.mtimeMs : latest, null);
    const fingerprint = createHash("sha256")
      .update(JSON.stringify(entries))
      .digest("hex")
      .slice(0, 16);
    return {
      fingerprint: entries.length === 0 ? null : fingerprint,
      latestMtime: latestMtimeMs === null ? null : new Date(latestMtimeMs).toISOString(),
      latestMtimeMs,
      sampledSkillNames: entries.map((entry) => entry.name).slice(0, 12),
    };
  } catch {
    return { fingerprint: null, latestMtime: null, latestMtimeMs: null, sampledSkillNames: [] };
  }
}

function mdtodoCliPath(skillsPath: string): string {
  return resolve(skillsPath, "mdtodo-edit", "scripts", "mdtodo-edit-cli.py");
}

function probeMdtodoSafeTitleStdin(skillsPath: string): MdtodoSafeTitleStdinProbe {
  const path = mdtodoCliPath(skillsPath);
  const exists = existsSync(path);
  const commands = { add: false, "add-sub": false, update: false };
  if (exists) {
    for (const command of Object.keys(commands) as Array<keyof typeof commands>) {
      const result = spawnSync("python3", [path, command, "--help"], { encoding: "utf8", timeout: 2000 });
      commands[command] = result.status === 0 && `${result.stdout ?? ""}\n${result.stderr ?? ""}`.includes("--stdin");
    }
  }
  const ready = Object.values(commands).every(Boolean);
  let identity: string | null = null;
  if (exists) {
    try {
      identity = createHash("sha256").update(readFileSync(path)).digest("hex").slice(0, 16);
    } catch {
      identity = null;
    }
  }
  return { path, exists, commands, ready, identity };
}

export function collectMdtodoSafeTitleStdinCapability(sourcePath: string, targetPath: string): MdtodoSafeTitleStdinCapability {
  const source = probeMdtodoSafeTitleStdin(sourcePath);
  const target = probeMdtodoSafeTitleStdin(targetPath);
  const freshness = source.ready && target.ready
    ? "ready"
    : source.ready
      ? "target-stale"
      : source.exists || target.exists
        ? "source-stale"
        : "missing";
  const warning = freshness === "ready"
    ? null
    : freshness === "target-stale"
      ? "MDTODO safe title stdin is available in the approved source but missing from the runner target projection."
      : "MDTODO safe title stdin is missing from the approved source; update the managed agent_skills source before refreshing projections.";
  return {
    name: "mdtodo-safe-title-stdin",
    nonBlocking: true,
    ready: target.ready,
    freshness,
    source,
    target,
    warning,
    next: "bun scripts/cli.ts codex skills-sync --dry-run --full",
  };
}

export function renderMdtodoSafeTitleCommand(file: string, command: "add" | "add-sub" | "update", taskId?: string): string {
  const positional = command === "add" ? shellQuote(file) : `${shellQuote(file)} ${shellQuote(taskId ?? "<taskId>")}`;
  return `python3 "$HOME/.agents/skills/mdtodo-edit/scripts/mdtodo-edit-cli.py" ${command} ${positional} --stdin <<'EOF'\n<title>\nEOF`;
}

function accessProbe(path: string, mode: number, operation: string): { ok: boolean; failure: { path: string; operation: string; error: string } | null } {
  try {
    accessSync(path, mode);
    return { ok: true, failure: null };
  } catch (error) {
    return {
      ok: false,
      failure: {
        path,
        operation,
        error: error instanceof Error ? error.message : String(error),
      },
    };
  }
}

function emptySkillStatuses(target: string, requiredSkills: string[]): SkillAvailabilityReport["skills"] {
  return requiredSkills.map((name) => ({
    name,
    present: false,
    skillMdPresent: false,
    path: resolve(target, name),
  }));
}

function collectSyncPathReport(path: string, approved: boolean, requiredSkills: string[]): { report: SkillSyncPathReport; permissionFailures: SkillSyncPreflightReport["permissionFailures"] } {
  const mountInfo = mountInfoForPath(path);
  const exists = existsSync(path);
  const permissionFailures: SkillSyncPreflightReport["permissionFailures"] = [];
  let directory = false;
  let symlink = false;
  let realPath: string | null = null;
  let readable = false;
  let writable = false;
  let skillCount = 0;
  let skills = emptySkillStatuses(path, requiredSkills);
  let error: string | null = null;

  if (exists) {
    try {
      const lstat = lstatSync(path);
      symlink = lstat.isSymbolicLink();
      try {
        realPath = realpathSync(path);
      } catch {
        realPath = null;
      }
      const stat = statSync(path);
      directory = stat.isDirectory();
    } catch (probeError) {
      error = probeError instanceof Error ? probeError.message : String(probeError);
    }
  }

  if (exists && directory) {
    const read = accessProbe(path, constants.R_OK | constants.X_OK, "read-directory");
    readable = read.ok;
    if (read.failure !== null) permissionFailures.push(read.failure);
    const write = accessProbe(path, constants.W_OK, "write-check");
    writable = write.ok;
    if (readable) {
      try {
        const entries = readdirSync(path, { withFileTypes: true });
        skillCount = entries.filter((entry) => entry.isDirectory()).length;
        skills = skillStatus(path, requiredSkills);
      } catch (probeError) {
        error = probeError instanceof Error ? probeError.message : String(probeError);
        permissionFailures.push({ path, operation: "list-skills", error });
      }
    }
  }

  const missingSkills = skills.filter((skill) => !skill.present || !skill.skillMdPresent).map((skill) => skill.name);
  return {
    report: {
      path,
      approved,
      exists,
      directory,
      readable,
      writable,
      readonly: exists && directory && (mountInfo.readonly === true || !writable),
      mountPoint: mountInfo.mountPoint,
      symlink,
      realPath,
      skillCount,
      version: skillSetVersion(path, readable),
      requiredSkills,
      missingSkills,
      skills,
      error,
    },
    permissionFailures,
  };
}

function targetBlockerFor(report: SkillSyncPathReport): string | null {
  if (!report.exists) return "skills-target-missing";
  if (!report.directory) return "skills-target-not-directory";
  if (!report.readable) return "skills-target-permission-failed";
  if (!report.readonly) return "skills-target-not-readonly";
  if (report.missingSkills.length > 0) return "required-target-skills-missing";
  return null;
}

function sourceBlockerFor(report: SkillSyncPathReport): string | null {
  if (!report.exists) return "skills-source-missing";
  if (!report.directory) return "skills-source-not-directory";
  if (!report.readable) return "skills-source-permission-failed";
  if (report.missingSkills.length > 0) return "required-source-skills-missing";
  return null;
}

function reportHasRequiredSkills(report: SkillSyncPathReport): boolean {
  return report.exists && report.directory && report.readable && report.missingSkills.length === 0;
}

function sameResolvedPath(left: string | null, right: string | null): boolean {
  const normalizedLeft = normalizedResolvedPath(left);
  const normalizedRight = normalizedResolvedPath(right);
  return normalizedLeft !== null && normalizedRight !== null && normalizedLeft === normalizedRight;
}

export function collectSkillAvailability(options: SkillAvailabilityOptions): SkillAvailabilityReport {
  const target = options.target;
  const source = options.source ?? defaultSource;
  const requiredSkills = options.requiredSkills ?? defaultRequiredSkills;
  const checkedAt = new Date().toISOString();
  const forbiddenSourceConfigured = isForbiddenSkillsPath(source);
  const forbiddenTargetConfigured = isForbiddenSkillsPath(target);
  const forbiddenPathConfigured = forbiddenSourceConfigured || forbiddenTargetConfigured;
  const forbiddenPathExists = forbiddenTargets.some((path) => existsSync(path));
  const sourceProbe = collectSyncPathReport(source, source === defaultSource, requiredSkills).report;
  const targetProbe = collectSyncPathReport(target, target === expectedTarget || normalizedPathText(target) === normalizedPathText(source), requiredSkills).report;
  const targetSymlinkToSource = targetProbe.symlink && sameResolvedPath(targetProbe.realPath, sourceProbe.realPath);
  const sourceHasRequiredSkills = reportHasRequiredSkills(sourceProbe);
  const targetHasRequiredSkills = reportHasRequiredSkills(targetProbe);
  const targetReadonlyOk = targetProbe.readonly || targetSymlinkToSource || normalizedPathText(target) === normalizedPathText(source);
  const targetReady = targetHasRequiredSkills && targetReadonlyOk;
  const targetBlocker = targetHasRequiredSkills && targetSymlinkToSource ? null : targetBlockerFor(targetProbe);
  const sourceBlocker = sourceBlockerFor(sourceProbe);
  const missingBoth = !targetProbe.exists && !sourceProbe.exists;
  const resolutionSource: SkillAvailabilityReport["resolvedPathSource"] = forbiddenPathConfigured || !targetReady
    ? "missing"
    : targetSymlinkToSource ? "target-symlink" : "target";
  const resolvedPath = target;
  const selectedReport = targetProbe;
  const mdtodoSafeTitleStdin = collectMdtodoSafeTitleStdinCapability(source, target);
  const runnerUsable = !forbiddenPathConfigured && targetReady && !forbiddenPathExists;
  const contractOk = runnerUsable;
  const blocker = forbiddenPathConfigured
    ? "forbidden-skills-path-configured"
    : forbiddenPathExists
    ? "forbidden-skills-path-present"
    : missingBoth
    ? "skills-source-and-target-missing"
    : !runnerUsable
      ? targetBlocker ?? sourceBlocker ?? "required-skills-missing"
      : null;
  const degradedReason = contractOk ? null : blocker;
  const ok = contractOk;
  const missingSkills = selectedReport.missingSkills;
  const skills = targetProbe.skills;
  return {
    ok,
    runnerUsable,
    contractOk,
    available: runnerUsable,
    degraded: !contractOk,
    blocker,
    degradedReason,
    checkedAt,
    source,
    target,
    path: resolvedPath,
    resolvedPath,
    resolvedPathSource: resolutionSource,
    resolution: {
      env: "UNIDESK_SKILLS_PATH",
      configuredTarget: target,
      configuredSource: source,
      resolvedPath,
      resolvedPathSource: resolutionSource,
      runnerEnvValue: resolvedPath,
      passesToRunnerEnv: true,
      targetBlocker,
      degradedReason,
      hostRolloutRequired: sourceHasRequiredSkills && !contractOk,
    },
    mountPoint: selectedReport.mountPoint,
    exists: targetProbe.exists,
    directory: targetProbe.directory,
    readable: targetProbe.readable,
    readonly: targetProbe.readonly,
    skillCount: selectedReport.skillCount,
    targetExists: targetProbe.exists,
    targetDirectory: targetProbe.directory,
    targetReadable: targetProbe.readable,
    targetReadonly: targetProbe.readonly,
    targetSkillCount: targetProbe.skillCount,
    targetMissingSkills: targetProbe.missingSkills,
    targetSymlink: targetProbe.symlink,
    targetRealPath: targetProbe.realPath,
    sourceExists: sourceProbe.exists,
    sourceDirectory: sourceProbe.directory,
    sourceReadable: sourceProbe.readable,
    sourceReadonly: sourceProbe.readonly,
    sourceSkillCount: sourceProbe.skillCount,
    sourceMissingSkills: sourceProbe.missingSkills,
    sourceMountPoint: sourceProbe.mountPoint,
    sourceSymlink: sourceProbe.symlink,
    sourceRealPath: sourceProbe.realPath,
    requiredSkills,
    missingSkills,
    skills,
    pathSpelling: {
      expectedTarget,
      forbiddenPathChecked: true,
      forbiddenPathExists,
      forbiddenPathConfigured,
      forbiddenPathRoles: [
        ...(forbiddenSourceConfigured ? ["source" as const] : []),
        ...(forbiddenTargetConfigured ? ["target" as const] : []),
      ],
      forbiddenPathMustNotBeUsed: true,
    },
    expectedMount: `${defaultSource} mounted read-only to ${expectedTarget}`,
    version: {
      selectedFingerprint: selectedReport.version.fingerprint,
      selectedLatestMtime: selectedReport.version.latestMtime,
      sourceFingerprint: sourceProbe.version.fingerprint,
      targetFingerprint: targetProbe.version.fingerprint,
      sourceLatestMtime: sourceProbe.version.latestMtime,
      targetLatestMtime: targetProbe.version.latestMtime,
      sourceSampledSkillNames: sourceProbe.version.sampledSkillNames,
      targetSampledSkillNames: targetProbe.version.sampledSkillNames,
    },
    capabilities: { mdtodoSafeTitleStdin },
    warnings: mdtodoSafeTitleStdin.warning === null ? [] : [mdtodoSafeTitleStdin.warning],
    next: {
      inspect: mdtodoSafeTitleStdin.next,
      safeTitleExample: renderMdtodoSafeTitleCommand("docs/MDTODO/example.md", "add"),
    },
    repairHint: contractOk
      ? null
      : sourceHasRequiredSkills
        ? `Source ${defaultSource} has the required skills; restore the read-only ${defaultSource} -> ${expectedTarget} projection before starting runners.`
        : `Mount ${defaultSource} read-only at ${expectedTarget}, set UNIDESK_SKILLS_PATH=${expectedTarget}, and remove any forbidden skills path spelling.`,
    error: selectedReport.error,
    valuesPrinted: false,
  };
}

export function collectSkillSyncPreflight(options: SkillSyncPreflightOptions = {}): SkillSyncPreflightReport {
  const sourcePath = options.source ?? defaultSource;
  const targetPath = options.target ?? expectedTarget;
  const requiredSkills = options.requiredSkills ?? defaultRequiredSkills;
  const checkedAt = new Date().toISOString();
  const source = collectSyncPathReport(sourcePath, sourcePath === defaultSource, requiredSkills);
  const target = collectSyncPathReport(targetPath, targetPath === expectedTarget, requiredSkills);
  const forbiddenSourceConfigured = isForbiddenSkillsPath(sourcePath);
  const forbiddenTargetConfigured = isForbiddenSkillsPath(targetPath);
  const forbiddenPathConfigured = forbiddenSourceConfigured || forbiddenTargetConfigured;
  const forbiddenPathExists = forbiddenTargets.some((path) => existsSync(path));
  const permissionFailures = [...source.permissionFailures, ...target.permissionFailures];
  const mdtodoSafeTitleStdin = collectMdtodoSafeTitleStdinCapability(sourcePath, targetPath);
  const blocker = forbiddenPathConfigured
    ? "forbidden-skills-path-configured"
    : !source.report.approved
    ? "unapproved-source"
    : !target.report.approved
      ? "unapproved-target"
      : !source.report.exists
        ? "skills-source-missing"
        : !source.report.directory
          ? "skills-source-not-directory"
          : !source.report.readable
            ? "skills-source-permission-failed"
            : !target.report.exists
              ? "skills-target-missing"
              : !target.report.directory
                ? "skills-target-not-directory"
                : !target.report.readable
                  ? "skills-target-permission-failed"
                  : !target.report.readonly
                    ? "skills-target-not-readonly"
                    : source.report.missingSkills.length > 0
                      ? "required-source-skills-missing"
                      : target.report.missingSkills.length > 0
                        ? "required-target-skills-missing"
                        : forbiddenPathExists
                          ? "forbidden-skills-path-present"
                          : null;
  const ok = blocker === null;
  return {
    ok,
    degraded: !ok,
    blocker,
    checkedAt,
    mode: "dry-run",
    dryRun: true,
    mutation: false,
    syncMode: "hostPath-read-only-projection",
    source: source.report,
    target: target.report,
    expected: {
      source: defaultSource,
      target: expectedTarget,
      env: "UNIDESK_SKILLS_PATH",
      envValue: expectedTarget,
      mount: `${defaultSource} mounted read-only to ${expectedTarget}`,
      requiredSkills,
    },
    counts: {
      sourceSkills: source.report.skillCount,
      targetSkills: target.report.skillCount,
      requiredSkills: requiredSkills.length,
      missingSourceSkills: source.report.missingSkills.length,
      missingTargetSkills: target.report.missingSkills.length,
    },
    version: {
      sourceFingerprint: source.report.version.fingerprint,
      targetFingerprint: target.report.version.fingerprint,
      sourceLatestMtime: source.report.version.latestMtime,
      targetLatestMtime: target.report.version.latestMtime,
      sourceSampledSkillNames: source.report.version.sampledSkillNames,
      targetSampledSkillNames: target.report.version.sampledSkillNames,
    },
    capabilities: { mdtodoSafeTitleStdin },
    warnings: mdtodoSafeTitleStdin.warning === null ? [] : [mdtodoSafeTitleStdin.warning],
    missing: {
      sourceSkills: source.report.missingSkills,
      targetSkills: target.report.missingSkills,
    },
    permissionFailures,
    pathSpelling: {
      expectedTarget,
      forbiddenPathChecked: true,
      forbiddenPathExists,
      forbiddenPathConfigured,
      forbiddenPathRoles: [
        ...(forbiddenSourceConfigured ? ["source" as const] : []),
        ...(forbiddenTargetConfigured ? ["target" as const] : []),
      ],
      forbiddenPathMustNotBeUsed: true,
    },
    plannedActions: {
      copy: false,
      writesSource: false,
      writesTarget: false,
      restartRequired: false,
      readsSecrets: false,
      copyFromArbitraryPath: false,
    },
    instructions: [
      `Use ${defaultSource} as the only approved runner skills source and ${expectedTarget} as the only approved Code Queue container target.`,
      "Keep the lifecycle as a read-only hostPath projection; this dry-run does not copy files, restart services, roll pods, or read secrets.",
      "Update the approved source through the controlled skills installation path, then let production and dev Code Queue pods read it through the manifest mount.",
      "Use the full command only when per-skill status or permission failure detail is needed.",
    ],
    commands: {
      dryRun: "bun scripts/cli.ts codex skills-sync --dry-run",
      full: "bun scripts/cli.ts codex skills-sync --dry-run --full",
      health: "bun scripts/cli.ts microservice health code-queue",
      runtimePreflight: "bun scripts/cli.ts codex pr-preflight --remote",
      contractTest: "bun scripts/code-queue-runner-skills-contract-test.ts",
      safeTitleExample: renderMdtodoSafeTitleCommand("docs/MDTODO/example.md", "add"),
    },
    valuesPrinted: false,
  };
}

export function skillAvailabilityJson(report: SkillAvailabilityReport): JsonValue {
  return report as unknown as JsonValue;
}

export function skillSyncPreflightJson(report: SkillSyncPreflightReport): JsonValue {
  return report as unknown as JsonValue;
}
