#!/usr/bin/env node
import {
  existsSync,
  mkdirSync,
  readFileSync,
  readdirSync,
  realpathSync,
  statSync,
  writeFileSync,
} from "node:fs";
import { dirname, join, relative, resolve, sep } from "node:path";

const DEFAULT_WEB_ROOT = ".oysterun/site";
const DEFAULT_ACCESS = "owner_only";
const ACCESS_VALUES = new Set(["owner_only", "password", "public"]);
const TEXT_EXTENSIONS = new Set([
  ".html",
  ".htm",
  ".css",
  ".js",
  ".mjs",
  ".json",
  ".svg",
  ".txt",
  ".md",
  ".xml",
  ".webmanifest",
]);

function usage() {
  return [
    "Usage: site_author.mjs <validate|manifest|init> --agent-folder <path> --agent-id <id> [--access owner_only|password|public]",
    "",
    "Examples:",
    "  node .codex/skills/oysterun-website-authoring/scripts/site_author.mjs validate --agent-folder /tmp/agent --agent-id route-agent",
    "  node .codex/skills/oysterun-website-authoring/scripts/site_author.mjs manifest --agent-folder /tmp/agent --agent-id route-agent",
  ].join("\n");
}

function fail(message) {
  console.error(`site_author: ${message}`);
  process.exit(1);
}

function parseArgs(argv) {
  const [command, ...rest] = argv;
  if (!command || command === "--help" || command === "-h") {
    fail(usage());
  }
  const args = { command };
  for (let index = 0; index < rest.length; index += 1) {
    const token = rest[index];
    if (!token.startsWith("--")) fail(`unexpected positional argument: ${token}`);
    const key = token.slice(2);
    const value = rest[index + 1];
    if (!value || value.startsWith("--")) fail(`missing value for --${key}`);
    args[key] = value;
    index += 1;
  }
  return args;
}

function requireString(value, label) {
  if (typeof value !== "string" || !value.trim()) {
    fail(`${label} is required`);
  }
  return value.trim();
}

function readJsonObject(filePath, label) {
  if (!existsSync(filePath)) return {};
  let parsed;
  try {
    parsed = JSON.parse(readFileSync(filePath, "utf8"));
  } catch (error) {
    fail(`invalid JSON in ${label} ${filePath}: ${error.message}`);
  }
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    fail(`invalid ${label} root object in ${filePath}`);
  }
  return parsed;
}

function ensureInside(parentPath, childPath, label) {
  const normalizedParent = parentPath.endsWith(sep) ? parentPath : `${parentPath}${sep}`;
  if (childPath !== parentPath && !childPath.startsWith(normalizedParent)) {
    fail(`${label} must stay inside agent folder`);
  }
}

function resolveExistingDirectory(folderPath, label) {
  const resolved = resolve(folderPath);
  if (!existsSync(resolved)) fail(`${label} does not exist: ${resolved}`);
  const stats = statSync(resolved);
  if (!stats.isDirectory()) fail(`${label} is not a directory: ${resolved}`);
  return realpathSync(resolved);
}

function getWebConfig(agentRoot) {
  const configPath = join(agentRoot, ".oysterun", "config.json");
  const config = readJsonObject(configPath, "agent config");
  const web = config.web ?? {};
  if (!web || typeof web !== "object" || Array.isArray(web)) {
    fail(`invalid web object in ${configPath}`);
  }
  const root = typeof web.root === "string" && web.root.trim() ? web.root.trim() : DEFAULT_WEB_ROOT;
  const access = typeof web.access === "string" && web.access.trim() ? web.access.trim() : DEFAULT_ACCESS;
  if (!ACCESS_VALUES.has(access)) {
    fail(`web.access must be one of ${[...ACCESS_VALUES].join(", ")}`);
  }
  return {
    configPath,
    configured: Object.prototype.hasOwnProperty.call(config, "web"),
    root,
    access,
  };
}

function resolveSiteRoot(agentRoot, webRoot) {
  const resolved = resolve(agentRoot, webRoot);
  ensureInside(agentRoot, resolved, "web.root");
  return resolved;
}

function walkFiles(rootPath) {
  if (!existsSync(rootPath)) return [];
  const out = [];
  const stack = [rootPath];
  while (stack.length) {
    const current = stack.pop();
    for (const entry of readdirSync(current, { withFileTypes: true })) {
      const entryPath = join(current, entry.name);
      if (entry.isDirectory()) {
        stack.push(entryPath);
      } else if (entry.isFile()) {
        out.push(entryPath);
      }
    }
  }
  return out.sort();
}

function hasUnsafeTextReference(contents) {
  return /(?:file:\/\/|\/Users\/|\/Volumes\/|http:\/\/127\.0\.0\.1|http:\/\/localhost)/.test(contents);
}

function extensionOf(filePath) {
  const dot = filePath.lastIndexOf(".");
  return dot === -1 ? "" : filePath.slice(dot).toLowerCase();
}

function validateSite({ agentFolder, agentId }) {
  const agentRoot = resolveExistingDirectory(agentFolder, "agent folder");
  const normalizedAgentId = requireString(agentId, "agent id");
  const web = getWebConfig(agentRoot);
  const siteRoot = resolveSiteRoot(agentRoot, web.root);
  const indexPath = join(siteRoot, "index.html");

  if (!existsSync(siteRoot)) fail(`website root does not exist: ${siteRoot}`);
  if (!statSync(siteRoot).isDirectory()) fail(`website root is not a directory: ${siteRoot}`);
  if (!existsSync(indexPath)) fail(`website entry does not exist: ${indexPath}`);
  if (!statSync(indexPath).isFile()) fail(`website entry is not a file: ${indexPath}`);

  const realSiteRoot = realpathSync(siteRoot);
  ensureInside(agentRoot, realSiteRoot, "resolved website root");

  const files = walkFiles(realSiteRoot);
  const unsafeFiles = [];
  for (const filePath of files) {
    ensureInside(realSiteRoot, realpathSync(filePath), "website asset");
    if (!TEXT_EXTENSIONS.has(extensionOf(filePath))) continue;
    const contents = readFileSync(filePath, "utf8");
    if (hasUnsafeTextReference(contents)) {
      unsafeFiles.push(relative(agentRoot, filePath));
    }
  }
  if (unsafeFiles.length) {
    fail(`unsafe local/server references found: ${unsafeFiles.join(", ")}`);
  }

  return {
    ok: true,
    agent_id: normalizedAgentId,
    site_root: relative(agentRoot, realSiteRoot) || ".",
    entry_source: relative(agentRoot, indexPath),
    entry_route: `/sites/${encodeURIComponent(normalizedAgentId)}/`,
    access: web.access,
    configured: web.configured,
    file_count: files.length,
  };
}

function writeInitialSite(agentRoot, agentId, access) {
  const oysterRoot = join(agentRoot, ".oysterun");
  const siteRoot = join(agentRoot, DEFAULT_WEB_ROOT);
  mkdirSync(join(siteRoot, "assets"), { recursive: true });
  const configPath = join(oysterRoot, "config.json");
  if (!existsSync(configPath)) {
    mkdirSync(dirname(configPath), { recursive: true });
    writeFileSync(
      configPath,
      `${JSON.stringify({ web: { root: DEFAULT_WEB_ROOT, access } }, null, 2)}\n`
    );
  }
  const indexPath = join(siteRoot, "index.html");
  if (!existsSync(indexPath)) {
    writeFileSync(
      indexPath,
      [
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">',
        `  <title>${escapeHtml(agentId)} Site</title>`,
        '  <meta name="theme-color" content="#f8fafc">',
        "</head>",
        "<body>",
        `  <main><h1>${escapeHtml(agentId)} Site</h1></main>`,
        "</body>",
        "</html>",
        "",
      ].join("\n")
    );
  }
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

function main() {
  const args = parseArgs(process.argv.slice(2));
  const command = args.command;
  const agentFolder = requireString(args["agent-folder"], "agent folder");
  const agentId = requireString(args["agent-id"], "agent id");

  if (command === "init") {
    const access = args.access ? requireString(args.access, "access") : DEFAULT_ACCESS;
    if (!ACCESS_VALUES.has(access)) {
      fail(`access must be one of ${[...ACCESS_VALUES].join(", ")}`);
    }
    const agentRoot = resolveExistingDirectory(agentFolder, "agent folder");
    writeInitialSite(agentRoot, agentId, access);
  } else if (command !== "validate" && command !== "manifest") {
    fail(`unknown command: ${command}\n${usage()}`);
  }

  const manifest = validateSite({ agentFolder, agentId });
  console.log(JSON.stringify(manifest, null, 2));
}

main();
