import type { Redis } from 'ioredis';
import type { Client } from 'discord.js';

const KEY_COMMANDS_TOTAL = 'nexumi:metrics:commands_total';
const KEY_COMMANDS_ERRORS = 'nexumi:metrics:commands_errors';
const KEY_COMMAND_DURATION_SUM = 'nexumi:metrics:command_duration_ms_sum';
const KEY_COMMAND_DURATION_COUNT = 'nexumi:metrics:command_duration_ms_count';
const KEY_EVENTS_TOTAL = 'nexumi:metrics:events_total';
const shardKey = (id: number) => `nexumi:metrics:shard:${id}`;

export async function recordCommandMetric(
  redis: Redis,
  opts: { ok: boolean; durationMs: number }
): Promise<void> {
  const multi = redis.multi();
  multi.incr(KEY_COMMANDS_TOTAL);
  if (!opts.ok) {
    multi.incr(KEY_COMMANDS_ERRORS);
  }
  multi.incrbyfloat(KEY_COMMAND_DURATION_SUM, opts.durationMs);
  multi.incr(KEY_COMMAND_DURATION_COUNT);
  await multi.exec();
}

export async function recordEventMetric(redis: Redis): Promise<void> {
  await redis.incr(KEY_EVENTS_TOTAL);
}

export async function publishShardMetrics(redis: Redis, client: Client): Promise<void> {
  const ids = client.shard?.ids ?? [0];
  const ping = client.ws.ping;
  const guilds = client.guilds.cache.size;
  const payload = JSON.stringify({
    ping,
    guilds,
    updatedAt: Date.now()
  });
  await Promise.all(ids.map((id) => redis.set(shardKey(id), payload, 'EX', 120)));
}

export type MetricsSnapshot = {
  up: 1;
  commandsTotal: number;
  commandsErrors: number;
  commandDurationSumMs: number;
  commandDurationCount: number;
  eventsTotal: number;
  shards: Array<{ id: number; ping: number; guilds: number; updatedAt: number }>;
  queueWaiting?: number;
};

export async function collectMetricsSnapshot(redis: Redis): Promise<MetricsSnapshot> {
  const [commandsTotal, commandsErrors, durationSum, durationCount, eventsTotal, shardKeys] =
    await Promise.all([
      redis.get(KEY_COMMANDS_TOTAL),
      redis.get(KEY_COMMANDS_ERRORS),
      redis.get(KEY_COMMAND_DURATION_SUM),
      redis.get(KEY_COMMAND_DURATION_COUNT),
      redis.get(KEY_EVENTS_TOTAL),
      redis.keys('nexumi:metrics:shard:*')
    ]);

  const shards: MetricsSnapshot['shards'] = [];
  if (shardKeys.length > 0) {
    const values = await redis.mget(...shardKeys);
    for (let i = 0; i < shardKeys.length; i++) {
      const id = Number(shardKeys[i]!.replace('nexumi:metrics:shard:', ''));
      const raw = values[i];
      if (!raw || Number.isNaN(id)) continue;
      try {
        const parsed = JSON.parse(raw) as { ping: number; guilds: number; updatedAt: number };
        shards.push({ id, ping: parsed.ping, guilds: parsed.guilds, updatedAt: parsed.updatedAt });
      } catch {
        // ignore malformed
      }
    }
    shards.sort((a, b) => a.id - b.id);
  }

  return {
    up: 1,
    commandsTotal: Number(commandsTotal ?? 0),
    commandsErrors: Number(commandsErrors ?? 0),
    commandDurationSumMs: Number(durationSum ?? 0),
    commandDurationCount: Number(durationCount ?? 0),
    eventsTotal: Number(eventsTotal ?? 0),
    shards
  };
}

export function formatPrometheus(snapshot: MetricsSnapshot): string {
  const lines: string[] = [
    '# HELP nexumi_up 1 if the metrics endpoint is healthy',
    '# TYPE nexumi_up gauge',
    `nexumi_up ${snapshot.up}`,
    '# HELP nexumi_commands_total Total slash commands handled',
    '# TYPE nexumi_commands_total counter',
    `nexumi_commands_total ${snapshot.commandsTotal}`,
    '# HELP nexumi_commands_errors_total Total slash command failures',
    '# TYPE nexumi_commands_errors_total counter',
    `nexumi_commands_errors_total ${snapshot.commandsErrors}`,
    '# HELP nexumi_command_duration_ms_sum Sum of command durations in milliseconds',
    '# TYPE nexumi_command_duration_ms_sum counter',
    `nexumi_command_duration_ms_sum ${snapshot.commandDurationSumMs}`,
    '# HELP nexumi_command_duration_ms_count Count of timed commands',
    '# TYPE nexumi_command_duration_ms_count counter',
    `nexumi_command_duration_ms_count ${snapshot.commandDurationCount}`,
    '# HELP nexumi_events_total Approximate event throughput counter',
    '# TYPE nexumi_events_total counter',
    `nexumi_events_total ${snapshot.eventsTotal}`,
    '# HELP nexumi_guilds Guild count per shard',
    '# TYPE nexumi_guilds gauge',
    '# HELP nexumi_shard_ping_ms Websocket heartbeat latency per shard',
    '# TYPE nexumi_shard_ping_ms gauge'
  ];

  for (const shard of snapshot.shards) {
    lines.push(`nexumi_guilds{shard="${shard.id}"} ${shard.guilds}`);
    lines.push(`nexumi_shard_ping_ms{shard="${shard.id}"} ${shard.ping}`);
  }

  if (snapshot.queueWaiting !== undefined) {
    lines.push('# HELP nexumi_queue_waiting Approximate waiting jobs across known queues');
    lines.push('# TYPE nexumi_queue_waiting gauge');
    lines.push(`nexumi_queue_waiting ${snapshot.queueWaiting}`);
  }

  return `${lines.join('\n')}\n`;
}
