type RuntimeLaneDefaults = {
  defaultNodeId?: string;
  defaultGitopsRoot?: string;
  defaultBranch?: string;
  defaultGitopsBranch?: string;
  defaultCatalogPath?: string;
  defaultRuntimeEndpoint?: string;
  defaultWebEndpoint?: string;
  defaultV02RuntimeEndpoint?: string;
  defaultSourceRepo?: string;
};

export type GitOpsRenderArgs = {
  lane: string;
  nodeId?: string;
  outDir: string;
  gitopsRoot?: string;
  sourceBranch: string;
  gitopsBranch: string;
  catalogPath: string;
  imageTagMode: string;
  runtimeEndpoint: string;
  webEndpoint: string;
  sourceRepo: string;
  [key: string]: unknown;
};

export type RuntimeLaneConfig = {
  node?: string;
  sourceBranch?: string;
  gitopsBranch?: string;
  artifactCatalog?: string;
  runtimePath?: string;
  imageTagMode?: string;
  endpoint?: string;
  externalPostgres?: {
    enabled?: boolean;
    serviceName?: string;
    endpointAddress?: string;
    port?: number;
  };
  publicEndpoints?: {
    frontend?: string;
    api?: string;
    [key: string]: string | undefined;
  };
  observability?: {
    traceExplorerUrlTemplate?: string;
  };
  sourceRepo?: string;
};

export type NodeConfig = {
  gitopsRoot?: string;
  sourceRepo?: string;
  publicHost?: string;
};

export type RuntimeLaneMirrorSpec = {
  lane: string;
  sourceBranch: string;
  gitopsBranch: string;
  artifactCatalog: string;
  runtimePath: string;
};

export type DeployConfig = {
  nodes?: Record<string, NodeConfig | undefined>;
  lanes?: Record<string, RuntimeLaneConfig | undefined>;
};

const defaultNodeId = "node";
const defaultGitopsRoot = "deploy/gitops/node";
const defaultPublicHost = "74.48.78.17";

export function isRuntimeLane(lane: unknown): lane is string {
  return /^v[0-9]{2,}$/u.test(String(lane ?? ""));
}

export function runtimeLaneMinor(lane: string): number {
  invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
  return Number.parseInt(lane.slice(1), 10);
}

export function versionNameForRuntimeLane(lane: string): string {
  return `v0.${runtimeLaneMinor(lane)}`;
}

export function namespaceNameForRuntimeLane(lane: string): string {
  invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
  return `hwlab-${lane}`;
}

export function runtimePathForRuntimeLane(lane: string): string {
  invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
  return `runtime-${lane}`;
}

export function nodeConfig(deploy: DeployConfig, nodeId: string): NodeConfig {
  const config = deploy.nodes?.[nodeId];
  return isObject(config) ? config : {};
}

export function nodeIdForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string {
  const laneConfig = runtimeLaneConfig(deploy, lane);
  return laneConfig.node ?? defaults.defaultNodeId ?? defaultNodeId;
}

export function gitopsRootForNode(deploy: DeployConfig, nodeId: string, defaults: RuntimeLaneDefaults = {}): string {
  const config = nodeConfig(deploy, nodeId);
  return normalizeRepoPath(config.gitopsRoot ?? defaults.defaultGitopsRoot ?? defaultGitopsRoot);
}

export function runtimeGitopsPathForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string {
  const laneConfig = runtimeLaneConfig(deploy, lane);
  const runtimePath = normalizeRepoPath(laneConfig.runtimePath ?? runtimePathForRuntimeLane(lane));
  if (runtimePath.startsWith("deploy/")) return runtimePath;
  return joinRepoPath(gitopsRootForNode(deploy, nodeIdForRuntimeLane(deploy, lane, defaults), defaults), runtimePath);
}

export function defaultPortForRuntimeLane(lane: string): number {
  return 17666 + runtimeLaneMinor(lane) * 1000;
}

export function publicHostLabel(host = defaultPublicHost): string {
  return host.replaceAll(".", "-");
}

export function defaultEndpointForRuntimeLane(lane: string, options: { host?: string; v02Endpoint?: string } = {}): string {
  if (lane === "v02" && options.v02Endpoint) return options.v02Endpoint;
  return `https://hwlab-${lane}.${publicHostLabel(options.host ?? defaultPublicHost)}.nip.io`;
}

export function runtimeLaneConfig(deploy: DeployConfig, lane: string): RuntimeLaneConfig {
  invariant(isRuntimeLane(lane), `runtime lane must look like v02/v03, got ${lane}`);
  const config = deploy.lanes?.[lane];
  invariant(isObject(config), `deploy.lanes.${lane} is required`);
  return config;
}

export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): GitOpsRenderArgs {
  if (!isRuntimeLane(args.lane)) return args;
  const result: GitOpsRenderArgs = { ...args };
  const laneConfig = runtimeLaneConfig(deploy, args.lane);
  const nodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults);
  const node = nodeConfig(deploy, nodeId);
  const versionName = versionNameForRuntimeLane(args.lane);
  const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {};
  if (node.publicHost) defaultEndpointOptions.host = node.publicHost;
  if (defaults.defaultV02RuntimeEndpoint) defaultEndpointOptions.v02Endpoint = defaults.defaultV02RuntimeEndpoint;
  const defaultEndpoint = defaultEndpointForRuntimeLane(args.lane, defaultEndpointOptions);
  const configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint;
  const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint;
  const configuredRuntimeEndpoint = laneConfig.publicEndpoints?.api ?? configuredEndpoint;
  const defaultBranch = defaults.defaultBranch ?? "node";
  const defaultGitopsBranch = defaults.defaultGitopsBranch ?? "node-gitops";
  const defaultCatalogPath = defaults.defaultCatalogPath ?? "deploy/artifact-catalog.dev.json";
  const defaultRuntimeEndpoint = defaults.defaultRuntimeEndpoint ?? "http://74.48.78.17:17667";
  const defaultWebEndpoint = defaults.defaultWebEndpoint ?? "http://74.48.78.17:17666";
  const defaultSourceRepo = defaults.defaultSourceRepo ?? "git@github.com:pikasTech/HWLAB.git";
  const defaultCatalog = `deploy/artifact-catalog.${args.lane}.json`;

  if (!result.nodeId || result.nodeId === defaultNodeId) result.nodeId = nodeId;
  if (!result.gitopsRoot || result.gitopsRoot === (defaults.defaultGitopsRoot ?? defaultGitopsRoot)) result.gitopsRoot = gitopsRootForNode(deploy, nodeId, defaults);
  if (result.sourceBranch === defaultBranch || result.sourceBranch === versionName) result.sourceBranch = laneConfig.sourceBranch ?? versionName;
  if (result.gitopsBranch === defaultGitopsBranch || result.gitopsBranch === `${versionName}-gitops`) result.gitopsBranch = laneConfig.gitopsBranch ?? `${versionName}-gitops`;
  if (result.catalogPath === defaultCatalogPath || result.catalogPath === defaultCatalog) result.catalogPath = laneConfig.artifactCatalog ?? defaultCatalog;
  if (result.imageTagMode === "full") result.imageTagMode = laneConfig.imageTagMode ?? result.imageTagMode;
  if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint;
  if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint;
  if (result.sourceRepo === defaultSourceRepo) result.sourceRepo = laneConfig.sourceRepo ?? node.sourceRepo ?? result.sourceRepo;
  return result;
}

export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] {
  return Object.entries(deploy.lanes ?? {})
    .filter(([lane, config]) => isRuntimeLane(lane) && isObject(config))
    .sort(([left], [right]) => runtimeLaneMinor(left) - runtimeLaneMinor(right))
    .map(([lane, config]) => {
      const laneConfig = config as RuntimeLaneConfig;
      const versionName = versionNameForRuntimeLane(lane);
      return {
        lane,
        sourceBranch: laneConfig.sourceBranch ?? versionName,
        gitopsBranch: laneConfig.gitopsBranch ?? `${versionName}-gitops`,
        artifactCatalog: laneConfig.artifactCatalog ?? `deploy/artifact-catalog.${lane}.json`,
        runtimePath: runtimeGitopsPathForRuntimeLane(deploy, lane, defaults),
      };
    });
}

function normalizeRepoPath(value: string): string {
  return value.replaceAll("\\\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, "");
}

function joinRepoPath(left: string, right: string): string {
  return `${normalizeRepoPath(left)}/${normalizeRepoPath(right).replace(/^\/+/, "")}`;
}

function isObject(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function invariant(condition: unknown, message: string): asserts condition {
  if (!condition) throw new Error(message);
}
