#!/usr/bin/env lua
-- zeta-makepkg — build Zeta binary packages from declarative .recipe files.
--
-- Usage:
--   zeta-makepkg <recipe> [flags]    Build a package from a recipe
--   zeta-makepkg --index [flags]     Regenerate packages/index.lua
--
-- Flags:
--   --output <dir>   Output repository root (default: .)
--   --no-index       Skip index.lua update
--   --keep-work      Preserve build work directory
--   --force          Overwrite existing package
--   --help           Show this help
--
-- Pipeline:
--   LOAD recipe → FETCH source → EXTRACT → BUILD → TEST → PACKAGE → INDEX

-- Lua 5.1 compat: arg[0] is the script path, not the interpreter.
local here
if arg and arg[0] and arg[0]:match("[/\\]") then
  here = arg[0]:match("^(.*)[/\\][^/\\]*$")
elseif arg and arg[-1] then
  -- Lua 5.1 sends script path as arg[-1] when using env.
  here = arg[-1]:match("^(.*)[/\\][^/\\]*$")
else
  here = "."
end

-- Resolve the real directory in case we were invoked via a symlink.
if here then
  local f = io.popen("cd " .. here:gsub("'", "'\\''") .. " 2>/dev/null && pwd")
  if f then
    local real = f:read("*l")
    f:close()
    if real and real ~= "" then here = real end
  end
else
  here = "."
end

-- Library path: works both from the repo tree (../lib/) and when installed
-- via 'make install' (/usr/local/lib/zeta-toolchain/).
package.path = here .. "/../lib/?.lua;"
  .. here .. "/../lib/zeta-toolchain/?.lua;"
  .. package.path

local recipe  = require("recipe")
local builder = require("builder")
local packager = require("packager")
local indexer = require("indexer")
local path    = require("path")
local log     = require("log")

---------------------------------------------------------------------------
-- Help
---------------------------------------------------------------------------

local HELP = [[
zeta-makepkg — build Zeta binary packages from declarative .recipe files.

Usage:
  zeta-makepkg <recipe> [flags]
  zeta-makepkg --index [flags]

Commands:
  <recipe>           Build a package from a .recipe file
  --index            Regenerate packages/index.lua from existing package.lua files

Flags:
  --output <dir>     Output repository root (default: .)
                     Creates packages/<name>/ with tarball + package.lua
  --repo <url>       Base URL for generated package.lua manifests
                     (default: https://raw.githubusercontent.com/gretagen/...)
  -j, --jobs <N>    Number of parallel build jobs (default: nproc)
  --no-index         Don't update packages/index.lua after building
  --keep-work        Don't remove the build work directory
  --force            Overwrite existing package tarball
  --help             Show this help

Cache:
  Downloaded sources are cached in ~/.cache/zeta-makepkg/sources/
  Build work is done in /tmp/zeta-makepkg/<name>-<timestamp>/

Examples:
  zeta-makepkg mypkg.recipe
  zeta-makepkg mypkg.recipe --output ~/zeta-packages --force
  zeta-makepkg --index --output ~/zeta-packages
]]

---------------------------------------------------------------------------
-- CLI parsing
---------------------------------------------------------------------------

local function parse_cli(args)
  local opts = {
    output    = ".",
    no_index  = false,
    keep_work = false,
    force     = false,
    index     = false,
    help      = false,
    recipe    = nil,
    jobs      = nil,  -- nil = auto-detect (nproc)
    repo      = "https://raw.githubusercontent.com/gretagen/zeta-packages/refs/heads/main",
  }

  local i = 1
  while i <= #args do
    local a = args[i]
    if a == "--help" or a == "-h" then
      opts.help = true
    elseif a == "--index" then
      opts.index = true
    elseif a == "--output" then
      i = i + 1
      if i > #args then
        io.stderr:write("error: --output requires a directory argument\n")
        return nil
      end
      opts.output = args[i]
    elseif a == "--repo" then
      i = i + 1
      if i > #args then
        io.stderr:write("error: --repo requires a URL argument\n")
        return nil
      end
      opts.repo = args[i]
    elseif a == "--no-index" then
      opts.no_index = true
    elseif a == "--keep-work" then
      opts.keep_work = true
    elseif a == "--force" then
      opts.force = true
    elseif a == "--jobs" then
      i = i + 1
      if i > #args then
        io.stderr:write("error: --jobs requires a number argument\n")
        return nil
      end
      local n = tonumber(args[i])
      if not n or n < 1 then
        io.stderr:write(("error: invalid jobs value %q\n"):format(args[i]))
        return nil
      end
      opts.jobs = n
    elseif a:match("^%-j%d+$") then
      local n = tonumber(a:match("^%-j(%d+)$"))
      opts.jobs = n
    elseif a == "-j" then
      i = i + 1
      if i > #args then
        io.stderr:write("error: -j requires a number argument\n")
        return nil
      end
      local n = tonumber(args[i])
      if not n or n < 1 then
        io.stderr:write(("error: invalid jobs value %q\n"):format(args[i]))
        return nil
      end
      opts.jobs = n
    elseif a:match("^%-") then
      io.stderr:write(("error: unknown flag %s\n"):format(a))
      return nil
    elseif not opts.recipe and not opts.index then
      opts.recipe = a
    else
      io.stderr:write(("error: unexpected argument %s\n"):format(a))
      return nil
    end
    i = i + 1
  end

  return opts
end

---------------------------------------------------------------------------
-- Build pipeline
---------------------------------------------------------------------------

local function run_build(recipe_path, opts)
  -- 1. Load recipe
  local r, err = recipe.load(recipe_path)
  if not r then
    log.fatal(("recipe error: %s"):format(tostring(err)))
  end

  -- 2. Set up output directory
  local packages_dir = path.join(opts.output, "packages")
  local pkg_dir = path.join(packages_dir, r.name)

  if path.exists(pkg_dir) and not opts.force then
    log.fatal(("package %s already exists at %s (use --force to overwrite)"):format(
      r.name, pkg_dir))
  end
  path.mkdir_p(pkg_dir)

  -- 3. Work directories
  local stamp = os.date("%Y%m%d-%H%M%S")
  local work_dir = path.join("/tmp/zeta-makepkg", r.name .. "-" .. stamp)
  local stage_dir = path.join(work_dir, "stage")
  path.mkdir_p(stage_dir)

  log.info(("work directory: %s"):format(work_dir))

  -- 4. Cache directory
  local cache_dir = path.join(os.getenv("HOME") or "/tmp", ".cache", "zeta-makepkg", "sources")

  -- 5. Fetch source
  local cache_file = builder.fetch_source(r, cache_dir)

  -- 6. Extract source
  local source_dir = builder.extract_source(r, cache_file, work_dir)

  -- 7. Build
  builder.run_build(r, source_dir, stage_dir, { jobs = opts.jobs })

  -- 8. Test (optional)
  if r.test then
    builder.run_test(r.test, stage_dir)
  else
    log.info("no test command — skipping verification")
  end

  -- 9. Package
  local tarball_path = path.join(pkg_dir, r.name .. "-" .. r.version .. ".tar.gz")
  local tarball_sha256 = packager.create_tarball(stage_dir, tarball_path)

  -- 10. Write package.lua manifest
  packager.write_manifest(r, tarball_sha256, pkg_dir, opts.repo)

  -- 11. Update index
  if not opts.no_index then
    indexer.generate(packages_dir)
  else
    log.info("--no-index: skipping index update")
  end

  -- 12. Cleanup
  if not opts.keep_work then
    path.run("rm -rf " .. path.quote(work_dir))
  else
    log.info(("--keep-work: work directory preserved at %s"):format(work_dir))
  end

  log.ok(("%s-%s built successfully"):format(r.name, r.version))
  log.info(("output: %s"):format(pkg_dir))
end

local function run_index(opts)
  local packages_dir = path.join(opts.output, "packages")
  indexer.generate(packages_dir)
end

---------------------------------------------------------------------------
-- Main
---------------------------------------------------------------------------

local args = {}
for i = 1, #arg do args[i] = arg[i] end

local opts = parse_cli(args)
if not opts then
  io.stderr:write("Run 'zeta-makepkg --help' for usage.\n")
  os.exit(1)
end

if opts.help then
  io.write(HELP)
  os.exit(0)
end

if opts.index then
  run_index(opts)
elseif opts.recipe then
  local ok, err = pcall(run_build, opts.recipe, opts)
  if not ok then
    log.fatal(tostring(err))
  end
else
  io.stderr:write("error: no recipe specified\n")
  io.stderr:write("Run 'zeta-makepkg --help' for usage.\n")
  os.exit(1)
end
