import {
  createConfigIO,
  resolveConfigPath,
  resolveGatewayPort,
  resolveStateDir,
} from "../../config/config.js";
import type {
  OpenClawConfig,
  GatewayBindMode,
  GatewayControlUiConfig,
} from "../../config/types.js";
import {
  hasConfiguredSecretInput,
  normalizeSecretInputString,
  resolveSecretInputRef,
} from "../../config/types.secrets.js";
import { readLastGatewayErrorLine } from "../../daemon/diagnostics.js";
import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
import { findExtraGatewayServices } from "../../daemon/inspect.js";
import type { ServiceConfigAudit } from "../../daemon/service-audit.js";
import { auditGatewayServiceConfig } from "../../daemon/service-audit.js";
import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js";
import { resolveGatewayService } from "../../daemon/service.js";
import { resolveGatewayBindHost } from "../../gateway/net.js";
import {
  formatPortDiagnostics,
  inspectPortUsage,
  type PortListener,
  type PortUsageStatus,
} from "../../infra/ports.js";
import { pickPrimaryTailnetIPv4 } from "../../infra/tailnet.js";
import { loadGatewayTlsRuntime } from "../../infra/tls/gateway.js";
import { secretRefKey } from "../../secrets/ref-contract.js";
import { resolveSecretRefValues } from "../../secrets/resolve.js";
import { probeGatewayStatus } from "./probe.js";
import { normalizeListenerAddress, parsePortFromArgs, pickProbeHostForBind } from "./shared.js";
import type { GatewayRpcOpts } from "./types.js";

type ConfigSummary = {
  path: string;
  exists: boolean;
  valid: boolean;
  issues?: Array<{ path: string; message: string }>;
  controlUi?: GatewayControlUiConfig;
};

type GatewayStatusSummary = {
  bindMode: GatewayBindMode;
  bindHost: string;
  customBindHost?: string;
  port: number;
  portSource: "service args" | "env/config";
  probeUrl: string;
  probeNote?: string;
};

export type DaemonStatus = {
  service: {
    label: string;
    loaded: boolean;
    loadedText: string;
    notLoadedText: string;
    command?: {
      programArguments: string[];
      workingDirectory?: string;
      environment?: Record<string, string>;
      sourcePath?: string;
    } | null;
    runtime?: GatewayServiceRuntime;
    configAudit?: ServiceConfigAudit;
  };
  config?: {
    cli: ConfigSummary;
    daemon?: ConfigSummary;
    mismatch?: boolean;
  };
  gateway?: GatewayStatusSummary;
  port?: {
    port: number;
    status: PortUsageStatus;
    listeners: PortListener[];
    hints: string[];
  };
  portCli?: {
    port: number;
    status: PortUsageStatus;
    listeners: PortListener[];
    hints: string[];
  };
  lastError?: string;
  rpc?: {
    ok: boolean;
    error?: string;
    url?: string;
  };
  extraServices: Array<{ label: string; detail: string; scope: string }>;
};

function shouldReportPortUsage(status: PortUsageStatus | undefined, rpcOk?: boolean) {
  if (status !== "busy") {
    return false;
  }
  if (rpcOk === true) {
    return false;
  }
  return true;
}

function trimToUndefined(value: unknown): string | undefined {
  if (typeof value !== "string") {
    return undefined;
  }
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function readGatewayTokenEnv(env: Record<string, string | undefined>): string | undefined {
  return trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN) ?? trimToUndefined(env.CLAWDBOT_GATEWAY_TOKEN);
}

function readGatewayPasswordEnv(env: Record<string, string | undefined>): string | undefined {
  return (
    trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD) ?? trimToUndefined(env.CLAWDBOT_GATEWAY_PASSWORD)
  );
}

async function resolveDaemonProbeToken(params: {
  daemonCfg: OpenClawConfig;
  mergedDaemonEnv: Record<string, string | undefined>;
  explicitToken?: string;
  explicitPassword?: string;
}): Promise<string | undefined> {
  const explicitToken = trimToUndefined(params.explicitToken);
  if (explicitToken) {
    return explicitToken;
  }
  const envToken = readGatewayTokenEnv(params.mergedDaemonEnv);
  if (envToken) {
    return envToken;
  }
  const defaults = params.daemonCfg.secrets?.defaults;
  const configured = params.daemonCfg.gateway?.auth?.token;
  const { ref } = resolveSecretInputRef({
    value: configured,
    defaults,
  });
  if (!ref) {
    return normalizeSecretInputString(configured);
  }
  const authMode = params.daemonCfg.gateway?.auth?.mode;
  if (authMode === "password" || authMode === "none" || authMode === "trusted-proxy") {
    return undefined;
  }
  if (authMode !== "token") {
    const passwordCandidate =
      trimToUndefined(params.explicitPassword) ||
      readGatewayPasswordEnv(params.mergedDaemonEnv) ||
      (hasConfiguredSecretInput(params.daemonCfg.gateway?.auth?.password, defaults)
        ? "__configured__"
        : undefined);
    if (passwordCandidate) {
      return undefined;
    }
  }
  const resolved = await resolveSecretRefValues([ref], {
    config: params.daemonCfg,
    env: params.mergedDaemonEnv as NodeJS.ProcessEnv,
  });
  const token = trimToUndefined(resolved.get(secretRefKey(ref)));
  if (!token) {
    throw new Error("gateway.auth.token resolved to an empty or non-string value.");
  }
  return token;
}

async function resolveDaemonProbePassword(params: {
  daemonCfg: OpenClawConfig;
  mergedDaemonEnv: Record<string, string | undefined>;
  explicitToken?: string;
  explicitPassword?: string;
}): Promise<string | undefined> {
  const explicitPassword = trimToUndefined(params.explicitPassword);
  if (explicitPassword) {
    return explicitPassword;
  }
  const envPassword = readGatewayPasswordEnv(params.mergedDaemonEnv);
  if (envPassword) {
    return envPassword;
  }
  const defaults = params.daemonCfg.secrets?.defaults;
  const configured = params.daemonCfg.gateway?.auth?.password;
  const { ref } = resolveSecretInputRef({
    value: configured,
    defaults,
  });
  if (!ref) {
    return normalizeSecretInputString(configured);
  }
  const authMode = params.daemonCfg.gateway?.auth?.mode;
  if (authMode === "token" || authMode === "none" || authMode === "trusted-proxy") {
    return undefined;
  }
  if (authMode !== "password") {
    const tokenCandidate =
      trimToUndefined(params.explicitToken) ||
      readGatewayTokenEnv(params.mergedDaemonEnv) ||
      (hasConfiguredSecretInput(params.daemonCfg.gateway?.auth?.token, defaults)
        ? "__configured__"
        : undefined);
    if (tokenCandidate) {
      return undefined;
    }
  }
  const resolved = await resolveSecretRefValues([ref], {
    config: params.daemonCfg,
    env: params.mergedDaemonEnv as NodeJS.ProcessEnv,
  });
  const password = trimToUndefined(resolved.get(secretRefKey(ref)));
  if (!password) {
    throw new Error("gateway.auth.password resolved to an empty or non-string value.");
  }
  return password;
}

export async function gatherDaemonStatus(
  opts: {
    rpc: GatewayRpcOpts;
    probe: boolean;
    deep?: boolean;
  } & FindExtraGatewayServicesOptions,
): Promise<DaemonStatus> {
  const service = resolveGatewayService();
  const [loaded, command, runtime] = await Promise.all([
    service.isLoaded({ env: process.env }).catch(() => false),
    service.readCommand(process.env).catch(() => null),
    service.readRuntime(process.env).catch((err) => ({ status: "unknown", detail: String(err) })),
  ]);
  const configAudit = await auditGatewayServiceConfig({
    env: process.env,
    command,
  });

  const serviceEnv = command?.environment ?? undefined;
  const mergedDaemonEnv = {
    ...(process.env as Record<string, string | undefined>),
    ...(serviceEnv ?? undefined),
  } satisfies Record<string, string | undefined>;

  const cliConfigPath = resolveConfigPath(process.env, resolveStateDir(process.env));
  const daemonConfigPath = resolveConfigPath(
    mergedDaemonEnv as NodeJS.ProcessEnv,
    resolveStateDir(mergedDaemonEnv as NodeJS.ProcessEnv),
  );

  const cliIO = createConfigIO({ env: process.env, configPath: cliConfigPath });
  const daemonIO = createConfigIO({
    env: mergedDaemonEnv,
    configPath: daemonConfigPath,
  });

  const [cliSnapshot, daemonSnapshot] = await Promise.all([
    cliIO.readConfigFileSnapshot().catch(() => null),
    daemonIO.readConfigFileSnapshot().catch(() => null),
  ]);
  const cliCfg = cliIO.loadConfig();
  const daemonCfg = daemonIO.loadConfig();

  const cliConfigSummary: ConfigSummary = {
    path: cliSnapshot?.path ?? cliConfigPath,
    exists: cliSnapshot?.exists ?? false,
    valid: cliSnapshot?.valid ?? true,
    ...(cliSnapshot?.issues?.length ? { issues: cliSnapshot.issues } : {}),
    controlUi: cliCfg.gateway?.controlUi,
  };
  const daemonConfigSummary: ConfigSummary = {
    path: daemonSnapshot?.path ?? daemonConfigPath,
    exists: daemonSnapshot?.exists ?? false,
    valid: daemonSnapshot?.valid ?? true,
    ...(daemonSnapshot?.issues?.length ? { issues: daemonSnapshot.issues } : {}),
    controlUi: daemonCfg.gateway?.controlUi,
  };
  const configMismatch = cliConfigSummary.path !== daemonConfigSummary.path;

  const portFromArgs = parsePortFromArgs(command?.programArguments);
  const daemonPort = portFromArgs ?? resolveGatewayPort(daemonCfg, mergedDaemonEnv);
  const portSource: GatewayStatusSummary["portSource"] = portFromArgs
    ? "service args"
    : "env/config";

  const bindMode = (daemonCfg.gateway?.bind ?? "loopback") as
    | "auto"
    | "lan"
    | "loopback"
    | "custom"
    | "tailnet";
  const customBindHost = daemonCfg.gateway?.customBindHost;
  const bindHost = await resolveGatewayBindHost(bindMode, customBindHost);
  const tailnetIPv4 = pickPrimaryTailnetIPv4();
  const probeHost = pickProbeHostForBind(bindMode, tailnetIPv4, customBindHost);
  const probeUrlOverride =
    typeof opts.rpc.url === "string" && opts.rpc.url.trim().length > 0 ? opts.rpc.url.trim() : null;
  const scheme = daemonCfg.gateway?.tls?.enabled === true ? "wss" : "ws";
  const probeUrl = probeUrlOverride ?? `${scheme}://${probeHost}:${daemonPort}`;
  const probeNote =
    !probeUrlOverride && bindMode === "lan"
      ? `bind=lan listens on 0.0.0.0 (all interfaces); probing via ${probeHost}.`
      : !probeUrlOverride && bindMode === "loopback"
        ? "Loopback-only gateway; only local clients can connect."
        : undefined;

  const cliPort = resolveGatewayPort(cliCfg, process.env);
  const [portDiagnostics, portCliDiagnostics] = await Promise.all([
    inspectPortUsage(daemonPort).catch(() => null),
    cliPort !== daemonPort ? inspectPortUsage(cliPort).catch(() => null) : null,
  ]);
  const portStatus: DaemonStatus["port"] | undefined = portDiagnostics
    ? {
        port: portDiagnostics.port,
        status: portDiagnostics.status,
        listeners: portDiagnostics.listeners,
        hints: portDiagnostics.hints,
      }
    : undefined;
  const portCliStatus: DaemonStatus["portCli"] | undefined = portCliDiagnostics
    ? {
        port: portCliDiagnostics.port,
        status: portCliDiagnostics.status,
        listeners: portCliDiagnostics.listeners,
        hints: portCliDiagnostics.hints,
      }
    : undefined;

  const extraServices = await findExtraGatewayServices(
    process.env as Record<string, string | undefined>,
    { deep: Boolean(opts.deep) },
  ).catch(() => []);

  const timeoutMsRaw = Number.parseInt(String(opts.rpc.timeout ?? "10000"), 10);
  const timeoutMs = Number.isFinite(timeoutMsRaw) && timeoutMsRaw > 0 ? timeoutMsRaw : 10_000;

  const tlsEnabled = daemonCfg.gateway?.tls?.enabled === true;
  const shouldUseLocalTlsRuntime = opts.probe && !probeUrlOverride && tlsEnabled;
  const tlsRuntime = shouldUseLocalTlsRuntime
    ? await loadGatewayTlsRuntime(daemonCfg.gateway?.tls)
    : undefined;
  const daemonProbePassword = opts.probe
    ? await resolveDaemonProbePassword({
        daemonCfg,
        mergedDaemonEnv,
        explicitToken: opts.rpc.token,
        explicitPassword: opts.rpc.password,
      })
    : undefined;
  const daemonProbeToken = opts.probe
    ? await resolveDaemonProbeToken({
        daemonCfg,
        mergedDaemonEnv,
        explicitToken: opts.rpc.token,
        explicitPassword: opts.rpc.password,
      })
    : undefined;

  const rpc = opts.probe
    ? await probeGatewayStatus({
        url: probeUrl,
        token: daemonProbeToken,
        password: daemonProbePassword,
        tlsFingerprint:
          shouldUseLocalTlsRuntime && tlsRuntime?.enabled
            ? tlsRuntime.fingerprintSha256
            : undefined,
        timeoutMs,
        json: opts.rpc.json,
        configPath: daemonConfigSummary.path,
      })
    : undefined;

  let lastError: string | undefined;
  if (loaded && runtime?.status === "running" && portStatus && portStatus.status !== "busy") {
    lastError = (await readLastGatewayErrorLine(mergedDaemonEnv as NodeJS.ProcessEnv)) ?? undefined;
  }

  return {
    service: {
      label: service.label,
      loaded,
      loadedText: service.loadedText,
      notLoadedText: service.notLoadedText,
      command,
      runtime,
      configAudit,
    },
    config: {
      cli: cliConfigSummary,
      daemon: daemonConfigSummary,
      ...(configMismatch ? { mismatch: true } : {}),
    },
    gateway: {
      bindMode,
      bindHost,
      customBindHost,
      port: daemonPort,
      portSource,
      probeUrl,
      ...(probeNote ? { probeNote } : {}),
    },
    port: portStatus,
    ...(portCliStatus ? { portCli: portCliStatus } : {}),
    lastError,
    ...(rpc ? { rpc: { ...rpc, url: probeUrl } } : {}),
    extraServices,
  };
}

export function renderPortDiagnosticsForCli(status: DaemonStatus, rpcOk?: boolean): string[] {
  if (!status.port || !shouldReportPortUsage(status.port.status, rpcOk)) {
    return [];
  }
  return formatPortDiagnostics({
    port: status.port.port,
    status: status.port.status,
    listeners: status.port.listeners,
    hints: status.port.hints,
  });
}

export function resolvePortListeningAddresses(status: DaemonStatus): string[] {
  const addrs = Array.from(
    new Set(
      status.port?.listeners
        ?.map((l) => (l.address ? normalizeListenerAddress(l.address) : ""))
        .filter((v): v is string => Boolean(v)) ?? [],
    ),
  );
  return addrs;
}
