import os from "node:os";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import {
  approveDevicePairing,
  listDevicePairing,
  resolveGatewayBindUrl,
  runPluginCommandWithTimeout,
  resolveTailnetHostWithRunner,
} from "openclaw/plugin-sdk/core";
import qrcode from "qrcode-terminal";
import {
  armPairNotifyOnce,
  formatPendingRequests,
  handleNotifyCommand,
  registerPairingNotifierService,
} from "./notify.js";

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

const DEFAULT_GATEWAY_PORT = 18789;

type DevicePairPluginConfig = {
  publicUrl?: string;
};

type SetupPayload = {
  url: string;
  token?: string;
  password?: string;
};

type ResolveUrlResult = {
  url?: string;
  source?: string;
  error?: string;
};

type ResolveAuthResult = {
  token?: string;
  password?: string;
  label?: string;
  error?: string;
};

function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null {
  const candidate = raw.trim();
  if (!candidate) {
    return null;
  }
  const parsedUrl = parseNormalizedGatewayUrl(candidate);
  if (parsedUrl) {
    return parsedUrl;
  }
  const hostPort = candidate.split("/", 1)[0]?.trim() ?? "";
  return hostPort ? `${schemeFallback}://${hostPort}` : null;
}

function parseNormalizedGatewayUrl(raw: string): string | null {
  try {
    const parsed = new URL(raw);
    const scheme = parsed.protocol.slice(0, -1);
    const normalizedScheme = scheme === "http" ? "ws" : scheme === "https" ? "wss" : scheme;
    if (!(normalizedScheme === "ws" || normalizedScheme === "wss")) {
      return null;
    }
    if (!parsed.hostname) {
      return null;
    }
    return `${normalizedScheme}://${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}`;
  } catch {
    return null;
  }
}

function parsePositiveInteger(raw: string | undefined): number | null {
  if (!raw) {
    return null;
  }
  const parsed = Number.parseInt(raw, 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
}

function resolveGatewayPort(cfg: OpenClawPluginApi["config"]): number {
  const envPort =
    parsePositiveInteger(process.env.OPENCLAW_GATEWAY_PORT?.trim()) ??
    parsePositiveInteger(process.env.CLAWDBOT_GATEWAY_PORT?.trim());
  if (envPort) {
    return envPort;
  }
  const configPort = cfg.gateway?.port;
  if (typeof configPort === "number" && Number.isFinite(configPort) && configPort > 0) {
    return configPort;
  }
  return DEFAULT_GATEWAY_PORT;
}

function resolveScheme(
  cfg: OpenClawPluginApi["config"],
  opts?: { forceSecure?: boolean },
): "ws" | "wss" {
  if (opts?.forceSecure) {
    return "wss";
  }
  return cfg.gateway?.tls?.enabled === true ? "wss" : "ws";
}

function isPrivateIPv4(address: string): boolean {
  const parts = address.split(".");
  if (parts.length != 4) {
    return false;
  }
  const octets = parts.map((part) => Number.parseInt(part, 10));
  if (octets.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) {
    return false;
  }
  const [a, b] = octets;
  if (a === 10) {
    return true;
  }
  if (a === 172 && b >= 16 && b <= 31) {
    return true;
  }
  if (a === 192 && b === 168) {
    return true;
  }
  return false;
}

function isTailnetIPv4(address: string): boolean {
  const parts = address.split(".");
  if (parts.length !== 4) {
    return false;
  }
  const octets = parts.map((part) => Number.parseInt(part, 10));
  if (octets.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) {
    return false;
  }
  const [a, b] = octets;
  return a === 100 && b >= 64 && b <= 127;
}

function pickMatchingIPv4(predicate: (address: string) => boolean): string | null {
  const nets = os.networkInterfaces();
  for (const entries of Object.values(nets)) {
    if (!entries) {
      continue;
    }
    for (const entry of entries) {
      const family = entry?.family;
      // Check for IPv4 (string "IPv4" on Node 18+, number 4 on older)
      const isIpv4 = family === "IPv4" || String(family) === "4";
      if (!entry || entry.internal || !isIpv4) {
        continue;
      }
      const address = entry.address?.trim() ?? "";
      if (!address) {
        continue;
      }
      if (predicate(address)) {
        return address;
      }
    }
  }
  return null;
}

function pickLanIPv4(): string | null {
  return pickMatchingIPv4(isPrivateIPv4);
}

function pickTailnetIPv4(): string | null {
  return pickMatchingIPv4(isTailnetIPv4);
}

async function resolveTailnetHost(): Promise<string | null> {
  return await resolveTailnetHostWithRunner((argv, opts) =>
    runPluginCommandWithTimeout({
      argv,
      timeoutMs: opts.timeoutMs,
    }),
  );
}

function resolveAuth(cfg: OpenClawPluginApi["config"]): ResolveAuthResult {
  const mode = cfg.gateway?.auth?.mode;
  const token =
    pickFirstDefined([
      process.env.OPENCLAW_GATEWAY_TOKEN,
      process.env.CLAWDBOT_GATEWAY_TOKEN,
      cfg.gateway?.auth?.token,
    ]) ?? undefined;
  const password =
    pickFirstDefined([
      process.env.OPENCLAW_GATEWAY_PASSWORD,
      process.env.CLAWDBOT_GATEWAY_PASSWORD,
      cfg.gateway?.auth?.password,
    ]) ?? undefined;

  if (mode === "token" || mode === "password") {
    return resolveRequiredAuth(mode, { token, password });
  }
  if (token) {
    return { token, label: "token" };
  }
  if (password) {
    return { password, label: "password" };
  }
  return { error: "Gateway auth is not configured (no token or password)." };
}

function pickFirstDefined(candidates: Array<unknown>): string | null {
  for (const value of candidates) {
    if (typeof value !== "string") {
      continue;
    }
    const trimmed = value.trim();
    if (trimmed) {
      return trimmed;
    }
  }
  return null;
}

function resolveRequiredAuth(
  mode: "token" | "password",
  values: { token?: string; password?: string },
): ResolveAuthResult {
  if (mode === "token") {
    return values.token
      ? { token: values.token, label: "token" }
      : { error: "Gateway auth is set to token, but no token is configured." };
  }
  return values.password
    ? { password: values.password, label: "password" }
    : { error: "Gateway auth is set to password, but no password is configured." };
}

async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResult> {
  const cfg = api.config;
  const pluginCfg = (api.pluginConfig ?? {}) as DevicePairPluginConfig;
  const scheme = resolveScheme(cfg);
  const port = resolveGatewayPort(cfg);

  if (typeof pluginCfg.publicUrl === "string" && pluginCfg.publicUrl.trim()) {
    const url = normalizeUrl(pluginCfg.publicUrl, scheme);
    if (url) {
      return { url, source: "plugins.entries.device-pair.config.publicUrl" };
    }
    return { error: "Configured publicUrl is invalid." };
  }

  const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
  if (tailscaleMode === "serve" || tailscaleMode === "funnel") {
    const host = await resolveTailnetHost();
    if (!host) {
      return { error: "Tailscale Serve is enabled, but MagicDNS could not be resolved." };
    }
    return { url: `wss://${host}`, source: `gateway.tailscale.mode=${tailscaleMode}` };
  }

  const remoteUrl = cfg.gateway?.remote?.url;
  if (typeof remoteUrl === "string" && remoteUrl.trim()) {
    const url = normalizeUrl(remoteUrl, scheme);
    if (url) {
      return { url, source: "gateway.remote.url" };
    }
  }

  const bindResult = resolveGatewayBindUrl({
    bind: cfg.gateway?.bind,
    customBindHost: cfg.gateway?.customBindHost,
    scheme,
    port,
    pickTailnetHost: pickTailnetIPv4,
    pickLanHost: pickLanIPv4,
  });
  if (bindResult) {
    return bindResult;
  }

  return {
    error:
      "Gateway is only bound to loopback. Set gateway.bind=lan, enable tailscale serve, or configure plugins.entries.device-pair.config.publicUrl.",
  };
}

function encodeSetupCode(payload: SetupPayload): string {
  const json = JSON.stringify(payload);
  const base64 = Buffer.from(json, "utf8").toString("base64");
  return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
}

function formatSetupReply(payload: SetupPayload, authLabel: string): string {
  const setupCode = encodeSetupCode(payload);
  return [
    "Pairing setup code generated.",
    "",
    "1) Open the iOS app → Settings → Gateway",
    "2) Paste the setup code below and tap Connect",
    "3) Back here, run /pair approve",
    "",
    "Setup code:",
    setupCode,
    "",
    `Gateway: ${payload.url}`,
    `Auth: ${authLabel}`,
  ].join("\n");
}

function formatSetupInstructions(): string {
  return [
    "Pairing setup code generated.",
    "",
    "1) Open the iOS app → Settings → Gateway",
    "2) Paste the setup code from my next message and tap Connect",
    "3) Back here, run /pair approve",
  ].join("\n");
}

export default function register(api: OpenClawPluginApi) {
  registerPairingNotifierService(api);

  api.registerCommand({
    name: "pair",
    description: "Generate setup codes and approve device pairing requests.",
    acceptsArgs: true,
    handler: async (ctx) => {
      const args = ctx.args?.trim() ?? "";
      const tokens = args.split(/\s+/).filter(Boolean);
      const action = tokens[0]?.toLowerCase() ?? "";
      api.logger.info?.(
        `device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
          action || "new"
        }`,
      );

      if (action === "status" || action === "pending") {
        const list = await listDevicePairing();
        return { text: formatPendingRequests(list.pending) };
      }

      if (action === "notify") {
        const notifyAction = tokens[1]?.trim().toLowerCase() ?? "status";
        return await handleNotifyCommand({
          api,
          ctx,
          action: notifyAction,
        });
      }

      if (action === "approve") {
        const requested = tokens[1]?.trim();
        const list = await listDevicePairing();
        if (list.pending.length === 0) {
          return { text: "No pending device pairing requests." };
        }

        let pending: (typeof list.pending)[number] | undefined;
        if (requested) {
          if (requested.toLowerCase() === "latest") {
            pending = [...list.pending].toSorted((a, b) => (b.ts ?? 0) - (a.ts ?? 0))[0];
          } else {
            pending = list.pending.find((entry) => entry.requestId === requested);
          }
        } else if (list.pending.length === 1) {
          pending = list.pending[0];
        } else {
          return {
            text:
              `${formatPendingRequests(list.pending)}\n\n` +
              "Multiple pending requests found. Approve one explicitly:\n" +
              "/pair approve <requestId>\n" +
              "Or approve the most recent:\n" +
              "/pair approve latest",
          };
        }
        if (!pending) {
          return { text: "Pairing request not found." };
        }
        const approved = await approveDevicePairing(pending.requestId);
        if (!approved) {
          return { text: "Pairing request not found." };
        }
        const label = approved.device.displayName?.trim() || approved.device.deviceId;
        const platform = approved.device.platform?.trim();
        const platformLabel = platform ? ` (${platform})` : "";
        return { text: `✅ Paired ${label}${platformLabel}.` };
      }

      const auth = resolveAuth(api.config);
      if (auth.error) {
        return { text: `Error: ${auth.error}` };
      }

      const urlResult = await resolveGatewayUrl(api);
      if (!urlResult.url) {
        return { text: `Error: ${urlResult.error ?? "Gateway URL unavailable."}` };
      }

      const payload: SetupPayload = {
        url: urlResult.url,
        token: auth.token,
        password: auth.password,
      };

      if (action === "qr") {
        const setupCode = encodeSetupCode(payload);
        const qrAscii = await renderQrAscii(setupCode);
        const authLabel = auth.label ?? "auth";

        const channel = ctx.channel;
        const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
        let autoNotifyArmed = false;

        if (channel === "telegram" && target) {
          try {
            autoNotifyArmed = await armPairNotifyOnce({ api, ctx });
          } catch (err) {
            api.logger.warn?.(
              `device-pair: failed to arm one-shot pairing notify (${String(
                (err as Error)?.message ?? err,
              )})`,
            );
          }
        }

        if (channel === "telegram" && target) {
          try {
            const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
            if (send) {
              await send(
                target,
                ["Scan this QR code with the OpenClaw iOS app:", "", "```", qrAscii, "```"].join(
                  "\n",
                ),
                {
                  ...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
                  ...(ctx.accountId ? { accountId: ctx.accountId } : {}),
                },
              );
              return {
                text: [
                  `Gateway: ${payload.url}`,
                  `Auth: ${authLabel}`,
                  "",
                  autoNotifyArmed
                    ? "After scanning, wait here for the pairing request ping."
                    : "After scanning, come back here and run `/pair approve` to complete pairing.",
                  ...(autoNotifyArmed
                    ? [
                        "I’ll auto-ping here when the pairing request arrives, then auto-disable.",
                        "If the ping does not arrive, run `/pair approve latest` manually.",
                      ]
                    : []),
                ].join("\n"),
              };
            }
          } catch (err) {
            api.logger.warn?.(
              `device-pair: telegram QR send failed, falling back (${String(
                (err as Error)?.message ?? err,
              )})`,
            );
          }
        }

        // Render based on channel capability
        api.logger.info?.(`device-pair: QR fallback channel=${channel} target=${target}`);
        const infoLines = [
          `Gateway: ${payload.url}`,
          `Auth: ${authLabel}`,
          "",
          autoNotifyArmed
            ? "After scanning, wait here for the pairing request ping."
            : "After scanning, run `/pair approve` to complete pairing.",
          ...(autoNotifyArmed
            ? [
                "I’ll auto-ping here when the pairing request arrives, then auto-disable.",
                "If the ping does not arrive, run `/pair approve latest` manually.",
              ]
            : []),
        ];

        // WebUI + CLI/TUI: ASCII QR
        return {
          text: [
            "Scan this QR code with the OpenClaw iOS app:",
            "",
            "```",
            qrAscii,
            "```",
            "",
            ...infoLines,
          ].join("\n"),
        };
      }

      const channel = ctx.channel;
      const target = ctx.senderId?.trim() || ctx.from?.trim() || ctx.to?.trim() || "";
      const authLabel = auth.label ?? "auth";

      if (channel === "telegram" && target) {
        try {
          const runtimeKeys = Object.keys(api.runtime ?? {});
          const channelKeys = Object.keys(api.runtime?.channel ?? {});
          api.logger.debug?.(
            `device-pair: runtime keys=${runtimeKeys.join(",") || "none"} channel keys=${
              channelKeys.join(",") || "none"
            }`,
          );
          const send = api.runtime?.channel?.telegram?.sendMessageTelegram;
          if (!send) {
            throw new Error(
              `telegram runtime unavailable (runtime keys: ${runtimeKeys.join(",")}; channel keys: ${channelKeys.join(
                ",",
              )})`,
            );
          }
          await send(target, formatSetupInstructions(), {
            ...(ctx.messageThreadId != null ? { messageThreadId: ctx.messageThreadId } : {}),
            ...(ctx.accountId ? { accountId: ctx.accountId } : {}),
          });
          api.logger.info?.(
            `device-pair: telegram split send ok target=${target} account=${ctx.accountId ?? "none"} thread=${
              ctx.messageThreadId ?? "none"
            }`,
          );
          return { text: encodeSetupCode(payload) };
        } catch (err) {
          api.logger.warn?.(
            `device-pair: telegram split send failed, falling back to single message (${String(
              (err as Error)?.message ?? err,
            )})`,
          );
        }
      }

      return {
        text: formatSetupReply(payload, authLabel),
      };
    },
  });
}
