import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { execFile } from 'child_process';
import { InjectRepository } from '@nestjs/typeorm';
import { randomUUID } from 'crypto';
import { mkdir, rm, writeFile } from 'fs/promises';
import net from 'net';
import path from 'path';
import { Repository } from 'typeorm';
import { promisify } from 'util';
import { CreateMonitorTargetDto, UpdateMonitorTargetDto } from './monitoring.dto';
import { MonitorAgentEntity } from './monitor-agent.entity';
import { MonitorLogEventEntity } from './monitor-log-event.entity';
import { MonitorRunEntity } from './monitor-run.entity';
import { MonitorTargetEntity, MonitorTargetType } from './monitor-target.entity';
import { MonitorExecutionResult, MonitorStatus, MonitorTargetConfig } from './monitoring.types';
import { LogAnalysisService } from './log-analysis.service';
import { MonitorAlertService } from './monitor-alert.service';
import { IncidentsService } from '../incidents/incidents.service';

const execFileAsync = promisify(execFile);
type RunFilter = {
  targetId?: string;
  status?: 'queued' | 'running' | 'success' | 'failed' | 'partial';
  limit?: number;
  latestPerTarget?: boolean;
};

@Injectable()
export class MonitoringService {
  constructor(
    @InjectRepository(MonitorAgentEntity) private readonly agentRepository: Repository<MonitorAgentEntity>,
    @InjectRepository(MonitorTargetEntity) private readonly targetRepository: Repository<MonitorTargetEntity>,
    @InjectRepository(MonitorRunEntity) private readonly runRepository: Repository<MonitorRunEntity>,
    @InjectRepository(MonitorLogEventEntity) private readonly logEventRepository: Repository<MonitorLogEventEntity>,
    private readonly logAnalysisService: LogAnalysisService,
    private readonly monitorAlertService: MonitorAlertService,
    private readonly incidentsService: IncidentsService,
  ) {}

  async getSummary() {
    const [agentCount, targetCount, runCount, exceptionCount] = await Promise.all([
      this.agentRepository.count(),
      this.targetRepository.count(),
      this.runRepository.count(),
      this.runRepository.createQueryBuilder('run').where('run.status IN (:...statuses)', { statuses: ['failed', 'partial'] }).getCount(),
    ]);
    return { agentCount, targetCount, runCount, exceptionCount };
  }

  async getAgents() {
    const list = await this.agentRepository.find({ order: { createdAt: 'DESC' } });
    return { list: list.map((agent) => ({ ...agent, availability: this.resolveAgentAvailability(agent) })), total: list.length };
  }

  async createAgent(dto: { name: string; agentKey: string; host?: string; platform: 'linux' | 'windows' }) {
    if (await this.agentRepository.findOneBy({ agentKey: dto.agentKey })) throw new BadRequestException('agent_key 已存在，请更换后重试');
    const agent = this.agentRepository.create({ ...dto, host: dto.host?.trim() || null, status: 'disabled', monitoringEnabled: false, logIngestEnabled: false, proactivePushEnabled: false, version: null, lastHeartbeatAt: null, lastHeartbeatMetaJson: null, lastVerifyAt: null, lastVerifyStatus: null, lastVerifyMessage: null });
    await this.agentRepository.save(agent);
    return { agentId: agent.id, status: agent.status };
  }

  async updateAgent(id: string, dto: { name: string; agentKey: string; host?: string; platform: 'linux' | 'windows' }) {
    const agent = await this.mustFindAgent(id);
    const duplicate = await this.agentRepository.findOneBy({ agentKey: dto.agentKey });
    if (duplicate && duplicate.id !== id) throw new BadRequestException('agent_key 已存在，请更换后重试');
    Object.assign(agent, { ...dto, host: dto.host?.trim() || null });
    await this.agentRepository.save(agent);
    return { agentId: agent.id, status: agent.status };
  }

  async updateAgentStatus(id: string, status: 'enabled' | 'disabled') {
    const agent = await this.mustFindAgent(id);
    agent.status = status;
    await this.agentRepository.save(agent);
    return { ...agent, availability: this.resolveAgentAvailability(agent) };
  }

  async updateAgentFeatures(id: string, dto: { monitoringEnabled: boolean; logIngestEnabled: boolean; proactivePushEnabled: boolean }) {
    const agent = await this.mustFindAgent(id);
    Object.assign(agent, dto);
    await this.agentRepository.save(agent);
    return { ...agent, availability: this.resolveAgentAvailability(agent) };
  }

  async verifyAgent(id: string) {
    const agent = await this.mustFindAgent(id);
    const availability = this.resolveAgentAvailability(agent);
    agent.lastVerifyAt = new Date();
    agent.lastVerifyStatus = availability.state;
    agent.lastVerifyMessage = availability.message;
    await this.agentRepository.save(agent);
    return { ...agent, availability };
  }

  async buildAgentPackage(
    id: string,
    options?: { targetOs?: 'linux' | 'windows'; targetArch?: 'amd64' | 'arm64' | 'loong64'; requestApiBase?: string },
  ) {
    const agent = await this.mustFindAgent(id);
    const safeKey = agent.agentKey.replace(/[^a-zA-Z0-9-_]/g, '-');
    const targetOs = options?.targetOs ?? (agent.platform === 'windows' ? 'windows' : 'linux');
    const targetArch = options?.targetArch ?? 'amd64';
    const packageName = `${safeKey}-${targetOs}-${targetArch}`;
    const packageRoot = path.join(process.cwd(), 'storage', 'agent-packages', packageName);
    const zipPath = path.join(process.cwd(), 'storage', 'agent-packages', `${packageName}.zip`);
    await rm(packageRoot, { recursive: true, force: true });
    await rm(zipPath, { force: true });
    await mkdir(packageRoot, { recursive: true });
    const apiBaseUrl = options?.requestApiBase ?? process.env.APP_API_BASE_URL ?? process.env.NEXT_PUBLIC_API_BASE_URL ?? 'http://localhost:3001';
    const token = this.readRequiredToken(
      'MONITORING_AGENT_TOKEN',
      'Agent 认证',
    );
    await writeFile(path.join(packageRoot, 'agent-config.env'), [
      '# SentAI 宿主机 Agent 配置',
      '# 说明：这里不配置具体监测哪些程序，具体监测目标统一在平台 Web 的 /monitors 页面维护。',
      '# 平台地址：Agent 通过它回连平台、发送心跳、领取待执行监测任务。',
      `MONITORING_API_BASE_URL=${apiBaseUrl}`,
      '# Agent 唯一标识：必须与平台 /agents 页面里该节点的 agentKey 一致。',
      `MONITORING_AGENT_KEY=${agent.agentKey}`,
      '# Agent 认证令牌：用于让平台确认“这台 Agent 是合法节点”。它不是业务程序密码。',
      `MONITORING_AGENT_TOKEN=${token}`,
      '# 轮询间隔，单位毫秒。表示 Agent 多久向平台取一次待执行任务。',
      'MONITORING_AGENT_POLL_MS=10000',
    ].join('\n'), 'utf8');
    let readme = '';
    if (targetOs === 'linux') {
      await this.buildNativeLinuxAgentBinary(packageRoot, targetArch);
      await writeFile(path.join(packageRoot, 'install.sh'), this.buildNativeLinuxInstallScript(agent.agentKey), 'utf8');
      await writeFile(path.join(packageRoot, 'uninstall.sh'), this.buildNativeLinuxUninstallScript(), 'utf8');
      await writeFile(path.join(packageRoot, 'sentai-host-agent.service'), this.buildNativeLinuxSystemdService(), 'utf8');
      readme = this.buildNativeAgentReadme(agent, apiBaseUrl, targetArch);
    } else {
      await writeFile(path.join(packageRoot, 'sentai-host-agent.js'), this.buildLinuxAgentScript(), 'utf8');
      await writeFile(path.join(packageRoot, 'install.sh'), this.buildLinuxInstallScript(agent.agentKey), 'utf8');
      await writeFile(path.join(packageRoot, 'sentai-host-agent.service'), this.buildLinuxSystemdService(), 'utf8');
      readme = this.buildAgentReadme(agent, apiBaseUrl);
    }
    await writeFile(path.join(packageRoot, 'README.md'), readme, 'utf8');
    await execFileAsync('powershell', ['-NoProfile', '-Command', `Compress-Archive -Path '${packageRoot}\\*' -DestinationPath '${zipPath}' -Force`]);
    return { agentId: agent.id, fileName: `${packageName}.zip`, downloadUrl: `/agent-packages/${packageName}.zip`, readmePreview: readme };
  }

  async getTargets(filters: { keyword?: string; status?: 'enabled' | 'disabled' }) {
    const query = this.targetRepository.createQueryBuilder('target');
    if (filters.keyword) query.andWhere('(target.name LIKE :keyword OR target.endpoint LIKE :keyword OR target.serviceKey LIKE :keyword)', { keyword: `%${filters.keyword}%` });
    if (filters.status) query.andWhere('target.status = :status', { status: filters.status });
    const [list, total] = await query.orderBy('target.createdAt', 'DESC').getManyAndCount();
    return { list, total };
  }

  async createTarget(dto: CreateMonitorTargetDto) {
    this.parseConfig(dto.configJson, dto.secretJson);
    const target = this.targetRepository.create({ ...dto, status: 'enabled', secretJson: dto.secretJson ?? null, lastRunAt: null });
    await this.targetRepository.save(target);
    return { targetId: target.id, status: target.status };
  }

  async updateTarget(id: string, dto: UpdateMonitorTargetDto) {
    const target = await this.targetRepository.findOneBy({ id });
    if (!target) throw new NotFoundException('监测目标不存在');
    this.parseConfig(dto.configJson, dto.secretJson);
    Object.assign(target, { ...dto, secretJson: dto.secretJson ?? null });
    await this.targetRepository.save(target);
    return { targetId: target.id, status: target.status };
  }

  async updateTargetStatus(id: string, status: 'enabled' | 'disabled') {
    const target = await this.targetRepository.findOneBy({ id });
    if (!target) throw new NotFoundException('监测目标不存在');
    target.status = status;
    await this.targetRepository.save(target);
    return this.targetRepository.findOneByOrFail({ id });
  }

  async deleteTarget(id: string) {
    const target = await this.targetRepository.findOneBy({ id });
    if (!target) throw new NotFoundException('监测目标不存在');
    await this.runRepository.delete({ targetId: id });
    await this.targetRepository.delete({ id });
    return { deleted: true, targetId: id };
  }

  async getRuns(filters: RunFilter) {
    const query = this.runRepository.createQueryBuilder('run').leftJoinAndSelect('run.target', 'target');
    if (filters.targetId) query.andWhere('run.targetId = :targetId', { targetId: filters.targetId });
    if (filters.status) query.andWhere('run.status = :status', { status: filters.status });
    const take = Math.min(Math.max(filters.limit ?? 30, 1), 200);
    const [list, total] = await query.orderBy('run.startedAt', 'DESC').take(filters.latestPerTarget ? 200 : take).getManyAndCount();

    if (!filters.latestPerTarget) return { list, total };

    const latestByTarget = new Map<string, MonitorRunEntity>();
    for (const run of list) {
      if (!latestByTarget.has(run.targetId)) latestByTarget.set(run.targetId, run);
    }
    return { list: Array.from(latestByTarget.values()).slice(0, take), total };
  }

  async getRunDetail(runId: string) {
    const run = await this.runRepository.findOne({ where: { id: runId }, relations: { target: true } });
    if (!run) throw new NotFoundException('监测记录不存在');
    const config = this.parseConfig(run.target.configJson, run.target.secretJson);
    const recentLogs = await this.getRecentLogs(run.target.serviceKey, config.nodeKey);
    const analysis = await this.logAnalysisService.analyzeRun(
      run,
      config,
      async () => recentLogs,
    );
    return {
      run,
      detail: {
        metrics: this.parseJson(run.metricsJson),
        details: this.parseJson(run.detailsJson),
        logExcerpt:
          run.logExcerpt ??
          this.renderLogExcerptFromEvents(recentLogs),
        recentLogs,
        analysis,
        resultJsonUrl: run.resultJsonUrl,
      },
    };
  }

  async deleteRun(runId: string) {
    const run = await this.runRepository.findOneBy({ id: runId });
    if (!run) throw new NotFoundException('监测记录不存在');
    await this.runRepository.delete({ id: runId });
    return { deleted: true, runId };
  }

  async deleteRuns(runIds: string[]) {
    const normalizedIds = Array.from(
      new Set(
        (runIds ?? [])
          .map((item) => String(item).trim())
          .filter(Boolean),
      ),
    );
    if (!normalizedIds.length) {
      throw new BadRequestException('至少选择一条执行记录。');
    }

    const existingRuns = await this.runRepository.find({
      where: normalizedIds.map((id) => ({ id })),
      select: { id: true },
    });
    if (!existingRuns.length) {
      throw new NotFoundException('没有找到可删除的执行记录。');
    }

    await this.runRepository.delete(existingRuns.map((item) => item.id));
    return { deleted: existingRuns.length, runIds: existingRuns.map((item) => item.id) };
  }

  async requeueRun(runId: string) {
    const run = await this.runRepository.findOne({
      where: { id: runId },
      relations: { target: true },
    });
    if (!run) throw new NotFoundException('监测记录不存在');
    if (run.status === 'success') {
      throw new BadRequestException('成功记录不支持直接重置为排队。请使用“立即执行”创建新的测试任务。');
    }

    Object.assign(run, {
      status: 'queued',
      errorTag: null,
      summary: '已手动重新入队，等待 Agent 重新领取执行。',
      failureReason: null,
      metricsJson: null,
      detailsJson: null,
      logExcerpt: null,
      resultJsonUrl: null,
      startedAt: new Date(),
      endedAt: null,
    });
    await this.runRepository.save(run);
    return { requeued: true, runId: run.id, status: run.status };
  }

  async clearQueuedRuns(filters: { targetId?: string; olderThanMinutes?: number }) {
    const query = this.runRepository.createQueryBuilder('run');
    query.where('run.status = :status', { status: 'queued' });
    if (filters.targetId) {
      query.andWhere('run.targetId = :targetId', { targetId: filters.targetId });
    }
    if (filters.olderThanMinutes && Number.isFinite(filters.olderThanMinutes)) {
      const cutoff = new Date(Date.now() - filters.olderThanMinutes * 60 * 1000);
      query.andWhere('run.createdAt <= :cutoff', { cutoff });
    }

    const queuedRuns = await query.getMany();
    if (!queuedRuns.length) return { cleared: 0 };
    await this.runRepository.delete(queuedRuns.map((item) => item.id));
    return { cleared: queuedRuns.length };
  }

  async requeueStaleRuns(filters: { targetId?: string; staleMinutes?: number }) {
    const staleMinutes = Number.isFinite(filters.staleMinutes)
      ? Math.max(filters.staleMinutes ?? 20, 1)
      : 20;
    const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000);

    const query = this.runRepository.createQueryBuilder('run');
    query.where('run.status = :status', { status: 'running' });
    query.andWhere('run.startedAt <= :cutoff', { cutoff });
    if (filters.targetId) {
      query.andWhere('run.targetId = :targetId', { targetId: filters.targetId });
    }

    const staleRuns = await query.getMany();
    for (const run of staleRuns) {
      Object.assign(run, {
        status: 'queued',
        errorTag: null,
        summary: '检测到长时间未完成，已自动回退为排队状态。',
        failureReason: null,
        metricsJson: null,
        detailsJson: null,
        logExcerpt: null,
        resultJsonUrl: null,
        startedAt: new Date(),
        endedAt: null,
      });
    }
    if (staleRuns.length) await this.runRepository.save(staleRuns);
    return { requeued: staleRuns.length, staleMinutes };
  }

  async ingestLogs(payload: unknown, ingestToken?: string) {
    const expectedToken = this.readRequiredToken(
      'MONITORING_INGEST_TOKEN',
      '日志接入',
    );
    if (ingestToken !== expectedToken) throw new BadRequestException('日志接入令牌无效');
    const records = (Array.isArray(payload) ? payload : payload && typeof payload === 'object' && Array.isArray((payload as { logs?: unknown[] }).logs) ? (payload as { logs: unknown[] }).logs : [payload]).map((item) => item as Record<string, unknown>);
    const events = records.map((record) => this.logEventRepository.create({ serviceKey: String(record.serviceKey ?? record.service_key ?? '').trim(), nodeKey: record.nodeKey ? String(record.nodeKey) : null, host: record.host ? String(record.host) : null, sourceType: (String(record.sourceType ?? 'vector') as 'vector' | 'fluent-bit' | 'manual'), sourceName: record.sourceName ? String(record.sourceName) : null, level: record.level ? String(record.level) : null, messageText: String(record.message ?? record.log ?? '').trim(), eventAt: new Date(String(record.timestamp ?? new Date().toISOString())), metaJson: JSON.stringify(record) }));
    await this.logEventRepository.save(events.filter((item) => item.serviceKey && item.messageText));
    return { ingested: events.length };
  }

  async getRecentLogs(serviceKey: string, nodeKey?: string) {
    return this.logEventRepository.find({ where: { serviceKey, ...(nodeKey ? { nodeKey } : {}) }, order: { eventAt: 'DESC' }, take: 20 });
  }

  async recordAgentHeartbeat(body: { agentKey: string; host?: string; platform?: string; version?: string; meta?: Record<string, unknown> }, agentToken?: string) {
    this.assertAgentToken(agentToken);
    const agent = await this.agentRepository.findOneBy({ agentKey: body.agentKey });
    if (!agent) throw new BadRequestException('Agent 尚未在平台注册，请先在 Web 端创建 Agent 节点');
    agent.host = body.host?.trim() || agent.host;
    agent.platform = body.platform?.trim() || agent.platform;
    agent.version = body.version?.trim() || agent.version;
    agent.lastHeartbeatAt = new Date();
    agent.lastHeartbeatMetaJson = body.meta ? JSON.stringify(body.meta) : agent.lastHeartbeatMetaJson;
    await this.agentRepository.save(agent);
    return { ok: true, control: { enabled: agent.status === 'enabled', monitoringEnabled: agent.monitoringEnabled, logIngestEnabled: agent.logIngestEnabled, proactivePushEnabled: agent.proactivePushEnabled, heartbeatIntervalMs: 10000 }, availability: this.resolveAgentAvailability(agent) };
  }

  async claimAgentJob(agentKey: string, agentToken?: string) {
    this.assertAgentToken(agentToken);
    const agent = await this.agentRepository.findOneBy({ agentKey });
    if (!agent || agent.status !== 'enabled' || !agent.monitoringEnabled) return { job: null };
    const queuedRuns = await this.runRepository.find({
      where: { status: 'queued' },
      relations: { target: true },
      order: { createdAt: 'ASC' },
      take: 200,
    });
    for (const run of queuedRuns) {
      if (run.target.status !== 'enabled') continue;
      const config = this.parseConfig(run.target.configJson, run.target.secretJson);
      if (config.collectionMode !== 'host-agent' || config.agentKey !== agentKey) continue;
      run.status = 'running';
      run.startedAt = new Date();
      await this.runRepository.save(run);
      return { job: { runId: run.id, traceId: run.traceId, targetId: run.targetId, targetName: run.target.name, serviceKey: run.target.serviceKey, type: run.target.type, endpoint: run.target.endpoint, config } };
    }
    return { job: null };
  }

  async submitAgentJobResult(runId: string, payload: { status: MonitorStatus; errorTag: string | null; summary: string; failureReason: string | null; metrics?: Record<string, unknown>; details?: Record<string, unknown>; logExcerpt?: string | null }, agentToken?: string) {
    this.assertAgentToken(agentToken);
    const run = await this.runRepository.findOne({ where: { id: runId }, relations: { target: true } });
    if (!run) throw new NotFoundException('监测记录不存在');
    const persisted = await this.persistMonitoringResult(run.target, run.traceId, { type: run.target.type, status: payload.status, errorTag: payload.errorTag, summary: payload.summary, failureReason: payload.failureReason, metrics: payload.metrics ?? {}, details: payload.details ?? {}, logExcerpt: payload.logExcerpt ?? null, resultJsonUrl: '' });
    Object.assign(run, { status: payload.status, errorTag: payload.errorTag, summary: payload.summary, failureReason: payload.failureReason, metricsJson: JSON.stringify(payload.metrics ?? {}), detailsJson: JSON.stringify(payload.details ?? {}), logExcerpt: payload.logExcerpt ?? null, resultJsonUrl: persisted.resultJsonUrl, endedAt: new Date() });
    await this.runRepository.save(run);
    run.target.lastRunAt = run.endedAt;
    await this.targetRepository.save(run.target);
    const incidentInput = await this.monitorAlertService.syncAlertsForRun(
      run,
      this.parseConfig(run.target.configJson, run.target.secretJson),
      async () => this.getRecentLogs(run.target.serviceKey, this.parseConfig(run.target.configJson, run.target.secretJson).nodeKey),
    );
    await this.incidentsService.syncCaseForRun(
      run,
      incidentInput.analysis,
      incidentInput.alerts,
    );
    return { ok: true, runId: run.id };
  }
  async executeTarget(
    targetId: string,
    options?: { replaceQueued?: boolean },
  ) {
    const target = await this.targetRepository.findOneBy({ id: targetId });
    if (!target) throw new NotFoundException('监测目标不存在');
    if (target.status !== 'enabled') {
      throw new BadRequestException('监测目标已停用，不能执行。');
    }
    const config = this.parseConfig(target.configJson, target.secretJson);
    let clearedQueuedCount = 0;

    if (options?.replaceQueued) {
      const queuedRuns = await this.runRepository.find({
        where: { targetId: target.id, status: 'queued' },
      });
      if (queuedRuns.length) {
        clearedQueuedCount = queuedRuns.length;
        await this.runRepository.delete(queuedRuns.map((item) => item.id));
      }
    }

    const traceId = randomUUID();
    const run = this.runRepository.create({ targetId: target.id, traceId, status: config.collectionMode === 'host-agent' ? 'queued' : 'running', errorTag: null, summary: null, failureReason: null, metricsJson: null, detailsJson: null, logExcerpt: null, resultJsonUrl: null, startedAt: new Date(), endedAt: null });
    await this.runRepository.save(run);
    if (config.collectionMode === 'host-agent') return { runId: run.id, traceId, status: run.status, errorTag: null, clearedQueuedCount };
    const result = await this.executeCheck(target, traceId, config);
    Object.assign(run, { status: result.status, errorTag: result.errorTag, summary: result.summary, failureReason: result.failureReason, metricsJson: JSON.stringify(result.metrics), detailsJson: JSON.stringify(result.details), logExcerpt: result.logExcerpt, resultJsonUrl: result.resultJsonUrl, endedAt: new Date() });
    await this.runRepository.save(run);
    target.lastRunAt = run.endedAt;
    await this.targetRepository.save(target);
    run.target = target;
    const incidentInput = await this.monitorAlertService.syncAlertsForRun(
      run,
      config,
      async () => this.getRecentLogs(target.serviceKey, config.nodeKey),
    );
    await this.incidentsService.syncCaseForRun(
      run as MonitorRunEntity & { target: MonitorTargetEntity },
      incidentInput.analysis,
      incidentInput.alerts,
    );
    return { runId: run.id, traceId, status: run.status, errorTag: run.errorTag, clearedQueuedCount };
  }

  private async executeCheck(target: MonitorTargetEntity, traceId: string, config: MonitorTargetConfig) {
    if (config.collectionMode === 'blackbox' && config.blackboxBaseUrl) {
      const body = await this.fetchText(config.blackboxBaseUrl, '/probe', { module: String(config.blackboxModule ?? 'http_2xx'), target: String(config.blackboxTarget ?? target.endpoint) });
      return this.persistMonitoringResult(target, traceId, { type: target.type, status: body.includes('probe_success 1') ? 'success' : 'failed', errorTag: body.includes('probe_success 1') ? null : 'BLACKBOX_PROBE_FAILED', summary: body.includes('probe_success 1') ? 'Blackbox 探测通过' : 'Blackbox 探测失败', failureReason: body.includes('probe_success 1') ? null : 'probe_success != 1', metrics: { samplePreview: body.split('\n').slice(0, 30) }, details: { module: config.blackboxModule ?? 'http_2xx', target: config.blackboxTarget ?? target.endpoint }, logExcerpt: null, resultJsonUrl: '' });
    }
    if (target.type === 'tcp') {
      const host = config.tcpHost ?? target.endpoint;
      const port = config.tcpPort ?? 0;
      if (!host || !port) throw new BadRequestException('TCP / Redis 监测需要 host 和 port');
      const success = await new Promise<boolean>((resolve) => { const socket = new net.Socket(); const done = (state: boolean) => { socket.destroy(); resolve(state); }; socket.setTimeout(config.timeoutMs ?? 5000); socket.once('connect', () => done(true)); socket.once('timeout', () => done(false)); socket.once('error', () => done(false)); socket.connect(port, host); });
      return this.persistMonitoringResult(target, traceId, { type: target.type, status: success ? 'success' : 'failed', errorTag: success ? null : 'TCP_CONNECT_FAILED', summary: success ? 'TCP 检测通过' : 'TCP 检测失败', failureReason: success ? null : `${host}:${String(port)} 无法连接`, metrics: { host, port }, details: { host, port }, logExcerpt: null, resultJsonUrl: '' });
    }
    if (target.type === 'redis') {
      const host = config.tcpHost ?? target.endpoint;
      const port = config.tcpPort ?? 6379;
      const responses = await this.runRedisCommand(host, port, [['PING'], ['INFO']], config.timeoutMs ?? 5000, config.authPassword);
      const ping = responses.find((item) => item.includes('PONG')) ?? '';
      const info = this.parseRedisInfo(responses[responses.length - 1] ?? '');
      const success = ping.includes('PONG');
      return this.persistMonitoringResult(target, traceId, { type: target.type, status: success ? 'success' : 'failed', errorTag: success ? null : 'REDIS_PING_FAILED', summary: success ? 'Redis 检测通过' : 'Redis 检测失败', failureReason: success ? null : 'PING 未返回 PONG', metrics: { role: info.role ?? null, usedMemory: info.used_memory ?? null, connectedClients: info.connected_clients ?? null, opsPerSec: info.instantaneous_ops_per_sec ?? null }, details: { host, port, infoPreview: Object.fromEntries(Object.entries(info).slice(0, 30)) }, logExcerpt: null, resultJsonUrl: '' });
    }
    if (target.type === 'rabbitmq') {
      const headers = this.basicHeaders(config);
      const overview = await this.fetchJson(target.endpoint, String(config.rabbitmqOverviewPath ?? '/api/overview'), headers);
      const queues = await this.fetchJson(target.endpoint, String(config.rabbitmqQueuesPath ?? '/api/queues'), headers);
      const nodes = await this.fetchJson(target.endpoint, String(config.rabbitmqNodesPath ?? '/api/nodes'), headers);
      return this.persistMonitoringResult(target, traceId, { type: target.type, status: 'success', errorTag: null, summary: 'RabbitMQ 管理接口检测通过', failureReason: null, metrics: { queueCount: Array.isArray(queues) ? queues.length : 0, nodeCount: Array.isArray(nodes) ? nodes.length : 0 }, details: { overview, queuePreview: Array.isArray(queues) ? queues.slice(0, 10) : [], nodePreview: Array.isArray(nodes) ? nodes.slice(0, 10) : [] }, logExcerpt: null, resultJsonUrl: '' });
    }
    if (target.type === 'elasticsearch') {
      const headers = this.basicHeaders(config);
      const health = await this.fetchJson(target.endpoint, String(config.elasticsearchHealthPath ?? '/_cluster/health'), headers);
      const nodes = await this.fetchJson(target.endpoint, String(config.elasticsearchNodesPath ?? '/_nodes/stats'), headers);
      const clusterStatus = String((health as Record<string, unknown>).status ?? 'unknown').toLowerCase();
      const success = clusterStatus === 'green' || clusterStatus === 'yellow';
      return this.persistMonitoringResult(target, traceId, { type: target.type, status: success ? 'success' : 'failed', errorTag: success ? null : 'ES_CLUSTER_UNHEALTHY', summary: success ? 'Elasticsearch 集群检测通过' : 'Elasticsearch 集群异常', failureReason: success ? null : `cluster status=${clusterStatus}`, metrics: { clusterStatus, nodeCount: (health as Record<string, unknown>).number_of_nodes ?? null }, details: { health, nodeKeys: (nodes as Record<string, unknown>).nodes ? Object.keys((nodes as Record<string, Record<string, unknown>>).nodes) : [] }, logExcerpt: null, resultJsonUrl: '' });
    }
    if (target.type === 'java-app') {
      const endpoint = new URL(String(config.javaHealthPath ?? '/actuator/health'), target.endpoint).toString();
      const response = await fetch(endpoint, { signal: AbortSignal.timeout(config.timeoutMs ?? 8000), headers: this.basicHeaders(config) });
      const text = await response.text();
      let jmxPreview: string[] = [];
      if (config.jmxExporterUrl) {
        try {
          const jmxMetrics = await this.fetchText(config.jmxExporterUrl, String(config.jmxMetricsPath ?? ''));
          jmxPreview = jmxMetrics.split('\n').slice(0, 30);
        } catch {
          jmxPreview = [];
        }
      }
      const success = response.ok;
      return this.persistMonitoringResult(target, traceId, { type: target.type, status: success ? 'success' : 'failed', errorTag: success ? null : 'JAVA_HEALTH_FAILED', summary: success ? 'Java 应用检测通过' : 'Java 应用检测失败', failureReason: success ? null : `HTTP_${String(response.status)}`, metrics: { statusCode: response.status, responseBytes: Buffer.byteLength(text), jmxEnabled: Boolean(config.jmxExporterUrl), jmxSampleLines: jmxPreview.length }, details: { endpoint, bodyPreview: text.slice(0, 1200), jmxPreview }, logExcerpt: null, resultJsonUrl: '' });
    }
    const endpoint = target.endpoint;
    const response = await fetch(endpoint, { signal: AbortSignal.timeout(config.timeoutMs ?? 8000), headers: this.basicHeaders(config) });
    const text = await response.text();
    const success = response.ok;
    return this.persistMonitoringResult(target, traceId, { type: target.type, status: success ? 'success' : 'failed', errorTag: success ? null : 'HTTP_CHECK_FAILED', summary: success ? `${target.type} 检测通过` : `${target.type} 检测失败`, failureReason: success ? null : `HTTP_${String(response.status)}`, metrics: { statusCode: response.status, responseBytes: Buffer.byteLength(text) }, details: { endpoint, bodyPreview: text.slice(0, 1200) }, logExcerpt: null, resultJsonUrl: '' });
  }

  private basicHeaders(config: MonitorTargetConfig) {
    if (!config.authUsername || !config.authPassword) return undefined;
    return { Authorization: `Basic ${Buffer.from(`${config.authUsername}:${config.authPassword}`).toString('base64')}` };
  }

  private async fetchText(baseUrl: string, pathName: string, query?: Record<string, string>) {
    const url = new URL(pathName || '/', baseUrl);
    if (query) for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP_${String(response.status)}: ${String(response.statusText)}`);
    return response.text();
  }

  private async fetchJson(baseUrl: string, pathName: string, headers?: Record<string, string>) {
    const response = await fetch(new URL(pathName || '/', baseUrl), { headers });
    if (!response.ok) throw new Error(`HTTP_${String(response.status)}: ${String(response.statusText)}`);
    return response.json();
  }

  private async runRedisCommand(host: string, port: number, commands: string[][], timeoutMs: number, password?: string) {
    return new Promise<string[]>((resolve, reject) => {
      const socket = new net.Socket();
      const results: string[] = [];
      let buffer = '';
      let expectedResponses = commands.length + (password ? 1 : 0);
      const finish = () => {
        socket.destroy();
        resolve(results);
      };
      socket.setTimeout(timeoutMs);
      socket.on('data', (chunk: Buffer) => {
        buffer += chunk.toString('utf8');
        while (buffer.includes('\r\n')) {
          const lineEnd = buffer.indexOf('\r\n');
          const line = buffer.slice(0, lineEnd);
          buffer = buffer.slice(lineEnd + 2);
          if (line.startsWith('$')) {
            const length = Number(line.slice(1));
            if (Number.isFinite(length) && buffer.length >= length + 2) {
              const payload = buffer.slice(0, length);
              buffer = buffer.slice(length + 2);
              results.push(payload);
              expectedResponses -= 1;
            }
          } else {
            results.push(line);
            expectedResponses -= 1;
          }
          if (expectedResponses <= 0) {
            finish();
            return;
          }
        }
      });
      socket.on('timeout', () => {
        socket.destroy();
        reject(new Error('Redis 检测超时'));
      });
      socket.on('error', (error) => reject(error));
      socket.on('connect', () => {
        if (password) socket.write(this.encodeRedisCommand(['AUTH', password]));
        for (const command of commands) socket.write(this.encodeRedisCommand(command));
      });
      socket.connect(port, host);
    });
  }

  private encodeRedisCommand(parts: string[]) {
    return `*${String(parts.length)}\r\n${parts.map((part) => `$${Buffer.byteLength(part)}\r\n${part}\r\n`).join('')}`;
  }

  private parseRedisInfo(infoText: string) {
    const metrics: Record<string, string | number> = {};
    for (const line of infoText.split('\n')) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#') || !trimmed.includes(':')) continue;
      const [key, value] = trimmed.split(':', 2);
      const numeric = Number(value);
      metrics[key] = Number.isFinite(numeric) ? numeric : value;
    }
    return metrics;
  }

  private buildLinuxAgentScript() {
    return [
      '#!/usr/bin/env node',
      'const fs = require("fs");',
      'const os = require("os");',
      'const path = require("path");',
      'const { execFile } = require("child_process");',
      'const { promisify } = require("util");',
      'const execFileAsync = promisify(execFile);',
      'const ENV = path.join(__dirname, "agent-config.env");',
      'if (fs.existsSync(ENV)) {',
      '  for (const line of fs.readFileSync(ENV, "utf8").split(/\\r?\\n/)) {',
      '    const trimmed = line.trim();',
      '    if (!trimmed || trimmed.startsWith("#")) continue;',
      '    const index = trimmed.indexOf("=");',
      '    if (index === -1) continue;',
      '    const key = trimmed.slice(0, index).trim();',
      '    const value = trimmed.slice(index + 1).trim();',
      '    if (!process.env[key]) process.env[key] = value;',
      '  }',
      '}',
      'const API = (process.env.MONITORING_API_BASE_URL || "http://localhost:3001").replace(/\\/$/, "");',
      'const KEY = process.env.MONITORING_AGENT_KEY || "linux-demo-agent";',
      'const TOKEN = process.env.MONITORING_AGENT_TOKEN || "";',
      'if (!TOKEN) throw new Error("MONITORING_AGENT_TOKEN is required");',
      'const POLL = Number(process.env.MONITORING_AGENT_POLL_MS || "10000");',
      'const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));',
      'async function req(url, init) {',
      '  const headers = new Headers((init && init.headers) || {});',
      '  headers.set("Content-Type", "application/json");',
      '  headers.set("x-monitoring-agent-token", TOKEN);',
      '  const response = await fetch(API + url, { ...(init || {}), headers });',
      '  if (!response.ok) throw new Error("HTTP_" + response.status + ": " + await response.text());',
      '  return response.json();',
      '}',
      'async function sh(command) {',
      '  return execFileAsync("bash", ["-lc", command], { maxBuffer: 1024 * 1024 * 8 });',
      '}',
      'function authHeader(config) {',
      '  if (!config.authUsername || !config.authPassword) return undefined;',
      '  return "Basic " + Buffer.from(String(config.authUsername) + ":" + String(config.authPassword)).toString("base64");',
      '}',
      'async function httpJob(job) {',
      '  const response = await fetch(job.endpoint);',
      '  const body = await response.text();',
      '  return { status: response.ok ? "success" : "failed", errorTag: response.ok ? null : "HTTP_CHECK_FAILED", summary: response.ok ? "HTTP 目标检测通过" : "HTTP 目标检测失败", failureReason: response.ok ? null : `HTTP_${response.status}`, metrics: { statusCode: response.status, responseBytes: Buffer.byteLength(body) }, details: { bodyPreview: body.slice(0, 1200) }, logExcerpt: null };',
      '}',
      'async function tcpJob(job) {',
      '  const c = job.config || {};',
      '  const host = String(c.tcpHost || job.endpoint || "").trim();',
      '  const port = Number(c.tcpPort || 0);',
      '  if (!host || !port) throw new Error("tcpHost and tcpPort are required");',
      '  try {',
      '    await sh(`timeout 5 bash -lc "</dev/tcp/${host}/${port}"`);',
      '    return { status: "success", errorTag: null, summary: "TCP 端口连通", failureReason: null, metrics: { host, port }, details: { host, port }, logExcerpt: null };',
      '  } catch (error) {',
      '    return { status: "failed", errorTag: "TCP_CONNECT_FAILED", summary: "TCP 端口不可达", failureReason: error instanceof Error ? error.message : "TCP 连接失败", metrics: { host, port }, details: { host, port }, logExcerpt: null };',
      '  }',
      '}',
      'async function redisJob(job) {',
      '  const c = job.config || {};',
      '  const host = String(c.tcpHost || job.endpoint || "").trim();',
      '  const port = Number(c.tcpPort || 6379);',
      '  const password = c.authPassword ? ` -a "${String(c.authPassword).replace(/"/g, "\\\\\\"")}"` : "";',
      '  try {',
      '    const ping = (await sh(`redis-cli -h "${host}" -p ${port}${password} PING`)).stdout.trim();',
      '    const infoText = (await sh(`redis-cli -h "${host}" -p ${port}${password} INFO`)).stdout;',
      '    return { status: ping.toUpperCase().includes("PONG") ? "success" : "failed", errorTag: ping.toUpperCase().includes("PONG") ? null : "REDIS_PING_FAILED", summary: ping.toUpperCase().includes("PONG") ? "Redis 检测通过" : "Redis 检测失败", failureReason: ping.toUpperCase().includes("PONG") ? null : "PING 未返回 PONG", metrics: { host, port }, details: { infoPreview: infoText.split(/\\r?\\n/).slice(0, 40) }, logExcerpt: null };',
      '  } catch (error) {',
      '    return { status: "failed", errorTag: "REDIS_COMMAND_FAILED", summary: "Redis 检测失败", failureReason: error instanceof Error ? error.message : "redis-cli 执行失败", metrics: { host, port }, details: { host, port }, logExcerpt: null };',
      '  }',
      '}',
      'async function rabbitJob(job) {',
      '  const c = job.config || {};',
      '  const headers = authHeader(c) ? { Authorization: authHeader(c) } : undefined;',
      '  const overview = await fetch(new URL(String(c.rabbitmqOverviewPath || "/api/overview"), job.endpoint), { headers }).then((res) => res.json());',
      '  const queues = await fetch(new URL(String(c.rabbitmqQueuesPath || "/api/queues"), job.endpoint), { headers }).then((res) => res.json());',
      '  const nodes = await fetch(new URL(String(c.rabbitmqNodesPath || "/api/nodes"), job.endpoint), { headers }).then((res) => res.json());',
      '  return { status: "success", errorTag: null, summary: "RabbitMQ 管理接口检测通过", failureReason: null, metrics: { queueCount: Array.isArray(queues) ? queues.length : 0, nodeCount: Array.isArray(nodes) ? nodes.length : 0 }, details: { overview, queuePreview: Array.isArray(queues) ? queues.slice(0, 10) : [], nodePreview: Array.isArray(nodes) ? nodes.slice(0, 10) : [] }, logExcerpt: null };',
      '}',
      'async function esJob(job) {',
      '  const c = job.config || {};',
      '  const headers = authHeader(c) ? { Authorization: authHeader(c) } : undefined;',
      '  const health = await fetch(new URL(String(c.elasticsearchHealthPath || "/_cluster/health"), job.endpoint), { headers }).then((res) => res.json());',
      '  const nodes = await fetch(new URL(String(c.elasticsearchNodesPath || "/_nodes/stats"), job.endpoint), { headers }).then((res) => res.json());',
      '  const clusterStatus = String(health.status || "unknown").toLowerCase();',
      '  const success = clusterStatus === "green" || clusterStatus === "yellow";',
      '  return { status: success ? "success" : "failed", errorTag: success ? null : "ES_CLUSTER_UNHEALTHY", summary: success ? "Elasticsearch 集群检测通过" : "Elasticsearch 集群异常", failureReason: success ? null : `cluster status=${clusterStatus}`, metrics: { clusterStatus, nodeCount: health.number_of_nodes || null }, details: { health, nodeKeys: nodes.nodes ? Object.keys(nodes.nodes) : [] }, logExcerpt: null };',
      '}',
      'async function javaJob(job) {',
      '  const c = job.config || {};',
      '  const keyword = String(c.processMatchValue || "").trim();',
      '  if (!keyword) throw new Error("processMatchValue is required");',
      '  const { stdout } = await sh(`ps -eo pid=,command= | grep java | grep -F "${keyword.replace(/"/g, "\\\\\\"")}" | grep -v grep | head -n 1`);',
      '  const line = stdout.trim();',
      '  if (!line) throw new Error(`未找到命令行包含 ${keyword} 的 Java 进程`);',
      '  const match = line.match(/^\\s*(\\d+)\\s+(.*)$/);',
      '  if (!match) throw new Error("无法解析 Java 进程信息");',
      '  const pid = Number(match[1]);',
      '  const commandLine = match[2];',
      '  let jcmdResult = "";',
      '  let jstatResult = "";',
      '  try { jcmdResult = (await execFileAsync(String(c.jcmdPath || "/usr/bin/jcmd"), [String(pid), "VM.command_line"], { maxBuffer: 1024 * 1024 * 8 })).stdout.trim(); } catch (error) { jcmdResult = error instanceof Error ? error.message : "jcmd failed"; }',
      '  try { jstatResult = (await execFileAsync(String(c.jstatPath || "/usr/bin/jstat"), ["-gcutil", String(pid)], { maxBuffer: 1024 * 1024 * 8 })).stdout.trim(); } catch (error) { jstatResult = error instanceof Error ? error.message : "jstat failed"; }',
      '  return { status: "success", errorTag: null, summary: "宿主机 Agent 已完成 Java 程序监测", failureReason: null, metrics: { pid, host: os.hostname(), jstatResult }, details: { process: { pid, commandLine }, jcmdResult, agentKey: KEY }, logExcerpt: null };',
      '}',
      'async function execJob(job) {',
      '  if (job.type === "java-app") return javaJob(job);',
      '  if (job.type === "redis") return redisJob(job);',
      '  if (job.type === "rabbitmq") return rabbitJob(job);',
      '  if (job.type === "elasticsearch") return esJob(job);',
      '  if (job.type === "tcp") return tcpJob(job);',
      '  return httpJob(job);',
      '}',
      'async function loop() {',
      '  while (true) {',
      '    const heartbeat = await req("/api/monitoring/agent/heartbeat", { method: "POST", body: JSON.stringify({ agentKey: KEY, host: os.hostname(), platform: process.platform, version: "host-agent-linux-js" }) });',
      '    if (!heartbeat.control.enabled || !heartbeat.control.monitoringEnabled) {',
      '      await sleep(heartbeat.control.heartbeatIntervalMs || POLL);',
      '      continue;',
      '    }',
      '    const claim = await req("/api/monitoring/agent/jobs/claim", { method: "POST", body: JSON.stringify({ agentKey: KEY }) });',
      '    if (!claim.job) {',
      '      await sleep(heartbeat.control.heartbeatIntervalMs || POLL);',
      '      continue;',
      '    }',
      '    try {',
      '      const result = await execJob(claim.job);',
      '      await req(`/api/monitoring/agent/jobs/${claim.job.runId}/result`, { method: "POST", body: JSON.stringify(result) });',
      '    } catch (error) {',
      '      await req(`/api/monitoring/agent/jobs/${claim.job.runId}/result`, { method: "POST", body: JSON.stringify({ status: "failed", errorTag: "AGENT_EXECUTION_ERROR", summary: "宿主机 Agent 执行失败", failureReason: error instanceof Error ? error.message : "unknown error", metrics: {}, details: { agentKey: KEY }, logExcerpt: null }) });',
      '    }',
      '  }',
      '}',
      'loop().catch((error) => { console.error(error); process.exit(1); });',
    ].join('\n');
  }

  private buildLinuxInstallScript(agentKey: string) {
    return ['#!/usr/bin/env bash','set -euo pipefail','APP_DIR=/opt/sentai-host-agent','if ! command -v node >/dev/null 2>&1; then echo "Node.js is required on the target host."; exit 1; fi','sudo mkdir -p "$APP_DIR"','sudo cp sentai-host-agent.js "$APP_DIR/"','sudo cp agent-config.env "$APP_DIR/"','sudo cp sentai-host-agent.service /etc/systemd/system/','sudo chmod +x "$APP_DIR/sentai-host-agent.js"','sudo systemctl daemon-reload','sudo systemctl enable sentai-host-agent.service','echo "Install complete. Use systemctl start|stop|restart|status sentai-host-agent"',`echo "Current Agent Key: ${agentKey}"`].join('\n');
  }

  private buildLinuxSystemdService() {
    return ['[Unit]','Description=SentAI Host Agent','After=network.target','','[Service]','Type=simple','WorkingDirectory=/opt/sentai-host-agent','EnvironmentFile=/opt/sentai-host-agent/agent-config.env','ExecStart=/usr/bin/node /opt/sentai-host-agent/sentai-host-agent.js','Restart=always','RestartSec=5','User=root','','[Install]','WantedBy=multi-user.target'].join('\n');
  }

  private resolveGoExecutable() {
    const configured = process.env.GO_BIN_PATH?.trim();
    if (configured) return configured;
    return 'C:\\Program Files\\Go\\bin\\go.exe';
  }

  private async buildNativeLinuxAgentBinary(packageRoot: string, targetArch: 'amd64' | 'arm64' | 'loong64') {
    const goExecutable = this.resolveGoExecutable();
    const sourceDir = path.join(process.cwd(), '..', '..', 'agents', 'host-agent-go');
    const outputPath = path.join(packageRoot, 'sentai-host-agent');
    await execFileAsync(goExecutable, ['build', '-trimpath', '-ldflags', '-s -w', '-o', outputPath, '.'], {
      cwd: sourceDir,
      env: {
        ...process.env,
        CGO_ENABLED: '0',
        GOOS: 'linux',
        GOARCH: targetArch,
      },
    });
  }

  private buildNativeLinuxInstallScript(agentKey: string) {
    return [
      '#!/usr/bin/env bash',
      'set -euo pipefail',
      'APP_DIR=/opt/sentai-host-agent',
      'sudo mkdir -p "$APP_DIR"',
      'sudo cp sentai-host-agent "$APP_DIR/"',
      'sudo cp agent-config.env "$APP_DIR/"',
      'sudo cp sentai-host-agent.service /etc/systemd/system/',
      'sudo chmod +x "$APP_DIR/sentai-host-agent"',
      'sudo systemctl daemon-reload',
      'sudo systemctl enable sentai-host-agent.service',
      'echo "Install complete. Use systemctl start|stop|restart|status sentai-host-agent"',
      `echo "Current Agent Key: ${agentKey}"`,
    ].join('\n');
  }

  private buildNativeLinuxUninstallScript() {
    return [
      '#!/usr/bin/env bash',
      'set -euo pipefail',
      'sudo systemctl disable --now sentai-host-agent.service || true',
      'sudo rm -f /etc/systemd/system/sentai-host-agent.service',
      'sudo rm -rf /opt/sentai-host-agent',
      'sudo systemctl daemon-reload',
      'echo "SentAI Host Agent removed."',
    ].join('\n');
  }

  private buildNativeLinuxSystemdService() {
    return ['[Unit]','Description=SentAI Host Agent','After=network.target','','[Service]','Type=simple','WorkingDirectory=/opt/sentai-host-agent','EnvironmentFile=/opt/sentai-host-agent/agent-config.env','ExecStart=/opt/sentai-host-agent/sentai-host-agent','Restart=always','RestartSec=5','User=root','','[Install]','WantedBy=multi-user.target'].join('\n');
  }

  private buildNativeAgentReadme(agent: MonitorAgentEntity, apiBaseUrl: string, targetArch: 'amd64' | 'arm64' | 'loong64') {
    return [
      '# SentAI 原生宿主机 Agent 部署说明',
      '',
      '## 这是什么',
      '- 这是 Linux 原生单文件 Agent 部署包。',
      '- 不依赖 Node.js、Python、Java 运行时。',
      '- 解压、改配置、执行安装脚本后即可作为 systemd 服务运行。',
      '',
      '当前节点信息：',
      `- 节点名称：${agent.name}`,
      `- Agent Key：${agent.agentKey}`,
      `- 主机备注：${agent.host ?? '未填写'}`,
      `- 平台地址：${apiBaseUrl}`,
      `- 目标架构：linux-${targetArch}`,
      '',
      '## 包内文件',
      '- sentai-host-agent：Linux 原生可执行文件',
      '- agent-config.env：平台地址、Agent Key、Token 配置',
      '- install.sh：安装并注册 systemd 服务',
      '- uninstall.sh：卸载脚本',
      '- sentai-host-agent.service：systemd 服务文件',
      '- README.md：当前说明文档',
      '',
      '## 使用顺序',
      '1. 在平台 /agents 页面创建 Agent 节点。',
      '2. 下载此部署包并上传到目标 Linux 服务器。',
      '3. 修改 agent-config.env，确认平台地址、Agent Key、Token。',
      '4. 执行：chmod +x install.sh uninstall.sh sentai-host-agent && sudo ./install.sh',
      '5. 回到平台 /agents 页面，确认节点在线，再启用“监测执行”等开关。',
      '6. 在平台 /monitors 页面创建并绑定 Redis、MQ、ES、Java、HTTP 等监测目标。',
      '',
      '## 关键说明',
      '- 具体监测哪些程序、日志路径是什么，仍然在平台 /monitors 页面配置。',
      '- 一个 Agent 节点可以同时执行多个监测目标。',
      '- Agent 默认不会自动开始监测，必须在平台里启用节点并打开监测执行。',
      '- Token 就在 agent-config.env 的 MONITORING_AGENT_TOKEN 字段里。',
      '',
      '## 启停与开机自启',
      '- 安装：sudo ./install.sh',
      '- 启动：sudo systemctl start sentai-host-agent',
      '- 停止：sudo systemctl stop sentai-host-agent',
      '- 重启：sudo systemctl restart sentai-host-agent',
      '- 状态：sudo systemctl status sentai-host-agent',
      '- 开机自启：sudo systemctl enable sentai-host-agent',
      '- 取消开机自启：sudo systemctl disable sentai-host-agent',
      '- 卸载：sudo ./uninstall.sh',
      '',
      '## 当前原生 Agent 支持的监测',
      '- HTTP / HTTPS 健康检查',
      '- TCP 端口连通性',
      '- Redis 连通性与 INFO',
      '- RabbitMQ management API',
      '- Elasticsearch 集群健康检查',
      '- Java 进程匹配、jcmd、jstat、日志摘录',
      '',
      '## 端口说明',
      '- Agent 默认不需要额外开放入站端口。',
      `- 只需要目标服务器能主动访问平台地址：${apiBaseUrl}`,
      '- 被监测服务的端口、地址、日志路径都在平台监测目标里配置。',
    ].join('\n');
  }

  private buildAgentReadme(agent: MonitorAgentEntity, apiBaseUrl: string) {
    return [
      '# SentAI 宿主机 Agent 部署说明',
      '',
      '## 这个包是做什么的',
      '- 这个包只负责把 Agent 部署到目标 Linux 服务器上。',
      '- Agent 启动后会回连平台、发送心跳、接收平台下发的监测任务。',
      '- 具体监测哪些服务、日志路径是什么、监测周期是什么，都在平台 Web 页面配置，不在这个包里手工配置。',
      '',
      '当前节点信息：',
      `- 节点名称：${agent.name}`,
      `- Agent Key：${agent.agentKey}`,
      `- 主机备注：${agent.host ?? '未填写'}`,
      `- 平台地址：${apiBaseUrl}`,
      '',
      '## 正确使用顺序',
      '1. 在平台 /agents 页面创建 Agent 节点。',
      '2. 下载这个部署包，上传到目标 Linux 服务器。',
      '3. 按下面步骤安装并启动 Agent。',
      '4. 回到平台 /agents 页面，确认节点在线并启用“监测执行”等开关。',
      '5. 再到平台 /monitors 页面创建监测目标，例如 Redis、MQ、ES、Java、HTTP。',
      '6. 监测目标绑定到这个 Agent 节点后，平台才会把对应任务派给它。',
      '',
      '## agent-config.env 怎么理解',
      '- MONITORING_API_BASE_URL：平台接口地址。Agent 靠它回连平台。',
      '- MONITORING_AGENT_KEY：当前节点唯一标识，必须和平台 /agents 里一致。',
      '- MONITORING_AGENT_TOKEN：Agent 与平台通信的认证令牌，只用于节点认证，不是业务程序密码。',
      '- MONITORING_AGENT_POLL_MS：轮询平台任务的时间间隔。',
      '',
      '## 默认行为',
      '- Agent 连上平台后，不代表立即开始监测。',
      '- 你仍然需要在平台 /agents 页面启用该节点。',
      '- 你还需要打开“监测执行”开关，这个节点才会真正领取监测任务。',
      '- 所以以后启停监测，优先在平台页面控制，不需要每次都登录服务器操作。',
      '',
      '## Linux 安装步骤',
      '1. 解压部署包。',
      '2. 打开 agent-config.env，确认平台地址、Agent Key、Agent Token 是否正确。',
      '3. 执行：chmod +x install.sh && sudo ./install.sh',
      '4. 查看状态：systemctl status sentai-host-agent',
      '5. 回到平台 /agents 页面，确认节点在线。',
      '',
      '## 启停与开机自启',
      '- 启动：systemctl start sentai-host-agent',
      '- 停止：systemctl stop sentai-host-agent',
      '- 重启：systemctl restart sentai-host-agent',
      '- 查看状态：systemctl status sentai-host-agent',
      '- 开机自启：systemctl enable sentai-host-agent',
      '- 取消开机自启：systemctl disable sentai-host-agent',
      '',
      '## 这个 Agent 目前可执行的监测类型',
      '- HTTP / HTTPS 健康检查',
      '- TCP 端口连通性',
      '- Redis 连通性与基础信息',
      '- RabbitMQ management API',
      '- Elasticsearch 集群健康检查',
      '- Java 进程检查、ps / jcmd / jstat',
      '',
      '## 最关键的配置边界',
      '- 监测目标例如“Java-iip1”是在平台 /monitors 页面创建的。',
      '- 日志路径、进程关键字、端口、服务地址，主要都在平台的监测目标里配置。',
      '- 一个 Agent 节点可以同时执行多种目标组合，例如 Redis + ES + Java。',
      '- 只要这些目标都绑定到同一个 Agent 节点，平台就会统一调度它去执行。',
      '',
      '## 端口说明',
      '- Agent 默认不需要开放自己的对外监听端口。',
      `- 只需要服务器能主动访问平台地址：${apiBaseUrl}`,
      '- Redis、MQ、ES、Java、HTTP 这些服务端口，是在平台监测目标里配置的，不是在 Agent 包里配置。',
    ].join('\n');
  }
  private resolveAgentAvailability(agent: MonitorAgentEntity) {
    if (!agent.lastHeartbeatAt) return { state: 'offline' as const, message: '尚未收到心跳，请先部署并启动 Agent' };
    return Date.now() - agent.lastHeartbeatAt.getTime() <= 60000 ? { state: 'online' as const, message: '最近 1 分钟内有心跳，Agent 在线' } : { state: 'offline' as const, message: '最近 1 分钟内未收到心跳，请检查 Agent 进程' };
  }

  private parseJson(raw: string | null) {
    try { return raw ? JSON.parse(raw) : {}; } catch { return {}; }
  }

  private parseConfig(configJson: string, secretJson?: string | null) {
    try { return { ...(JSON.parse(configJson) || {}), ...(secretJson ? JSON.parse(secretJson) : {}) } as MonitorTargetConfig; } catch { throw new BadRequestException('config_json 或 secret_json 不是合法 JSON'); }
  }

  private async mustFindAgent(id: string) {
    const agent = await this.agentRepository.findOneBy({ id });
    if (!agent) throw new NotFoundException('Agent 节点不存在');
    return agent;
  }

  private assertAgentToken(agentToken?: string) {
    const expectedToken = this.readRequiredToken(
      'MONITORING_AGENT_TOKEN',
      'Agent 认证',
    );
    if (agentToken !== expectedToken) throw new BadRequestException('Agent 令牌无效');
  }

  private readRequiredToken(name: string, scenario: string) {
    const token = process.env[name]?.trim();
    if (!token) {
      throw new BadRequestException(`${scenario}令牌未配置：${name}`);
    }
    return token;
  }

  private async buildRecentLogExcerpt(serviceKey: string, nodeKey?: string) {
    const logs = await this.getRecentLogs(serviceKey, nodeKey);
    return this.renderLogExcerptFromEvents(logs);
  }

  private renderLogExcerptFromEvents(logs: MonitorLogEventEntity[]) {
    return logs.length
      ? logs
          .slice(0, 12)
          .map(
            (log) =>
              `[${log.eventAt.toISOString()}] ${log.host ?? log.nodeKey ?? '-'} ${log.level ?? 'INFO'} ${log.messageText}`,
          )
          .join('\n')
      : null;
  }

  private async persistMonitoringResult(target: MonitorTargetEntity, traceId: string, result: MonitorExecutionResult) {
    const runDir = path.resolve(process.cwd(), 'storage', 'monitoring', traceId);
    await mkdir(runDir, { recursive: true });
    const resultJsonUrl = `/monitoring/${traceId}/result.json`;
    await writeFile(path.join(runDir, 'result.json'), JSON.stringify({ target: { id: target.id, name: target.name, type: target.type, serviceKey: target.serviceKey, endpoint: target.endpoint }, ...result, generatedAt: new Date().toISOString() }, null, 2), 'utf8');
    return { ...result, resultJsonUrl };
  }
}
