import { formatCliCommand } from "../cli/command-format.js";
import type { ConfigWriteOptions } from "../config/io.js";
import type { OpenClawConfig } from "../config/types.js";
import type { ConfigFileSnapshot } from "../config/types.openclaw.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
import { resolveGatewayAuthToken } from "../gateway/auth-token-resolution.js";
import { resolveGatewayAuth } from "../gateway/auth.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
  readConfigFileSnapshotForWrite,
  writeConfigFile,
} from "./gateway-install-token.persist.runtime.js";
import { randomToken } from "./random-token.js";

type GatewayInstallTokenOptions = {
  config: OpenClawConfig;
  configSnapshot?: ConfigFileSnapshot;
  configWriteOptions?: ConfigWriteOptions;
  env: NodeJS.ProcessEnv;
  explicitToken?: string;
  autoGenerateWhenMissing?: boolean;
  persistGeneratedToken?: boolean;
};

export type GatewayInstallTokenResolution = {
  token?: string;
  tokenRefConfigured: boolean;
  unavailableReason?: string;
  warnings: string[];
};

async function maybePersistAutoGeneratedGatewayInstallToken(params: {
  token: string;
  config: OpenClawConfig;
  configSnapshot?: ConfigFileSnapshot;
  configWriteOptions?: ConfigWriteOptions;
  warnings: string[];
}): Promise<string | undefined> {
  try {
    const prepared =
      params.configSnapshot && params.configWriteOptions
        ? {
            snapshot: params.configSnapshot,
            writeOptions: params.configWriteOptions,
          }
        : await readConfigFileSnapshotForWrite();
    const snapshot = params.configSnapshot ?? prepared.snapshot;
    if (snapshot.exists && !snapshot.valid) {
      params.warnings.push(
        "Warning: config file exists but is invalid; skipping token persistence.",
      );
      return params.token;
    }

    const baseConfig = snapshot.exists ? (snapshot.sourceConfig ?? snapshot.config) : {};
    const existingTokenRef = resolveSecretInputRef({
      value: baseConfig.gateway?.auth?.token,
      defaults: baseConfig.secrets?.defaults,
    }).ref;
    const baseConfigToken =
      existingTokenRef || typeof baseConfig.gateway?.auth?.token !== "string"
        ? undefined
        : normalizeOptionalString(baseConfig.gateway.auth.token);
    if (!existingTokenRef && !baseConfigToken) {
      await writeConfigFile(
        {
          ...baseConfig,
          gateway: {
            ...baseConfig.gateway,
            auth: {
              ...baseConfig.gateway?.auth,
              mode: baseConfig.gateway?.auth?.mode ?? "token",
              token: params.token,
            },
          },
        },
        {
          baseSnapshot: snapshot,
          ...prepared.writeOptions,
          ...params.configWriteOptions,
          skipRuntimeSnapshotRefresh: true,
        },
      );
      return params.token;
    }
    if (baseConfigToken) {
      return baseConfigToken;
    }
    params.warnings.push(
      "Warning: gateway.auth.token is SecretRef-managed; skipping plaintext token persistence.",
    );
    return undefined;
  } catch (err) {
    params.warnings.push(`Warning: could not persist token to config: ${String(err)}`);
    return params.token;
  }
}

function formatAmbiguousGatewayAuthModeReason(): string {
  return [
    "gateway.auth.token and gateway.auth.password are both configured while gateway.auth.mode is unset.",
    `Set ${formatCliCommand("openclaw config set gateway.auth.mode token")} or ${formatCliCommand("openclaw config set gateway.auth.mode password")}.`,
  ].join(" ");
}

export async function resolveGatewayInstallToken(
  options: GatewayInstallTokenOptions,
): Promise<GatewayInstallTokenResolution> {
  const cfg = options.config;
  const warnings: string[] = [];

  if (hasAmbiguousGatewayAuthModeConfig(cfg)) {
    const tokenRefConfigured = Boolean(
      resolveSecretInputRef({
        value: cfg.gateway?.auth?.token,
        defaults: cfg.secrets?.defaults,
      }).ref,
    );
    return {
      token: undefined,
      tokenRefConfigured,
      unavailableReason: formatAmbiguousGatewayAuthModeReason(),
      warnings,
    };
  }

  const resolvedAuth = resolveGatewayAuth({
    authConfig: cfg.gateway?.auth,
    env: options.env,
    tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
  });
  const needsToken =
    shouldRequireGatewayTokenForInstall(cfg, options.env) && !resolvedAuth.allowTailscale;
  if (!needsToken) {
    const tokenRefConfigured = Boolean(
      resolveSecretInputRef({
        value: cfg.gateway?.auth?.token,
        defaults: cfg.secrets?.defaults,
      }).ref,
    );
    return {
      token: undefined,
      tokenRefConfigured,
      unavailableReason: undefined,
      warnings,
    };
  }

  const resolvedToken = await resolveGatewayAuthToken({
    cfg,
    env: options.env,
    explicitToken: options.explicitToken,
    envFallback: "no-secret-ref",
    unresolvedReasonStyle: "detailed",
  });
  const tokenRefConfigured = resolvedToken.secretRefConfigured;
  let token = resolvedToken.source === "secretRef" ? undefined : resolvedToken.token;
  let unavailableReason: string | undefined;

  if (tokenRefConfigured && resolvedToken.source === "secretRef" && needsToken) {
    warnings.push(
      "gateway.auth.token is SecretRef-managed; install will not persist a resolved token in service environment. Ensure the SecretRef is resolvable in the daemon runtime context.",
    );
  } else if (tokenRefConfigured && !token && needsToken) {
    unavailableReason = `gateway.auth.token SecretRef is configured but unresolved (${resolvedToken.unresolvedRefReason ?? "unknown reason"}).`;
  }

  const allowAutoGenerate = options.autoGenerateWhenMissing ?? false;
  const persistGeneratedToken = options.persistGeneratedToken ?? false;
  if (!token && !tokenRefConfigured && allowAutoGenerate) {
    token = randomToken();
    warnings.push(
      persistGeneratedToken
        ? "No gateway token found. Auto-generated one and saving to config."
        : "No gateway token found. Auto-generated one for this run without saving to config.",
    );

    if (persistGeneratedToken) {
      token = await maybePersistAutoGeneratedGatewayInstallToken({
        token,
        config: cfg,
        configSnapshot: options.configSnapshot,
        configWriteOptions: options.configWriteOptions,
        warnings,
      });
    }
  }

  return {
    token,
    tokenRefConfigured,
    unavailableReason,
    warnings,
  };
}
