import type { PrismaClient } from "@prisma/client";
import type { FastifyBaseLogger } from "fastify";
import { decimalToNumberOrNull, formatInteger, formatUsd } from "../agent/cost.js";
import type { ModelRegistry, RoleEntry } from "../agent/models.js";
import type { RuntimeSettings } from "../settings/runtime.js";
import { lockActiveProjectOrganization } from "../org/status.js";
import { sendText, type FeishuRuntime, type SendMessageOptions } from "./client.js";
import type { TriggerQueue } from "./triggerQueue.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: string;
  readonly usage: string;
  readonly summary: string;
  readonly details: readonly string[];
  run(context: SlashCommandRunContext): Promise<void>;
}

export interface SlashCommandRegistryDeps {
  readonly prisma: PrismaClient;
  readonly settings: RuntimeSettings;
  readonly logger: FastifyBaseLogger;
  readonly triggerQueue: TriggerQueue;
}

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 parseSlashHelpSubcommand(invocation: SlashInvocation): string | null {
  if (invocation.name === "help") return null;
  return invocation.args.length === 1 && invocation.args[0] === "help"
    ? invocation.name
    : null;
}

export function createSlashCommandRegistry(deps: SlashCommandRegistryDeps): 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 [command]",
    summary: "查看可用 slash 命令或单个命令说明。",
    details: [
      "不创建 agent run,也不改变当前会话。",
      "支持 /help new 和 /new help 两种写法。",
    ],
    run: async ({ invocation, projectId, chatId, rt, sendOptions }) => {
      const registry = await deps.settings.modelRegistry({ projectId });
      await sendText(rt, chatId, formatHelpCommandInvocation(invocation, registry, commands), sendOptions);
    },
  });

  add({
    name: "new",
    usage: "/new",
    summary: "开新会话,下次 @bot 将从头开始。",
    details: [
      "归档当前未归档的 agent session。",
      "不会清空当前项目的等待队列。",
    ],
    run: async ({ projectId, chatId, rt, sendOptions }) => {
      await deps.prisma.$transaction(async (tx) => {
        await lockActiveProjectOrganization(tx, projectId);
        await tx.agentSession.updateMany({
          where: { projectId, archivedAt: null },
          data: { archivedAt: new Date() },
        });
      });
      await sendText(rt, chatId, "已开新会话,下次 @bot 将从头开始。", sendOptions);
    },
  });

  add({
    name: "resume",
    usage: "/resume",
    summary: "恢复最近一次已归档的会话。",
    details: [
      "只恢复当前项目最近归档的 agent session。",
      "没有可恢复会话时只回复提示,不会创建 agent run。",
    ],
    run: async ({ projectId, chatId, rt, sendOptions }) => {
      const resumed = await deps.prisma.$transaction(async (tx) => {
        await lockActiveProjectOrganization(tx, projectId);
        const latest = await tx.agentSession.findFirst({
          where: { projectId, archivedAt: { not: null } },
          orderBy: { archivedAt: "desc" },
          select: { id: true },
        });
        if (latest === null) return false;
        await tx.agentSession.update({
          where: { id: latest.id },
          data: { archivedAt: null },
        });
        return true;
      });
      if (!resumed) {
        await sendText(rt, chatId, "没有可恢复的会话。", sendOptions);
        return;
      }
      await sendText(rt, chatId, "已恢复上一个会话。", sendOptions);
    },
  });

  add({
    name: "cost",
    usage: "/cost",
    summary: "查看当前会话已记录的 agent 成本。",
    details: [
      "只读取当前项目未归档 agent session 下已经结束的 run,不创建 agent run。",
      "只统计运行时真实记录到 AgentRun.costUsd 的成本;未记录成本的 run 会单独列出。",
    ],
    run: async ({ invocation, projectId, chatId, rt, sendOptions }) => {
      if (invocation.args.length > 0) {
        await sendText(rt, chatId, ["用法错误: /cost 暂不接受参数。", "", formatBuiltinSlashCommandHelp(commands.get("cost")!)].join("\n"), sendOptions);
        return;
      }

      const sessions = await deps.prisma.agentSession.findMany({
        where: { projectId, archivedAt: null },
        orderBy: { updatedAt: "asc" },
        select: { id: true },
      });
      if (sessions.length === 0) {
        await sendText(rt, chatId, "当前会话还没有 agent session。", sendOptions);
        return;
      }

      const runs = await deps.prisma.agentRun.findMany({
        where: {
          projectId,
          sessionId: { in: sessions.map((session) => session.id) },
          status: { in: [...TERMINAL_RUN_STATUSES] },
          finishedAt: { not: null },
        },
        orderBy: { finishedAt: "asc" },
        select: {
          model: true,
          provider: true,
          inputTokens: true,
          outputTokens: true,
          costUsd: true,
        },
      });

      await sendText(rt, chatId, formatCostReport(runs), sendOptions);
    },
  });

  add({
    name: "reset",
    usage: "/reset",
    summary: "重置当前会话并清空等待队列。",
    details: [
      "归档当前未归档的 agent session。",
      "清空当前项目已经排队、尚未开始的触发请求。",
    ],
    run: async ({ projectId, chatId, rt, sendOptions }) => {
      await deps.prisma.$transaction(async (tx) => {
        await lockActiveProjectOrganization(tx, projectId);
        await tx.agentSession.updateMany({
          where: { projectId, archivedAt: null },
          data: { archivedAt: new Date() },
        });
      });
      const cleared = deps.triggerQueue.clear(projectId);
      if (cleared > 0) {
        deps.logger.info({ projectId, cleared }, "feishu trigger: cleared queued triggers on reset");
      }
      await sendText(rt, chatId, "已重置,下次 @bot 将从头开始。", sendOptions);
    },
  });

  return commands;
}

interface CostReportRun {
  readonly model: string;
  readonly provider: string;
  readonly inputTokens: number | null;
  readonly outputTokens: number | null;
  readonly costUsd: unknown;
}

interface CostReportBucket {
  readonly provider: string;
  readonly model: string;
  runs: number;
  inputTokens: number;
  outputTokens: number;
  costUsd: number;
}

function formatCostReport(runs: readonly CostReportRun[]): string {
  if (runs.length === 0) {
    return "当前会话还没有已结束的 agent run。";
  }

  const buckets = new Map<string, CostReportBucket>();
  let recordedRuns = 0;
  let unrecordedRuns = 0;
  let totalInputTokens = 0;
  let totalOutputTokens = 0;
  let totalCostUsd = 0;

  for (const run of runs) {
    const costUsd = decimalToNumberOrNull(run.costUsd);
    if (costUsd === null) {
      unrecordedRuns++;
      continue;
    }

    recordedRuns++;
    const inputTokens = run.inputTokens ?? 0;
    const outputTokens = run.outputTokens ?? 0;
    totalInputTokens += inputTokens;
    totalOutputTokens += outputTokens;
    totalCostUsd += costUsd;

    const key = `${run.provider}\u0000${run.model}`;
    let bucket = buckets.get(key);
    if (bucket === undefined) {
      bucket = {
        provider: run.provider,
        model: run.model,
        runs: 0,
        inputTokens: 0,
        outputTokens: 0,
        costUsd: 0,
      };
      buckets.set(key, bucket);
    }
    bucket.runs++;
    bucket.inputTokens += inputTokens;
    bucket.outputTokens += outputTokens;
    bucket.costUsd += costUsd;
  }

  if (recordedRuns === 0) {
    return [
      "当前会话已有已结束 agent run,但还没有任何 run 记录到真实成本。",
      `未记录成本: ${formatInteger(unrecordedRuns)} runs。`,
      "后续 run 需要 SDK 返回 total_cost_usd 才会进入 /cost 合计。",
    ].join("\n");
  }

  const lines = [
    "当前会话已记录 agent 成本:",
    `总计: ${formatUsd(totalCostUsd)}`,
    `Runs: ${formatInteger(recordedRuns)} 已记录${unrecordedRuns > 0 ? ` / ${formatInteger(unrecordedRuns)} 未记录` : ""}`,
    `Tokens: input ${formatInteger(totalInputTokens)} / output ${formatInteger(totalOutputTokens)}`,
    "",
    "按模型:",
  ];

  const sortedBuckets = [...buckets.values()].sort((a, b) => b.costUsd - a.costUsd);
  for (const bucket of sortedBuckets) {
    lines.push(
      `- ${bucket.provider} / ${bucket.model}: ${formatInteger(bucket.runs)} runs, ${formatUsd(bucket.costUsd)}, input ${formatInteger(bucket.inputTokens)} / output ${formatInteger(bucket.outputTokens)}`,
    );
  }

  return lines.join("\n");
}

function formatHelpCommandInvocation(
  invocation: SlashInvocation,
  registry: ModelRegistry,
  slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
): string {
  if (invocation.args.length > 1) {
    const helpCommand = slashCommands.get("help");
    if (helpCommand === undefined) {
      throw new Error("slash command registry is missing /help");
    }
    return [
      "用法错误: /help 只接受一个命令名。",
      "",
      formatBuiltinSlashCommandHelp(helpCommand),
    ].join("\n");
  }

  const target = invocation.args[0];
  if (target === undefined) return formatSlashOverview(registry, slashCommands);
  const normalizedTarget = normalizeHelpTarget(target);
  if (normalizedTarget === "") return formatSlashOverview(registry, slashCommands);
  return formatSlashHelpTarget(normalizedTarget, registry, slashCommands);
}

export function formatSlashHelpTarget(
  target: string,
  registry: ModelRegistry,
  slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
): string {
  const normalizedTarget = normalizeHelpTarget(target);
  if (normalizedTarget === "") return formatSlashOverview(registry, slashCommands);
  return formatSlashCommandHelp(normalizedTarget, registry, slashCommands) ?? formatUnknownSlashHelp(normalizedTarget, registry, slashCommands);
}

function normalizeHelpTarget(target: string): string {
  return target.trim().replace(/^\/+/, "");
}

function formatSlashOverview(
  registry: ModelRegistry,
  slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
): string {
  const lines = [
    "可用 slash 命令:",
    ...[...slashCommands.values()].map((command) => `/${command.name} - ${command.summary}`),
  ];
  const roles = visibleRoleCommands(registry, slashCommands);
  if (roles.length > 0) {
    lines.push("", "角色命令:");
    for (const role of roles) {
      lines.push(`/${role.id} <需求> - 使用“${role.label}”角色发起请求。`);
    }
  } else {
    lines.push("", "当前没有配置角色命令。");
  }
  lines.push("", "查看单个命令: /help new 或 /new help。");
  return lines.join("\n");
}

function formatSlashCommandHelp(
  commandName: string,
  registry: ModelRegistry,
  slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
): string | null {
  const normalizedName = normalizeHelpTarget(commandName);
  const slashCommand = slashCommands.get(normalizedName);
  if (slashCommand !== undefined) return formatBuiltinSlashCommandHelp(slashCommand);

  const role = registry.role(normalizedName);
  if (role === undefined) return null;
  if (slashCommands.has(role.id)) return null;
  return formatRoleSlashCommandHelp(role);
}

function formatBuiltinSlashCommandHelp(command: SlashCommandDefinition): string {
  const helpUsage = command.name === "help"
    ? "帮助: /help help"
    : `帮助: /help ${command.name} 或 /${command.name} help`;
  return [
    `/${command.name}`,
    command.summary,
    "",
    `用法: ${command.usage}`,
    ...command.details.map((detail) => `- ${detail}`),
    "",
    helpUsage,
  ].join("\n");
}

function formatRoleSlashCommandHelp(role: RoleEntry): string {
  const lines = [
    `/${role.id}`,
    `使用“${role.label}”角色发起一次 agent run。`,
    "",
    `用法: /${role.id} <需求>`,
    "- 真正运行时仍会按当前项目的 role.trigger 授权检查。",
  ];
  if (role.defaultModel !== undefined) {
    lines.push(`- 默认模型: ${role.defaultModel}`);
  }
  lines.push(
    `- 工具范围: ${roleToolsDescription(role)}`,
    `- Skills: ${roleSkillsDescription(role)}`,
    "",
    `帮助: /help ${role.id} 或 /${role.id} help`,
  );
  return lines.join("\n");
}

function roleSkillsDescription(role: RoleEntry): string {
  if (role.skills === undefined || role.skills.length === 0) return "无";
  return role.skills.map((skill) => `${skill.name}@${skill.version}`).join(", ");
}

function roleToolsDescription(role: RoleEntry): string {
  if (role.tools === undefined) return "全部已注册工具";
  if (role.tools.length === 0) return "无";
  return role.tools.join(", ");
}

function formatUnknownSlashHelp(
  commandName: string,
  registry: ModelRegistry,
  slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
): string {
  const normalizedName = normalizeHelpTarget(commandName);
  return [
    `未知 slash 命令 /${normalizedName}。`,
    "",
    formatSlashOverview(registry, slashCommands),
  ].join("\n");
}

function visibleRoleCommands(
  registry: ModelRegistry,
  slashCommands: ReadonlyMap<string, SlashCommandDefinition>,
): readonly RoleEntry[] {
  return registry
    .listRoles()
    .filter((role) => !slashCommands.has(role.id));
}
