import type { Command } from "commander";
import qrcode from "qrcode-terminal";
import { loadConfig } from "../config/config.js";
import { resolveSecretInputRef } from "../config/types.secrets.js";
import { resolvePairingSetupFromConfig, encodePairingSetupCode } from "../pairing/setup-code.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime } from "../runtime.js";
import { secretRefKey } from "../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../secrets/resolve.js";
import { formatDocsLink } from "../terminal/links.js";
import { theme } from "../terminal/theme.js";
import { resolveCommandSecretRefsViaGateway } from "./command-secret-gateway.js";
import { getQrRemoteCommandSecretTargetIds } from "./command-secret-targets.js";

type QrCliOptions = {
  json?: boolean;
  setupCodeOnly?: boolean;
  ascii?: boolean;
  remote?: boolean;
  url?: string;
  publicUrl?: string;
  token?: string;
  password?: string;
};

function renderQrAscii(data: string): Promise<string> {
  return new Promise((resolve) => {
    qrcode.generate(data, { small: true }, (output: string) => {
      resolve(output);
    });
  });
}

function readDevicePairPublicUrlFromConfig(cfg: ReturnType<typeof loadConfig>): string | undefined {
  const value = cfg.plugins?.entries?.["device-pair"]?.config?.["publicUrl"];
  if (typeof value !== "string") {
    return undefined;
  }
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function readGatewayTokenEnv(env: NodeJS.ProcessEnv): string | undefined {
  const primary = typeof env.OPENCLAW_GATEWAY_TOKEN === "string" ? env.OPENCLAW_GATEWAY_TOKEN : "";
  if (primary.trim().length > 0) {
    return primary.trim();
  }
  const legacy = typeof env.CLAWDBOT_GATEWAY_TOKEN === "string" ? env.CLAWDBOT_GATEWAY_TOKEN : "";
  if (legacy.trim().length > 0) {
    return legacy.trim();
  }
  return undefined;
}

function readGatewayPasswordEnv(env: NodeJS.ProcessEnv): string | undefined {
  const primary =
    typeof env.OPENCLAW_GATEWAY_PASSWORD === "string" ? env.OPENCLAW_GATEWAY_PASSWORD : "";
  if (primary.trim().length > 0) {
    return primary.trim();
  }
  const legacy =
    typeof env.CLAWDBOT_GATEWAY_PASSWORD === "string" ? env.CLAWDBOT_GATEWAY_PASSWORD : "";
  if (legacy.trim().length > 0) {
    return legacy.trim();
  }
  return undefined;
}

function shouldResolveLocalGatewayPasswordSecret(
  cfg: ReturnType<typeof loadConfig>,
  env: NodeJS.ProcessEnv,
): boolean {
  if (readGatewayPasswordEnv(env)) {
    return false;
  }
  const authMode = cfg.gateway?.auth?.mode;
  if (authMode === "password") {
    return true;
  }
  if (authMode === "token" || authMode === "none" || authMode === "trusted-proxy") {
    return false;
  }
  const envToken = readGatewayTokenEnv(env);
  const configToken =
    typeof cfg.gateway?.auth?.token === "string" && cfg.gateway.auth.token.trim().length > 0
      ? cfg.gateway.auth.token.trim()
      : undefined;
  return !envToken && !configToken;
}

async function resolveLocalGatewayPasswordSecretIfNeeded(
  cfg: ReturnType<typeof loadConfig>,
): Promise<void> {
  const authPassword = cfg.gateway?.auth?.password;
  const { ref } = resolveSecretInputRef({
    value: authPassword,
    defaults: cfg.secrets?.defaults,
  });
  if (!ref) {
    return;
  }
  const resolved = await resolveSecretRefValues([ref], {
    config: cfg,
    env: process.env,
  });
  const value = resolved.get(secretRefKey(ref));
  if (typeof value !== "string" || value.trim().length === 0) {
    throw new Error("gateway.auth.password resolved to an empty or non-string value.");
  }
  if (!cfg.gateway?.auth) {
    return;
  }
  cfg.gateway.auth.password = value.trim();
}

function emitQrSecretResolveDiagnostics(diagnostics: string[], opts: QrCliOptions): void {
  if (diagnostics.length === 0) {
    return;
  }
  const toStderr = opts.json === true || opts.setupCodeOnly === true;
  for (const entry of diagnostics) {
    const message = theme.warn(`[secrets] ${entry}`);
    if (toStderr) {
      defaultRuntime.error(message);
    } else {
      defaultRuntime.log(message);
    }
  }
}

export function registerQrCli(program: Command) {
  program
    .command("qr")
    .description("Generate an iOS pairing QR code and setup code")
    .addHelpText(
      "after",
      () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/qr", "docs.openclaw.ai/cli/qr")}\n`,
    )
    .option(
      "--remote",
      "Use gateway.remote.url and gateway.remote token/password (ignores device-pair publicUrl)",
      false,
    )
    .option("--url <url>", "Override gateway URL used in the setup payload")
    .option("--public-url <url>", "Override gateway public URL used in the setup payload")
    .option("--token <token>", "Override gateway token for setup payload")
    .option("--password <password>", "Override gateway password for setup payload")
    .option("--setup-code-only", "Print only the setup code", false)
    .option("--no-ascii", "Skip ASCII QR rendering")
    .option("--json", "Output JSON", false)
    .action(async (opts: QrCliOptions) => {
      try {
        if (opts.token && opts.password) {
          throw new Error("Use either --token or --password, not both.");
        }

        const token = typeof opts.token === "string" ? opts.token.trim() : "";
        const password = typeof opts.password === "string" ? opts.password.trim() : "";
        const wantsRemote = opts.remote === true;

        const loadedRaw = loadConfig();
        if (wantsRemote && !opts.url && !opts.publicUrl) {
          const tailscaleMode = loadedRaw.gateway?.tailscale?.mode ?? "off";
          const remoteUrl = loadedRaw.gateway?.remote?.url;
          const hasRemoteUrl = typeof remoteUrl === "string" && remoteUrl.trim().length > 0;
          const hasTailscaleServe = tailscaleMode === "serve" || tailscaleMode === "funnel";
          if (!hasRemoteUrl && !hasTailscaleServe) {
            throw new Error(
              "qr --remote requires gateway.remote.url (or gateway.tailscale.mode=serve/funnel).",
            );
          }
        }
        let loaded = loadedRaw;
        let remoteDiagnostics: string[] = [];
        if (wantsRemote && !token && !password) {
          const resolvedRemote = await resolveCommandSecretRefsViaGateway({
            config: loadedRaw,
            commandName: "qr --remote",
            targetIds: getQrRemoteCommandSecretTargetIds(),
          });
          loaded = resolvedRemote.resolvedConfig;
          remoteDiagnostics = resolvedRemote.diagnostics;
        }
        const cfg = {
          ...loaded,
          gateway: {
            ...loaded.gateway,
            auth: {
              ...loaded.gateway?.auth,
            },
          },
        };
        emitQrSecretResolveDiagnostics(remoteDiagnostics, opts);

        if (token) {
          cfg.gateway.auth.mode = "token";
          cfg.gateway.auth.token = token;
          cfg.gateway.auth.password = undefined;
        }
        if (password) {
          cfg.gateway.auth.mode = "password";
          cfg.gateway.auth.password = password;
          cfg.gateway.auth.token = undefined;
        }
        if (wantsRemote && !token && !password) {
          const remoteToken =
            typeof cfg.gateway?.remote?.token === "string" ? cfg.gateway.remote.token.trim() : "";
          const remotePassword =
            typeof cfg.gateway?.remote?.password === "string"
              ? cfg.gateway.remote.password.trim()
              : "";
          if (remoteToken) {
            cfg.gateway.auth.mode = "token";
            cfg.gateway.auth.token = remoteToken;
            cfg.gateway.auth.password = undefined;
          } else if (remotePassword) {
            cfg.gateway.auth.mode = "password";
            cfg.gateway.auth.password = remotePassword;
            cfg.gateway.auth.token = undefined;
          }
        }
        if (
          !wantsRemote &&
          !password &&
          !token &&
          shouldResolveLocalGatewayPasswordSecret(cfg, process.env)
        ) {
          await resolveLocalGatewayPasswordSecretIfNeeded(cfg);
        }

        const explicitUrl =
          typeof opts.url === "string" && opts.url.trim()
            ? opts.url.trim()
            : typeof opts.publicUrl === "string" && opts.publicUrl.trim()
              ? opts.publicUrl.trim()
              : undefined;
        const publicUrl =
          explicitUrl ?? (wantsRemote ? undefined : readDevicePairPublicUrlFromConfig(cfg));

        const resolved = await resolvePairingSetupFromConfig(cfg, {
          publicUrl,
          preferRemoteUrl: wantsRemote,
          runCommandWithTimeout: async (argv, runOpts) =>
            await runCommandWithTimeout(argv, {
              timeoutMs: runOpts.timeoutMs,
            }),
        });

        if (!resolved.ok) {
          throw new Error(resolved.error);
        }

        const setupCode = encodePairingSetupCode(resolved.payload);

        if (opts.setupCodeOnly) {
          defaultRuntime.log(setupCode);
          return;
        }

        if (opts.json) {
          defaultRuntime.log(
            JSON.stringify(
              {
                setupCode,
                gatewayUrl: resolved.payload.url,
                auth: resolved.authLabel,
                urlSource: resolved.urlSource,
              },
              null,
              2,
            ),
          );
          return;
        }

        const lines: string[] = [
          theme.heading("Pairing QR"),
          "Scan this with the OpenClaw iOS app (Onboarding -> Scan QR).",
          "",
        ];

        if (opts.ascii !== false) {
          const qrAscii = await renderQrAscii(setupCode);
          lines.push(qrAscii.trimEnd(), "");
        }

        lines.push(
          `${theme.muted("Setup code:")} ${setupCode}`,
          `${theme.muted("Gateway:")} ${resolved.payload.url}`,
          `${theme.muted("Auth:")} ${resolved.authLabel}`,
          `${theme.muted("Source:")} ${resolved.urlSource}`,
          "",
          "Approve after scan with:",
          `  ${theme.command("openclaw devices list")}`,
          `  ${theme.command("openclaw devices approve <requestId>")}`,
        );

        defaultRuntime.log(lines.join("\n"));
      } catch (err) {
        defaultRuntime.error(String(err));
        defaultRuntime.exit(1);
      }
    });
}
