#!/usr/bin/env node

import fs from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const scriptSource = await fs.readFile(__filename, "utf8");

async function main() {
  const args = parseArgs(process.argv.slice(2));
  const workspaceRoot = path.resolve(args.root ?? path.join(__dirname, ".."));
  const workflowPath = path.join(workspaceRoot, ".ai-workflow", "workflow.md");
  const manifestPath = path.join(workspaceRoot, ".ai-workflow", "manifest.json");

  const workflow = await fs.readFile(workflowPath, "utf8");
  const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));

  validateManifest(manifest);

  const outputs = buildOutputs({
    manifest,
    workflow,
    scriptSource,
  });

  if (args.mode === "check") {
    const drifted = await findDriftedOutputs(workspaceRoot, outputs);
    if (drifted.length > 0) {
      console.error("AI workflow outputs are out of sync:");
      for (const drift of drifted) {
        console.error(`- ${drift}`);
      }
      process.exit(1);
    }

    console.log("AI workflow outputs are in sync.");
    return;
  }

  const changed = await writeOutputs(workspaceRoot, outputs);
  if (changed.length === 0) {
    console.log("AI workflow outputs are already up to date.");
    return;
  }

  console.log("Updated AI workflow outputs:");
  for (const changedPath of changed) {
    console.log(`- ${changedPath}`);
  }
}

function parseArgs(argv) {
  const args = {
    mode: null,
    root: null,
  };

  for (let index = 0; index < argv.length; index += 1) {
    const arg = argv[index];

    if (arg === "--write") {
      args.mode = "write";
      continue;
    }

    if (arg === "--check") {
      args.mode = "check";
      continue;
    }

    if (arg === "--root") {
      args.root = argv[index + 1];
      index += 1;
      continue;
    }

    throw new Error(`Unsupported argument: ${arg}`);
  }

  if (!args.mode) {
    throw new Error("Expected --write or --check.");
  }

  return args;
}

function validateManifest(manifest) {
  const requiredKeys = ["projects", "assistants", "commands", "generated_outputs", "sync_targets"];
  for (const key of requiredKeys) {
    if (!(key in manifest)) {
      throw new Error(`Manifest is missing required key: ${key}`);
    }
  }

  const seenPaths = new Set();
  for (const output of manifest.generated_outputs) {
    if (!output.path || !output.template) {
      throw new Error("Each generated output must define path and template.");
    }

    if (seenPaths.has(output.path)) {
      throw new Error(`Duplicate generated output path: ${output.path}`);
    }

    seenPaths.add(output.path);
  }
}

function buildOutputs(context) {
  const outputs = [];
  for (const output of context.manifest.generated_outputs) {
    outputs.push({
      path: normalizeRelativePath(output.path),
      content: renderTemplate(output, context),
    });
  }
  return outputs;
}

function renderTemplate(output, context) {
  const templateMap = {
    codex_workspace_environment: () => renderWorkspaceCodexEnvironment(context.manifest),
    codex_project_environment: () => renderProjectCodexEnvironment(context.manifest, output.project),
    aiassistant_backend_tests_rule: () =>
      renderAiAssistantRule(context.manifest.projects["backend-php"].generated_content.aiassistant_tests_lines),
    aiassistant_backend_routes_rule: () =>
      renderAiAssistantRule(context.manifest.projects["backend-php"].generated_content.aiassistant_routes_lines),
    aiassistant_frontend_tests_rule: () =>
      renderAiAssistantRule(context.manifest.projects["front-end-vue"].generated_content.aiassistant_tests_lines),
    junie_backend_guidelines: () =>
      renderJunieGuidelines(context.manifest.projects["backend-php"].generated_content.junie_lines),
    junie_frontend_guidelines: () =>
      renderJunieGuidelines(context.manifest.projects["front-end-vue"].generated_content.junie_lines),
    copilot_dispatcher_workflow: () => renderCopilotWorkflow(context.manifest.copilot),
    workflow_snapshot: () => renderWorkflowSnapshot(context.workflow, output.project),
    project_snapshot_manifest: () => renderProjectSnapshotManifest(context.manifest, output.project),
    project_sync_script: () => context.scriptSource,
  };

  const renderer = templateMap[output.template];
  if (!renderer) {
    throw new Error(`Unsupported template: ${output.template}`);
  }

  return ensureTrailingNewline(renderer());
}

function renderWorkspaceCodexEnvironment(manifest) {
  return renderCodexEnvironment({
    name: manifest.workspace.name,
    setup: manifest.workspace.codex_environment.setup,
    actions: manifest.workspace.codex_environment.actions,
    manifest,
    workspaceScoped: true,
  });
}

function renderProjectCodexEnvironment(manifest, projectKey) {
  const project = manifest.projects[projectKey];
  return renderCodexEnvironment({
    name: project.codex_environment.name,
    setup: project.commands.setup,
    actions: project.codex_environment.actions,
    manifest,
    projectKey,
    workspaceScoped: false,
  });
}

function renderCodexEnvironment({ name, setup, actions, manifest, projectKey = null, workspaceScoped }) {
  const lines = [
    "# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY",
    "version = 1",
    `name = "${name}"`,
    "",
  ];

  appendTomlCommandBlock(lines, "setup", setup);

  for (const action of actions) {
    lines.push("");
    lines.push("[[actions]]");
    lines.push(`name = "${escapeTomlString(action.name)}"`);
    lines.push(`icon = "${escapeTomlString(action.icon)}"`);
    lines.push(`command = ${renderTomlMultiline(resolveActionCommand(action, manifest, projectKey, workspaceScoped))}`);

    if (action.platform) {
      lines.push(`platform = "${escapeTomlString(action.platform)}"`);
    }
  }

  return lines.join("\n");
}

function appendTomlCommandBlock(lines, key, commandSpec = {}) {
  lines.push(`[${key}]`);
  lines.push(`script = ${renderTomlMultiline(commandSpec.default ?? "")}`);

  if (commandSpec.win32 !== undefined) {
    lines.push("");
    lines.push(`[${key}.win32]`);
    lines.push(`script = ${renderTomlMultiline(commandSpec.win32)}`);
  }
}

function resolveActionCommand(action, manifest, projectKey, workspaceScoped) {
  if (action.literal_command) {
    return action.literal_command.default ?? "";
  }

  if (!action.command_id) {
    throw new Error(`Action ${action.name} is missing command_id or literal_command.`);
  }

  const resolvedProjectKey = action.project ?? projectKey;
  if (!resolvedProjectKey) {
    throw new Error(`Action ${action.name} does not resolve to a project.`);
  }

  const project = manifest.projects[resolvedProjectKey];
  const command = project.commands[action.command_id]?.default ?? "";

  if (!workspaceScoped) {
    return command;
  }

  return joinCommands([`cd ${project.relative_root}`, command]);
}

function renderAiAssistantRule(lines) {
  return [
    "---",
    "apply: always",
    "---",
    "",
    "<!-- AUTOGENERATED: Run `node scripts/sync-ai-workflow.mjs --write`. -->",
    "",
    ...lines,
  ].join("\n");
}

function renderJunieGuidelines(lines) {
  return [
    "<!-- AUTOGENERATED: Run `node scripts/sync-ai-workflow.mjs --write`. -->",
    "",
    ...lines,
  ].join("\n");
}

function renderCopilotWorkflow(copilot) {
  const body = copilot.body_lines
    .map((line) => line.replaceAll("{{ACTOR}}", "$ACTOR").replaceAll("{{TASK}}", "$TASK"))
    .join("\n");

  return [
    `name: ${copilot.workflow_name}`,
    "on:",
    "  workflow_dispatch:",
    "    inputs:",
    "      task:",
    `        description: '${copilot.input_description}'`,
    "        required: true",
    "        type: string",
    "",
    "jobs:",
    "  assign-task:",
    "    runs-on: ubuntu-latest",
    "    permissions:",
    "      issues: write",
    "    steps:",
    "      - name: Checkout repository",
    "        uses: actions/checkout@v4",
    "",
    "      - name: Create GitHub Issue for Copilot",
    "        env:",
    "          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}",
    "          TASK: ${{ github.event.inputs.task }}",
    "          ACTOR: ${{ github.actor }}",
    "        run: |",
    "          BODY=\"$(cat <<EOF",
    ...body.split("\n").map((line) => `          ${line}`),
    "          EOF",
    "          )\"",
    "          gh issue create \\",
    `            --title "${copilot.title_prefix} $TASK" \\`,
    "            --body \"$BODY\" \\",
    `            --label "${copilot.issue_label}"`,
  ].join("\n");
}

function renderWorkflowSnapshot(workflow, projectKey) {
  return [
    `<!-- AUTOGENERATED SNAPSHOT for ${projectKey}: refresh from the canonical workspace with \`node scripts/sync-ai-workflow.mjs --write\`. -->`,
    "",
    workflow.trimEnd(),
  ].join("\n");
}

function renderProjectSnapshotManifest(manifest, projectKey) {
  const snapshotManifest = buildProjectSnapshotManifest(manifest, projectKey);
  return `${JSON.stringify(snapshotManifest, null, 2)}\n`;
}

function buildProjectSnapshotManifest(manifest, projectKey) {
  const project = JSON.parse(JSON.stringify(manifest.projects[projectKey]));
  const prefix = `${manifest.projects[projectKey].relative_root}/`;

  project.relative_root = ".";

  const snapshotOutputs = manifest.generated_outputs
    .filter((output) => output.project === projectKey)
    .filter((output) => output.template !== "workflow_snapshot")
    .filter((output) => output.template !== "project_snapshot_manifest")
    .filter((output) => output.template !== "project_sync_script")
    .map((output) => ({
      ...output,
      path: normalizeRelativePath(output.path.slice(prefix.length)),
    }));

  const snapshotManifest = {
    "version": manifest.version,
    "snapshot_of": projectKey,
    "projects": {
      [projectKey]: project,
    },
    "assistants": manifest.assistants,
    "commands": manifest.commands,
    "generated_outputs": snapshotOutputs,
    "sync_targets": {
      [projectKey]: {
        "source_root": ".",
        "destination": manifest.sync_targets[projectKey]?.destination ?? "",
        "supported_metadata_dirs": manifest.sync_targets[projectKey]?.supported_metadata_dirs ?? [],
      },
    },
  };

  if (projectKey === "backend-php" && manifest.copilot) {
    snapshotManifest.copilot = manifest.copilot;
  }

  if (projectKey === "front-end-vue" && manifest.unsupported) {
    snapshotManifest.unsupported = manifest.unsupported;
  }

  return snapshotManifest;
}

async function findDriftedOutputs(workspaceRoot, outputs) {
  const drifted = [];
  for (const output of outputs) {
    const absolutePath = path.join(workspaceRoot, output.path);
    const currentContent = await readIfExists(absolutePath);
    if (currentContent !== output.content) {
      drifted.push(output.path);
    }
  }
  return drifted;
}

async function writeOutputs(workspaceRoot, outputs) {
  const changed = [];
  for (const output of outputs) {
    const absolutePath = path.join(workspaceRoot, output.path);
    const currentContent = await readIfExists(absolutePath);
    if (currentContent === output.content) {
      continue;
    }

    await fs.mkdir(path.dirname(absolutePath), { recursive: true });
    await fs.writeFile(absolutePath, output.content, "utf8");
    changed.push(output.path);
  }
  return changed;
}

async function readIfExists(filePath) {
  try {
    return await fs.readFile(filePath, "utf8");
  } catch (error) {
    if (error && error.code === "ENOENT") {
      return null;
    }
    throw error;
  }
}

function joinCommands(commands) {
  return commands.filter((command) => command && command.trim() !== "").join("\n");
}

function renderTomlMultiline(value) {
  return `'''\n${value ?? ""}\n'''`;
}

function escapeTomlString(value) {
  return String(value).replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
}

function normalizeRelativePath(filePath) {
  return filePath.replaceAll("\\", "/");
}

function ensureTrailingNewline(value) {
  return value.endsWith("\n") ? value : `${value}\n`;
}

main().catch((error) => {
  console.error(error.message);
  process.exit(1);
});
