import path from "path";

const PORT_STRIDE = 100;

export function resolveWorkerRuntimeConfigs(targetConfig, runtimeConfigs, workerId, workerStateDir) {
  const portMap = buildPortMap(runtimeConfigs, workerId);
  const baseUrlByService = new Map();
  const readyUrlByService = new Map();

  for (const config of runtimeConfigs) {
    if (!config.testkit.local) continue;
    baseUrlByService.set(
      config.name,
      resolveRuntimeUrl(config.testkit.local.baseUrl, config.name, targetConfig, workerId, {
        workerStateDir,
        portMap,
        baseUrlByService,
        readyUrlByService,
      })
    );
    readyUrlByService.set(
      config.name,
      resolveRuntimeUrl(config.testkit.local.readyUrl, config.name, targetConfig, workerId, {
        workerStateDir,
        portMap,
        baseUrlByService,
        readyUrlByService,
      })
    );
  }

  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) =>
    resolveWorkerConfig(
      config,
      targetConfig,
      workerId,
      workerStateDir,
      portMap,
      baseUrlByService,
      readyUrlByService,
      urlMappings
    )
  );
}

export function buildPortMap(runtimeConfigs, workerId) {
  const portMap = new Map();
  const seen = new Map();
  const offset = PORT_STRIDE * (workerId - 1);

  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;
    const existing = seen.get(actualPort);
    if (existing) {
      throw new Error(
        `Worker port collision: services "${existing}" and "${config.name}" both resolve to ${actualPort}. ` +
          `Assign distinct local.port/baseUrl ports in testkit.config.json.`
      );
    }
    seen.set(actualPort, config.name);
    portMap.set(config.name, actualPort);
  }

  return portMap;
}

export function resolveWorkerConfig(
  config,
  targetConfig,
  workerId,
  workerStateDir,
  portMap,
  baseUrlByService,
  readyUrlByService,
  urlMappings
) {
  const stateDir = resolveServiceStateDir(workerStateDir, targetConfig.name, config);
  const context = {
    workerId,
    serviceName: config.name,
    targetName: targetConfig.name,
    serviceStateDir: stateDir,
    portMap,
    baseUrlByService,
    readyUrlByService,
    urlMappings,
  };

  const database = config.testkit.database
    ? {
        ...config.testkit.database,
      }
    : undefined;

  const migrate = config.testkit.migrate
    ? {
        ...config.testkit.migrate,
        cmd: finalizeString(config.testkit.migrate.cmd, context),
        cwd:
          config.testkit.migrate.cwd !== undefined
            ? finalizeString(config.testkit.migrate.cwd, context)
            : config.testkit.migrate.cwd,
      }
    : undefined;

  const seed = config.testkit.seed
    ? {
        ...config.testkit.seed,
        cmd: finalizeString(config.testkit.seed.cmd, context),
        cwd:
          config.testkit.seed.cwd !== undefined
            ? finalizeString(config.testkit.seed.cwd, context)
            : config.testkit.seed.cwd,
      }
    : undefined;

  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: Object.fromEntries(
          Object.entries(config.testkit.local.env || {}).map(([key, value]) => [
            key,
            finalizeString(String(value), context),
          ])
        ),
      }
    : undefined;

  return {
    ...config,
    stateDir,
    workerId,
    workerLabel: `w${workerId}`,
    targetName: targetConfig.name,
    testkit: {
      ...config.testkit,
      database,
      migrate,
      seed,
      local,
    },
  };
}

export function resolveServiceStateDir(workerStateDir, targetName, config) {
  const dbSource = config.testkit.databaseFrom || config.name;
  return getWorkerServiceStateDir(workerStateDir, targetName, dbSource);
}

export function getWorkerServiceStateDir(workerStateDir, targetName, serviceName) {
  if (targetName === serviceName) {
    return workerStateDir;
  }
  return path.join(workerStateDir, "deps", serviceName);
}

export function buildExecutionEnv(config, extraEnv = {}, processEnv = process.env) {
  const inheritedEnv = { ...processEnv };
  const env = {
    ...inheritedEnv,
    ...(config.testkit.serviceEnv || {}),
    ...extraEnv,
    TESTKIT_ACTIVE: "1",
    ...(config.workerId ? { TESTKIT_WORKER_ID: String(config.workerId) } : {}),
  };
  delete env.DATABASE_URL;
  return env;
}

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

export function resolveRuntimeUrl(rawUrl, serviceName, targetConfig, workerId, context) {
  const resolved = resolveTemplateString(rawUrl, {
    ...context,
    targetName: targetConfig.name,
    workerId,
    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 "worker":
        return String(context.workerId);
      case "target":
        return context.targetName;
      case "service":
        return context.serviceName;
      case "stateDir":
        return context.serviceStateDir;
      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;
      }
      default:
        throw new Error(`Unsupported template token "{${token}${arg ? `:${arg}` : ""}}"`);
    }
  });
}

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;
}
