import type { PrismaClient } from "@prisma/client";
import { decimalToNumberOrNull, formatInteger, formatUsd } from "../agent/cost.js";
import { sendText, type FeishuRuntime, type SendMessageOptions } from "./client.js";

export interface SlashInvocation {
  readonly name: string;
  readonly args: readonly string[];
}

export interface SlashCommandRunContext {
  readonly invocation: SlashInvocation;
  readonly projectId: string;
  readonly chatId: string;
  readonly rt: FeishuRuntime;
  readonly sendOptions?: SendMessageOptions | undefined;
}

export interface SlashCommandDefinition {
  readonly name: "help" | "project" | "usage";
  readonly usage: string;
  readonly summary: string;
  readonly details: readonly string[];
  run(context: SlashCommandRunContext): Promise<void>;
}

const TERMINAL_RUN_STATUSES = ["COMPLETED", "FAILED", "TIMED_OUT", "CANCELED"] as const;

export function parseSlashInvocation(prompt: string): SlashInvocation | null {
  const trimmed = prompt.trim();
  if (!trimmed.startsWith("/")) return null;
  const tokens = trimmed.split(/\s+/);
  const rawCommand = tokens[0];
  if (rawCommand === undefined || rawCommand.length <= 1) return null;
  return { name: rawCommand.slice(1), args: tokens.slice(1) };
}

export function createSlashCommandRegistry(
  deps: { readonly prisma: PrismaClient },
): ReadonlyMap<string, SlashCommandDefinition> {
  const commands = new Map<string, SlashCommandDefinition>();
  const add = (command: SlashCommandDefinition): void => {
    if (commands.has(command.name)) throw new Error(`duplicate slash command definition: /${command.name}`);
    commands.set(command.name, command);
  };

  add({
    name: "help",
    usage: "/help [project|usage]",
    summary: "查看 Hub 命令。",
    details: ["未知 slash 命令会明确报错，不会发送给 Agent。"],
    run: async ({ invocation, chatId, rt, sendOptions }) => {
      if (invocation.args.length > 1) {
        await sendText(rt, chatId, "用法: /help [project|usage]", sendOptions);
        return;
      }
      const target = invocation.args[0];
      await sendText(rt, chatId, target === undefined
        ? formatSlashOverview(commands)
        : formatSlashHelpTarget(target, commands), sendOptions);
    },
  });

  add({
    name: "project",
    usage: "/project",
    summary: "打开当前项目控制台，切换角色、管理会话和项目。",
    details: ["不同区域按当前操作者的项目与组织权限显示。"],
    run: async ({ chatId, rt, sendOptions }) => {
      await sendText(rt, chatId, "请在绑定的项目群中使用 /project。", sendOptions);
    },
  });

  add({
    name: "usage",
    usage: "/usage [current|project]",
    summary: "查看 Hub 记录的真实 Agent 用量与成本。",
    details: ["current 只统计当前角色的活跃会话；project 统计整个项目。"],
    run: async ({ invocation, projectId, chatId, rt, sendOptions }) => {
      const scope = invocation.args[0] ?? "current";
      if (invocation.args.length > 1 || (scope !== "current" && scope !== "project")) {
        await sendText(rt, chatId, "用法: /usage [current|project]", sendOptions);
        return;
      }
      const sessionIds = scope === "project"
        ? undefined
        : await currentRoleSessionIds(deps.prisma, projectId, chatId);
      const runs = await deps.prisma.agentRun.findMany({
        where: {
          projectId,
          ...(sessionIds === undefined ? {} : { sessionId: { in: sessionIds } }),
          status: { in: [...TERMINAL_RUN_STATUSES] },
          finishedAt: { not: null },
        },
        orderBy: { finishedAt: "asc" },
        select: {
          usageFacts: {
            select: {
              provider: true,
              model: true,
              inputTokens: true,
              outputTokens: true,
              costUsd: true,
            },
            orderBy: { occurredAt: "asc" },
          },
        },
      });
      await sendText(rt, chatId, formatUsageReport(runs, scope), sendOptions);
    },
  });

  return commands;
}

async function currentRoleSessionIds(
  prisma: PrismaClient,
  projectId: string,
  chatId: string,
): Promise<string[]> {
  const binding = await prisma.projectGroupBinding.findFirst({
    where: { projectId, chatId, archivedAt: null },
    select: { selectedRole: { select: { roleId: true } } },
  });
  if (binding === null) throw new Error("active project-group binding not found");
  const sessions = await prisma.agentSession.findMany({
    where: { projectId, roleId: binding.selectedRole.roleId, archivedAt: null },
    select: { id: true },
  });
  return sessions.map((session) => session.id);
}

interface UsageRunWithFacts {
  readonly usageFacts: readonly {
    readonly provider: string;
    readonly model: string | null;
    readonly inputTokens: number | null;
    readonly outputTokens: number | null;
    readonly costUsd: unknown;
  }[];
}

function formatUsageReport(runs: readonly UsageRunWithFacts[], scope: "current" | "project"): string {
  if (runs.length === 0) return `${scope === "current" ? "当前角色会话" : "当前项目"}还没有已结束的 Agent run。`;
  // Bucket by (fact.provider, fact.model) — ADR-0026: external capabilities
  // carry their own provider/model and contribute their own meter, so a run
  // with a main loop + an external call lands in two buckets. A run with no
  // cost-bearing fact is "unrecorded" (ADR-0022: missing cost ≠ zero).
  const buckets = new Map<string, {
    provider: string; model: string; facts: number; inputTokens: number; outputTokens: number; costUsd: number;
  }>();
  let recordedRuns = 0;
  let unrecordedRuns = 0;
  let totalInputTokens = 0;
  let totalOutputTokens = 0;
  let totalCostUsd = 0;
  for (const run of runs) {
    const facts = run.usageFacts;
    if (facts.length === 0 || !facts.some((f) => f.costUsd !== null && f.costUsd !== undefined)) {
      unrecordedRuns++;
      // Tokens from unrecorded runs still count toward totals (matches pre-0026).
      for (const f of facts) {
        totalInputTokens += f.inputTokens ?? 0;
        totalOutputTokens += f.outputTokens ?? 0;
      }
      continue;
    }
    recordedRuns++;
    for (const f of facts) {
      const costUsd = decimalToNumberOrNull(f.costUsd);
      if (costUsd === null) continue;
      const model = f.model ?? "(unknown model)";
      const key = `${f.provider}\u0000${model}`;
      const bucket = buckets.get(key) ?? {
        provider: f.provider, model, facts: 0, inputTokens: 0, outputTokens: 0, costUsd: 0,
      };
      bucket.facts += 1;
      bucket.inputTokens += f.inputTokens ?? 0;
      bucket.outputTokens += f.outputTokens ?? 0;
      bucket.costUsd += costUsd;
      buckets.set(key, bucket);
      totalInputTokens += f.inputTokens ?? 0;
      totalOutputTokens += f.outputTokens ?? 0;
      totalCostUsd += costUsd;
    }
  }
  const lines = [
    `${scope === "current" ? "当前角色会话" : "当前项目"}用量`,
    `已记录成本: ${formatInteger(recordedRuns)} runs · ${formatUsd(totalCostUsd)}`,
    `Tokens: input ${formatInteger(totalInputTokens)} / output ${formatInteger(totalOutputTokens)}`,
  ];
  if (unrecordedRuns > 0) lines.push(`另有 ${formatInteger(unrecordedRuns)} 个 run 未记录成本。`);
  for (const bucket of buckets.values()) {
    lines.push(`- ${bucket.provider} / ${bucket.model}: ${formatInteger(bucket.facts)} 次, ${formatUsd(bucket.costUsd)}`);
  }
  return lines.join("\n");
}

export function formatSlashHelpTarget(
  target: string,
  commands: ReadonlyMap<string, SlashCommandDefinition>,
): string {
  const normalized = target.trim().replace(/^\/+/, "");
  const command = commands.get(normalized);
  if (command === undefined) return [`未知 slash 命令 /${normalized}。`, "", formatSlashOverview(commands)].join("\n");
  return [
    `/${command.name}`,
    command.summary,
    "",
    `用法: ${command.usage}`,
    ...command.details.map((detail) => `- ${detail}`),
  ].join("\n");
}

function formatSlashOverview(commands: ReadonlyMap<string, SlashCommandDefinition>): string {
  return [
    "可用 slash 命令:",
    ...[...commands.values()].map((command) => `/${command.name} - ${command.summary}`),
    "",
    "普通消息会使用 /project 中选定的当前角色。",
  ].join("\n");
}
