// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only

import { SIDEKICK_PACK_ID, SIDEKICK_PACK_VERSION, SIDEKICK_POLICY_ID_PREFIX } from './constants.js';
import {
  MANIFEST_POLICY_DECISIONS,
  MANIFEST_POLICY_TRIGGERS,
  SidekickManifestSchema,
  type SidekickManifestTool,
  type SidekickPackManifest,
} from './manifest-schema.js';
import {
  TOOL_PERMISSIONS,
  TOOL_SCOPE_ACCESS,
  TOOL_SCOPE_TYPES,
  type PathScope,
  type ToolPermission,
} from './tools/types.js';
import { SIDEKICK_EVENT_KIND_VALUES } from './sidekick-events.js';

// ---------------------------------------------------------------------------
// Scope constants
// ---------------------------------------------------------------------------

const SIDEKICK_SCOPE_READ: PathScope = {
  type: TOOL_SCOPE_TYPES.PATH,
  pattern: '.sidekick/**',
  access: TOOL_SCOPE_ACCESS.READ,
};

const SIDEKICK_SCOPE_WRITE: PathScope = {
  type: TOOL_SCOPE_TYPES.PATH,
  pattern: '.sidekick/**',
  access: TOOL_SCOPE_ACCESS.WRITE,
};

// ---------------------------------------------------------------------------
// Sidekick tool contract
// ---------------------------------------------------------------------------

const TOOL_PERMISSIONS_MAP = {
  // Task tools (6)
  'task:create': TOOL_PERMISSIONS.WRITE,
  'task:list': TOOL_PERMISSIONS.READ,
  'task:update': TOOL_PERMISSIONS.WRITE,
  'task:cancel': TOOL_PERMISSIONS.ADMIN,
  'task:complete': TOOL_PERMISSIONS.WRITE,
  'task:schedule': TOOL_PERMISSIONS.WRITE,
  // Memory tools (4)
  'memory:store': TOOL_PERMISSIONS.WRITE,
  'memory:recall': TOOL_PERMISSIONS.READ,
  'memory:update': TOOL_PERMISSIONS.WRITE,
  'memory:forget': TOOL_PERMISSIONS.ADMIN,
  // Channel tools (5)
  'channel:configure': TOOL_PERMISSIONS.WRITE,
  'channel:list': TOOL_PERMISSIONS.READ,
  'channel:delete': TOOL_PERMISSIONS.ADMIN,
  'channel:send': TOOL_PERMISSIONS.WRITE,
  'channel:receive': TOOL_PERMISSIONS.READ,
  // Routine tools (5)
  'routine:create': TOOL_PERMISSIONS.WRITE,
  'routine:list': TOOL_PERMISSIONS.READ,
  'routine:update': TOOL_PERMISSIONS.WRITE,
  'routine:delete': TOOL_PERMISSIONS.ADMIN,
  'routine:run': TOOL_PERMISSIONS.READ, // plan-only, no execution
  // WU-2738 (INIT-060, ADR-013 §6 governance): plan-to-commit flow routed
  // through agent:execute-turn. Runtime-callable tool, no top-level
  // surface -- the governed dispatch is the only path to commit a plan.
  'sidekick:routine:commit_plan': TOOL_PERMISSIONS.WRITE,
  // System tools (3)
  'sidekick:init': TOOL_PERMISSIONS.WRITE,
  'sidekick:status': TOOL_PERMISSIONS.READ,
  'sidekick:export': TOOL_PERMISSIONS.READ, // returns data, no file write
} as const satisfies Record<string, ToolPermission>;

type SidekickToolName = keyof typeof TOOL_PERMISSIONS_MAP;

const TASK_TOOLS_ENTRY = 'tool-impl/task-tools.ts';
const MEMORY_TOOLS_ENTRY = 'tool-impl/memory-tools.ts';
const CHANNEL_TOOLS_ENTRY = 'tool-impl/channel-tools.ts';
const ROUTINE_TOOLS_ENTRY = 'tool-impl/routine-tools.ts';
const ROUTINE_COMMIT_TOOL_ENTRY = 'tool-impl/routine-commit.ts';
const SYSTEM_TOOLS_ENTRY = 'tool-impl/system-tools.ts';
const POLICY_FACTORY_ENTRY = 'policy-factory.ts#createSidekickPolicyFactory';

const TOOL_ENTRIES: Record<SidekickToolName, string> = {
  'task:create': TASK_TOOLS_ENTRY,
  'task:list': TASK_TOOLS_ENTRY,
  'task:update': TASK_TOOLS_ENTRY,
  'task:cancel': TASK_TOOLS_ENTRY,
  'task:complete': TASK_TOOLS_ENTRY,
  'task:schedule': TASK_TOOLS_ENTRY,
  'memory:store': MEMORY_TOOLS_ENTRY,
  'memory:recall': MEMORY_TOOLS_ENTRY,
  'memory:update': MEMORY_TOOLS_ENTRY,
  'memory:forget': MEMORY_TOOLS_ENTRY,
  'channel:configure': CHANNEL_TOOLS_ENTRY,
  'channel:list': CHANNEL_TOOLS_ENTRY,
  'channel:delete': CHANNEL_TOOLS_ENTRY,
  'channel:send': CHANNEL_TOOLS_ENTRY,
  'channel:receive': CHANNEL_TOOLS_ENTRY,
  'routine:create': ROUTINE_TOOLS_ENTRY,
  'routine:list': ROUTINE_TOOLS_ENTRY,
  'routine:update': ROUTINE_TOOLS_ENTRY,
  'routine:delete': ROUTINE_TOOLS_ENTRY,
  'routine:run': ROUTINE_TOOLS_ENTRY,
  'sidekick:routine:commit_plan': ROUTINE_COMMIT_TOOL_ENTRY,
  'sidekick:init': SYSTEM_TOOLS_ENTRY,
  'sidekick:status': SYSTEM_TOOLS_ENTRY,
  'sidekick:export': SYSTEM_TOOLS_ENTRY,
};

// ---------------------------------------------------------------------------
// Input schemas (JSON Schema objects)
// ---------------------------------------------------------------------------

const TOOL_INPUT_SCHEMAS: Record<SidekickToolName, Record<string, unknown>> = {
  'task:create': {
    type: 'object',
    properties: {
      title: { type: 'string', minLength: 1 },
      description: { type: 'string' },
      priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
      due_at: { type: 'string' },
      tags: { type: 'array', items: { type: 'string' } },
      dry_run: { type: 'boolean' },
    },
    required: ['title'],
    additionalProperties: false,
  },
  'task:list': {
    type: 'object',
    properties: {
      status: { type: 'string', enum: ['pending', 'done', 'canceled'] },
      priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
      tags: { type: 'array', items: { type: 'string' } },
      search: { type: 'string' },
      due_before: { type: 'string' },
      limit: { type: 'integer', minimum: 1 },
    },
    additionalProperties: false,
  },
  'task:update': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      title: { type: 'string', minLength: 1 },
      description: { type: 'string' },
      priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
      tags: { type: 'array', items: { type: 'string' } },
      due_at: { type: 'string' },
      cron: { type: 'string' },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'task:cancel': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'task:complete': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      note: { type: 'string' },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'task:schedule': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      due_at: { type: 'string' },
      cron: { type: 'string' },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'memory:store': {
    type: 'object',
    properties: {
      type: { type: 'string', enum: ['fact', 'preference', 'note', 'snippet'] },
      content: { type: 'string', minLength: 1 },
      tags: { type: 'array', items: { type: 'string' } },
      dry_run: { type: 'boolean' },
    },
    required: ['type', 'content'],
    additionalProperties: false,
  },
  'memory:recall': {
    type: 'object',
    properties: {
      query: { type: 'string' },
      type: { type: 'string', enum: ['fact', 'preference', 'note', 'snippet'] },
      tags: { type: 'array', items: { type: 'string' } },
      limit: { type: 'integer', minimum: 1 },
    },
    additionalProperties: false,
  },
  'memory:forget': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'memory:update': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      type: { type: 'string', enum: ['fact', 'preference', 'note', 'snippet'] },
      content: { type: 'string', minLength: 1 },
      tags: { type: 'array', items: { type: 'string' } },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'channel:configure': {
    type: 'object',
    properties: {
      name: { type: 'string', minLength: 1 },
      type: { type: 'string', enum: ['terminal'] },
      dry_run: { type: 'boolean' },
    },
    required: ['name'],
    additionalProperties: false,
  },
  'channel:list': {
    type: 'object',
    properties: {},
    additionalProperties: false,
  },
  'channel:delete': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'channel:send': {
    type: 'object',
    properties: {
      provider: { type: 'string' },
      channel: { type: 'string' },
      content: { type: 'string', minLength: 1 },
      sender: { type: 'string' },
      metadata: { type: 'object', additionalProperties: true },
      dry_run: { type: 'boolean' },
    },
    required: ['content'],
    additionalProperties: false,
  },
  'channel:receive': {
    type: 'object',
    properties: {
      provider: { type: 'string' },
      channel: { type: 'string' },
      cursor: { type: 'string' },
      limit: { type: 'integer', minimum: 1 },
      since: { type: 'string' },
      metadata: { type: 'object', additionalProperties: true },
    },
    additionalProperties: false,
  },
  'routine:create': {
    type: 'object',
    properties: {
      name: { type: 'string', minLength: 1 },
      steps: {
        type: 'array',
        minItems: 1,
        items: {
          type: 'object',
          properties: {
            tool: { type: 'string', minLength: 1 },
            input: { type: 'object', additionalProperties: true },
          },
          required: ['tool'],
          additionalProperties: false,
        },
      },
      cron: { type: 'string' },
      enabled: { type: 'boolean' },
      dry_run: { type: 'boolean' },
    },
    required: ['name', 'steps'],
    additionalProperties: false,
  },
  'routine:list': {
    type: 'object',
    properties: {
      enabled_only: { type: 'boolean' },
      limit: { type: 'integer', minimum: 1 },
    },
    additionalProperties: false,
  },
  'routine:update': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      name: { type: 'string', minLength: 1 },
      steps: {
        type: 'array',
        minItems: 1,
        items: {
          type: 'object',
          properties: {
            tool: { type: 'string', minLength: 1 },
            input: { type: 'object', additionalProperties: true },
          },
          required: ['tool'],
          additionalProperties: false,
        },
      },
      cron: { type: 'string' },
      enabled: { type: 'boolean' },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'routine:delete': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
      dry_run: { type: 'boolean' },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'routine:run': {
    type: 'object',
    properties: {
      id: { type: 'string', minLength: 1 },
    },
    required: ['id'],
    additionalProperties: false,
  },
  'sidekick:routine:commit_plan': {
    type: 'object',
    properties: {
      plan_id: { type: 'string', minLength: 1 },
      attestation: {
        type: 'object',
        properties: {
          actor: { type: 'string', minLength: 1 },
          reason: { type: 'string', minLength: 1 },
        },
        required: ['actor', 'reason'],
        additionalProperties: true,
      },
    },
    required: ['plan_id', 'attestation'],
    additionalProperties: false,
  },
  'sidekick:init': {
    type: 'object',
    properties: {},
    additionalProperties: false,
  },
  'sidekick:status': {
    type: 'object',
    properties: {},
    additionalProperties: false,
  },
  'sidekick:export': {
    type: 'object',
    properties: {
      include_audit: { type: 'boolean' },
    },
    additionalProperties: false,
  },
};

// ---------------------------------------------------------------------------
// Generic output schema
// ---------------------------------------------------------------------------

const GENERIC_OUTPUT_SCHEMA: Record<string, unknown> = {
  type: 'object',
  properties: {
    success: { type: 'boolean' },
    data: { type: 'object', additionalProperties: true },
    error: { type: 'object', additionalProperties: true },
  },
  additionalProperties: true,
};

// ---------------------------------------------------------------------------
// Builder helpers
// ---------------------------------------------------------------------------

function resolveRequiredScopes(permission: ToolPermission): PathScope[] {
  if (permission === TOOL_PERMISSIONS.READ) {
    return [SIDEKICK_SCOPE_READ];
  }
  return [SIDEKICK_SCOPE_READ, SIDEKICK_SCOPE_WRITE];
}

// WU-2738 (INIT-060, ADR-013 §6 governance): tool-level approval surface
// declarations. Empty array means the default policy permits dispatch; a
// populated array signals the cloud conductor to render an approval prompt
// before the governed `agent:execute-turn` dispatch invokes the tool.
const TOOL_REQUIRED_APPROVALS: Partial<Record<SidekickToolName, string[]>> = {
  'sidekick:routine:commit_plan': [],
};

function buildTool(name: SidekickToolName): SidekickManifestTool {
  const permission = TOOL_PERMISSIONS_MAP[name];
  const requiredApprovals = TOOL_REQUIRED_APPROVALS[name];
  return {
    name,
    entry: TOOL_ENTRIES[name],
    permission,
    required_scopes: resolveRequiredScopes(permission),
    input_schema: TOOL_INPUT_SCHEMAS[name],
    output_schema: GENERIC_OUTPUT_SCHEMA,
    ...(requiredApprovals !== undefined ? { required_approvals: requiredApprovals } : {}),
  };
}

// ---------------------------------------------------------------------------
// Exported manifest
// ---------------------------------------------------------------------------

export const SIDEKICK_TOOL_NAMES = Object.keys(TOOL_PERMISSIONS_MAP) as SidekickToolName[];

/**
 * WU-2780 (ADR-013 §6 governance): tool names the sidekick pack forbids from
 * the top-level remote surface (POST /tools/:name). These tools remain
 * registered as runtime-callable — `agent:execute-turn` dispatches them via
 * the governed manifest path — but any HTTP allowlist that includes them
 * MUST be rejected fail-closed. ADR-013 §6 explicitly names `channel.send`:
 * "The sidekick pack does NOT expose `channel.send` as a top-level surface
 * the agent can call outside a turn. It is registered only as a
 * runtime-callable tool."
 */
const ADR_013_SECTION_6_RUNTIME_ONLY_TOOLS: readonly SidekickToolName[] = ['channel:send'] as const;

/**
 * WU-2780 (INIT-060, ADR-013 §6): sidekick pack tools that MAY be exposed on
 * the HTTP tool-api surface (POST /tools/:name). Mirrors the
 * `REMOTE_CALLABLE_TOOLS` export pattern in sibling packs (software-delivery,
 * agent-runtime) so integrators have a single source of truth when wiring
 * the surface allowlist. ADR-013 §6 runtime-only tools are excluded by
 * construction; the HTTP tool-api also enforces this fail-closed at router
 * construction.
 */
export const SIDEKICK_REMOTE_CALLABLE_TOOLS: readonly SidekickToolName[] =
  SIDEKICK_TOOL_NAMES.filter(
    (name): name is SidekickToolName => !ADR_013_SECTION_6_RUNTIME_ONLY_TOOLS.includes(name),
  );

/**
 * WU-2780 (INIT-060, ADR-013 §6): returns a fresh copy of the ordered
 * remote-callable tool name list. Keeps the export immutable for callers
 * that prefer arrays over readonly tuples.
 */
export function getSidekickRemoteCallableToolNames(): SidekickToolName[] {
  return [...SIDEKICK_REMOTE_CALLABLE_TOOLS];
}

const SIDEKICK_MANIFEST_TEMPLATE = {
  id: SIDEKICK_PACK_ID,
  version: SIDEKICK_PACK_VERSION,
  config_key: SIDEKICK_PACK_ID,
  policy_factory: POLICY_FACTORY_ENTRY,
  task_types: ['sidekick'],
  tools: SIDEKICK_TOOL_NAMES.map((name) => buildTool(name)),
  policies: [
    {
      id: `${SIDEKICK_POLICY_ID_PREFIX}.default`,
      trigger: MANIFEST_POLICY_TRIGGERS.ON_TOOL_REQUEST,
      decision: MANIFEST_POLICY_DECISIONS.ALLOW,
      reason: 'Default sidekick policy permits declared tools within scoped access.',
    },
  ],
  evidence_types: ['sidekick.audited.tool-call'],
  emitted_event_kinds: [...SIDEKICK_EVENT_KIND_VALUES],
  state_aliases: {},
  lane_templates: [],
  // WU-2735 (INIT-060 WU-7a, ADR-013 §ChannelBridge): the sidekick pack
  // requires the `sidekick-channel` transport surface. The kernel refuses
  // to activate the pack if the surface isn't registered at startup.
  surfaces_required: ['sidekick-channel'],
};

export const SIDEKICK_MANIFEST: SidekickPackManifest = SidekickManifestSchema.parse(
  SIDEKICK_MANIFEST_TEMPLATE,
);

export function getSidekickManifestToolByName(name: string): SidekickManifestTool | undefined {
  return SIDEKICK_MANIFEST.tools.find((tool) => tool.name === name);
}

export function getSidekickToolCount(): number {
  return SIDEKICK_MANIFEST.tools.length;
}
