import { createHash } from "node:crypto";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import http from "node:http";
import https from "node:https";
import path from "node:path";
import type {
  AuthSessionSummary,
  CreditSummary,
  ModelCapability,
  OpenClawDailyReportPayload,
  OpenClawDailyReportResponse,
  ModelConfigFallbackMode,
  ModelConfigSummary,
  ModelRecommendation,
  ModelRoutingMode,
  RuntimeCloudConfigSummary,
  RuntimeCloudFetchAction,
  RuntimeCloudStatus,
  SkillModelBindingMode,
  SkillSummary,
  UserProfileSummary
} from "@qjclaw/shared-types";
import { getRuntimeCloudApiTarget } from "./app-config.js";
import type { AppConfigService, RuntimeCloudApiBaseUrlSource } from "./app-config.js";
import type { PackagedBootstrapService } from "./packaged-bootstrap.js";
import type { RemoteSkillAsset } from "./skill-store.js";
import type { SecretManager } from "./secrets.js";
import type { StartupLogger } from "./startup-logger.js";

interface SessionPayload {
  user?: { displayName?: string; email?: string };
  organization?: { name?: string };
  expiresAt?: string;
}

interface ProfilePayload {
  id?: string;
  displayName?: string;
  email?: string;
  organizationName?: string;
  avatarUrl?: string;
  title?: string;
}

interface CreditsPayload {
  balance?: number;
  granted?: number;
  used?: number;
  updatedAt?: string;
  expiresAt?: string;
}

interface SkillPayload {
  id?: string;
  name?: string;
  description?: string;
  category?: string;
  enabled?: boolean;
  requiresCredits?: number;
}

interface ModelConfigPayload {
  version?: string;
  updatedAt?: string;
  routingMode?: string;
  fallbackMode?: string;
  defaults?: {
    chatModelId?: string;
    chatModelLabel?: string;
    skillModelId?: string;
    skillModelLabel?: string;
  };
  items?: ModelPayload[];
  skillBindings?: SkillModelBindingPayload[];
}

interface ModelPayload {
  id?: string;
  label?: string;
  provider?: string;
  family?: string;
  description?: string;
  enabled?: boolean;
  contextWindow?: number;
  maxOutputTokens?: number;
  capabilities?: string[];
  recommendedFor?: string[];
}

interface SkillModelBindingPayload {
  skillId?: string;
  skillName?: string;
  modelId?: string;
  modelLabel?: string;
  routingMode?: string;
}

interface OpenClawProviderPayload {
  name?: string;
  base_url?: string;
  api_key?: string;
  provider_type?: string;
}

interface OpenClawLlmPayload {
  model_id?: string;
  temperature?: number;
  max_tokens?: number;
  display_name?: string;
  tier?: string;
  credit_multiplier?: number;
  max_context_length?: number;
  provider?: OpenClawProviderPayload;
}

interface OpenClawSkillBindingPayload {
  binding_id?: string;
  skill_id?: string;
  skill_config?: Record<string, unknown>;
  skill?: {
    id?: string;
    title?: string;
    description?: string;
    category?: string;
    file_name?: string;
    file_size?: number;
    download_url?: string;
  };
}

interface OpenClawChannelPayload {
  id?: string;
  type?: string;
  name?: string;
  config?: Record<string, unknown>;
}

interface OpenClawEmployeeConfigPayload {
  changed?: boolean;
  employee_id?: string;
  name?: string;
  status?: string;
  deployment_type?: string;
  persona_prompt?: string | null;
  welcome_message?: string | null;
  work_hours?: Record<string, unknown> | null;
  auto_reply_rules?: Array<Record<string, unknown>>;
  resource_quota?: Record<string, unknown> | null;
  llm?: OpenClawLlmPayload;
  skills?: OpenClawSkillBindingPayload[];
  channels?: OpenClawChannelPayload[];
  endpoints?: {
    heartbeat?: string;
    events?: string;
    config?: string;
  };
  config_version?: string;
}

class CloudApiError extends Error {
  readonly status: number;

  constructor(message: string, status: number) {
    super(message);
    this.name = "CloudApiError";
    this.status = status;
  }
}

function normalizeModelRoutingMode(value?: string): ModelRoutingMode {
  return value === "skill-bound" || value === "user-selectable" ? value : "platform-managed";
}

function normalizeModelFallbackMode(value?: string): ModelConfigFallbackMode {
  return value === "last-known-cache" ? value : "cloud-required";
}

function normalizeModelCapability(value: string): ModelCapability | null {
  switch (value) {
    case "chat":
    case "reasoning":
    case "tools":
    case "vision":
    case "audio-input":
    case "audio-output":
      return value;
    default:
      return null;
  }
}

function normalizeModelRecommendation(value: string): ModelRecommendation | null {
  switch (value) {
    case "default":
    case "chat":
    case "skills":
    case "analysis":
      return value;
    default:
      return null;
  }
}

function normalizeSkillModelBindingMode(value?: string): SkillModelBindingMode {
  return value === "forced" || value === "selectable" ? value : "default";
}

function cloneJson<T>(value: T): T {
  return JSON.parse(JSON.stringify(value)) as T;
}

function buildApiKeyFingerprint(apiKey: string): string {
  return createHash("sha256").update(apiKey).digest("hex");
}

function toDiagnosticKeyFingerprint(apiKey?: string): string | undefined {
  if (!apiKey) {
    return undefined;
  }

  return buildApiKeyFingerprint(apiKey).slice(0, 12);
}

function toBaseHost(baseUrl?: string): string | undefined {
  if (!baseUrl) {
    return undefined;
  }

  try {
    return new URL(baseUrl).host;
  } catch {
    return undefined;
  }
}

function classifyRuntimeCloudError(message: string): string {
  const normalized = message.toLowerCase();
  if (normalized.includes("invalid api_key or employee not found")) {
    return "员工密钥无效，或当前客户端正连接到未收录该密钥的绑定环境。";
  }

  return message;
}

function mergeAdditionalModelProvidersFromEnv(config: Record<string, unknown>): Record<string, unknown> {
  const rawProviders = process.env.QJCLAW_EXTRA_MODEL_PROVIDERS_JSON?.trim();
  if (!rawProviders) {
    return config;
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(rawProviders);
  } catch (error) {
    console.warn("[runtime-cloud]", "failed to parse QJCLAW_EXTRA_MODEL_PROVIDERS_JSON", error);
    return config;
  }

  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    return config;
  }

  const nextConfig = cloneJson(config);
  const modelsSection = asRecord(nextConfig.models);
  const providers = asRecord(modelsSection.providers);

  for (const [providerKey, providerValue] of Object.entries(parsed)) {
    if (providerKey.trim().length === 0) {
      continue;
    }
    const incomingProvider = asRecord(providerValue);
    if (Object.keys(incomingProvider).length === 0) {
      continue;
    }
    const existingProvider = asRecord(providers[providerKey]);
    providers[providerKey] = {
      ...existingProvider,
      ...incomingProvider
    };
  }

  modelsSection.mode = "merge";
  modelsSection.providers = providers;
  nextConfig.models = modelsSection;
  return nextConfig;
}

function asRecord(value: unknown): Record<string, unknown> {
  return typeof value === "object" && value !== null ? value as Record<string, unknown> : {};
}


function toRemoteSkillAssets(payload?: OpenClawEmployeeConfigPayload | null): RemoteSkillAsset[] {
  return (payload?.skills ?? []).map((binding, index) => ({
    bindingId: binding.binding_id ?? `binding-${index + 1}`,
    skillId: binding.skill_id ?? binding.binding_id ?? `skill-${index + 1}`,
    name: binding.skill?.title ?? binding.skill_id ?? `Skill ${index + 1}`,
    description: binding.skill?.description ?? "Enabled for this employee and ready to use in chat.",
    category: binding.skill?.category ?? "general",
    fileName: binding.skill?.file_name ?? undefined,
    fileSize: typeof binding.skill?.file_size === "number" ? binding.skill.file_size : undefined,
    downloadUrl: binding.skill?.download_url ?? undefined
  }));
}

type RuntimeCloudPayloadListener = (payload: {
  action: RuntimeCloudFetchAction;
  config: RuntimeCloudConfigSummary;
  skills: RemoteSkillAsset[];
}) => Promise<void> | void;

const DEFAULT_HTTP_REQUEST_TIMEOUT_MS = 15_000;

interface OpenClawConfigCacheRecord {
  keyFingerprint: string;
  payload: OpenClawEmployeeConfigPayload;
  summary: RuntimeCloudConfigSummary;
}

class HttpJsonClient {
  request(
    url: URL,
    options: {
      method: "GET" | "POST";
      headers?: Record<string, string>;
      body?: unknown;
      timeoutMs?: number;
    }
  ): Promise<string> {
    const client = url.protocol === "https:" ? https : http;
    const timeoutMs = options.timeoutMs ?? DEFAULT_HTTP_REQUEST_TIMEOUT_MS;

    return new Promise((resolve, reject) => {
      let settled = false;
      const finishReject = (error: Error) => {
        if (settled) {
          return;
        }
        settled = true;
        reject(error);
      };
      const finishResolve = (body: string) => {
        if (settled) {
          return;
        }
        settled = true;
        resolve(body);
      };

      const request = client.request(url, {
        method: options.method,
        headers: {
          Accept: "application/json",
          ...(options.body ? { "Content-Type": "application/json; charset=utf-8" } : {}),
          ...(options.headers ?? {})
        }
      }, (response) => {
        let body = "";
        response.setEncoding("utf8");
        response.on("data", (chunk) => {
          body += chunk;
        });
        response.on("end", () => {
          const status = response.statusCode ?? 500;
          if (status < 200 || status >= 300) {
            finishReject(new CloudApiError(this.extractErrorMessage(body, status), status));
            return;
          }
          finishResolve(body);
        });
      });

      request.setTimeout(timeoutMs, () => {
        request.destroy(new Error(`Cloud API request timed out after ${Math.round(timeoutMs / 1000)}s: ${url.origin}`));
      });

      request.on("error", (error) => {
        finishReject(new Error(`Cloud API request failed: ${error.message}`));
      });

      if (options.body !== undefined) {
        request.write(JSON.stringify(options.body));
      }
      request.end();
    });
  }

  private extractErrorMessage(body: string, status: number): string {
    try {
      const payload = JSON.parse(body) as { message?: string; error?: string };
      return payload.message ?? payload.error ?? `Cloud API request failed with status ${status}.`;
    } catch {
      return `Cloud API request failed with status ${status}.`;
    }
  }
}

class ProductCloudApiClient {
  private readonly configService: AppConfigService;
  private readonly secretManager: SecretManager;
  private readonly httpClient = new HttpJsonClient();

  constructor(configService: AppConfigService, secretManager: SecretManager) {
    this.configService = configService;
    this.secretManager = secretManager;
  }

  async getBaseUrl(): Promise<string> {
    const config = await this.configService.load();
    return config.cloudApiBaseUrl.trim().replace(/\/$/, "");
  }

  async getToken(): Promise<string | undefined> {
    return this.secretManager.getAuthToken();
  }

  async setToken(token?: string): Promise<void> {
    await this.secretManager.setAuthToken(token);
  }

  async getSessionSummary(): Promise<AuthSessionSummary> {
    const token = await this.getToken();
    if (!token) {
      return { state: "anonymous", tokenConfigured: false, message: "Sign in with a cloud access token to load profile, credits, Skills, and model policy." };
    }

    try {
      const payload = await this.request<SessionPayload>("/v1/auth/session", token);
      return {
        state: "authenticated",
        tokenConfigured: true,
        message: payload.expiresAt ? `Session validated. Expires at ${payload.expiresAt}.` : "Session validated.",
        displayName: payload.user?.displayName,
        email: payload.user?.email,
        organizationName: payload.organization?.name,
        lastValidatedAt: new Date().toISOString()
      };
    } catch (error) {
      if (error instanceof CloudApiError && (error.status === 401 || error.status === 403)) {
        return { state: "expired", tokenConfigured: true, message: error.message, lastValidatedAt: new Date().toISOString() };
      }
      return { state: "error", tokenConfigured: true, message: error instanceof Error ? error.message : String(error), lastValidatedAt: new Date().toISOString() };
    }
  }

  async signIn(accessToken: string): Promise<AuthSessionSummary> {
    const trimmed = accessToken.trim();
    if (!trimmed) {
      throw new Error("Access token is required.");
    }

    const payload = await this.request<SessionPayload>("/v1/auth/session", trimmed);
    await this.setToken(trimmed);
    return {
      state: "authenticated",
      tokenConfigured: true,
      message: payload.expiresAt ? `Session validated. Expires at ${payload.expiresAt}.` : "Session validated.",
      displayName: payload.user?.displayName,
      email: payload.user?.email,
      organizationName: payload.organization?.name,
      lastValidatedAt: new Date().toISOString()
    };
  }

  async signOut(): Promise<AuthSessionSummary> {
    await this.setToken(undefined);
    return { state: "anonymous", tokenConfigured: false, message: "Signed out from the cloud API session." };
  }

  async getProfile(): Promise<UserProfileSummary> {
    const payload = await this.request<ProfilePayload>("/v1/me/profile");
    return {
      userId: payload.id ?? "unknown-user",
      displayName: payload.displayName ?? "Unknown User",
      email: payload.email ?? "unknown@example.com",
      organizationName: payload.organizationName ?? "Unknown Organization",
      avatarUrl: payload.avatarUrl,
      title: payload.title
    };
  }

  async getCredits(): Promise<CreditSummary> {
    const payload = await this.request<CreditsPayload>("/v1/me/credits");
    const balance = Number(payload.balance ?? 0);
    return {
      balance,
      granted: Number(payload.granted ?? balance),
      used: Number(payload.used ?? 0),
      currency: "credits",
      status: balance <= 0 ? "empty" : balance < 20 ? "low" : "ok",
      updatedAt: payload.updatedAt ?? new Date().toISOString(),
      expiresAt: payload.expiresAt
    };
  }

  async listSkills(): Promise<SkillSummary[]> {
    const payload = await this.request<{ items?: SkillPayload[] }>("/v1/skills");
    return (payload.items ?? []).map((item, index) => ({
      id: item.id ?? `skill-${index + 1}`,
      name: item.name ?? `Skill ${index + 1}`,
      description: item.description ?? "No description provided.",
      category: item.category ?? "general",
      enabled: item.enabled ?? true,
      ready: item.enabled ?? true,
      downloadState: "ready",
      requiresCredits: item.requiresCredits
    }));
  }

  async getModelConfig(): Promise<ModelConfigSummary> {
    const payload = await this.request<ModelConfigPayload>("/v1/model-config");
    const fetchedAt = new Date().toISOString();
    const items = (payload.items ?? []).map((item, index) => ({
      id: item.id ?? `model-${index + 1}`,
      label: item.label ?? item.id ?? `Model ${index + 1}`,
      provider: item.provider ?? "platform",
      family: item.family ?? item.provider ?? "general",
      description: item.description,
      enabled: item.enabled ?? true,
      contextWindow: typeof item.contextWindow === "number" ? item.contextWindow : undefined,
      maxOutputTokens: typeof item.maxOutputTokens === "number" ? item.maxOutputTokens : undefined,
      capabilities: (item.capabilities ?? []).map(normalizeModelCapability).filter((value): value is ModelCapability => value !== null),
      recommendedFor: (item.recommendedFor ?? []).map(normalizeModelRecommendation).filter((value): value is ModelRecommendation => value !== null)
    }));
    const defaultChatModel = items.find((item) => item.id === payload.defaults?.chatModelId) ?? items[0];
    const defaultSkillModel = items.find((item) => item.id === payload.defaults?.skillModelId)
      ?? items.find((item) => item.id === payload.defaults?.chatModelId)
      ?? items[0];

    return {
      source: "cloud",
      catalogVersion: payload.version,
      updatedAt: payload.updatedAt ?? fetchedAt,
      fetchedAt,
      routingMode: normalizeModelRoutingMode(payload.routingMode),
      fallbackMode: normalizeModelFallbackMode(payload.fallbackMode),
      defaultChatModelId: payload.defaults?.chatModelId ?? defaultChatModel?.id ?? "unknown-model",
      defaultChatModelLabel: payload.defaults?.chatModelLabel ?? defaultChatModel?.label ?? "Unknown Model",
      defaultSkillModelId: payload.defaults?.skillModelId ?? defaultSkillModel?.id,
      defaultSkillModelLabel: payload.defaults?.skillModelLabel ?? defaultSkillModel?.label,
      items,
      skillBindings: (payload.skillBindings ?? []).map((binding, index) => ({
        skillId: binding.skillId ?? `skill-binding-${index + 1}`,
        skillName: binding.skillName ?? binding.skillId ?? `Skill Binding ${index + 1}`,
        modelId: binding.modelId ?? defaultSkillModel?.id ?? defaultChatModel?.id ?? "unknown-model",
        modelLabel: binding.modelLabel ?? defaultSkillModel?.label ?? defaultChatModel?.label ?? "Unknown Model",
        routingMode: normalizeSkillModelBindingMode(binding.routingMode)
      })),
      message: items.length > 0
        ? `${items.length} cloud-managed models available. Default chat model: ${payload.defaults?.chatModelLabel ?? defaultChatModel?.label ?? "Unknown Model"}.`
        : "Cloud model policy returned no enabled models."
    };
  }

  async request<T>(pathname: string, tokenOverride?: string): Promise<T> {
    const baseUrl = await this.getBaseUrl();
    if (!baseUrl) {
      throw new Error("Cloud API base URL is not configured.");
    }

    const token = tokenOverride ?? await this.getToken();
    if (!token) {
      throw new Error("Cloud access token is not configured.");
    }

    const url = new URL(`${baseUrl}${pathname}`);
    const body = await this.httpClient.request(url, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${token}`
      }
    });

    try {
      return JSON.parse(body) as T;
    } catch {
      throw new Error("Cloud API returned invalid JSON.");
    }
  }
}

export class OpenClawConfigClient {
  private readonly configService: AppConfigService;
  private readonly secretManager: SecretManager;
  private readonly httpClient = new HttpJsonClient();
  private readonly payloadListeners = new Set<RuntimeCloudPayloadListener>();
  private readonly cachePath: string;
  private readonly startupLogger?: StartupLogger;
  private readonly packagedBootstrap?: Pick<PackagedBootstrapService, "materializeForApiKey">;
  private payloadCache: OpenClawEmployeeConfigPayload | null = null;
  private statusCache: RuntimeCloudStatus = {
    state: "unconfigured",
    baseUrl: "",
    baseUrlSource: "default",
    apiKeyConfigured: false
  };
  private cacheLoaded = false;

  constructor(
    configService: AppConfigService,
    secretManager: SecretManager,
    startupLogger?: StartupLogger,
    packagedBootstrap?: Pick<PackagedBootstrapService, "materializeForApiKey">
  ) {
    this.configService = configService;
    this.secretManager = secretManager;
    this.cachePath = this.configService.getDataPath("config", "runtime-cloud-cache.json");
    this.startupLogger = startupLogger;
    this.packagedBootstrap = packagedBootstrap;
  }

  async hydrateCache(): Promise<void> {
    if (this.cacheLoaded) {
      return;
    }

    this.cacheLoaded = true;
    const apiKey = (await this.secretManager.getApiKey())?.trim();
    if (apiKey && this.packagedBootstrap) {
      await this.packagedBootstrap.materializeForApiKey(apiKey).catch(() => undefined);
    }
    try {
      const raw = await readFile(this.cachePath, "utf8");
      const parsed = JSON.parse(raw) as OpenClawConfigCacheRecord;
      if (!parsed || typeof parsed !== "object" || !parsed.payload || !parsed.summary) {
        throw new Error("Runtime cloud cache is malformed.");
      }
      if (!parsed.summary.configVersion || !parsed.summary.employeeId) {
        throw new Error("Runtime cloud cache is missing required summary fields.");
      }

      if (!apiKey || parsed.keyFingerprint !== buildApiKeyFingerprint(apiKey)) {
        return;
      }

      this.payloadCache = cloneJson(parsed.payload);
      this.statusCache = {
        ...this.statusCache,
        state: "ready",
        lastFetchedAt: parsed.summary.fetchedAt,
        config: cloneJson(parsed.summary),
        lastError: undefined
      };
    } catch (error) {
      if (!this.shouldIgnoreCacheError(error)) {
        this.statusCache = {
          ...this.statusCache,
          lastError: error instanceof Error ? error.message : String(error)
        };
      }
    }
  }

  async clearCache(): Promise<void> {
    this.payloadCache = null;
    this.statusCache = {
      state: "unconfigured",
      baseUrl: this.statusCache.baseUrl,
      apiKeyConfigured: this.statusCache.apiKeyConfigured
    };
    this.cacheLoaded = false;
    await rm(this.cachePath, { force: true }).catch(() => undefined);
  }

  hasCachedPayload(): boolean {
    return Boolean(this.payloadCache && this.statusCache.config);
  }

  async getStatus(): Promise<RuntimeCloudStatus> {
    await this.hydrateCache();
    const config = await this.configService.load();
    const apiKey = (await this.secretManager.getApiKey())?.trim();
    const { baseUrl, source } = getRuntimeCloudApiTarget(config);
    return {
      ...this.statusCache,
      baseUrl,
      baseUrlSource: source,
      baseHost: toBaseHost(baseUrl),
      apiKeyConfigured: Boolean(apiKey),
      apiKeyFingerprint: toDiagnosticKeyFingerprint(apiKey)
    };
  }

  async fetchConfig(action: RuntimeCloudFetchAction = "init"): Promise<RuntimeCloudStatus> {
    await this.fetchPayload(action);
    return this.getStatus();
  }

  async buildManagedConfig(defaultConfig: Record<string, unknown>, action: RuntimeCloudFetchAction = "init"): Promise<Record<string, unknown>> {
    await this.hydrateCache();
    const payload = action === "init" && this.payloadCache
      ? this.payloadCache
      : await this.fetchPayload(action);
    const effectivePayload = action === "sync" && payload.changed === false && this.payloadCache
      ? this.payloadCache
      : payload;
    return this.mergeConfig(defaultConfig, effectivePayload);
  }
  getRemoteSkillAssets(): RemoteSkillAsset[] {
    return toRemoteSkillAssets(this.payloadCache);
  }

  onPayloadUpdated(listener: RuntimeCloudPayloadListener): () => void {
    this.payloadListeners.add(listener);
    return () => {
      this.payloadListeners.delete(listener);
    };
  }

  private shouldIgnoreCacheError(error: unknown): boolean {
    if (!error || typeof error !== "object") {
      return false;
    }

    const candidate = error as NodeJS.ErrnoException;
    return candidate.code === "ENOENT" || error instanceof SyntaxError;
  }

  private async fetchPayload(action: RuntimeCloudFetchAction): Promise<OpenClawEmployeeConfigPayload> {
    await this.hydrateCache();
    if (action === "init" && this.payloadCache && this.statusCache.config) {
      return this.payloadCache;
    }
    const config = await this.configService.load();
    const startedAt = Date.now();
    const { baseUrl, source } = getRuntimeCloudApiTarget(config);
    const baseHost = toBaseHost(baseUrl);
    const apiKey = (await this.secretManager.getApiKey())?.trim();
    const apiKeyFingerprint = toDiagnosticKeyFingerprint(apiKey);

    if (!baseUrl) {
      return this.fail(baseUrl, source, Boolean(apiKey), apiKeyFingerprint, "\u004f\u0070\u0065\u006e\u0043\u006c\u0061\u0077\u0020\u8fd0\u884c\u65f6\u4e91\u7aef\u5730\u5740\u672a\u914d\u7f6e\u3002");
    }

    if (!apiKey) {
      return this.fail(baseUrl, source, false, undefined, "\u8bf7\u5148\u7ed1\u5b9a\u0020\u004f\u0070\u0065\u006e\u0043\u006c\u0061\u0077\u0020\u0065\u006d\u0070\u006c\u006f\u0079\u0065\u0065\u0020\u0041\u0050\u0049\u0020\u004b\u0065\u0079\u3002");
    }

    this.statusCache = {
      ...this.statusCache,
      state: "loading",
      baseUrl,
      baseUrlSource: source,
      baseHost,
      apiKeyConfigured: true,
      apiKeyFingerprint,
      lastError: undefined
    };

    try {
      const url = new URL(`${baseUrl}/openclaw-employee-config`);
      const body = await this.httpClient.request(url, {
        method: "POST",
        body: {
          api_key: apiKey,
          action,
          ...(action === "sync" && this.statusCache.config?.configVersion ? { config_version: this.statusCache.config.configVersion } : {})
        }
      });

      let payload: OpenClawEmployeeConfigPayload;
      try {
        payload = JSON.parse(body) as OpenClawEmployeeConfigPayload;
      } catch {
        throw new Error("\u004f\u0070\u0065\u006e\u0043\u006c\u0061\u0077\u0020\u914d\u7f6e\u63a5\u53e3\u8fd4\u56de\u4e86\u65e0\u6548\u0020\u004a\u0053\u004f\u004e\u3002");
      }

      const fetchedAt = new Date().toISOString();
      if (action === "sync" && payload.changed === false && this.statusCache.config) {
        const summary = {
          ...this.statusCache.config,
          configVersion: payload.config_version ?? this.statusCache.config.configVersion,
          fetchedAt
        };
        this.statusCache = {
          state: "ready",
          baseUrl,
          baseUrlSource: source,
          baseHost,
          apiKeyConfigured: true,
          apiKeyFingerprint,
          lastFetchedAt: fetchedAt,
          config: summary,
          lastError: undefined
        };
        if (this.payloadCache) {
          await this.persistCache(summary, this.payloadCache, apiKey);
        }
        return payload;
      }

      this.validatePayload(payload);
      this.payloadCache = cloneJson(payload);
      const summary = this.toSummary(payload, fetchedAt);
      this.statusCache = {
        state: "ready",
        baseUrl,
        baseUrlSource: source,
        baseHost,
        apiKeyConfigured: true,
        apiKeyFingerprint,
        lastFetchedAt: summary.fetchedAt,
        config: summary,
        lastError: undefined
      };
      await this.persistCache(summary, this.payloadCache, apiKey);
      await this.notifyPayloadUpdated(action, summary);
      return payload;
    } catch (error) {
      const rawMessage = error instanceof Error ? error.message : String(error);
      const message = classifyRuntimeCloudError(rawMessage);
      if (this.payloadCache && this.statusCache.config) {
        this.statusCache = {
          ...this.statusCache,
          state: "ready",
          baseUrl,
          baseUrlSource: source,
          baseHost,
          apiKeyConfigured: true,
          apiKeyFingerprint,
          lastError: message
        };
        throw new Error(message);
      }
      await this.startupLogger?.error("runtime-cloud", "fetch.error", "Runtime cloud fetch failed without cache fallback.", {
        action,
        baseUrl,
        baseUrlSource: source,
        baseHost,
        apiKeyFingerprint,
        elapsedMs: Date.now() - startedAt,
        error: message
      });
      return this.fail(baseUrl, source, true, apiKeyFingerprint, message);
    }
  }

  private async persistCache(summary: RuntimeCloudConfigSummary, payload: OpenClawEmployeeConfigPayload, apiKey: string): Promise<void> {
    const record: OpenClawConfigCacheRecord = {
      keyFingerprint: buildApiKeyFingerprint(apiKey),
      summary,
      payload
    };
    await mkdir(path.dirname(this.cachePath), { recursive: true });
    await writeFile(this.cachePath, JSON.stringify(record, null, 2), "utf8");
  }

  private async notifyPayloadUpdated(action: RuntimeCloudFetchAction, config: RuntimeCloudConfigSummary): Promise<void> {
    const skills = this.getRemoteSkillAssets();
    for (const listener of this.payloadListeners) {
      try {
        await listener({ action, config, skills });
      } catch {
        // Keep runtime cloud config usable even if local skill sync fails.
      }
    }
  }

  private fail(
    baseUrl: string,
    baseUrlSource: RuntimeCloudApiBaseUrlSource,
    apiKeyConfigured: boolean,
    apiKeyFingerprint: string | undefined,
    message: string
  ): never {
    this.statusCache = {
      ...this.statusCache,
      state: apiKeyConfigured ? "error" : "unconfigured",
      baseUrl,
      baseUrlSource,
      baseHost: toBaseHost(baseUrl),
      apiKeyConfigured,
      apiKeyFingerprint,
      lastError: message
    };
    throw new Error(message);
  }

  private validatePayload(payload: OpenClawEmployeeConfigPayload): void {
    if (!payload.employee_id || !payload.name) {
      throw new Error("\u004f\u0070\u0065\u006e\u0043\u006c\u0061\u0077\u0020\u914d\u7f6e\u63a5\u53e3\u7f3a\u5c11\u5458\u5de5\u6807\u8bc6\u6216\u540d\u79f0\u3002");
    }
    if (!payload.config_version) {
      throw new Error("\u004f\u0070\u0065\u006e\u0043\u006c\u0061\u0077\u0020\u914d\u7f6e\u63a5\u53e3\u7f3a\u5c11\u0020\u0063\u006f\u006e\u0066\u0069\u0067\u005f\u0076\u0065\u0072\u0073\u0069\u006f\u006e\u3002");
    }
  }

  private toSummary(payload: OpenClawEmployeeConfigPayload, fetchedAt: string): RuntimeCloudConfigSummary {
    return {
      employeeId: payload.employee_id ?? "unknown-employee",
      employeeName: payload.name ?? "Unknown Employee",
      employeeStatus: payload.status,
      deploymentType: payload.deployment_type,
      configVersion: payload.config_version ?? fetchedAt,
      fetchedAt,
      providerName: payload.llm?.provider?.name,
      providerType: payload.llm?.provider?.provider_type,
      baseUrl: payload.llm?.provider?.base_url,
      modelId: payload.llm?.model_id,
      modelDisplayName: payload.llm?.display_name,
      temperature: payload.llm?.temperature,
      maxTokens: payload.llm?.max_tokens,
      skillCount: payload.skills?.length ?? 0,
      channelCount: payload.channels?.length ?? 0,
      personaPromptConfigured: Boolean(payload.persona_prompt),
      welcomeMessage: payload.welcome_message ?? undefined,
      heartbeatUrl: payload.endpoints?.heartbeat,
      eventsUrl: payload.endpoints?.events
    };
  }

  private mergeConfig(defaultConfig: Record<string, unknown>, payload: OpenClawEmployeeConfigPayload): Record<string, unknown> {
    const nextConfig = cloneJson(defaultConfig);
    void payload;
    return mergeAdditionalModelProvidersFromEnv(nextConfig);
  }
}
export class AuthClient {
  private readonly api: ProductCloudApiClient;

  constructor(configService: AppConfigService, secretManager: SecretManager) {
    this.api = new ProductCloudApiClient(configService, secretManager);
  }

  getSessionSummary(): Promise<AuthSessionSummary> {
    return this.api.getSessionSummary();
  }

  signIn(accessToken: string): Promise<AuthSessionSummary> {
    return this.api.signIn(accessToken);
  }

  signOut(): Promise<AuthSessionSummary> {
    return this.api.signOut();
  }
}

export class ProfileClient {
  private readonly api: ProductCloudApiClient;

  constructor(configService: AppConfigService, secretManager: SecretManager) {
    this.api = new ProductCloudApiClient(configService, secretManager);
  }

  getSummary(): Promise<UserProfileSummary> {
    return this.api.getProfile();
  }
}

export class CreditClient {
  private readonly api: ProductCloudApiClient;

  constructor(configService: AppConfigService, secretManager: SecretManager) {
    this.api = new ProductCloudApiClient(configService, secretManager);
  }

  getSummary(): Promise<CreditSummary> {
    return this.api.getCredits();
  }
}

export class OpenClawDailyReportClient {
  private readonly configService: AppConfigService;
  private readonly secretManager: SecretManager;
  private readonly httpClient = new HttpJsonClient();

  constructor(configService: AppConfigService, secretManager: SecretManager) {
    this.configService = configService;
    this.secretManager = secretManager;
  }

  async submit(payload: Omit<OpenClawDailyReportPayload, "api_key">): Promise<OpenClawDailyReportResponse> {
    const config = await this.configService.load();
    const { baseUrl } = getRuntimeCloudApiTarget(config);
    const apiKey = (await this.secretManager.getApiKey())?.trim();

    if (!baseUrl) {
      throw new Error("OpenClaw 运行时云端地址未配置。");
    }

    if (!apiKey) {
      throw new Error("请先绑定 OpenClaw employee API Key。");
    }

    const url = new URL(`${baseUrl}/openclaw-daily-report`);
    const body = await this.httpClient.request(url, {
      method: "POST",
      body: {
        api_key: apiKey,
        ...payload
      }
    });

    let response: {
      ok?: boolean;
      summary_id?: string;
      employee_id?: string;
      summary_date?: string;
    };

    try {
      response = JSON.parse(body) as {
        ok?: boolean;
        summary_id?: string;
        employee_id?: string;
        summary_date?: string;
      };
    } catch {
      throw new Error("OpenClaw 日报接口返回了无效 JSON。");
    }

    return {
      ok: response.ok !== false,
      summaryId: response.summary_id,
      employeeId: response.employee_id,
      summaryDate: response.summary_date ?? payload.summary_date
    };
  }
}
export class ModelConfigClient {
  private readonly api: ProductCloudApiClient;

  constructor(configService: AppConfigService, secretManager: SecretManager) {
    this.api = new ProductCloudApiClient(configService, secretManager);
  }

  getSummary(): Promise<ModelConfigSummary> {
    return this.api.getModelConfig();
  }
}












