import { runCommandWithTimeout } from "../process/exec.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { isTailnetIPv4 } from "./tailnet.js";
import { resolveWideAreaDiscoveryDomain } from "./widearea-dns.js";

export type GatewayBonjourBeacon = {
  instanceName: string;
  domain?: string;
  displayName?: string;
  host?: string;
  port?: number;
  lanHost?: string;
  tailnetDns?: string;
  gatewayPort?: number;
  sshPort?: number;
  gatewayTls?: boolean;
  gatewayTlsFingerprintSha256?: string;
  cliPath?: string;
  role?: string;
  transport?: string;
  txt?: Record<string, string>;
};

export type GatewayDiscoveryResolvedEndpoint = {
  host: string;
  port: number;
  gatewayTls: boolean;
  gatewayTlsFingerprintSha256?: string;
  scheme: "ws" | "wss";
  wsUrl: string;
};

export function resolveGatewayDiscoveryEndpoint(
  beacon: GatewayBonjourBeacon,
): GatewayDiscoveryResolvedEndpoint | null {
  const host = beacon.host?.trim();
  const port = beacon.port;
  if (!host || typeof port !== "number" || !Number.isFinite(port) || port <= 0) {
    return null;
  }
  const gatewayTls = beacon.gatewayTls === true;
  const scheme = gatewayTls ? "wss" : "ws";
  return {
    host,
    port,
    gatewayTls,
    gatewayTlsFingerprintSha256: beacon.gatewayTlsFingerprintSha256,
    scheme,
    wsUrl: `${scheme}://${host}:${port}`,
  };
}

export function pickResolvedGatewayHost(beacon: GatewayBonjourBeacon): string | null {
  return resolveGatewayDiscoveryEndpoint(beacon)?.host ?? null;
}

export function pickResolvedGatewayPort(beacon: GatewayBonjourBeacon): number | null {
  return resolveGatewayDiscoveryEndpoint(beacon)?.port ?? null;
}

export type GatewayBonjourDiscoverOpts = {
  timeoutMs?: number;
  domains?: string[];
  wideAreaDomain?: string | null;
  platform?: NodeJS.Platform;
  run?: typeof runCommandWithTimeout;
};

const DEFAULT_TIMEOUT_MS = 2000;
const GATEWAY_SERVICE_TYPE = "_openclaw-gw._tcp";

function decodeDnsSdEscapes(value: string): string {
  let decoded = false;
  const bytes: number[] = [];
  let pending = "";

  const flush = () => {
    if (!pending) {
      return;
    }
    bytes.push(...Buffer.from(pending, "utf8"));
    pending = "";
  };

  for (let i = 0; i < value.length; i += 1) {
    const ch = value[i] ?? "";
    if (ch === "\\" && i + 3 < value.length) {
      const escaped = value.slice(i + 1, i + 4);
      if (/^[0-9]{3}$/.test(escaped)) {
        const byte = Number.parseInt(escaped, 10);
        if (!Number.isFinite(byte) || byte < 0 || byte > 255) {
          pending += ch;
          continue;
        }
        flush();
        bytes.push(byte);
        decoded = true;
        i += 3;
        continue;
      }
    }
    pending += ch;
  }

  if (!decoded) {
    return value;
  }
  flush();
  return Buffer.from(bytes).toString("utf8");
}

function parseDigShortLines(stdout: string): string[] {
  return stdout
    .split("\n")
    .map((l) => l.trim())
    .filter(Boolean);
}

function parseDigTxt(stdout: string): string[] {
  // dig +short TXT prints one or more lines of quoted strings:
  // "k=v" "k2=v2"
  const tokens: string[] = [];
  for (const raw of stdout.split("\n")) {
    const line = raw.trim();
    if (!line) {
      continue;
    }
    const matches = Array.from(line.matchAll(/"([^"]*)"/g), (m) => m[1] ?? "");
    for (const m of matches) {
      const unescaped = m.replaceAll("\\\\", "\\").replaceAll('\\"', '"').replaceAll("\\n", "\n");
      tokens.push(unescaped);
    }
  }
  return tokens;
}

function parseDigSrv(stdout: string): { host: string; port: number } | null {
  // dig +short SRV: "0 0 18790 host.domain."
  const line = stdout
    .split("\n")
    .map((l) => l.trim())
    .find(Boolean);
  if (!line) {
    return null;
  }
  const parts = line.split(/\s+/).filter(Boolean);
  if (parts.length < 4) {
    return null;
  }
  const port = Number.parseInt(parts[2] ?? "", 10);
  const hostRaw = parts[3] ?? "";
  if (!Number.isFinite(port) || port <= 0) {
    return null;
  }
  const host = hostRaw.replace(/\.$/, "");
  if (!host) {
    return null;
  }
  return { host, port };
}

function parseTailscaleStatusIPv4s(stdout: string): string[] {
  const parsed = stdout ? (JSON.parse(stdout) as Record<string, unknown>) : {};
  const out: string[] = [];

  const addIps = (value: unknown) => {
    if (!value || typeof value !== "object") {
      return;
    }
    const ips = (value as { TailscaleIPs?: unknown }).TailscaleIPs;
    if (!Array.isArray(ips)) {
      return;
    }
    for (const ip of ips) {
      if (typeof ip !== "string") {
        continue;
      }
      const trimmed = ip.trim();
      if (trimmed && isTailnetIPv4(trimmed)) {
        out.push(trimmed);
      }
    }
  };

  addIps((parsed as { Self?: unknown }).Self);

  const peerObj = (parsed as { Peer?: unknown }).Peer;
  if (peerObj && typeof peerObj === "object") {
    for (const peer of Object.values(peerObj as Record<string, unknown>)) {
      addIps(peer);
    }
  }

  return [...new Set(out)];
}

function parseIntOrNull(value: string | undefined): number | undefined {
  if (!value) {
    return undefined;
  }
  const parsed = Number.parseInt(value, 10);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function parseTxtTokens(tokens: string[]): Record<string, string> {
  const txt: Record<string, string> = {};
  for (const token of tokens) {
    const idx = token.indexOf("=");
    if (idx <= 0) {
      continue;
    }
    const key = token.slice(0, idx).trim();
    const value = decodeDnsSdEscapes(token.slice(idx + 1).trim());
    if (!key) {
      continue;
    }
    txt[key] = value;
  }
  return txt;
}

function parseDnsSdBrowse(stdout: string): string[] {
  const instances = new Set<string>();
  for (const raw of stdout.split("\n")) {
    const line = raw.trim();
    if (!line || !line.includes(GATEWAY_SERVICE_TYPE)) {
      continue;
    }
    if (!line.includes("Add")) {
      continue;
    }
    const match = line.match(/_openclaw-gw\._tcp\.?\s+(.+)$/);
    if (match?.[1]) {
      instances.add(decodeDnsSdEscapes(match[1].trim()));
    }
  }
  return Array.from(instances.values());
}

function parseDnsSdResolve(stdout: string, instanceName: string): GatewayBonjourBeacon | null {
  const decodedInstanceName = decodeDnsSdEscapes(instanceName);
  const beacon: GatewayBonjourBeacon = { instanceName: decodedInstanceName };
  let txt: Record<string, string> = {};
  for (const raw of stdout.split("\n")) {
    const line = raw.trim();
    if (!line) {
      continue;
    }

    if (line.includes("can be reached at")) {
      const match = line.match(/can be reached at\s+([^\s:]+):(\d+)/i);
      if (match?.[1]) {
        beacon.host = match[1].replace(/\.$/, "");
      }
      if (match?.[2]) {
        beacon.port = parseIntOrNull(match[2]);
      }
      continue;
    }

    if (line.startsWith("txt") || line.includes("txtvers=")) {
      const tokens = line.split(/\s+/).filter(Boolean);
      txt = parseTxtTokens(tokens);
    }
  }

  beacon.txt = Object.keys(txt).length ? txt : undefined;
  if (txt.displayName) {
    beacon.displayName = decodeDnsSdEscapes(txt.displayName);
  }
  if (txt.lanHost) {
    beacon.lanHost = txt.lanHost;
  }
  if (txt.tailnetDns) {
    beacon.tailnetDns = txt.tailnetDns;
  }
  if (txt.cliPath) {
    beacon.cliPath = txt.cliPath;
  }
  beacon.gatewayPort = parseIntOrNull(txt.gatewayPort);
  beacon.sshPort = parseIntOrNull(txt.sshPort);
  if (txt.gatewayTls) {
    const raw = normalizeOptionalLowercaseString(txt.gatewayTls);
    beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
  }
  if (txt.gatewayTlsSha256) {
    beacon.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256;
  }
  if (txt.role) {
    beacon.role = txt.role;
  }
  if (txt.transport) {
    beacon.transport = txt.transport;
  }

  if (!beacon.displayName) {
    beacon.displayName = decodedInstanceName;
  }
  return beacon;
}

async function discoverViaDnsSd(
  domain: string,
  timeoutMs: number,
  run: typeof runCommandWithTimeout,
): Promise<GatewayBonjourBeacon[]> {
  const browse = await run(["dns-sd", "-B", GATEWAY_SERVICE_TYPE, domain], {
    timeoutMs,
  });
  const instances = parseDnsSdBrowse(browse.stdout);
  const results: GatewayBonjourBeacon[] = [];
  for (const instance of instances) {
    const resolved = await run(["dns-sd", "-L", instance, GATEWAY_SERVICE_TYPE, domain], {
      timeoutMs,
    });
    const parsed = parseDnsSdResolve(resolved.stdout, instance);
    if (parsed) {
      results.push({ ...parsed, domain });
    }
  }
  return results;
}

async function discoverWideAreaViaTailnetDns(
  domain: string,
  timeoutMs: number,
  run: typeof runCommandWithTimeout,
): Promise<GatewayBonjourBeacon[]> {
  if (!domain || domain === "local.") {
    return [];
  }
  const startedAt = Date.now();
  const remainingMs = () => timeoutMs - (Date.now() - startedAt);

  const tailscaleCandidates = ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"];
  let ips: string[] = [];
  for (const candidate of tailscaleCandidates) {
    try {
      const res = await run([candidate, "status", "--json"], {
        timeoutMs: Math.max(1, Math.min(700, remainingMs())),
      });
      ips = parseTailscaleStatusIPv4s(res.stdout);
      if (ips.length > 0) {
        break;
      }
    } catch {
      // ignore
    }
  }
  if (ips.length === 0) {
    return [];
  }
  if (remainingMs() <= 0) {
    return [];
  }

  // Keep scans bounded: this is a fallback and should not block long.
  ips = ips.slice(0, 40);

  const probeName = `${GATEWAY_SERVICE_TYPE}.${domain.replace(/\.$/, "")}`;

  const concurrency = 6;
  let nextIndex = 0;
  let nameserver: string | null = null;
  let ptrs: string[] = [];

  const worker = async () => {
    while (nameserver === null) {
      const budget = remainingMs();
      if (budget <= 0) {
        return;
      }
      const i = nextIndex;
      nextIndex += 1;
      if (i >= ips.length) {
        return;
      }
      const ip = ips[i] ?? "";
      if (!ip) {
        continue;
      }
      try {
        const probe = await run(
          ["dig", "+short", "+time=1", "+tries=1", `@${ip}`, probeName, "PTR"],
          { timeoutMs: Math.max(1, Math.min(250, budget)) },
        );
        const lines = parseDigShortLines(probe.stdout);
        if (lines.length === 0) {
          continue;
        }
        nameserver = ip;
        ptrs = lines;
        return;
      } catch {
        // ignore
      }
    }
  };

  await Promise.all(Array.from({ length: Math.min(concurrency, ips.length) }, () => worker()));

  if (!nameserver || ptrs.length === 0) {
    return [];
  }
  if (remainingMs() <= 0) {
    return [];
  }
  const nameserverArg = `@${String(nameserver)}`;

  const results: GatewayBonjourBeacon[] = [];
  for (const ptr of ptrs) {
    const budget = remainingMs();
    if (budget <= 0) {
      break;
    }
    const ptrName = ptr.trim().replace(/\.$/, "");
    if (!ptrName) {
      continue;
    }
    const instanceName = ptrName.replace(/\.?_openclaw-gw\._tcp\..*$/, "");

    const srv = await run(["dig", "+short", "+time=1", "+tries=1", nameserverArg, ptrName, "SRV"], {
      timeoutMs: Math.max(1, Math.min(350, budget)),
    }).catch(() => null);
    const srvParsed = srv ? parseDigSrv(srv.stdout) : null;
    if (!srvParsed) {
      continue;
    }

    const txtBudget = remainingMs();
    if (txtBudget <= 0) {
      results.push({
        instanceName: instanceName || ptrName,
        displayName: instanceName || ptrName,
        domain,
        host: srvParsed.host,
        port: srvParsed.port,
      });
      continue;
    }

    const txt = await run(["dig", "+short", "+time=1", "+tries=1", nameserverArg, ptrName, "TXT"], {
      timeoutMs: Math.max(1, Math.min(350, txtBudget)),
    }).catch(() => null);
    const txtTokens = txt ? parseDigTxt(txt.stdout) : [];
    const txtMap = txtTokens.length > 0 ? parseTxtTokens(txtTokens) : {};

    const beacon: GatewayBonjourBeacon = {
      instanceName: instanceName || ptrName,
      displayName: txtMap.displayName || instanceName || ptrName,
      domain,
      host: srvParsed.host,
      port: srvParsed.port,
      txt: Object.keys(txtMap).length ? txtMap : undefined,
      gatewayPort: parseIntOrNull(txtMap.gatewayPort),
      sshPort: parseIntOrNull(txtMap.sshPort),
      tailnetDns: txtMap.tailnetDns || undefined,
      cliPath: txtMap.cliPath || undefined,
    };
    if (txtMap.gatewayTls) {
      const raw = normalizeOptionalLowercaseString(txtMap.gatewayTls);
      beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
    }
    if (txtMap.gatewayTlsSha256) {
      beacon.gatewayTlsFingerprintSha256 = txtMap.gatewayTlsSha256;
    }
    if (txtMap.role) {
      beacon.role = txtMap.role;
    }
    if (txtMap.transport) {
      beacon.transport = txtMap.transport;
    }

    results.push(beacon);
  }

  return results;
}

function parseAvahiBrowse(stdout: string): GatewayBonjourBeacon[] {
  const results: GatewayBonjourBeacon[] = [];
  let current: GatewayBonjourBeacon | null = null;

  for (const raw of stdout.split("\n")) {
    const line = raw.trimEnd();
    if (!line) {
      continue;
    }
    if (line.startsWith("=") && line.includes(GATEWAY_SERVICE_TYPE)) {
      if (current) {
        results.push(current);
      }
      const marker = ` ${GATEWAY_SERVICE_TYPE}`;
      const idx = line.indexOf(marker);
      const left = idx >= 0 ? line.slice(0, idx).trim() : line;
      const parts = left.split(/\s+/);
      const instanceName = parts.length > 3 ? parts.slice(3).join(" ") : left;
      current = {
        instanceName,
        displayName: instanceName,
      };
      continue;
    }

    if (!current) {
      continue;
    }

    const trimmed = line.trim();
    if (trimmed.startsWith("hostname =")) {
      const match = trimmed.match(/hostname\s*=\s*\[([^\]]+)\]/);
      if (match?.[1]) {
        current.host = match[1];
      }
      continue;
    }

    if (trimmed.startsWith("port =")) {
      const match = trimmed.match(/port\s*=\s*\[(\d+)\]/);
      if (match?.[1]) {
        current.port = parseIntOrNull(match[1]);
      }
      continue;
    }

    if (trimmed.startsWith("txt =")) {
      const tokens = Array.from(trimmed.matchAll(/"([^"]*)"/g), (m) => m[1]);
      const txt = parseTxtTokens(tokens);
      current.txt = Object.keys(txt).length ? txt : undefined;
      if (txt.displayName) {
        current.displayName = txt.displayName;
      }
      if (txt.lanHost) {
        current.lanHost = txt.lanHost;
      }
      if (txt.tailnetDns) {
        current.tailnetDns = txt.tailnetDns;
      }
      if (txt.cliPath) {
        current.cliPath = txt.cliPath;
      }
      current.gatewayPort = parseIntOrNull(txt.gatewayPort);
      current.sshPort = parseIntOrNull(txt.sshPort);
      if (txt.gatewayTls) {
        const raw = normalizeOptionalLowercaseString(txt.gatewayTls);
        current.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
      }
      if (txt.gatewayTlsSha256) {
        current.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256;
      }
      if (txt.role) {
        current.role = txt.role;
      }
      if (txt.transport) {
        current.transport = txt.transport;
      }
    }
  }

  if (current) {
    results.push(current);
  }
  return results;
}

async function discoverViaAvahi(
  domain: string,
  timeoutMs: number,
  run: typeof runCommandWithTimeout,
): Promise<GatewayBonjourBeacon[]> {
  const args = ["avahi-browse", "-rt", GATEWAY_SERVICE_TYPE];
  if (domain && domain !== "local.") {
    // avahi-browse wants a plain domain (no trailing dot)
    args.push("-d", domain.replace(/\.$/, ""));
  }
  const browse = await run(args, { timeoutMs });
  return parseAvahiBrowse(browse.stdout).map((beacon) => ({
    ...beacon,
    domain,
  }));
}

export async function discoverGatewayBeacons(
  opts: GatewayBonjourDiscoverOpts = {},
): Promise<GatewayBonjourBeacon[]> {
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
  const platform = opts.platform ?? process.platform;
  const run = opts.run ?? runCommandWithTimeout;
  const wideAreaDomain = resolveWideAreaDiscoveryDomain({ configDomain: opts.wideAreaDomain });
  const domainsRaw = Array.isArray(opts.domains) ? opts.domains : [];
  const defaultDomains = ["local.", ...(wideAreaDomain ? [wideAreaDomain] : [])];
  const domains = (domainsRaw.length > 0 ? domainsRaw : defaultDomains)
    .map((d) => d.trim())
    .filter(Boolean)
    .map((d) => (d.endsWith(".") ? d : `${d}.`));

  try {
    if (platform === "darwin") {
      const perDomain = await Promise.allSettled(
        domains.map(async (domain) => await discoverViaDnsSd(domain, timeoutMs, run)),
      );
      const discovered = perDomain.flatMap((r) => (r.status === "fulfilled" ? r.value : []));

      const wantsWideArea = wideAreaDomain ? domains.includes(wideAreaDomain) : false;
      const hasWideArea = wideAreaDomain
        ? discovered.some((b) => b.domain === wideAreaDomain)
        : false;

      if (wantsWideArea && !hasWideArea && wideAreaDomain) {
        const fallback = await discoverWideAreaViaTailnetDns(wideAreaDomain, timeoutMs, run).catch(
          () => [],
        );
        return [...discovered, ...fallback];
      }

      return discovered;
    }
    if (platform === "linux") {
      const perDomain = await Promise.allSettled(
        domains.map(async (domain) => await discoverViaAvahi(domain, timeoutMs, run)),
      );
      return perDomain.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
    }
  } catch {
    return [];
  }
  return [];
}
