/**
 * Docker adapter.
 *
 * Emits a production-grade multi-stage `Dockerfile` targeting the Bun
 * runtime plus a `.dockerignore`. Does not invoke `docker` itself —
 * the adapter's scope is reproducible artifact generation, deployment
 * is delegated to whatever registry pipeline the user prefers.
 *
 * ## Generated Dockerfile structure
 *
 *   1. `base`  — shared `oven/bun:X.Y-alpine` stage, wires WORKDIR + PORT
 *   2. `deps`  — installs production deps against `package.json` + lockfile
 *   3. `build` — runs `bun run build` to produce `.mandu/`
 *   4. runtime — copies the build output + `.mandu/` into a trim image
 *
 * The multi-stage split keeps the final image small (no build tooling,
 * no source) and makes `docker build` caching reliable because only the
 * runtime stage changes when application code changes.
 *
 * @module cli/commands/deploy/adapters/docker
 */
import path from "node:path";
import { CLI_ERROR_CODES } from "../../../errors/codes";
import {
  appendUniqueLines,
  writeArtifact,
} from "../artifact-writer";
import type {
  AdapterArtifact,
  AdapterCheckResult,
  AdapterIssue,
  DeployAdapter,
} from "../types";

/**
 * Pinned to match `packages/cli/package.json#engines.bun` so the runtime
 * image matches the Bun the developer actually ships with. Kept as a
 * constant (not a runtime lookup) so the artifact is deterministic even
 * in CI where `package.json` is read-only.
 */
const DEFAULT_BUN_IMAGE_TAG = "1.3.12-alpine";
const DEFAULT_PORT = 3333;

// ---------------------------------------------------------------------
// Dockerfile + .dockerignore templates
// ---------------------------------------------------------------------

export interface DockerfileOptions {
  /** Override the Bun image tag (e.g. `"1.3.12-debian"`). */
  bunImageTag?: string;
  /** Expose port (defaults to Mandu's 3333). */
  port?: number;
  /**
   * When `true` also emits a pre-build `RUN bun test` stage. Off by
   * default — Docker is not the place to fail-fast on tests, most teams
   * gate tests in CI.
   */
  runTestsInBuild?: boolean;
  /**
   * Whether to include a lockfile COPY line. We always copy `bun.lock*`
   * if the file is present so `bun install --frozen-lockfile` works.
   */
  hasLockfile: boolean;
}

export function renderDockerfile(options: DockerfileOptions): string {
  const bunTag = options.bunImageTag ?? DEFAULT_BUN_IMAGE_TAG;
  const port = options.port ?? DEFAULT_PORT;
  const lockfileCopy = options.hasLockfile
    ? 'COPY package.json bun.lock* ./\n'
    : 'COPY package.json ./\n';
  const testStage = options.runTestsInBuild
    ? "RUN bun test --bail\n"
    : "";

  return `# syntax=docker/dockerfile:1.6
# Generated by \`mandu deploy --target=docker\`.
# Multi-stage build: deps -> build -> runtime. Final image ships only
# the production node_modules + the \`.mandu/\` build output.

ARG BUN_IMAGE=oven/bun:${bunTag}

# ---- base ----
FROM \${BUN_IMAGE} AS base
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=${port}

# ---- deps ----
FROM base AS deps
${lockfileCopy}RUN bun install --frozen-lockfile --production

# ---- build ----
FROM base AS build
COPY . .
RUN bun install --frozen-lockfile
${testStage}RUN bun run build

# ---- runtime ----
FROM base AS runtime
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/.mandu ./.mandu
COPY --from=build /app/public ./public
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/app ./app

EXPOSE ${port}
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \\
  CMD wget -qO- http://127.0.0.1:${port}/ || exit 1

CMD ["bun", "run", "mandu", "start"]
`;
}

const DOCKERIGNORE_LINES = [
  "# Generated by mandu deploy --target=docker",
  "node_modules",
  ".git",
  ".mandu",
  "coverage",
  "dist",
  "build",
  ".env",
  ".env.*",
  "!.env.production.example",
  ".DS_Store",
  "npm-debug.log*",
  "yarn-debug.log*",
  "yarn-error.log*",
  "bun-debug.log*",
  "*.log",
  "tmp",
  "temp",
];

// ---------------------------------------------------------------------
// Adapter
// ---------------------------------------------------------------------

export const dockerAdapter: DeployAdapter = {
  name: "Docker",
  target: "docker",
  minimumCliVersion: null,
  secrets: [],

  async check(project): Promise<AdapterCheckResult> {
    const errors: AdapterIssue[] = [];
    const warnings: AdapterIssue[] = [];

    const packageJsonPath = path.join(project.rootDir, "package.json");
    if (!(await pathExists(packageJsonPath))) {
      errors.push({
        code: CLI_ERROR_CODES.DEPLOY_CONFIG_INVALID,
        message: "package.json is missing — cannot build a Docker image.",
      });
    }

    if (!project.manifest) {
      warnings.push({
        code: CLI_ERROR_CODES.DEPLOY_MANIFEST_MISSING,
        message: "Routes manifest not yet built — `prepare()` will emit the Dockerfile anyway.",
      });
    }

    return { ok: errors.length === 0, errors, warnings };
  },

  async prepare(project, options): Promise<AdapterArtifact[]> {
    const artifacts: AdapterArtifact[] = [];
    const rootDir = project.rootDir;

    const hasLockfile = await pathExists(path.join(rootDir, "bun.lock"));
    const dockerfileContent = renderDockerfile({
      hasLockfile,
      bunImageTag: DEFAULT_BUN_IMAGE_TAG,
      port: project.config.server?.port ?? DEFAULT_PORT,
    });
    const dockerfilePath = path.join(rootDir, "Dockerfile");
    const dockerfileResult = await writeArtifact({
      forbiddenValues: options.forbiddenSecrets,
      path: dockerfilePath,
      content: dockerfileContent,
    });
    artifacts.push({
      path: dockerfileResult.path,
      preserved: dockerfileResult.preserved,
      description: `Multi-stage Bun ${DEFAULT_BUN_IMAGE_TAG} image`,
    });

    const dockerignorePath = path.join(rootDir, ".dockerignore");
    await appendUniqueLines(dockerignorePath, DOCKERIGNORE_LINES);
    artifacts.push({
      path: dockerignorePath,
      description: "Ignore list for Docker build context",
    });

    // Consume options to keep TypeScript from flagging the parameter as
    // unused; future iterations may honor `options.env` for build args.
    void options;

    return artifacts;
  },

  // No `deploy()` — the adapter is artifact-only. Users run their own
  // `docker build` + `docker push` pipelines.
};

async function pathExists(target: string): Promise<boolean> {
  try {
    await (await import("node:fs/promises")).access(target);
    return true;
  } catch {
    return false;
  }
}
