import path from "path";
import { readDatabaseInfo } from "./state-io.mjs";

const PORT_STRIDE = 100;

export function resolveRuntimeInstanceConfigs(runtimeConfigs, runtimeId, runtimeDir, options = {}) {
  const runtimeLabel = buildRuntimeLabel(options.graphDirName, runtimeId);
  const portMap = buildPortMap(runtimeConfigs, runtimeId, {
    index: options.portNamespaceIndex || 0,
    stride: options.portNamespaceStride || 1,
  });
  const baseUrlByService = new Map();
  const readyUrlByService = new Map();
  const stateDirByService = new Map();

  for (const config of runtimeConfigs) {
    stateDirByService.set(config.name, resolveServiceStateDir(runtimeDir, config));
  }

  for (const config of runtimeConfigs) {
    if (!config.testkit.local) continue;
    baseUrlByService.set(
      config.name,
      resolveRuntimeUrl(config.testkit.local.baseUrl, config.name, runtimeId, {
        runtimeId,
        runtimeLabel,
        runtimeDir,
        portMap,
        baseUrlByService,
        readyUrlByService,
        stateDirByService,
      })
    );
    readyUrlByService.set(
      config.name,
      resolveRuntimeUrl(config.testkit.local.readyUrl, config.name, runtimeId, {
        runtimeId,
        runtimeLabel,
        runtimeDir,
        portMap,
        baseUrlByService,
        readyUrlByService,
        stateDirByService,
      })
    );
  }

  const urlMappings = [];
  for (const config of runtimeConfigs) {
    if (!config.testkit.local) continue;
    const resolvedBaseUrl = baseUrlByService.get(config.name);
    const resolvedReadyUrl = readyUrlByService.get(config.name);
    if (resolvedBaseUrl) urlMappings.push([config.testkit.local.baseUrl, resolvedBaseUrl]);
    if (resolvedReadyUrl) urlMappings.push([config.testkit.local.readyUrl, resolvedReadyUrl]);
  }

  return runtimeConfigs.map((config) =>
    resolveRuntimeConfig(
      config,
      runtimeId,
      runtimeLabel,
      runtimeDir,
      portMap,
      baseUrlByService,
      readyUrlByService,
      stateDirByService,
      urlMappings
    )
  );
}

export function buildPortMap(runtimeConfigs, runtimeId, namespace = {}) {
  const portMap = new Map();
  const seen = new Map();
  const offset = runtimePortOffset(runtimeId, namespace);

  for (const config of runtimeConfigs) {
    if (!config.testkit.local) continue;

    const basePort = config.testkit.local.port || numericPortFromUrl(config.testkit.local.baseUrl);
    if (!basePort) continue;

    const actualPort = basePort + offset;
    if (actualPort > 65_535) {
      throw new Error(
        `Runtime port resolution exceeded 65535 for service "${config.name}" (${actualPort}). ` +
          `Reduce runtime instances or choose lower local.port/baseUrl ports in testkit.setup.ts.`
      );
    }
    const existing = seen.get(actualPort);
    if (existing) {
      throw new Error(
        `Runtime port collision: services "${existing}" and "${config.name}" both resolve to ${actualPort}. ` +
          `Assign distinct local.port/baseUrl ports in testkit.setup.ts.`
      );
    }
    seen.set(actualPort, config.name);
    portMap.set(config.name, actualPort);
  }

  return portMap;
}

export function resolveRuntimeConfig(
  config,
  runtimeId,
  runtimeLabel,
  runtimeDir,
  portMap,
  baseUrlByService,
  readyUrlByService,
  stateDirByService,
  urlMappings
) {
  const stateDir = resolveServiceStateDir(runtimeDir, config);
  const prepareDir = resolveServicePrepareDir(runtimeDir, config);
  const context = {
    runtimeId,
    runtimeLabel,
    runtimeDir,
    serviceName: config.name,
    serviceStateDir: stateDir,
    prepareDir,
    portMap,
    baseUrlByService,
    readyUrlByService,
    stateDirByService,
    urlMappings,
    leaseId: null,
    leaseDir: null,
  };

  const database = config.testkit.database
    ? {
        ...config.testkit.database,
        template: finalizeDatabaseTemplate(config.testkit.database.template, context),
      }
    : undefined;

  const runtime = {
    ...config.testkit.runtime,
    prepare: finalizeRuntimePrepare(config.testkit.runtime.prepare, context),
  };

  const local = config.testkit.local
    ? {
        ...config.testkit.local,
        start: finalizeString(config.testkit.local.start, context),
        cwd:
          config.testkit.local.cwd !== undefined
            ? finalizeString(config.testkit.local.cwd, context)
            : config.testkit.local.cwd,
        port: portMap.get(config.name) || config.testkit.local.port,
        baseUrl: baseUrlByService.get(config.name),
        readyUrl: readyUrlByService.get(config.name),
        env: { ...(config.testkit.local.env || {}) },
      }
    : undefined;

  return {
    ...config,
    stateDir,
    runtimeId,
    runtimeLabel,
    testkit: {
      ...config.testkit,
      database,
      prepareDir,
      templateContext: context,
      runtime,
      local,
    },
  };
}

function finalizeDatabaseTemplate(template, context) {
  if (!template) {
    return {
      inputs: [],
      migrate: [],
      seed: [],
      verify: [],
    };
  }

  const finalizeStep = (step) => ({
    ...step,
    ...(typeof step.cmd === "string" ? { cmd: finalizeString(step.cmd, context) } : {}),
    ...(typeof step.cwd === "string" ? { cwd: finalizeString(step.cwd, context) } : {}),
    ...(typeof step.path === "string" ? { path: finalizeString(step.path, context) } : {}),
    ...(typeof step.specifier === "string"
      ? { specifier: finalizeString(step.specifier, context) }
      : {}),
    inputs: (step.inputs || []).map((input) => finalizeString(input, context)),
  });

  return {
    inputs: (template.inputs || []).map((input) => finalizeString(input, context)),
    migrate: (template.migrate || []).map(finalizeStep),
    seed: (template.seed || []).map(finalizeStep),
    verify: (template.verify || []).map(finalizeStep),
  };
}

export function resolveServiceStateDir(runtimeDir, config) {
  return path.join(runtimeDir, "services", config.name);
}

export function resolveServicePrepareDir(runtimeDir, config) {
  return path.join(resolveServiceStateDir(runtimeDir, config), "prepared");
}

export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
  return buildExecutionEnvWithContext(config, null, extraEnv, processEnv);
}

export function buildTaskExecutionEnv(config, lease, extraEnv = {}, processEnv = process.env) {
  return buildExecutionEnvWithContext(config, lease, extraEnv, processEnv);
}

function buildExecutionEnvWithContext(config, lease, extraEnv, processEnv) {
  const inheritedEnv = { ...processEnv };
  const templateContext = buildTemplateContext(config, lease);
  const env = {
    ...inheritedEnv,
    ...resolveEnvTemplates(config.testkit.serviceEnv || {}, templateContext),
    ...resolveEnvTemplates(extraEnv, templateContext),
    TESTKIT_ACTIVE: "1",
    ...(config.runtimeId ? { TESTKIT_RUNTIME_ID: String(config.runtimeId) } : {}),
    ...(lease?.leaseId ? { TESTKIT_LEASE_ID: String(lease.leaseId) } : {}),
    ...(lease?.leaseDir ? { TESTKIT_LEASE_DIR: lease.leaseDir } : {}),
  };
  delete env.DATABASE_URL;
  return env;
}

export function buildPlaywrightEnv(config, baseUrl, lease, processEnv = process.env) {
  return buildTaskExecutionEnv(
    config,
    lease,
    {
      BASE_URL: baseUrl,
      PLAYWRIGHT_HTML_OPEN: "never",
      PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS:
        processEnv.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS || "1",
      TESTKIT_MANAGED_SERVERS: "1",
    },
    processEnv
  );
}

function buildTemplateContext(config, lease) {
  const baseContext = config.testkit?.templateContext || {};
  return {
    ...baseContext,
    runtimeId: config.runtimeId || baseContext.runtimeId || null,
    runtimeLabel: config.runtimeLabel || baseContext.runtimeLabel || null,
    serviceName: config.name,
    serviceStateDir: config.stateDir || baseContext.serviceStateDir || null,
    prepareDir: config.testkit?.prepareDir || baseContext.prepareDir || null,
    leaseId: lease?.leaseId || null,
    leaseDir: lease?.leaseDir || null,
  };
}

export function resolveRuntimeUrl(rawUrl, serviceName, runtimeId, context) {
  const resolved = resolveTemplateString(rawUrl, {
    ...context,
    runtimeId,
    serviceName,
  });
  const actualPort = context.portMap.get(serviceName);
  return actualPort ? rewriteUrlPort(resolved, actualPort) : resolved;
}

export function finalizeString(value, context) {
  let resolved = resolveTemplateString(value, context);
  for (const [source, destination] of context.urlMappings || []) {
    if (source && destination && source !== destination) {
      resolved = resolved.split(source).join(destination);
    }
  }
  return resolved;
}

export function resolveTemplateString(value, context) {
  if (typeof value !== "string") return value;

  return value.replace(/\{([a-zA-Z]+)(?::([a-zA-Z0-9_-]+))?\}/g, (_match, token, arg) => {
    switch (token) {
      case "runtime":
        return String(context.runtimeId);
      case "service":
        return context.serviceName;
      case "stateDir":
        return context.serviceStateDir;
      case "prepareDir":
        return context.prepareDir || "";
      case "lease":
        return context.leaseId ? String(context.leaseId) : "";
      case "leaseDir":
        return context.leaseDir || "";
      case "port": {
        const serviceName = arg || context.serviceName;
        const port = context.portMap.get(serviceName);
        if (!port) {
          throw new Error(`Unknown port placeholder for service "${serviceName}"`);
        }
        return String(port);
      }
      case "baseUrl": {
        const serviceName = arg || context.serviceName;
        const baseUrl = context.baseUrlByService.get(serviceName);
        if (!baseUrl) {
          throw new Error(`Unknown baseUrl placeholder for service "${serviceName}"`);
        }
        return baseUrl;
      }
      case "readyUrl": {
        const serviceName = arg || context.serviceName;
        const readyUrl = context.readyUrlByService.get(serviceName);
        if (!readyUrl) {
          throw new Error(`Unknown readyUrl placeholder for service "${serviceName}"`);
        }
        return readyUrl;
      }
      case "dbUrl":
      case "dbHost":
      case "dbPort":
      case "dbName":
      case "dbUser":
      case "dbPassword": {
        const serviceName = arg || context.serviceName;
        return resolveDatabaseTemplateValue(token, serviceName, context);
      }
      default:
        throw new Error(`Unsupported template token "{${token}${arg ? `:${arg}` : ""}}"`);
    }
  });
}

function resolveEnvTemplates(values, templateContext) {
  return Object.fromEntries(
    Object.entries(values || {}).map(([key, value]) => [
      key,
      typeof value === "string" && templateContext ? finalizeString(value, templateContext) : value,
    ])
  );
}

function finalizeRuntimePrepare(prepare, context) {
  if (!prepare) {
    return {
      inputs: [],
      steps: [],
    };
  }

  const finalizeStep = (step) => ({
    ...step,
    ...(typeof step.cmd === "string" ? { cmd: finalizeString(step.cmd, context) } : {}),
    ...(typeof step.cwd === "string" ? { cwd: finalizeString(step.cwd, context) } : {}),
    ...(typeof step.path === "string" ? { path: finalizeString(step.path, context) } : {}),
    ...(typeof step.specifier === "string"
      ? { specifier: finalizeString(step.specifier, context) }
      : {}),
    inputs: (step.inputs || []).map((input) => finalizeString(input, context)),
  });

  return {
    inputs: (prepare.inputs || []).map((input) => finalizeString(input, context)),
    steps: (prepare.steps || []).map(finalizeStep),
  };
}

function resolveDatabaseTemplateValue(token, serviceName, context) {
  const stateDir = context.stateDirByService?.get(serviceName);
  if (!stateDir) {
    throw new Error(`Unknown database placeholder for service "${serviceName}"`);
  }

  const info = readDatabaseInfo(stateDir);
  if (!info) {
    throw new Error(
      `Database placeholder "{${token}:${serviceName}}" is unavailable before "${serviceName}" database preparation`
    );
  }

  switch (token) {
    case "dbUrl":
      return info.url;
    case "dbHost":
      return info.host;
    case "dbPort":
      return String(info.port);
    case "dbName":
      return info.database;
    case "dbUser":
      return info.user;
    case "dbPassword":
      return info.password;
    default:
      throw new Error(`Unsupported database placeholder "{${token}:${serviceName}}"`);
  }
}

export function rewriteUrlPort(rawUrl, port) {
  try {
    const original = new URL(rawUrl);
    if (!original.port) return rawUrl;

    const rewritten = new URL(rawUrl);
    rewritten.port = String(port);

    let next = rewritten.toString();
    if (!rawUrl.endsWith("/") && original.pathname === "/" && next.endsWith("/")) {
      next = next.slice(0, -1);
    }
    return next;
  } catch {
    return rawUrl;
  }
}

export function numericPortFromUrl(rawUrl) {
  try {
    const url = new URL(rawUrl);
    const port = Number(url.port);
    return Number.isInteger(port) && port > 0 ? port : null;
  } catch {
    return null;
  }
}

export function socketFromUrl(rawUrl) {
  try {
    const url = new URL(rawUrl);
    const port = Number(url.port);
    if (!Number.isInteger(port) || port <= 0) return null;

    const host = normalizeSocketHost(url.hostname);
    return host ? { host, port } : null;
  } catch {
    return null;
  }
}

export function normalizeSocketHost(hostname) {
  if (!hostname || hostname === "localhost") return "127.0.0.1";
  if (hostname === "[::1]") return "::1";
  return hostname;
}

function runtimePortOffset(runtimeId, namespace = {}) {
  const match = String(runtimeId).match(/(\d+)$/);
  const runtimeIndex = match ? Number.parseInt(match[1], 10) - 1 : 0;
  const namespaceIndex = Math.max(0, Number(namespace.index || 0));
  const namespaceStride = Math.max(1, Number(namespace.stride || 1));
  return PORT_STRIDE * (namespaceIndex * namespaceStride + runtimeIndex);
}

function buildRuntimeLabel(graphDirName, runtimeId) {
  if (!graphDirName) return runtimeId;
  return `${graphDirName}/${runtimeId}`;
}
