#!/usr/bin/env node

import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const DOCUMENT_TEMPLATE_BY_OUTPUT = {
  "AGENTS.md": "AGENTS.md.tmpl",
  "docs/project_spec.md": "project_spec.md.tmpl",
  "docs/prompt.md": "prompt.md.tmpl",
  "docs/plan.md": "plan.md.tmpl",
  "docs/implement.md": "implement.md.tmpl",
  "docs/documentation.md": "documentation.md.tmpl",
  "docs/failures/taxonomy.md": "taxonomy.md.tmpl",
  "docs/compaction-strategy.md": "compaction-strategy.md.tmpl",
  "docs/feedback-profile.json": "feedback-profile.json.tmpl",
  "docs/harness-manifest.json": "harness-manifest.json.tmpl",
};

const SCRIPT_TEMPLATE_BY_OUTPUT = {
  "scripts/validate-docs.sh": "validate-docs.sh.tmpl",
  "scripts/validate-docs.ps1": "validate-docs.ps1.tmpl",
  "scripts/quality-gates.sh": "quality-gates.sh.tmpl",
  "scripts/quality-gates.ps1": "quality-gates.ps1.tmpl",
};

const KNOWN_SUB_SKILLS = [
  "iterate",
  "quality-audit",
  "milestone-promote",
];

const FEEDBACK_PROFILE_PATH = "docs/feedback-profile.json";
const MANIFEST_PATH = "docs/harness-manifest.json";
const ALL_OUTPUTS = {
  ...DOCUMENT_TEMPLATE_BY_OUTPUT,
  ...SCRIPT_TEMPLATE_BY_OUTPUT,
};

function usage() {
  console.error(
    [
      "Usage:",
      "  node tools/render-scaffold.mjs --values <values.json> [--skill-root <path>] [--project-root <path>]",
    ].join("\n")
  );
}

function getArg(name, fallback = null) {
  const index = process.argv.indexOf(name);
  if (index === -1 || index === process.argv.length - 1) return fallback;
  return process.argv[index + 1];
}

function unique(values) {
  return [...new Set(values)];
}

function archiveRunId() {
  return `scaffold-replaced-${new Date().toISOString().replace(/[-:.]/g, "").replace("Z", "Z")}`;
}

function stringArray(value) {
  if (Array.isArray(value)) {
    return unique(value.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()));
  }
  if (typeof value === "string" && value.trim() !== "") {
    return [value.trim()];
  }
  return [];
}

function normalizeShell(shell) {
  return shell === "windows-powershell" || shell === "unix-shell" ? shell : null;
}

function commandScriptPath(commandText) {
  if (!commandText) return null;
  const match = String(commandText).match(/scripts[\\/][^\s"'`]+/i);
  return match ? match[0].replace(/\\/g, "/") : null;
}

function fallbackCommand(primaryShell, scriptBaseName) {
  if (primaryShell === "windows-powershell") {
    return `powershell -File scripts/${scriptBaseName}.ps1`;
  }
  return `bash scripts/${scriptBaseName}.sh`;
}

function buildDefaultManifest(values) {
  const primaryShell = normalizeShell(values.PRIMARY_SHELL) ?? (process.platform === "win32"
    ? "windows-powershell"
    : "unix-shell");
  const primaryScriptExtension = primaryShell === "windows-powershell" ? ".ps1" : ".sh";

  return {
    version: 1,
    documents: unique([
      "AGENTS.md",
      "docs/project_spec.md",
      "docs/prompt.md",
      "docs/plan.md",
      "docs/implement.md",
      "docs/documentation.md",
      "docs/failures/taxonomy.md",
      "docs/compaction-strategy.md",
      FEEDBACK_PROFILE_PATH,
      MANIFEST_PATH,
    ]),
    scripts: unique([
      `scripts/quality-gates${primaryScriptExtension}`,
      `scripts/validate-docs${primaryScriptExtension}`,
    ]),
    sub_skills: [...KNOWN_SUB_SKILLS],
    execution_contract: {
      primary_shell: primaryShell,
      primary_quality_gate_command: values.QUALITY_GATES_PRIMARY_COMMAND
        ?? fallbackCommand(primaryShell, "quality-gates"),
      primary_doc_validation_command: values.DOC_VALIDATION_PRIMARY_COMMAND
        ?? fallbackCommand(primaryShell, "validate-docs"),
      portable_fallbacks: {
        quality_gates: {
          "windows-powershell": "powershell -File scripts/quality-gates.ps1",
          "unix-shell": "bash scripts/quality-gates.sh",
        },
        doc_validation: {
          "windows-powershell": "powershell -File scripts/validate-docs.ps1",
          "unix-shell": "bash scripts/validate-docs.sh",
        },
      },
    },
    feedback_profile_path: FEEDBACK_PROFILE_PATH,
  };
}

function normalizeList(kind, valuesList, allowed) {
  if (valuesList == null) return [];
  if (!Array.isArray(valuesList)) {
    throw new Error(`Harness manifest field '${kind}' must be an array`);
  }

  const allowedSet = new Set(allowed);
  return unique(valuesList.map((item) => {
    if (typeof item !== "string") {
      throw new Error(`Harness manifest ${kind} entries must be strings`);
    }
    const normalized = item.replace(/\\/g, "/");
    if (!allowedSet.has(normalized)) {
      throw new Error(`Unknown ${kind} entry in harness manifest: ${item}`);
    }
    return normalized;
  }));
}

function resolveHarnessManifest(values) {
  const manifestInput = values.HARNESS_MANIFEST ?? values.harness_manifest ?? buildDefaultManifest(values);
  if (!manifestInput || typeof manifestInput !== "object" || Array.isArray(manifestInput)) {
    throw new Error("HARNESS_MANIFEST must be an object when provided");
  }

  const primaryShell = normalizeShell(
    manifestInput.execution_contract?.primary_shell ?? values.PRIMARY_SHELL
  ) ?? (process.platform === "win32" ? "windows-powershell" : "unix-shell");

  const documents = normalizeList("documents", manifestInput.documents, Object.keys(DOCUMENT_TEMPLATE_BY_OUTPUT));
  const feedbackProfilePath = String(manifestInput.feedback_profile_path ?? FEEDBACK_PROFILE_PATH).replace(/\\/g, "/");
  if (feedbackProfilePath !== FEEDBACK_PROFILE_PATH) {
    throw new Error(`feedback_profile_path must be ${FEEDBACK_PROFILE_PATH}`);
  }
  if (!documents.includes(FEEDBACK_PROFILE_PATH)) {
    documents.push(FEEDBACK_PROFILE_PATH);
  }
  if (!documents.includes(MANIFEST_PATH)) {
    documents.push(MANIFEST_PATH);
  }

  const scripts = normalizeList("scripts", manifestInput.scripts ?? [], Object.keys(SCRIPT_TEMPLATE_BY_OUTPUT));
  const subSkills = normalizeList("sub_skills", manifestInput.sub_skills ?? [], KNOWN_SUB_SKILLS);

  const executionContract = {
    primary_shell: primaryShell,
    primary_quality_gate_command: manifestInput.execution_contract?.primary_quality_gate_command
      ?? values.QUALITY_GATES_PRIMARY_COMMAND
      ?? fallbackCommand(primaryShell, "quality-gates"),
    primary_doc_validation_command: manifestInput.execution_contract?.primary_doc_validation_command
      ?? values.DOC_VALIDATION_PRIMARY_COMMAND
      ?? fallbackCommand(primaryShell, "validate-docs"),
    portable_fallbacks: manifestInput.execution_contract?.portable_fallbacks
      ?? buildDefaultManifest(values).execution_contract.portable_fallbacks,
  };

  const qualityScriptPath = commandScriptPath(executionContract.primary_quality_gate_command);
  if (qualityScriptPath && SCRIPT_TEMPLATE_BY_OUTPUT[qualityScriptPath] && !scripts.includes(qualityScriptPath)) {
    scripts.push(qualityScriptPath);
  }

  const docsScriptPath = commandScriptPath(executionContract.primary_doc_validation_command);
  if (docsScriptPath && SCRIPT_TEMPLATE_BY_OUTPUT[docsScriptPath] && !scripts.includes(docsScriptPath)) {
    scripts.push(docsScriptPath);
  }

  if (
    primaryShell === "windows-powershell" &&
    (!executionContract.primary_quality_gate_command.includes(".ps1")
      || !executionContract.primary_doc_validation_command.includes(".ps1"))
  ) {
    throw new Error("Windows primary shell requires .ps1 primary commands");
  }

  if (
    primaryShell === "unix-shell" &&
    (!executionContract.primary_quality_gate_command.includes(".sh")
      || !executionContract.primary_doc_validation_command.includes(".sh"))
  ) {
    throw new Error("Unix primary shell requires .sh primary commands");
  }

  return {
    version: manifestInput.version ?? 1,
    documents,
    scripts,
    sub_skills: subSkills,
    execution_contract: executionContract,
    feedback_profile_path: FEEDBACK_PROFILE_PATH,
  };
}

function normalizeFeedbackSurface(input, fallbackStatus = "unknown") {
  const surface = input && typeof input === "object" && !Array.isArray(input) ? input : {};
  return {
    status: typeof surface.status === "string" && surface.status.trim() !== ""
      ? surface.status.trim()
      : fallbackStatus,
    mechanisms: stringArray(surface.mechanisms),
    commands: stringArray(surface.commands),
    evidence_paths: stringArray(surface.evidence_paths),
    gaps: stringArray(surface.gaps),
  };
}

function normalizeBusinessFlows(input) {
  if (!Array.isArray(input)) return [];

  return input
    .map((flow) => {
      if (!flow || typeof flow !== "object" || Array.isArray(flow)) return null;
      const name = typeof flow.name === "string" ? flow.name.trim() : "";
      if (!name) return null;

      const classification = typeof flow.classification === "string" && flow.classification.trim() !== ""
        ? flow.classification.trim()
        : "inferred";

      return {
        name,
        classification,
        current_coverage: stringArray(flow.current_coverage),
        verification_target: typeof flow.verification_target === "string" && flow.verification_target.trim() !== ""
          ? flow.verification_target.trim()
          : "",
        evidence_paths: stringArray(flow.evidence_paths),
      };
    })
    .filter(Boolean);
}

function normalizeHumanInputs(input) {
  if (!Array.isArray(input)) return [];

  return input
    .map((item) => {
      if (typeof item === "string" && item.trim() !== "") {
        return {
          type: "human_decision",
          name: item.trim(),
          reason: "",
          blocking: false,
        };
      }
      if (!item || typeof item !== "object" || Array.isArray(item)) return null;

      const name = typeof item.name === "string" ? item.name.trim() : "";
      if (!name) return null;

      return {
        type: typeof item.type === "string" && item.type.trim() !== ""
          ? item.type.trim()
          : "human_decision",
        name,
        reason: typeof item.reason === "string" ? item.reason.trim() : "",
        blocking: Boolean(item.blocking),
      };
    })
    .filter(Boolean);
}

function buildDefaultFeedbackProfile(values, harnessManifest) {
  const userFacingSurfaces = stringArray(values.USER_FACING_SURFACES);
  return {
    version: 1,
    project: {
      name: typeof values.PROJECT_NAME === "string" && values.PROJECT_NAME.trim() !== ""
        ? values.PROJECT_NAME.trim()
        : "project",
      state: typeof values.PROJECT_STATE === "string" && values.PROJECT_STATE.trim() !== ""
        ? values.PROJECT_STATE.trim()
        : "existing-repo",
      kind: typeof values.PROJECT_KIND === "string" && values.PROJECT_KIND.trim() !== ""
        ? values.PROJECT_KIND.trim()
        : "unknown",
      languages: stringArray(values.PROJECT_LANGUAGES ?? values.LANGUAGES),
      user_facing_surfaces: userFacingSurfaces,
    },
    execution_contract: harnessManifest.execution_contract,
    manifest_path: MANIFEST_PATH,
    feedback_surfaces: {
      static_constraint: normalizeFeedbackSurface(null, values.STATIC_CONSTRAINT_STATUS ?? "unknown"),
      contract: normalizeFeedbackSurface(null, values.CONTRACT_STATUS ?? "unknown"),
      runtime: normalizeFeedbackSurface(null, values.RUNTIME_STATUS ?? "unknown"),
      business_flow: normalizeFeedbackSurface(
        null,
        userFacingSurfaces.length > 0 ? (values.BUSINESS_FLOW_STATUS ?? "unknown") : "not-applicable-yet"
      ),
    },
    business_flows: normalizeBusinessFlows(values.BUSINESS_FLOWS),
    observability: {
      logs: typeof values.LOG_ACCESS === "string" ? values.LOG_ACCESS : "Not mapped yet",
      metrics_traces: typeof values.METRICS_ACCESS === "string" ? values.METRICS_ACCESS : "Not mapped yet",
      runtime_debug: typeof values.UI_DEBUG_ACCESS === "string" ? values.UI_DEBUG_ACCESS : "Not mapped yet",
    },
    priority_gaps: stringArray(values.PRIORITY_GAPS),
    human_inputs: normalizeHumanInputs(values.HUMAN_INPUTS),
  };
}

function resolveFeedbackProfile(values, harnessManifest) {
  const input = values.FEEDBACK_PROFILE ?? values.feedback_profile ?? buildDefaultFeedbackProfile(values, harnessManifest);
  if (!input || typeof input !== "object" || Array.isArray(input)) {
    throw new Error("FEEDBACK_PROFILE must be an object when provided");
  }

  return {
    version: input.version ?? 1,
    project: {
      name: typeof input.project?.name === "string" && input.project.name.trim() !== ""
        ? input.project.name.trim()
        : buildDefaultFeedbackProfile(values, harnessManifest).project.name,
      state: typeof input.project?.state === "string" && input.project.state.trim() !== ""
        ? input.project.state.trim()
        : buildDefaultFeedbackProfile(values, harnessManifest).project.state,
      kind: typeof input.project?.kind === "string" && input.project.kind.trim() !== ""
        ? input.project.kind.trim()
        : buildDefaultFeedbackProfile(values, harnessManifest).project.kind,
      languages: stringArray(input.project?.languages ?? values.PROJECT_LANGUAGES ?? values.LANGUAGES),
      user_facing_surfaces: stringArray(input.project?.user_facing_surfaces ?? values.USER_FACING_SURFACES),
    },
    execution_contract: harnessManifest.execution_contract,
    manifest_path: MANIFEST_PATH,
    feedback_surfaces: {
      static_constraint: normalizeFeedbackSurface(
        input.feedback_surfaces?.static_constraint,
        values.STATIC_CONSTRAINT_STATUS ?? "unknown"
      ),
      contract: normalizeFeedbackSurface(
        input.feedback_surfaces?.contract,
        values.CONTRACT_STATUS ?? "unknown"
      ),
      runtime: normalizeFeedbackSurface(
        input.feedback_surfaces?.runtime,
        values.RUNTIME_STATUS ?? "unknown"
      ),
      business_flow: normalizeFeedbackSurface(
        input.feedback_surfaces?.business_flow,
        values.BUSINESS_FLOW_STATUS ?? "unknown"
      ),
    },
    business_flows: normalizeBusinessFlows(input.business_flows ?? values.BUSINESS_FLOWS),
    observability: {
      logs: typeof input.observability?.logs === "string" && input.observability.logs.trim() !== ""
        ? input.observability.logs.trim()
        : (typeof values.LOG_ACCESS === "string" ? values.LOG_ACCESS : "Not mapped yet"),
      metrics_traces: typeof input.observability?.metrics_traces === "string" && input.observability.metrics_traces.trim() !== ""
        ? input.observability.metrics_traces.trim()
        : (typeof values.METRICS_ACCESS === "string" ? values.METRICS_ACCESS : "Not mapped yet"),
      runtime_debug: typeof input.observability?.runtime_debug === "string" && input.observability.runtime_debug.trim() !== ""
        ? input.observability.runtime_debug.trim()
        : (typeof values.UI_DEBUG_ACCESS === "string" ? values.UI_DEBUG_ACCESS : "Not mapped yet"),
    },
    priority_gaps: stringArray(input.priority_gaps ?? values.PRIORITY_GAPS),
    human_inputs: normalizeHumanInputs(input.human_inputs ?? values.HUMAN_INPUTS),
  };
}

const valuesPathArg = getArg("--values");
if (!valuesPathArg) {
  usage();
  process.exit(1);
}

const projectRoot = resolve(getArg("--project-root", process.cwd()));
const currentFile = fileURLToPath(import.meta.url);
const skillRoot = resolve(getArg("--skill-root", dirname(dirname(currentFile))));
const valuesPath = resolve(valuesPathArg);

if (!existsSync(valuesPath)) {
  console.error(`Values file not found: ${valuesPath}`);
  process.exit(1);
}

const values = JSON.parse(readFileSync(valuesPath, "utf8"));
const harnessManifest = resolveHarnessManifest(values);
const feedbackProfile = resolveFeedbackProfile(values, harnessManifest);
const documentOutputs = new Set(harnessManifest.documents);
const archiveRoot = archiveRunId();
values.HARNESS_MANIFEST = harnessManifest;
values.HARNESS_MANIFEST_JSON = JSON.stringify(harnessManifest, null, 2);
values.FEEDBACK_PROFILE = feedbackProfile;
values.FEEDBACK_PROFILE_JSON = JSON.stringify(feedbackProfile, null, 2);
const validationRequiredFiles = unique([
  ...harnessManifest.documents,
  ...harnessManifest.scripts,
  ...harnessManifest.sub_skills.map((skill) => `.agents/skills/${skill}/SKILL.md`),
]);
values.VALIDATION_REQUIRED_FILES_SH = validationRequiredFiles.map((path) => `  "${path}"`).join("\n");
values.VALIDATION_REQUIRED_FILES_PS = validationRequiredFiles.map((path) => `  "${path}"`).join(",\n");
values.VALIDATION_SCAN_FILES_SH = values.VALIDATION_REQUIRED_FILES_SH;
values.VALIDATION_SCAN_FILES_PS = values.VALIDATION_REQUIRED_FILES_PS;
values.DOC_VALIDATION_GATE_COMMAND_SH = harnessManifest.execution_contract.portable_fallbacks?.doc_validation?.["unix-shell"]
  ?? "bash scripts/validate-docs.sh";
values.DOC_VALIDATION_GATE_COMMAND_PS = harnessManifest.execution_contract.portable_fallbacks?.doc_validation?.["windows-powershell"]
  ?? "powershell -File scripts/validate-docs.ps1";

function replacePlaceholders(content) {
  return content.replace(/\{\{([A-Z0-9_]+)\}\}/g, (_, key) => {
    if (!(key in values)) {
      throw new Error(`Missing template value: ${key}`);
    }
    const value = values[key];
    return typeof value === "string" ? value : JSON.stringify(value, null, 2);
  });
}

const filesWritten = [];
const archivedFiles = [];

for (const outputRelativePath of [...harnessManifest.documents, ...harnessManifest.scripts]) {
  const templateName = ALL_OUTPUTS[outputRelativePath];
  if (!templateName) {
    throw new Error(`No template registered for output: ${outputRelativePath}`);
  }

  const templatePath = join(skillRoot, "templates", templateName);
  if (!existsSync(templatePath)) {
    console.error(`Template not found: ${templatePath}`);
    process.exit(1);
  }

  const outputPath = join(projectRoot, outputRelativePath);
  mkdirSync(dirname(outputPath), { recursive: true });

  const templateContent = readFileSync(templatePath, "utf8");
  const rendered = replacePlaceholders(templateContent);

  if (documentOutputs.has(outputRelativePath) && existsSync(outputPath)) {
    const existing = readFileSync(outputPath, "utf8");
    if (existing !== rendered) {
      const archiveRelativePath = join("docs", "archive", archiveRoot, outputRelativePath).replace(/\\/g, "/");
      const archivePath = join(projectRoot, archiveRelativePath);
      mkdirSync(dirname(archivePath), { recursive: true });
      writeFileSync(archivePath, existing);
      archivedFiles.push({
        replaced: outputRelativePath,
        archived_to: archiveRelativePath,
      });
    }
  }

  writeFileSync(outputPath, rendered);
  filesWritten.push(outputRelativePath);
}

const subSkillsSource = join(skillRoot, "sub-skills");
const installedSkillsRoot = join(projectRoot, ".agents", "skills");
mkdirSync(installedSkillsRoot, { recursive: true });

for (const skillName of harnessManifest.sub_skills) {
  const sourcePath = join(subSkillsSource, skillName);
  if (!existsSync(sourcePath)) {
    throw new Error(`Sub-skill not found: ${skillName}`);
  }
  cpSync(sourcePath, join(installedSkillsRoot, skillName), { recursive: true });
  filesWritten.push(`.agents/skills/${skillName}/SKILL.md`);
}

console.log(
  JSON.stringify(
    {
      rendered: true,
      project_root: projectRoot,
      values_path: valuesPath,
      manifest_path: MANIFEST_PATH,
      harness_manifest: harnessManifest,
      archived_files: archivedFiles,
      files_written: filesWritten,
    },
    null,
    2
  )
);
