import crypto from "crypto";
import fs from "fs";
import path from "path";
import { build } from "esbuild";
import { execa, execaCommand } from "execa";
import { fileURLToPath, pathToFileURL } from "url";
import { resolveServiceCwd } from "../config/index.mjs";
import { buildExecutionEnv } from "../runner/template.mjs";

const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
const SETUP_ENTRY = path.join(PACKAGE_ROOT, "lib", "setup", "index.mjs");
const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
const KNOWN_FAILURES_ENTRY = path.join(PACKAGE_ROOT, "lib", "known-failures", "index.mjs");

export async function runTemplateStage(config, stageName, databaseUrl) {
  const steps = config.testkit.database?.template?.[stageName] || [];
  if (steps.length === 0) return;

  const env = {
    ...buildExecutionEnv(config, {}, process.env),
    DATABASE_URL: databaseUrl,
  };

  for (const [index, step] of steps.entries()) {
    const label = `template:${stageName}:${config.name}:${index + 1}`;
    console.log(`\n── ${label} ──`);
    await runTemplateStep(config, stageName, step, env);
  }
}

export function collectTemplateInputs(productDir, template = {}) {
  const inputs = new Set();
  for (const input of template.inputs || []) {
    inputs.add(resolveTemplatePath(productDir, null, input));
  }
  for (const stageName of ["migrate", "seed", "verify"]) {
    for (const step of template[stageName] || []) {
      if (step.kind === "sql-file") {
        inputs.add(resolveTemplatePath(productDir, step.cwd, step.path));
      }
      if (step.kind === "module") {
        inputs.add(resolveTemplatePath(productDir, step.cwd, parseModuleSpecifier(step.specifier).modulePath));
      }
      for (const input of step.inputs || []) {
        inputs.add(resolveTemplatePath(productDir, step.cwd, input));
      }
    }
  }
  return [...inputs].sort();
}

export async function captureTemplateSnapshot(config, outputPath, databaseUrl) {
  const templateDbUrl = databaseUrl;
  const absoluteOutputPath = path.resolve(config.productDir, outputPath);
  fs.mkdirSync(path.dirname(absoluteOutputPath), { recursive: true });

  await execa("pg_dump", [
    "--schema-only",
    "--no-owner",
    "--no-privileges",
    "--file",
    absoluteOutputPath,
    templateDbUrl,
  ], {
    cwd: config.productDir,
    env: {
      ...buildExecutionEnv(config, {}, process.env),
      DATABASE_URL: templateDbUrl,
    },
    stdio: "inherit",
  });

  sanitizeSnapshotFile(absoluteOutputPath);
  return absoluteOutputPath;
}

function sanitizeSnapshotFile(filePath) {
  const dump = fs.readFileSync(filePath, "utf8");
  const sanitized = dump
    .split("\n")
    .filter((line) => line.trim() !== "SET transaction_timeout = 0;")
    .join("\n");

  if (sanitized !== dump) {
    fs.writeFileSync(filePath, sanitized);
  }
}

async function runTemplateStep(config, stageName, step, env) {
  if (step.kind === "command") {
    await execaCommand(step.cmd, {
      cwd: resolveTemplateCwd(config.productDir, step.cwd),
      env,
      stdio: "inherit",
      shell: true,
    });
    return;
  }

  if (step.kind === "sql-file") {
    await execa("psql", [
      env.DATABASE_URL,
      "-v",
      "ON_ERROR_STOP=1",
      "-X",
      "-f",
      resolveTemplatePath(config.productDir, step.cwd, step.path),
    ], {
      cwd: resolveTemplateCwd(config.productDir, step.cwd),
      env,
      stdio: "inherit",
    });
    return;
  }

  if (step.kind === "module") {
    const moduleRef = await loadTemplateModule(config.productDir, step);
    const { exportName } = parseModuleSpecifier(step.specifier);
    const fn = moduleRef[exportName];
    if (typeof fn !== "function") {
      throw new Error(
        `Template module step "${step.specifier}" did not export a function named "${exportName}"`
      );
    }

    await withProcessContext(
      resolveTemplateCwd(config.productDir, step.cwd),
      env,
      async () => {
        await fn({
          productDir: config.productDir,
          cwd: resolveTemplateCwd(config.productDir, step.cwd),
          serviceName: config.name,
          stage: stageName,
          databaseUrl: env.DATABASE_URL,
          env: { ...env },
          runtimeId: config.runtimeId || null,
          stateDir: config.stateDir,
        });
      }
    );
    return;
  }

  throw new Error(`Unsupported template step kind "${step.kind}"`);
}

function resolveTemplateCwd(productDir, stepCwd) {
  return resolveServiceCwd(productDir, stepCwd || ".");
}

function resolveTemplatePath(productDir, stepCwd, targetPath) {
  return path.resolve(resolveTemplateCwd(productDir, stepCwd), targetPath);
}

async function loadTemplateModule(productDir, step) {
  const { modulePath } = parseModuleSpecifier(step.specifier);
  const absoluteModulePath = resolveTemplatePath(productDir, step.cwd, modulePath);
  const bundleDir = path.join(productDir, ".testkit", "_template-steps");
  fs.mkdirSync(bundleDir, { recursive: true });

  const cacheKey = buildModuleCacheKey(absoluteModulePath);
  const outputFile = path.join(bundleDir, `${path.basename(modulePath).replace(/\W+/g, "-")}-${cacheKey.slice(0, 12)}.mjs`);

  await build({
    absWorkingDir: productDir,
    bundle: true,
    entryPoints: [absoluteModulePath],
    format: "esm",
    legalComments: "none",
    outfile: outputFile,
    platform: "node",
    sourcemap: "inline",
    target: "es2020",
    plugins: [testkitAliasPlugin()],
  });

  return import(`${pathToFileURL(outputFile).href}?v=${cacheKey}`);
}

function buildModuleCacheKey(modulePath) {
  const content = fs.readFileSync(modulePath, "utf8");
  return crypto.createHash("sha256").update(modulePath).update("\0").update(content).digest("hex");
}

function testkitAliasPlugin() {
  return {
    name: "testkit-template-step-alias",
    setup(buildApi) {
      buildApi.onResolve({ filter: /^@elench\/testkit(?:\/.*)?$/ }, (args) => ({
        namespace: "file",
        path: resolvePackageSubpath(args.path),
      }));
    },
  };
}

function resolvePackageSubpath(specifier) {
  const subpath = specifier.slice("@elench/testkit".length);
  if (!subpath) return ROOT_ENTRY;
  if (subpath === "/setup") return SETUP_ENTRY;
  if (subpath === "/runtime") return RUNTIME_ENTRY;
  if (subpath === "/known-failures") return KNOWN_FAILURES_ENTRY;

  throw new Error(`Unsupported @elench/testkit import "${specifier}" while loading template step`);
}

function parseModuleSpecifier(specifier) {
  const [modulePath, exportName] = String(specifier).split("#", 2);
  return {
    modulePath,
    exportName: exportName || "default",
  };
}

async function withProcessContext(cwd, env, fn) {
  const previousCwd = process.cwd();
  const previousEnv = process.env;
  process.chdir(cwd);
  process.env = {
    ...previousEnv,
    ...env,
  };

  try {
    return await fn();
  } finally {
    process.chdir(previousCwd);
    process.env = previousEnv;
  }
}
