import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, isAbsolute, join } from "node:path";
import { fileURLToPath } from "node:url";

export interface UniDeskConfig {
  project: { name: string; timezone: string };
  runtime: { typescript: "bun"; bunVersion: string };
  network: {
    host: string;
    publicHost: string;
    core: { port: number; containerPort: number };
    frontend: { port: number; containerPort: number };
    devFrontend: { port: number; containerPort: number };
    database: { port: number; containerPort: number };
    providerIngress: { port: number; containerPort: number };
    providerData: { port: number; containerPort: number };
    restrictedHostAccess?: {
      bindHost: string;
      allowedSourceCidrs: string[];
      allowedForwardRules: Array<{ id: string; sourceCidr: string; destinationCidr: string; port: number; purpose: string }>;
    };
  };
  auth: { username: string; password: string; sessionSecret: string; sessionTtlSeconds: number };
  database: { user: string; password: string; name: string; volume: string; volumeSize: string };
  providerGateway: {
    id: string;
    name: string;
    token: string;
    labels: Record<string, unknown>;
    heartbeatIntervalMs: number;
    reconnectBaseMs: number;
    reconnectMaxMs: number;
    metrics: { diskPath: string };
    upgrade: {
      hostProjectRoot: string;
      workspacePath: string;
      composeFile: string;
      composeEnvFile: string;
      composeProject: string;
      service: string;
      runnerImage: string;
    };
  };
  docker: { composeFile: string; projectName: string };
  microservices: UniDeskMicroserviceConfig[];
  paths: { stateDir: string; logsDir: string; docsReferenceDir: string };
  sshForwarding: { mode: string; keyDir: string; host: string; port: number; user: string };
}

export interface UniDeskMicroserviceConfig {
  id: string;
  name: string;
  providerId: string;
  description: string;
  repository: {
    url: string;
    commitId: string;
    dockerfile: string;
    artifactSource?: {
      kind: "upstream-image";
      imageRef: string;
      digestPinRequired: boolean;
      mirrorRepository: string;
      ciDockerfileBuild: boolean;
      pullOnlyCd: boolean;
    };
    composeFile: string;
    composeService: string;
    containerName: string;
  };
  backend: {
    nodeBaseUrl: string;
    nodeBindHost: string;
    nodePort: number;
    proxyMode: string;
    frontendOnly: boolean;
    public: boolean;
    allowedMethods: string[];
    allowedPathPrefixes: string[];
    healthPath: string;
    timeoutMs: number;
  };
  deployment: {
    mode: "unidesk-direct" | "k3sctl-managed" | "internal-sidecar";
    adapterServiceId?: string;
    k3sServiceId?: string;
    namespace?: string;
    expectedNodeIds?: string[];
    activeNodeId?: string;
  };
  development: {
    providerId: string;
    sshPassthrough: boolean;
    worktreePath: string;
  };
  frontend: {
    route: string;
    integrated: boolean;
  };
}

const moduleDir = dirname(fileURLToPath(import.meta.url));
export const repoRoot = join(moduleDir, "..", "..");
export const stateRoot = join(homedir(), ".unidesk", ".state");

export function rootPath(...parts: string[]): string {
  const first = parts[0];
  if (first === undefined) return repoRoot;
  if (isAbsolute(first)) return join(first, ...parts.slice(1));
  if (first === ".state") return join(stateRoot, ...parts.slice(1));
  if (first.startsWith(".state/")) return join(stateRoot, first.slice(".state/".length), ...parts.slice(1));
  return join(repoRoot, ...parts);
}

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

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

function portPair(obj: Record<string, unknown>, key: string): { port: number; containerPort: number } {
  const value = asRecord(obj[key], `network.${key}`);
  return { port: numberField(value, "port", `network.${key}`), containerPort: numberField(value, "containerPort", `network.${key}`) };
}

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

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

function optionalArray(value: unknown, name: string): Record<string, unknown>[] {
  if (value === undefined) return [];
  if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
  return value.map((item, index) => asRecord(item, `${name}[${index}]`));
}

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

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

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

function optionalRestrictedHostAccess(network: Record<string, unknown>): UniDeskConfig["network"]["restrictedHostAccess"] {
  const value = network.restrictedHostAccess;
  if (value === undefined) return undefined;
  const record = asRecord(value, "network.restrictedHostAccess");
  const allowedSourceCidrs = stringArrayField(record, "allowedSourceCidrs", "network.restrictedHostAccess");
  if (allowedSourceCidrs.length === 0) throw new Error("network.restrictedHostAccess.allowedSourceCidrs must not be empty");
  const allowedForwardRules = optionalArray(record.allowedForwardRules, "network.restrictedHostAccess.allowedForwardRules").map((item, index) => {
    const path = `network.restrictedHostAccess.allowedForwardRules[${index}]`;
    return {
      id: stringField(item, "id", path),
      sourceCidr: stringField(item, "sourceCidr", path),
      destinationCidr: stringField(item, "destinationCidr", path),
      port: numberField(item, "port", path),
      purpose: stringField(item, "purpose", path),
    };
  });
  return {
    bindHost: stringField(record, "bindHost", "network.restrictedHostAccess"),
    allowedSourceCidrs,
    allowedForwardRules,
  };
}

function microserviceConfig(item: Record<string, unknown>, index: number): UniDeskMicroserviceConfig {
  const path = `microservices[${index}]`;
  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" : stringField(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: stringField(item, "id", path),
    name: stringField(item, "name", path),
    providerId: stringField(item, "providerId", path),
    description: stringField(item, "description", path),
    repository: {
      url: stringField(repository, "url", `${path}.repository`),
      commitId: stringField(repository, "commitId", `${path}.repository`),
      dockerfile: stringField(repository, "dockerfile", `${path}.repository`),
      artifactSource: optionalArtifactSource(repository, `${path}.repository`),
      composeFile: stringField(repository, "composeFile", `${path}.repository`),
      composeService: stringField(repository, "composeService", `${path}.repository`),
      containerName: stringField(repository, "containerName", `${path}.repository`),
    },
    backend: {
      nodeBaseUrl: stringField(backend, "nodeBaseUrl", `${path}.backend`),
      nodeBindHost: stringField(backend, "nodeBindHost", `${path}.backend`),
      nodePort: numberField(backend, "nodePort", `${path}.backend`),
      proxyMode: stringField(backend, "proxyMode", `${path}.backend`),
      frontendOnly: booleanField(backend, "frontendOnly", `${path}.backend`),
      public: booleanField(backend, "public", `${path}.backend`),
      allowedMethods: stringArrayField(backend, "allowedMethods", `${path}.backend`),
      allowedPathPrefixes: stringArrayField(backend, "allowedPathPrefixes", `${path}.backend`),
      healthPath: stringField(backend, "healthPath", `${path}.backend`),
      timeoutMs: numberField(backend, "timeoutMs", `${path}.backend`),
    },
    deployment: deployment === undefined
      ? { mode: "unidesk-direct" }
      : {
        mode: deploymentMode,
        adapterServiceId: optionalStringField(deployment, "adapterServiceId", `${path}.deployment`),
        k3sServiceId: optionalStringField(deployment, "k3sServiceId", `${path}.deployment`),
        namespace: optionalStringField(deployment, "namespace", `${path}.deployment`),
        expectedNodeIds: optionalStringArrayField(deployment, "expectedNodeIds", `${path}.deployment`),
        activeNodeId: optionalStringField(deployment, "activeNodeId", `${path}.deployment`),
      },
    development: {
      providerId: stringField(development, "providerId", `${path}.development`),
      sshPassthrough: booleanField(development, "sshPassthrough", `${path}.development`),
      worktreePath: stringField(development, "worktreePath", `${path}.development`),
    },
    frontend: {
      route: stringField(frontend, "route", `${path}.frontend`),
      integrated: booleanField(frontend, "integrated", `${path}.frontend`),
    },
  };
}

export function readConfig(): UniDeskConfig {
  const configPath = rootPath("config.json");
  if (!existsSync(configPath)) throw new Error(`config.json not found at ${configPath}`);
  const raw = readFileSync(configPath, "utf8");
  const parsed = asRecord(JSON.parse(raw) as unknown, "config.json");
  const project = asRecord(parsed.project, "project");
  const runtime = asRecord(parsed.runtime, "runtime");
  const network = asRecord(parsed.network, "network");
  const database = asRecord(parsed.database, "database");
  const auth = asRecord(parsed.auth, "auth");
  const providerGateway = asRecord(parsed.providerGateway, "providerGateway");
  const docker = asRecord(parsed.docker, "docker");
  const microservices = optionalArray(parsed.microservices, "microservices").map(microserviceConfig);
  const paths = asRecord(parsed.paths, "paths");
  const sshForwarding = asRecord(parsed.sshForwarding, "sshForwarding");
  const labels = asRecord(providerGateway.labels, "providerGateway.labels");
  const providerMetrics = asRecord(providerGateway.metrics, "providerGateway.metrics");
  const providerUpgrade = asRecord(providerGateway.upgrade, "providerGateway.upgrade");
  const typescript = stringField(runtime, "typescript", "runtime");
  if (typescript !== "bun") throw new Error("runtime.typescript must be bun");
  return {
    project: { name: stringField(project, "name", "project"), timezone: stringField(project, "timezone", "project") },
    runtime: { typescript, bunVersion: stringField(runtime, "bunVersion", "runtime") },
    network: {
      host: stringField(network, "host", "network"),
      publicHost: stringField(network, "publicHost", "network"),
      core: portPair(network, "core"),
      frontend: portPair(network, "frontend"),
      devFrontend: portPair(network, "devFrontend"),
      database: portPair(network, "database"),
      providerIngress: portPair(network, "providerIngress"),
      providerData: portPair(network, "providerData"),
      restrictedHostAccess: optionalRestrictedHostAccess(network),
    },
    database: {
      user: stringField(database, "user", "database"),
      password: stringField(database, "password", "database"),
      name: stringField(database, "name", "database"),
      volume: stringField(database, "volume", "database"),
      volumeSize: stringField(database, "volumeSize", "database"),
    },
    providerGateway: {
      id: stringField(providerGateway, "id", "providerGateway"),
      name: stringField(providerGateway, "name", "providerGateway"),
      token: stringField(providerGateway, "token", "providerGateway"),
      labels,
      heartbeatIntervalMs: numberField(providerGateway, "heartbeatIntervalMs", "providerGateway"),
      reconnectBaseMs: numberField(providerGateway, "reconnectBaseMs", "providerGateway"),
      reconnectMaxMs: numberField(providerGateway, "reconnectMaxMs", "providerGateway"),
      metrics: {
        diskPath: stringField(providerMetrics, "diskPath", "providerGateway.metrics"),
      },
      upgrade: {
        hostProjectRoot: stringField(providerUpgrade, "hostProjectRoot", "providerGateway.upgrade"),
        workspacePath: stringField(providerUpgrade, "workspacePath", "providerGateway.upgrade"),
        composeFile: stringField(providerUpgrade, "composeFile", "providerGateway.upgrade"),
        composeEnvFile: stringField(providerUpgrade, "composeEnvFile", "providerGateway.upgrade"),
        composeProject: stringField(providerUpgrade, "composeProject", "providerGateway.upgrade"),
        service: stringField(providerUpgrade, "service", "providerGateway.upgrade"),
        runnerImage: stringField(providerUpgrade, "runnerImage", "providerGateway.upgrade"),
      },
    },
    docker: { composeFile: stringField(docker, "composeFile", "docker"), projectName: stringField(docker, "projectName", "docker") },
    microservices,
    paths: {
      stateDir: stringField(paths, "stateDir", "paths"),
      logsDir: stringField(paths, "logsDir", "paths"),
      docsReferenceDir: stringField(paths, "docsReferenceDir", "paths"),
    },
    sshForwarding: {
      mode: stringField(sshForwarding, "mode", "sshForwarding"),
      keyDir: stringField(sshForwarding, "keyDir", "sshForwarding"),
      host: stringField(sshForwarding, "host", "sshForwarding"),
      port: numberField(sshForwarding, "port", "sshForwarding"),
      user: stringField(sshForwarding, "user", "sshForwarding"),
    },
    auth: {
      username: stringField(auth, "username", "auth"),
      password: stringField(auth, "password", "auth"),
      sessionSecret: stringField(auth, "sessionSecret", "auth"),
      sessionTtlSeconds: numberField(auth, "sessionTtlSeconds", "auth"),
    },
  };
}
