import { createHash } from "node:crypto";

import { z } from "zod";

import type { AgentScope } from "../domain/scopes.js";
import { structuredBlocksSchema } from "../domain/structured-blocks.js";
import type { AgentsRepository, AgentSessionRecord } from "../repositories/agents.js";
import { ForbiddenError, requireProjectGrant, requireScope } from "../security/authorization.js";
import type { TaskerClient } from "../tasker/client.js";

type JsonSchema = Record<string, unknown>;

export type McpToolRuntimeDefinition = {
  name: string;
  title: string;
  description: string;
  requiredScopes: AgentScope[];
  inputSchema: JsonSchema;
  annotations?: Record<string, unknown>;
};

export type McpToolResult = {
  content: Array<{
    type: "text";
    text: string;
  }>;
  structuredContent?: unknown;
  isError?: boolean;
};

type ExecuteToolDeps = {
  agentsRepository?: AgentsRepository | null;
  taskerClient: TaskerClient;
};

type ExecuteToolOptions = {
  source?: "mcp" | "rest";
  idempotencyKey?: string | null;
};

const emptyInputSchema = {
  type: "object",
  additionalProperties: false,
};

const projectInputSchema = {
  type: "object",
  properties: {
    project_id: { type: "string" },
    workspace_slug: { type: "string" },
  },
  required: ["project_id"],
  additionalProperties: false,
};

const projectAndIssueInputSchema = {
  type: "object",
  properties: {
    issue_id: { type: "string" },
    project_id: { type: "string" },
    workspace_slug: { type: "string" },
  },
  required: ["issue_id", "project_id"],
  additionalProperties: false,
};

const structuredBlocksJsonSchema = {
  type: "array",
  items: {
    oneOf: [
      {
        type: "object",
        properties: {
          id: { type: "string" },
          type: { const: "text" },
          title: {
            type: "string",
            description: "Visible block title. Put headings here, not inside body markdown.",
          },
          body: {
            type: "string",
            description: "Plain block body without a leading markdown heading.",
          },
        },
        required: ["id", "type", "title", "body"],
        additionalProperties: false,
      },
      {
        type: "object",
        properties: {
          id: { type: "string" },
          type: { const: "checker" },
          title: {
            type: "string",
            description: "Visible checklist title. Put headings here, not inside item text.",
          },
          items: {
            type: "array",
            items: {
              type: "object",
              properties: {
                id: { type: "string" },
                text: { type: "string" },
                checked: { type: "boolean" },
              },
              required: ["id", "text"],
              additionalProperties: false,
            },
          },
        },
        required: ["id", "type", "title", "items"],
        additionalProperties: false,
      },
    ],
  },
};

export const mcpRuntimeTools: McpToolRuntimeDefinition[] = [
  {
    name: "tasker_get_agent_instructions",
    title: "NODE.DC Ops: Get Agent Instructions",
    description:
      "Direct NODE.DC Ops MCP tool. Return effective Tasker card-writing rules, grants, scopes, and mode expectations. Use this before any Ops card read/write workflow.",
    requiredScopes: ["workspace:read"],
    inputSchema: emptyInputSchema,
    annotations: { readOnlyHint: true },
  },
  {
    name: "tasker_list_projects",
    title: "NODE.DC Ops: List Granted Projects",
    description:
      "Direct NODE.DC Ops MCP tool. List Tasker/Ops projects granted to the current Codex agent. Use this instead of Codex Apps workspace widgets.",
    requiredScopes: ["project:read"],
    inputSchema: emptyInputSchema,
    annotations: { readOnlyHint: true },
  },
  {
    name: "tasker_get_project_context",
    title: "NODE.DC Ops: Get Project Context",
    description:
      "Direct NODE.DC Ops MCP tool. Return states, labels, members, and card-writing context for one granted Tasker/Ops project.",
    requiredScopes: ["project:read"],
    inputSchema: projectInputSchema,
    annotations: { readOnlyHint: true },
  },
  {
    name: "tasker_create_project",
    title: "NODE.DC Ops: Create Project",
    description:
      "Direct NODE.DC Ops MCP write tool. Create a new Tasker project inside an explicitly granted workspace and extend the current agent grant to the created project.",
    requiredScopes: ["project:create"],
    inputSchema: {
      type: "object",
      properties: {
        workspace_slug: { type: "string" },
        name: { type: "string" },
        identifier: {
          type: "string",
          description: "Optional 1-12 character Tasker project identifier. A unique identifier is generated when omitted.",
        },
        description: { type: "string" },
        idempotency_key: { type: "string" },
      },
      required: ["workspace_slug", "name"],
      additionalProperties: false,
    },
    annotations: { destructiveHint: false, idempotentHint: false },
  },
  {
    name: "tasker_search_issues",
    title: "NODE.DC Ops: Search Cards",
    description:
      "Direct NODE.DC Ops MCP tool. Search Tasker/Ops cards and work items inside one granted project. Use this for 'show cards', 'find cards', and project context requests.",
    requiredScopes: ["issue:read"],
    inputSchema: {
      ...projectInputSchema,
      properties: {
        ...projectInputSchema.properties,
        query: { type: "string" },
      },
    },
    annotations: { readOnlyHint: true },
  },
  {
    name: "tasker_get_issue",
    title: "NODE.DC Ops: Get Card",
    description:
      "Direct NODE.DC Ops MCP tool. Return one granted Tasker/Ops card with structured blocks, labels, assignees, full comment bodies, and attachment metadata.",
    requiredScopes: ["issue:read"],
    inputSchema: projectAndIssueInputSchema,
    annotations: { readOnlyHint: true },
  },
  {
    name: "tasker_create_issue",
    title: "NODE.DC Ops: Create Card",
    description:
      "Direct NODE.DC Ops MCP write tool. Create a Tasker/Ops card with optional NODE.DC structured text/checker blocks.",
    requiredScopes: ["issue:create"],
    inputSchema: {
      type: "object",
      properties: {
        project_id: { type: "string" },
        workspace_slug: { type: "string" },
        title: { type: "string" },
        description: { type: "string" },
        priority: { type: "string", enum: ["none", "low", "medium", "high", "urgent"] },
        structured_blocks: structuredBlocksJsonSchema,
        idempotency_key: { type: "string" },
      },
      required: ["project_id", "title"],
      additionalProperties: false,
    },
    annotations: { destructiveHint: false, idempotentHint: false },
  },
  {
    name: "tasker_update_issue",
    title: "NODE.DC Ops: Update Card",
    description:
      "Direct NODE.DC Ops MCP write tool. Patch allowed Tasker/Ops card fields without delete, archive, or project transfer.",
    requiredScopes: ["issue:update"],
    inputSchema: {
      type: "object",
      properties: {
        issue_id: { type: "string" },
        project_id: { type: "string" },
        workspace_slug: { type: "string" },
        title: { type: "string" },
        description: { type: "string" },
        priority: { type: "string", enum: ["none", "low", "medium", "high", "urgent"] },
        structured_blocks: structuredBlocksJsonSchema,
        idempotency_key: { type: "string" },
      },
      required: ["issue_id", "project_id"],
      additionalProperties: false,
    },
    annotations: { destructiveHint: false, idempotentHint: false },
  },
  {
    name: "tasker_update_structured_blocks",
    title: "NODE.DC Ops: Update Structured Blocks",
    description:
      "Direct NODE.DC Ops MCP write tool. Replace NODE.DC structured text/checker blocks in a Tasker/Ops card detail layout.",
    requiredScopes: ["issue:update", "issue:structured_blocks:write"],
    inputSchema: {
      ...projectAndIssueInputSchema,
      properties: {
        ...projectAndIssueInputSchema.properties,
        structured_blocks: structuredBlocksJsonSchema,
        idempotency_key: { type: "string" },
      },
      required: ["issue_id", "project_id", "structured_blocks"],
    },
    annotations: { destructiveHint: false, idempotentHint: false },
  },
  {
    name: "tasker_move_issue",
    title: "NODE.DC Ops: Move Card",
    description: "Direct NODE.DC Ops MCP write tool. Move a Tasker/Ops card to an existing state in the same granted project.",
    requiredScopes: ["issue:move"],
    inputSchema: {
      ...projectAndIssueInputSchema,
      properties: {
        ...projectAndIssueInputSchema.properties,
        state_id: { type: "string" },
        idempotency_key: { type: "string" },
      },
      required: ["issue_id", "project_id", "state_id"],
    },
    annotations: { destructiveHint: false, idempotentHint: false },
  },
  {
    name: "tasker_append_comment",
    title: "NODE.DC Ops: Append Comment",
    description: "Direct NODE.DC Ops MCP write tool. Append a comment to a granted Tasker/Ops card.",
    requiredScopes: ["issue:comment"],
    inputSchema: {
      ...projectAndIssueInputSchema,
      properties: {
        ...projectAndIssueInputSchema.properties,
        body: { type: "string" },
        idempotency_key: { type: "string" },
      },
      required: ["issue_id", "project_id", "body"],
    },
    annotations: { destructiveHint: false, idempotentHint: false },
  },
  {
    name: "tasker_attach_file",
    title: "NODE.DC Ops: Attach File",
    description:
      "Direct NODE.DC Ops MCP write tool. Attach a base64-encoded local file to a granted card through Tasker's existing storage, quota, and deduplication path.",
    requiredScopes: ["issue:attachment:write"],
    inputSchema: {
      ...projectAndIssueInputSchema,
      properties: {
        ...projectAndIssueInputSchema.properties,
        file_name: { type: "string", description: "File name only; paths are rejected." },
        mime_type: { type: "string" },
        content_base64: {
          type: "string",
          maxLength: 7000000,
          contentEncoding: "base64",
          description: "Base64 file content. Raw decoded size must not exceed 5 MiB.",
        },
        idempotency_key: { type: "string" },
      },
      required: ["issue_id", "project_id", "file_name", "mime_type", "content_base64"],
    },
    annotations: { destructiveHint: false, idempotentHint: false },
  },
  {
    name: "tasker_ensure_labels",
    title: "NODE.DC Ops: Ensure Project Labels",
    description:
      "Direct NODE.DC Ops MCP write tool. Create missing labels in a granted project and return label ids for subsequent card labeling.",
    requiredScopes: ["issue:label"],
    inputSchema: {
      ...projectInputSchema,
      properties: {
        ...projectInputSchema.properties,
        labels: {
          type: "array",
          minItems: 1,
          maxItems: 50,
          items: {
            type: "object",
            properties: {
              name: { type: "string" },
              color: {
                type: "string",
                description: "Optional hex color in #RRGGBB format.",
              },
            },
            required: ["name"],
            additionalProperties: false,
          },
        },
        idempotency_key: { type: "string" },
      },
      required: ["project_id", "labels"],
    },
    annotations: { destructiveHint: false, idempotentHint: true },
  },
  {
    name: "tasker_set_issue_labels",
    title: "NODE.DC Ops: Set Card Labels",
    description: "Direct NODE.DC Ops MCP write tool. Replace card labels with existing or ensured labels from the granted project.",
    requiredScopes: ["issue:label"],
    inputSchema: {
      ...projectAndIssueInputSchema,
      properties: {
        ...projectAndIssueInputSchema.properties,
        label_ids: { type: "array", items: { type: "string" } },
        idempotency_key: { type: "string" },
      },
      required: ["issue_id", "project_id", "label_ids"],
    },
    annotations: { destructiveHint: false, idempotentHint: true },
  },
  {
    name: "tasker_assign_issue",
    title: "NODE.DC Ops: Assign Card",
    description: "Direct NODE.DC Ops MCP write tool. Replace card assignees with existing members of the granted project.",
    requiredScopes: ["issue:assign"],
    inputSchema: {
      ...projectAndIssueInputSchema,
      properties: {
        ...projectAndIssueInputSchema.properties,
        member_ids: { type: "array", items: { type: "string" } },
        idempotency_key: { type: "string" },
      },
      required: ["issue_id", "project_id", "member_ids"],
    },
    annotations: { destructiveHint: false, idempotentHint: true },
  },
];

const emptyArgsSchema = z.object({}).default({});
const projectArgsSchema = z.object({
  project_id: z.string().min(1),
  workspace_slug: z.string().min(1).nullish(),
});
const createProjectArgsSchema = z.object({
  workspace_slug: z.string().min(1).max(255),
  name: z.string().min(1).max(255),
  identifier: z
    .string()
    .min(1)
    .max(12)
    .regex(/^[A-Za-z0-9_]+$/)
    .optional(),
  description: z.string().max(20_000).optional(),
});
const searchIssuesArgsSchema = projectArgsSchema.extend({
  query: z.string().min(1).optional(),
});
const prioritySchema = z.enum(["none", "low", "medium", "high", "urgent"]);
const createIssueArgsSchema = z.object({
  project_id: z.string().min(1),
  workspace_slug: z.string().min(1).nullish(),
  title: z.string().min(1).max(500),
  description: z.string().max(20000).optional(),
  priority: prioritySchema.optional(),
  structured_blocks: structuredBlocksSchema.optional(),
  idempotency_key: z.string().optional(),
});
const issueArgsSchema = z.object({
  issue_id: z.string().min(1),
  project_id: z.string().min(1),
  workspace_slug: z.string().min(1).nullish(),
});
const updateIssueArgsSchema = issueArgsSchema.extend({
  title: z.string().min(1).max(500).optional(),
  description: z.string().max(20000).optional(),
  priority: prioritySchema.optional(),
  structured_blocks: structuredBlocksSchema.optional(),
});
const structuredBlocksArgsSchema = issueArgsSchema.extend({
  structured_blocks: structuredBlocksSchema,
});
const moveIssueArgsSchema = issueArgsSchema.extend({
  state_id: z.string().min(1),
});
const commentArgsSchema = issueArgsSchema.extend({
  body: z.string().min(1).max(20000),
});
const attachFileArgsSchema = issueArgsSchema
  .extend({
    file_name: z
      .string()
      .min(1)
      .max(255)
      .refine((value) => !/[\\/\0]/.test(value), "file_name must not contain a path"),
    mime_type: z
      .string()
      .min(3)
      .max(255)
      .regex(/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/),
    content_base64: z
      .string()
      .min(1)
      .max(7_000_000)
      .regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
  })
  .superRefine((input, context) => {
    if (Buffer.byteLength(input.content_base64, "base64") > 5 * 1024 * 1024) {
      context.addIssue({
        code: "custom",
        path: ["content_base64"],
        message: "Decoded attachment must not exceed 5 MiB.",
      });
    }
  });
const ensureLabelsArgsSchema = projectArgsSchema.extend({
  labels: z
    .array(
      z.object({
        name: z.string().min(1).max(255),
        color: z
          .string()
          .regex(/^#[0-9a-fA-F]{6}$/)
          .optional(),
      })
    )
    .min(1)
    .max(50),
});
const labelsArgsSchema = issueArgsSchema.extend({
  label_ids: z.array(z.string().min(1)).default([]),
});
const assigneesArgsSchema = issueArgsSchema.extend({
  member_ids: z.array(z.string().min(1)).default([]),
});

export function getToolsForSession(session: AgentSessionRecord): McpToolRuntimeDefinition[] {
  return mcpRuntimeTools.filter((tool) => tool.requiredScopes.every((scope) => hasScope(session, scope)));
}

export async function executeMcpTool(
  session: AgentSessionRecord,
  name: string,
  rawArguments: unknown,
  deps: ExecuteToolDeps,
  options: ExecuteToolOptions = {}
): Promise<McpToolResult> {
  const tool = mcpRuntimeTools.find((candidate) => candidate.name === name);

  if (!tool) {
    throw new Error(`Unknown MCP tool: ${name}`);
  }

  const { args, idempotencyKey } = prepareToolArguments(rawArguments, options.idempotencyKey);
  const isWriteTool = tool.annotations?.readOnlyHint !== true;

  if (!isWriteTool) {
    return executeMcpToolOnce(session, name, args, deps);
  }

  if (!deps.agentsRepository) {
    throw new ToolExecutionInputError("idempotency_unavailable", "Agent Gateway persistence is required for write tools.", 503);
  }

  if (!idempotencyKey) {
    throw new ToolExecutionInputError("idempotency_key_required", "Write tools require an idempotency key.", 400);
  }

  const requestHash = hashToolRequest(name, args);
  const claim = await deps.agentsRepository.claimIdempotencyKey(session.agent.id, idempotencyKey, requestHash);

  if (claim.status === "replay") {
    await deps.agentsRepository.createAuditEvent(session.agent.id, "agent.tool.replayed", session.agent.ownerUserId, {
      source: options.source ?? "mcp",
      toolName: name,
      idempotencyKey,
    });
    return claim.responseBody as McpToolResult;
  }

  if (claim.status === "conflict") {
    throw new ToolExecutionInputError("idempotency_key_conflict", "Idempotency key was already used with different arguments.", 409);
  }

  if (claim.status === "in_progress") {
    throw new ToolExecutionInputError("idempotency_key_in_progress", "Idempotency key is currently processing.", 409, {
      lockedUntil: claim.lockedUntil,
    });
  }

  try {
    const result = await executeMcpToolOnce(session, name, args, deps);
    await deps.agentsRepository.completeIdempotencyKey(session.agent.id, idempotencyKey, result);
    await deps.agentsRepository.createAuditEvent(session.agent.id, "agent.tool.executed", session.agent.ownerUserId, {
      source: options.source ?? "mcp",
      toolName: name,
      idempotencyKey,
      arguments: summarizeToolArguments(args),
    });
    return result;
  } catch (error) {
    await deps.agentsRepository.releaseIdempotencyKey(session.agent.id, idempotencyKey);
    await deps.agentsRepository.createAuditEvent(session.agent.id, "agent.tool.failed", session.agent.ownerUserId, {
      source: options.source ?? "mcp",
      toolName: name,
      idempotencyKey,
      error: error instanceof Error ? error.name : "unknown_error",
      message: error instanceof Error ? error.message : "Unknown tool execution error.",
    });
    throw error;
  }
}

async function executeMcpToolOnce(
  session: AgentSessionRecord,
  name: string,
  args: unknown,
  deps: ExecuteToolDeps
): Promise<McpToolResult> {

  switch (name) {
    case "tasker_get_agent_instructions":
      emptyArgsSchema.parse(args);
      requireScope(session, "workspace:read");
      return asToolResult(buildAgentInstructions(session));
    case "tasker_list_projects":
      emptyArgsSchema.parse(args);
      requireScope(session, "project:read");
      return asToolResult(await deps.taskerClient.listGrantedProjects(session));
    case "tasker_get_project_context": {
      const input = projectArgsSchema.parse(args);
      requireScope(session, "project:read");
      requireProjectGrant(session, { projectId: input.project_id, workspaceSlug: input.workspace_slug });
      return asToolResult(await deps.taskerClient.getProjectContext(session, input.project_id, input.workspace_slug));
    }
    case "tasker_create_project": {
      const input = createProjectArgsSchema.parse(args);
      if (session.grantSource !== "agent") {
        throw new ForbiddenError("Project creation requires an agent-scoped local token.");
      }
      const sourceGrant = requireWorkspaceScope(session, "project:create", input.workspace_slug);
      if (!deps.agentsRepository) {
        throw new ToolExecutionInputError("agent_repository_unavailable", "Agent Gateway persistence is required.", 503);
      }
      const payload = await deps.taskerClient.createProject(session, input);
      const createdProject = extractCreatedProject(payload);
      await deps.agentsRepository.upsertGrant(session.agent.id, {
        workspaceSlug: input.workspace_slug,
        projectId: createdProject.id,
        scopes: sourceGrant.scopes,
        mode: sourceGrant.mode,
        createdByUserId: session.agent.ownerUserId,
      });
      return asToolResult(payload);
    }
    case "tasker_search_issues": {
      const input = searchIssuesArgsSchema.parse(args);
      requireToolAccess(session, "issue:read", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.listIssues(session, input.project_id, input.workspace_slug, input.query));
    }
    case "tasker_get_issue": {
      const input = issueArgsSchema.parse(args);
      requireToolAccess(session, "issue:read", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.getIssue(session, input.issue_id, input));
    }
    case "tasker_create_issue": {
      const input = createIssueArgsSchema.parse(args);
      requireToolAccess(session, "issue:create", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.createIssue(session, input));
    }
    case "tasker_update_issue": {
      const input = updateIssueArgsSchema.parse(args);
      if (input.structured_blocks) {
        requireProjectScopes(session, input.project_id, input.workspace_slug, ["issue:update", "issue:structured_blocks:write"]);
      } else {
        requireToolAccess(session, "issue:update", input.project_id, input.workspace_slug);
      }
      return asToolResult(await deps.taskerClient.updateIssue(session, input.issue_id, input));
    }
    case "tasker_update_structured_blocks": {
      const input = structuredBlocksArgsSchema.parse(args);
      requireProjectScopes(session, input.project_id, input.workspace_slug, ["issue:update", "issue:structured_blocks:write"]);
      return asToolResult(
        await deps.taskerClient.updateIssue(session, input.issue_id, {
          project_id: input.project_id,
          workspace_slug: input.workspace_slug,
          structured_blocks: input.structured_blocks,
        })
      );
    }
    case "tasker_move_issue": {
      const input = moveIssueArgsSchema.parse(args);
      requireToolAccess(session, "issue:move", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.moveIssue(session, input.issue_id, input));
    }
    case "tasker_append_comment": {
      const input = commentArgsSchema.parse(args);
      requireToolAccess(session, "issue:comment", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.appendComment(session, input.issue_id, input));
    }
    case "tasker_attach_file": {
      const input = attachFileArgsSchema.parse(args);
      requireToolAccess(session, "issue:attachment:write", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.attachFile(session, input.issue_id, input));
    }
    case "tasker_ensure_labels": {
      const input = ensureLabelsArgsSchema.parse(args);
      requireToolAccess(session, "issue:label", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.ensureLabels(session, input));
    }
    case "tasker_set_issue_labels": {
      const input = labelsArgsSchema.parse(args);
      requireToolAccess(session, "issue:label", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.setLabels(session, input.issue_id, input));
    }
    case "tasker_assign_issue": {
      const input = assigneesArgsSchema.parse(args);
      requireToolAccess(session, "issue:assign", input.project_id, input.workspace_slug);
      return asToolResult(await deps.taskerClient.assignIssue(session, input.issue_id, input));
    }
    default:
      throw new Error(`Unknown MCP tool: ${name}`);
  }
}

function hasScope(session: AgentSessionRecord, scope: AgentScope): boolean {
  return session.grants.some((grant) => grant.scopes.includes(scope));
}

function requireToolAccess(session: AgentSessionRecord, scope: AgentScope, projectId: string, workspaceSlug?: string | null): void {
  requireProjectScopes(session, projectId, workspaceSlug, [scope]);
}

function requireWorkspaceScope(session: AgentSessionRecord, scope: AgentScope, workspaceSlug: string) {
  requireScope(session, scope);
  const grant = session.grants.find(
    (candidate) => candidate.workspaceSlug === workspaceSlug && candidate.scopes.includes(scope)
  );
  if (!grant) {
    throw new ForbiddenError(`Grant for workspace does not include required scope: ${scope}.`);
  }
  return grant;
}

function requireProjectScopes(
  session: AgentSessionRecord,
  projectId: string,
  workspaceSlug: string | null | undefined,
  scopes: AgentScope[]
): void {
  for (const scope of scopes) {
    requireScope(session, scope);
  }

  const grant = requireProjectGrant(session, { projectId, workspaceSlug });

  for (const scope of scopes) {
    if (!grant.scopes.includes(scope)) {
      throw new ForbiddenError(`Grant for project does not include required scope: ${scope}.`);
    }
  }
}

function asToolResult(payload: unknown): McpToolResult {
  return {
    content: [
      {
        type: "text",
        text: JSON.stringify(payload, null, 2),
      },
    ],
    structuredContent: payload,
    isError: false,
  };
}

function extractCreatedProject(payload: unknown): { id: string } {
  if (!isPlainRecord(payload) || !isPlainRecord(payload.project) || typeof payload.project.id !== "string") {
    throw new ToolExecutionInputError(
      "tasker_project_response_invalid",
      "Tasker did not return the created project id.",
      502
    );
  }
  return { id: payload.project.id };
}

export class ToolExecutionInputError extends Error {
  constructor(
    readonly code: string,
    message: string,
    readonly httpStatus: number,
    readonly details?: Record<string, unknown>
  ) {
    super(message);
    this.name = "ToolExecutionInputError";
  }
}

function prepareToolArguments(rawArguments: unknown, headerIdempotencyKey?: string | null): { args: unknown; idempotencyKey: string | null } {
  if (!isPlainRecord(rawArguments)) {
    return {
      args: rawArguments ?? {},
      idempotencyKey: normalizeIdempotencyKey(headerIdempotencyKey),
    };
  }

  const { idempotency_key: bodyIdempotencyKey, ...args } = rawArguments;

  return {
    args,
    idempotencyKey: normalizeIdempotencyKey(headerIdempotencyKey ?? (typeof bodyIdempotencyKey === "string" ? bodyIdempotencyKey : null)),
  };
}

function normalizeIdempotencyKey(value?: string | null): string | null {
  const normalized = value?.trim();

  if (!normalized) {
    return null;
  }

  if (normalized.length > 200) {
    throw new ToolExecutionInputError("idempotency_key_invalid", "Idempotency key must be 200 characters or fewer.", 400);
  }

  return normalized;
}

function hashToolRequest(name: string, args: unknown): string {
  return createHash("sha256").update(stableStringify({ name, args })).digest("hex");
}

function stableStringify(value: unknown): string {
  if (Array.isArray(value)) {
    return `[${value.map((item) => stableStringify(item)).join(",")}]`;
  }

  if (isPlainRecord(value)) {
    return `{${Object.keys(value)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
      .join(",")}}`;
  }

  return JSON.stringify(value);
}

function summarizeToolArguments(args: unknown): Record<string, unknown> {
  if (!isPlainRecord(args)) {
    return {};
  }

  return {
    workspace_slug: args.workspace_slug,
    project_id: args.project_id,
    issue_id: args.issue_id,
    state_id: args.state_id,
    project_name: args.name,
    file_name: args.file_name,
    attachment_bytes:
      typeof args.content_base64 === "string" ? Buffer.byteLength(args.content_base64, "base64") : undefined,
    labels_count: Array.isArray(args.labels) ? args.labels.length : undefined,
    has_structured_blocks: Array.isArray(args.structured_blocks),
  };
}

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function buildAgentInstructions(session: AgentSessionRecord): Record<string, unknown> {
  return {
    agent: {
      id: session.agent.id,
      display_name: session.agent.displayName,
      owner_user_id: session.agent.ownerUserId,
    },
    grants: session.grants.map((grant) => ({
      workspace_slug: grant.workspaceSlug,
      project_id: grant.projectId,
      mode: grant.mode,
      scopes: grant.scopes,
    })),
    grant_source: session.grantSource,
    rules: {
      card_structure: [
        "Keep the main issue description concise and conceptual.",
        "Use structured text blocks for current architecture, planned architecture, and implementation notes.",
        "Every structured text block must use its title field for the heading; do not duplicate markdown headings like ## Status inside the body.",
        "Wrong structured text block: title omitted and body starts with ## Текущая архитектура. Correct: title is Текущая архитектура and body contains only the section content.",
        "Use checker blocks with explicit titles for short verifiable phase items.",
        "After implementation, add a factual implementation block with files touched and validation performed.",
      ],
      labels: [
        "Use tasker_ensure_labels before setting a label that is not already present in project context.",
        "Use tasker_set_issue_labels with label ids returned by project context or tasker_ensure_labels.",
      ],
      hard_limits: [
        "Do not delete or archive issues.",
        "Create projects only through tasker_create_project when the effective workspace grant includes project:create.",
        "Attach files only through tasker_attach_file; never upload from arbitrary URLs or server paths.",
        "Do not create states, workspace invites, or workspace settings changes.",
        "Only create labels through tasker_ensure_labels inside granted projects.",
        "Only assign existing project members.",
        "Only use projects and workspaces present in effective grants.",
      ],
      reporting_mode: "If a grant has mode=reporting, keep issue status and comments up to date without pretending to enforce unmanaged local Codex execution.",
    },
  };
}
