import crypto from "crypto";
import fs from "fs";
import path from "path";
import { resolveServiceCwd } from "../config/index.mjs";
import { appendFileToHash, appendInputToHash } from "../database/fingerprint.mjs";
import { announceResolvedToolchain, resolveConfiguredToolchain } from "../toolchains/index.mjs";
import { readDatabaseUrl } from "./state-io.mjs";
import { buildExecutionEnv } from "./template.mjs";
import {
  collectConfiguredInputs,
  runConfiguredSteps,
} from "./template-steps.mjs";

const MANIFEST_FILE = "prepare-manifest.json";

export async function prepareRuntimeServices(runtimeConfigs) {
  for (const config of runtimeConfigs) {
    await prepareRuntimeService(config);
  }
}

export async function prepareRuntimeService(config) {
  const prepare = config.testkit.runtime.prepare;
  if (!prepare || prepare.steps.length === 0) return;

  const prepareDir = config.testkit.prepareDir;
  const fingerprint = await computeRuntimePrepareFingerprint(config);
  const manifestPath = path.join(prepareDir, MANIFEST_FILE);
  const existingManifest = readPrepareManifest(manifestPath);
  if (existingManifest?.fingerprint === fingerprint) {
    return;
  }

  fs.rmSync(prepareDir, { recursive: true, force: true });
  fs.mkdirSync(prepareDir, { recursive: true });

  const env = {
    ...buildExecutionEnv(config, config.testkit.local?.env || {}, process.env),
  };
  const databaseUrl = readDatabaseUrl(config.stateDir);
  if (databaseUrl) {
    env.DATABASE_URL = databaseUrl;
  }

  try {
    await announceResolvedToolchain(config, await resolveConfiguredToolchain(config));
    await runConfiguredSteps({
      config,
      steps: prepare.steps,
      env,
      labelPrefix: "runtime:prepare",
    });
    writePrepareManifest(manifestPath, {
      fingerprint,
      preparedAt: new Date().toISOString(),
      runtimeId: config.runtimeId || null,
      serviceName: config.name,
    });
  } catch (error) {
    fs.rmSync(prepareDir, { recursive: true, force: true });
    throw error;
  }
}

export async function computeRuntimePrepareFingerprint(config) {
  const resolvedToolchain = await resolveConfiguredToolchain(config);
  const hash = crypto.createHash("sha256");
  hash.update(
    JSON.stringify({
      prepare: config.testkit.runtime.prepare || null,
      toolchain: resolvedToolchain
        ? {
            kind: resolvedToolchain.kind,
            install: resolvedToolchain.install,
            nodeVersion: resolvedToolchain.nodeVersion,
            npmVersion: resolvedToolchain.npmVersion,
            nodeSource: resolvedToolchain.nodeSource,
            npmSource: resolvedToolchain.npmSource,
            fingerprint: resolvedToolchain.fingerprint,
          }
        : null,
      serviceEnv: config.testkit.serviceEnv || {},
      local: config.testkit.local
        ? {
            baseUrl: config.testkit.local.baseUrl,
            cwd: config.testkit.local.cwd,
            env: config.testkit.local.env || {},
            readyUrl: config.testkit.local.readyUrl,
            start: config.testkit.local.start,
          }
        : null,
    })
  );

  for (const envFile of config.testkit.envFiles || []) {
    appendFileToHash(hash, config.productDir, resolveServiceCwd(config.productDir, envFile));
  }
  for (const input of collectConfiguredInputs(config.productDir, config.testkit.runtime.prepare)) {
    appendResolvedInputToHash(hash, config.productDir, input);
  }

  return hash.digest("hex");
}

function appendResolvedInputToHash(hash, productDir, absPath) {
  const relative = path.relative(productDir, absPath);
  appendInputToHash(hash, productDir, relative);
}

function readPrepareManifest(manifestPath) {
  if (!fs.existsSync(manifestPath)) return null;
  try {
    return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
  } catch {
    return null;
  }
}

function writePrepareManifest(manifestPath, manifest) {
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
}
