import http from "node:http";
import https from "node:https";
import os from "node:os";
import type {
  RuntimeCloudEventType,
  RuntimeHeartbeatBillingSummary,
  RuntimeTelemetryStatus
} from "@qjclaw/shared-types";
import type { RuntimeManager } from "@qjclaw/runtime-manager";
import { getRuntimeCloudApiTarget, type AppConfigService } from "./app-config.js";
import type { OpenClawConfigClient } from "./cloud-api.js";
import type { SecretManager } from "./secrets.js";

interface RuntimeCloudRequestContext {
  apiKey: string;
  url: URL;
}

interface RuntimeCloudEvent {
  event_type: RuntimeCloudEventType;
  data?: Record<string, unknown>;
  conversation_id?: string;
  occurred_at: string;
}

export type RuntimeCloudActivityEvent =
  | {
      type: "message_received";
      occurredAt: string;
      sessionId: string;
      prompt: string;
      skillId?: string;
    }
  | {
      type: "message_sent";
      occurredAt: string;
      sessionId: string;
      replyContent: string;
      modelId?: string;
      skillId?: string;
    }
  | {
      type: "error";
      occurredAt: string;
      errorType: string;
      message: string;
      modelId?: string;
      sessionId?: string;
    }
  | {
      type: "heartbeat";
      occurredAt: string;
      ok: boolean;
      status?: string;
      heartbeatAt?: string;
      billing?: RuntimeHeartbeatBillingSummary;
    };

type RuntimeCloudActivityListener = (event: RuntimeCloudActivityEvent) => Promise<void> | void;

interface OpenClawHeartbeatResponse {
  ok?: boolean;
  status?: string;
  heartbeat_at?: string;
  billing?: {
    success?: boolean;
    token_delta?: number;
    credits_deducted?: number;
    balance_before?: number;
    balance_after?: number;
  };
}

interface OpenClawEventFlushResponse {
  ok?: boolean;
  accepted?: number;
}

interface RuntimeCloudSupervisorOptions {
  appVersion: string;
  configService: AppConfigService;
  runtimeCloudClient: OpenClawConfigClient;
  runtimeManager: RuntimeManager;
  secretManager: SecretManager;
}

const DEFAULT_HEARTBEAT_INTERVAL_MS = 30000;
const DEFAULT_CONFIG_SYNC_INTERVAL_MS = 4 * 60 * 60 * 1000;
const DEFAULT_EVENT_FLUSH_INTERVAL_MS = 10000;
const DEFAULT_EVENT_BATCH_SIZE = 20;
const MAX_EVENT_BATCH_SIZE = 100;
const MAX_EVENT_QUEUE_SIZE = 500;

function parseInterval(name: string, fallback: number): number {
  const raw = process.env[name];
  const parsed = raw ? Number.parseInt(raw, 10) : Number.NaN;
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

class RuntimeCloudHttpClient {
  async postJson<T>(url: URL, body: unknown): Promise<T> {
    const client = url.protocol === "https:" ? https : http;

    const raw = await new Promise<string>((resolve, reject) => {
      const request = client.request(url, {
        method: "POST",
        headers: {
          Accept: "application/json",
          "Content-Type": "application/json; charset=utf-8"
        }
      }, (response) => {
        let payload = "";
        response.setEncoding("utf8");
        response.on("data", (chunk) => {
          payload += chunk;
        });
        response.on("end", () => {
          const status = response.statusCode ?? 500;
          if (status < 200 || status >= 300) {
            try {
              const parsed = JSON.parse(payload) as { message?: string; error?: string };
              reject(new Error(parsed.message ?? parsed.error ?? `Runtime cloud request failed with status ${status}.`));
            } catch {
              reject(new Error(`Runtime cloud request failed with status ${status}.`));
            }
            return;
          }
          resolve(payload);
        });
      });

      request.on("error", (error) => reject(new Error(`Runtime cloud request failed: ${error.message}`)));
      request.write(JSON.stringify(body));
      request.end();
    });

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

class RuntimeCloudApiBase {
  protected readonly configService: AppConfigService;
  protected readonly runtimeCloudClient: OpenClawConfigClient;
  protected readonly secretManager: SecretManager;
  protected readonly httpClient = new RuntimeCloudHttpClient();

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

  protected async resolveRequestContext(endpoint: "heartbeat" | "events"): Promise<RuntimeCloudRequestContext> {
    const config = await this.configService.load();
    const { baseUrl } = getRuntimeCloudApiTarget(config);
    const apiKey = (await this.secretManager.getApiKey())?.trim();
    const runtimeCloudStatus = await this.runtimeCloudClient.getStatus();

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

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

    const preferredUrl = endpoint === "heartbeat"
      ? runtimeCloudStatus.config?.heartbeatUrl
      : runtimeCloudStatus.config?.eventsUrl;
    const fallbackPath = endpoint === "heartbeat" ? "/openclaw-heartbeat" : "/openclaw-employee-events";

    return {
      apiKey,
      url: new URL(preferredUrl ?? `${baseUrl}${fallbackPath}`)
    };
  }
}

class OpenClawHeartbeatClient extends RuntimeCloudApiBase {
  async sendHeartbeat(stats?: Record<string, number>): Promise<{
    ok: boolean;
    status?: string;
    heartbeatAt?: string;
    billing?: RuntimeHeartbeatBillingSummary;
  }> {
    const { apiKey, url } = await this.resolveRequestContext("heartbeat");
    const response = await this.httpClient.postJson<OpenClawHeartbeatResponse>(url, {
      api_key: apiKey,
      ...(stats && Object.keys(stats).length > 0 ? { stats } : {})
    });

    return {
      ok: response.ok !== false,
      status: response.status,
      heartbeatAt: response.heartbeat_at,
      billing: response.billing
        ? {
            success: response.billing.success !== false,
            tokenDelta: response.billing.token_delta,
            creditsDeducted: response.billing.credits_deducted,
            balanceBefore: response.billing.balance_before,
            balanceAfter: response.billing.balance_after
          }
        : undefined
    };
  }
}

class OpenClawEventClient extends RuntimeCloudApiBase {
  async flush(events: RuntimeCloudEvent[]): Promise<{ accepted: number }> {
    if (events.length === 0) {
      return { accepted: 0 };
    }

    const { apiKey, url } = await this.resolveRequestContext("events");
    const response = await this.httpClient.postJson<OpenClawEventFlushResponse>(url, {
      api_key: apiKey,
      events
    });

    return {
      accepted: typeof response.accepted === "number" ? response.accepted : events.length
    };
  }
}

export class RuntimeCloudSupervisor {
  private readonly appVersion: string;
  private readonly eventClient: OpenClawEventClient;
  private readonly heartbeatClient: OpenClawHeartbeatClient;
  private readonly runtimeCloudClient: OpenClawConfigClient;
  private readonly runtimeManager: RuntimeManager;
  private readonly heartbeatIntervalMs: number;
  private readonly configSyncIntervalMs: number;
  private readonly eventFlushIntervalMs: number;
  private readonly eventBatchSize: number;
  private readonly telemetry: RuntimeTelemetryStatus;
  private readonly activeConversationIds = new Set<string>();
  private readonly queue: RuntimeCloudEvent[] = [];
  private readonly activityListeners = new Set<RuntimeCloudActivityListener>();
  private heartbeatTimer?: NodeJS.Timeout;
  private configSyncTimer?: NodeJS.Timeout;
  private eventFlushTimer?: NodeJS.Timeout;
  private heartbeatInFlight = false;
  private configSyncInFlight = false;
  private eventFlushInFlight = false;

  constructor(options: RuntimeCloudSupervisorOptions) {
    this.appVersion = options.appVersion;
    this.runtimeCloudClient = options.runtimeCloudClient;
    this.runtimeManager = options.runtimeManager;
    this.heartbeatClient = new OpenClawHeartbeatClient(options.configService, options.runtimeCloudClient, options.secretManager);
    this.eventClient = new OpenClawEventClient(options.configService, options.runtimeCloudClient, options.secretManager);
    this.heartbeatIntervalMs = parseInterval("QJCLAW_RUNTIME_CLOUD_HEARTBEAT_INTERVAL_MS", DEFAULT_HEARTBEAT_INTERVAL_MS);
    this.configSyncIntervalMs = parseInterval("QJCLAW_RUNTIME_CLOUD_CONFIG_SYNC_INTERVAL_MS", DEFAULT_CONFIG_SYNC_INTERVAL_MS);
    this.eventFlushIntervalMs = parseInterval("QJCLAW_RUNTIME_CLOUD_EVENT_FLUSH_INTERVAL_MS", DEFAULT_EVENT_FLUSH_INTERVAL_MS);
    this.eventBatchSize = Math.min(MAX_EVENT_BATCH_SIZE, parseInterval("QJCLAW_RUNTIME_CLOUD_EVENT_BATCH_SIZE", DEFAULT_EVENT_BATCH_SIZE));
    this.telemetry = {
      state: "idle",
      heartbeatIntervalMs: this.heartbeatIntervalMs,
      configSyncIntervalMs: this.configSyncIntervalMs,
      eventFlushIntervalMs: this.eventFlushIntervalMs,
      eventBatchSize: this.eventBatchSize,
      queuedEventCount: 0,
      droppedEventCount: 0,
      messageCount: 0,
      activeConversationCount: 0,
      errorCount: 0,
      configSyncInFlight: false,
      configSyncSuccessCount: 0,
      heartbeatSuccessCount: 0,
      lastEventTypes: [],
      totalAcceptedEventCount: 0
    };
  }

  getStatus(): RuntimeTelemetryStatus {
    return {
      ...this.telemetry,
      lastEventTypes: [...this.telemetry.lastEventTypes],
      lastHeartbeatBilling: this.telemetry.lastHeartbeatBilling ? { ...this.telemetry.lastHeartbeatBilling } : undefined
    };
  }

  onActivity(listener: RuntimeCloudActivityListener): () => void {
    this.activityListeners.add(listener);
    return () => {
      this.activityListeners.delete(listener);
    };
  }

  async start(): Promise<RuntimeTelemetryStatus> {
    if (this.telemetry.state === "running") {
      return this.getStatus();
    }

    const runtimeStatus = await this.runtimeManager.status();
    const runtimeCloudStatus = await this.runtimeCloudClient.getStatus();

    if (runtimeStatus.activeMode !== "bundled-runtime" || runtimeStatus.processState !== "running") {
      this.fail("Bundled runtime is not running, so runtime cloud telemetry cannot start.");
      return this.getStatus();
    }

    if (runtimeCloudStatus.state !== "ready" || !runtimeCloudStatus.config) {
      this.fail("Runtime cloud configuration is not ready, so telemetry cannot start.");
      return this.getStatus();
    }

    this.clearTimers();
    this.telemetry.state = "running";
    this.telemetry.startedAt = new Date().toISOString();
    this.telemetry.stoppedAt = undefined;
    this.telemetry.lastError = undefined;
    this.telemetry.currentConfigVersion = runtimeCloudStatus.config.configVersion;
    this.telemetry.lastConfigSyncError = undefined;
    this.telemetry.lastHeartbeatError = undefined;
    this.telemetry.lastEventFlushError = undefined;

    this.enqueueEvent("startup", {
      version: this.appVersion,
      deployment_type: runtimeCloudStatus.config.deploymentType ?? "local",
      os_info: `${process.platform}/${process.arch}/${os.release()}`
    });

    await this.runHeartbeat();
    await this.flushEvents(true);

    this.heartbeatTimer = setInterval(() => {
      void this.runHeartbeat();
    }, this.heartbeatIntervalMs);
    this.configSyncTimer = setInterval(() => {
      void this.runConfigSync();
    }, this.configSyncIntervalMs);
    this.eventFlushTimer = setInterval(() => {
      void this.flushEvents();
    }, this.eventFlushIntervalMs);

    return this.getStatus();
  }

  async stop(reason = "desktop-app-stop"): Promise<RuntimeTelemetryStatus> {
    const wasRunning = this.telemetry.state === "running";
    this.clearTimers();

    if (wasRunning) {
      this.enqueueEvent("shutdown", {
        reason,
        uptime_seconds: this.telemetry.startedAt
          ? Math.max(0, Math.round((Date.now() - Date.parse(this.telemetry.startedAt)) / 1000))
          : 0
      });
      await this.flushEvents(true);
    }

    this.telemetry.state = "stopped";
    this.telemetry.stoppedAt = new Date().toISOString();
    return this.getStatus();
  }

  noteSessions(sessionIds: string[]): void {
    this.activeConversationIds.clear();
    for (const sessionId of sessionIds) {
      if (typeof sessionId === "string" && sessionId.trim()) {
        this.activeConversationIds.add(sessionId);
      }
    }
    this.telemetry.activeConversationCount = this.activeConversationIds.size;
  }

  noteMessageReceived(sessionId: string, prompt: string, skillId?: string): void {
    this.emitActivity({
      type: "message_received",
      occurredAt: new Date().toISOString(),
      sessionId,
      prompt,
      skillId
    });

    if (this.telemetry.state !== "running") {
      return;
    }

    this.touchConversation(sessionId);
    this.telemetry.messageCount += 1;
    this.enqueueEvent("message_received", {
      from: "desktop-user",
      content_length: prompt.length,
      content_preview: prompt.slice(0, 120),
      ...(skillId ? { skill_id: skillId } : {})
    }, sessionId);
  }

  noteMessageSent(sessionId: string, replyContent: string, modelId?: string, skillId?: string): void {
    this.emitActivity({
      type: "message_sent",
      occurredAt: new Date().toISOString(),
      sessionId,
      replyContent,
      modelId,
      skillId
    });

    if (this.telemetry.state !== "running") {
      return;
    }

    this.touchConversation(sessionId);
    this.telemetry.messageCount += 1;
    this.enqueueEvent("message_sent", {
      content_length: replyContent.length,
      ...(modelId ? { model_id: modelId } : {}),
      ...(skillId ? { skill_id: skillId } : {})
    }, sessionId);
  }

  noteError(errorType: string, message: string, options?: { emitEvent?: boolean; modelId?: string; sessionId?: string }): void {
    this.telemetry.errorCount += 1;
    this.telemetry.lastError = message;
    this.emitActivity({
      type: "error",
      occurredAt: new Date().toISOString(),
      errorType,
      message,
      modelId: options?.modelId,
      sessionId: options?.sessionId
    });
    if (options?.emitEvent === false || this.telemetry.state !== "running") {
      return;
    }

    this.enqueueEvent("error", {
      error_type: errorType,
      message,
      ...(options?.modelId ? { model_id: options.modelId } : {})
    }, options?.sessionId);
  }

  private touchConversation(sessionId: string): void {
    if (!sessionId) {
      return;
    }
    this.activeConversationIds.add(sessionId);
    this.telemetry.activeConversationCount = this.activeConversationIds.size;
  }

  private emitActivity(event: RuntimeCloudActivityEvent): void {
    for (const listener of this.activityListeners) {
      try {
        Promise.resolve(listener(event)).catch(() => undefined);
      } catch {
        // Keep runtime telemetry usable even if side observers fail.
      }
    }
  }

  private enqueueEvent(eventType: RuntimeCloudEventType, data?: Record<string, unknown>, conversationId?: string): void {
    if (this.queue.length >= MAX_EVENT_QUEUE_SIZE) {
      this.queue.shift();
      this.telemetry.droppedEventCount += 1;
    }

    this.queue.push({
      event_type: eventType,
      ...(data ? { data } : {}),
      ...(conversationId ? { conversation_id: conversationId } : {}),
      occurred_at: new Date().toISOString()
    });
    this.telemetry.queuedEventCount = this.queue.length;

    if (this.queue.length >= this.eventBatchSize) {
      void this.flushEvents();
    }
  }

  private async runHeartbeat(): Promise<void> {
    if (this.heartbeatInFlight || this.telemetry.state !== "running") {
      return;
    }

    const runtimeStatus = await this.runtimeManager.status();
    if (runtimeStatus.processState !== "running" || runtimeStatus.activeMode !== "bundled-runtime") {
      this.handleRuntimeUnavailable("Bundled runtime stopped before heartbeat could complete.");
      return;
    }

    this.heartbeatInFlight = true;
    try {
      const response = await this.heartbeatClient.sendHeartbeat({
        message_count: this.telemetry.messageCount,
        active_conversations: this.telemetry.activeConversationCount,
        error_count: this.telemetry.errorCount
      });
      this.telemetry.lastHeartbeatAt = response.heartbeatAt ?? new Date().toISOString();
      this.telemetry.lastHeartbeatOk = response.ok;
      this.telemetry.lastHeartbeatStatus = response.status;
      this.telemetry.lastHeartbeatBilling = response.billing;
      this.telemetry.lastHeartbeatError = undefined;
      this.telemetry.lastError = undefined;
      this.telemetry.heartbeatSuccessCount += 1;
      this.emitActivity({
        type: "heartbeat",
        occurredAt: new Date().toISOString(),
        ok: response.ok,
        status: response.status,
        heartbeatAt: response.heartbeatAt,
        billing: response.billing
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.telemetry.lastHeartbeatAt = new Date().toISOString();
      this.telemetry.lastHeartbeatOk = false;
      this.telemetry.lastHeartbeatError = message;
      this.telemetry.lastError = message;
      this.noteError("heartbeat_failed", message, { emitEvent: true });
    } finally {
      this.heartbeatInFlight = false;
    }
  }

  private async runConfigSync(): Promise<void> {
    if (this.configSyncInFlight || this.telemetry.state !== "running") {
      return;
    }

    const runtimeStatus = await this.runtimeManager.status();
    if (runtimeStatus.processState !== "running" || runtimeStatus.activeMode !== "bundled-runtime") {
      this.handleRuntimeUnavailable("Bundled runtime stopped before config sync could complete.");
      return;
    }

    this.configSyncInFlight = true;
    this.telemetry.configSyncInFlight = true;
    const startedAt = new Date().toISOString();
    const previousVersion = this.telemetry.currentConfigVersion ?? (await this.runtimeCloudClient.getStatus()).config?.configVersion;

    try {
      const status = await this.runtimeCloudClient.fetchConfig("sync");
      const nextVersion = status.config?.configVersion ?? previousVersion;
      const changed = Boolean(previousVersion && nextVersion && previousVersion !== nextVersion);
      this.telemetry.currentConfigVersion = nextVersion;
      this.telemetry.lastConfigSyncAt = startedAt;
      this.telemetry.lastConfigSyncChanged = changed;
      this.telemetry.lastConfigSyncError = undefined;
      this.telemetry.lastError = undefined;
      this.telemetry.configSyncSuccessCount += 1;

      if (changed) {
        await this.runtimeManager.syncManagedConfig("sync");
        this.enqueueEvent("config_updated", {
          old_version: previousVersion,
          new_version: nextVersion
        });
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.telemetry.lastConfigSyncAt = startedAt;
      this.telemetry.lastConfigSyncChanged = false;
      this.telemetry.lastConfigSyncError = message;
      this.telemetry.lastError = message;
      this.noteError("config_sync_failed", message, { emitEvent: true });
    } finally {
      this.configSyncInFlight = false;
      this.telemetry.configSyncInFlight = false;
    }
  }

  private async flushEvents(force = false): Promise<void> {
    if (this.eventFlushInFlight || this.queue.length === 0) {
      return;
    }
    if (this.telemetry.state !== "running" && !force) {
      return;
    }

    this.eventFlushInFlight = true;
    try {
      while (this.queue.length > 0) {
        const batch = this.queue.slice(0, Math.min(MAX_EVENT_BATCH_SIZE, this.eventBatchSize));
        const eventTypes = batch.map((event) => event.event_type);
        const response = await this.eventClient.flush(batch);
        const accepted = typeof response.accepted === "number" ? Math.max(0, Math.min(batch.length, response.accepted)) : batch.length;
        this.telemetry.lastEventFlushAt = new Date().toISOString();
        this.telemetry.lastAcceptedEventCount = accepted;
        this.telemetry.lastEventTypes = eventTypes;
        this.telemetry.lastEventFlushError = undefined;
        this.telemetry.lastError = undefined;

        if (accepted <= 0) {
          break;
        }

        this.queue.splice(0, accepted);
        this.telemetry.queuedEventCount = this.queue.length;
        this.telemetry.totalAcceptedEventCount += accepted;

        if (!force) {
          break;
        }
      }
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      this.telemetry.lastEventFlushError = message;
      this.telemetry.lastError = message;
      this.noteError("event_flush_failed", message, { emitEvent: false });
    } finally {
      this.eventFlushInFlight = false;
    }
  }

  private handleRuntimeUnavailable(message: string): void {
    this.clearTimers();
    this.telemetry.state = "stopped";
    this.telemetry.stoppedAt = new Date().toISOString();
    this.telemetry.lastError = message;
  }

  private fail(message: string): void {
    this.clearTimers();
    this.telemetry.state = "error";
    this.telemetry.lastError = message;
    this.telemetry.stoppedAt = new Date().toISOString();
  }

  private clearTimers(): void {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = undefined;
    }
    if (this.configSyncTimer) {
      clearInterval(this.configSyncTimer);
      this.configSyncTimer = undefined;
    }
    if (this.eventFlushTimer) {
      clearInterval(this.eventFlushTimer);
      this.eventFlushTimer = undefined;
    }
  }
}



