import type { MicroserviceConfig, RuntimeConfig } from "./types";

function requiredEnv(name: string): string {
  const value = process.env[name];
  if (value === undefined || value.length === 0) {
    throw new Error(`Missing required environment variable: ${name}`);
  }
  return value;
}

function readNumberEnv(name: string): number {
  const raw = requiredEnv(name);
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    throw new Error(`Environment variable ${name} must be a positive number, got ${raw}`);
  }
  return parsed;
}

function readOptionalNumberEnv(name: string, fallback: number): number {
  const raw = process.env[name];
  if (raw === undefined || raw.length === 0) return fallback;
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed <= 0) {
    throw new Error(`Environment variable ${name} must be a positive number, got ${raw}`);
  }
  return parsed;
}

function asRecord(value: unknown, name: string): Record<string, unknown> {
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${name} must be an object`);
  return value as Record<string, unknown>;
}

function stringFromRecord(value: Record<string, unknown>, key: string, path: string): string {
  const field = value[key];
  if (typeof field !== "string" || field.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
  return field;
}

function optionalStringFromRecord(value: Record<string, unknown>, key: string, path: string): string | undefined {
  const field = value[key];
  if (field === undefined) return undefined;
  if (typeof field !== "string" || field.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
  return field;
}

function numberFromRecord(value: Record<string, unknown>, key: string, path: string): number {
  const field = value[key];
  if (typeof field !== "number" || !Number.isFinite(field) || field <= 0) throw new Error(`${path}.${key} must be a positive number`);
  return field;
}

function booleanFromRecord(value: Record<string, unknown>, key: string, path: string): boolean {
  const field = value[key];
  if (typeof field !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
  return field;
}

function stringArrayFromRecord(value: Record<string, unknown>, key: string, path: string): string[] {
  const field = value[key];
  if (!Array.isArray(field) || field.some((item) => typeof item !== "string" || item.length === 0)) {
    throw new Error(`${path}.${key} must be an array of non-empty strings`);
  }
  return field as string[];
}

function optionalStringArrayFromRecord(value: Record<string, unknown>, key: string, path: string): string[] | undefined {
  const field = value[key];
  if (field === undefined) return undefined;
  return stringArrayFromRecord(value, key, path);
}

function optionalArtifactSourceFromRecord(repository: Record<string, unknown>, path: string): MicroserviceConfig["repository"]["artifactSource"] {
  const field = repository.artifactSource;
  if (field === undefined) return undefined;
  const source = asRecord(field, `${path}.artifactSource`);
  const kind = stringFromRecord(source, "kind", `${path}.artifactSource`);
  if (kind !== "upstream-image") throw new Error(`${path}.artifactSource.kind must be upstream-image`);
  return {
    kind,
    imageRef: stringFromRecord(source, "imageRef", `${path}.artifactSource`),
    digestPinRequired: booleanFromRecord(source, "digestPinRequired", `${path}.artifactSource`),
    mirrorRepository: stringFromRecord(source, "mirrorRepository", `${path}.artifactSource`),
    ciDockerfileBuild: booleanFromRecord(source, "ciDockerfileBuild", `${path}.artifactSource`),
    pullOnlyCd: booleanFromRecord(source, "pullOnlyCd", `${path}.artifactSource`),
  };
}

function parseMicroserviceConfig(value: unknown, index: number): MicroserviceConfig {
  const path = `MICROSERVICES_JSON[${index}]`;
  const item = asRecord(value, path);
  const repository = asRecord(item.repository, `${path}.repository`);
  const backend = asRecord(item.backend, `${path}.backend`);
  const development = asRecord(item.development, `${path}.development`);
  const frontend = asRecord(item.frontend, `${path}.frontend`);
  const deployment = item.deployment === undefined ? undefined : asRecord(item.deployment, `${path}.deployment`);
  const deploymentMode = deployment === undefined ? "unidesk-direct" : stringFromRecord(deployment, "mode", `${path}.deployment`);
  if (deploymentMode !== "unidesk-direct" && deploymentMode !== "k3sctl-managed" && deploymentMode !== "internal-sidecar") {
    throw new Error(`${path}.deployment.mode must be unidesk-direct, k3sctl-managed, or internal-sidecar`);
  }
  return {
    id: stringFromRecord(item, "id", path),
    name: stringFromRecord(item, "name", path),
    providerId: stringFromRecord(item, "providerId", path),
    description: stringFromRecord(item, "description", path),
    repository: {
      url: stringFromRecord(repository, "url", `${path}.repository`),
      commitId: stringFromRecord(repository, "commitId", `${path}.repository`),
      dockerfile: stringFromRecord(repository, "dockerfile", `${path}.repository`),
      artifactSource: optionalArtifactSourceFromRecord(repository, `${path}.repository`),
      composeFile: stringFromRecord(repository, "composeFile", `${path}.repository`),
      composeService: stringFromRecord(repository, "composeService", `${path}.repository`),
      containerName: stringFromRecord(repository, "containerName", `${path}.repository`),
    },
    backend: {
      nodeBaseUrl: stringFromRecord(backend, "nodeBaseUrl", `${path}.backend`),
      nodeBindHost: stringFromRecord(backend, "nodeBindHost", `${path}.backend`),
      nodePort: numberFromRecord(backend, "nodePort", `${path}.backend`),
      proxyMode: stringFromRecord(backend, "proxyMode", `${path}.backend`),
      frontendOnly: booleanFromRecord(backend, "frontendOnly", `${path}.backend`),
      public: booleanFromRecord(backend, "public", `${path}.backend`),
      allowedMethods: stringArrayFromRecord(backend, "allowedMethods", `${path}.backend`).map((method) => method.toUpperCase()),
      allowedPathPrefixes: stringArrayFromRecord(backend, "allowedPathPrefixes", `${path}.backend`),
      healthPath: stringFromRecord(backend, "healthPath", `${path}.backend`),
      timeoutMs: numberFromRecord(backend, "timeoutMs", `${path}.backend`),
    },
    deployment: deployment === undefined
      ? { mode: "unidesk-direct" }
      : {
        mode: deploymentMode,
        adapterServiceId: optionalStringFromRecord(deployment, "adapterServiceId", `${path}.deployment`),
        k3sServiceId: optionalStringFromRecord(deployment, "k3sServiceId", `${path}.deployment`),
        namespace: optionalStringFromRecord(deployment, "namespace", `${path}.deployment`),
        expectedNodeIds: optionalStringArrayFromRecord(deployment, "expectedNodeIds", `${path}.deployment`),
        activeNodeId: optionalStringFromRecord(deployment, "activeNodeId", `${path}.deployment`),
      },
    development: {
      providerId: stringFromRecord(development, "providerId", `${path}.development`),
      sshPassthrough: booleanFromRecord(development, "sshPassthrough", `${path}.development`),
      worktreePath: stringFromRecord(development, "worktreePath", `${path}.development`),
    },
    frontend: {
      route: stringFromRecord(frontend, "route", `${path}.frontend`),
      integrated: booleanFromRecord(frontend, "integrated", `${path}.frontend`),
    },
  };
}

function readMicroservicesEnv(): MicroserviceConfig[] {
  const raw = process.env.MICROSERVICES_JSON;
  if (raw === undefined || raw.length === 0) return [];
  const parsed = JSON.parse(raw) as unknown;
  if (!Array.isArray(parsed)) throw new Error("MICROSERVICES_JSON must be an array");
  return parsed.map(parseMicroserviceConfig);
}

function optionalEnv(name: string): string {
  return process.env[name]?.trim() ?? "";
}

function databaseNameFromUrl(databaseUrl: string): string {
  try {
    return new URL(databaseUrl).pathname.replace(/^\/+/u, "");
  } catch {
    return "";
  }
}

export function readConfig(): RuntimeConfig {
  const databaseUrl = requiredEnv("DATABASE_URL");
  return {
    port: readNumberEnv("PORT"),
    providerPort: readNumberEnv("PROVIDER_PORT"),
    databaseUrl,
    identity: {
      environment: optionalEnv("UNIDESK_ENV") || "prod",
      namespace: optionalEnv("UNIDESK_NAMESPACE"),
      databaseName: optionalEnv("UNIDESK_DATABASE_NAME") || optionalEnv("UNIDESK_DEV_DATABASE_NAME") || databaseNameFromUrl(databaseUrl),
      deployRef: optionalEnv("UNIDESK_DEPLOY_REF"),
      serviceId: optionalEnv("UNIDESK_DEPLOY_SERVICE_ID") || "backend-core",
      repo: optionalEnv("UNIDESK_DEPLOY_REPO"),
      commit: optionalEnv("UNIDESK_DEPLOY_COMMIT"),
      requestedCommit: optionalEnv("UNIDESK_DEPLOY_REQUESTED_COMMIT"),
    },
    providerToken: requiredEnv("PROVIDER_TOKEN"),
    heartbeatTimeoutMs: readNumberEnv("HEARTBEAT_TIMEOUT_MS"),
    taskPendingTimeoutMs: readOptionalNumberEnv("TASK_PENDING_TIMEOUT_MS", 10 * 60 * 1000),
    databaseVolumeName: requiredEnv("DATABASE_VOLUME_NAME"),
    databaseVolumeSize: requiredEnv("DATABASE_VOLUME_SIZE"),
    pgdataBackupStagingDir: process.env.PGDATA_BACKUP_STAGING_DIR || "/data/baidu-netdisk-staging",
    baiduNetdiskInternalUrl: process.env.BAIDU_NETDISK_INTERNAL_URL || "http://baidu-netdisk:4244",
    microservices: readMicroservicesEnv(),
    logFile: requiredEnv("LOG_FILE"),
    databasePoolMax: Math.max(1, Math.min(16, readOptionalNumberEnv("DATABASE_POOL_MAX", 4))),
  };
}
