/**
 * RequestPackage — META_TOOL for the agent-initiated Python package request.
 *
 * When an agent needs a Python wheel that is neither preinstalled nor already
 * in the workspace overlay, it calls RequestPackage({ name, reason }). The
 * request is recorded so it surfaces in the task detail; when the run can reach
 * a user (interactive) the movement PARKS for approval. A task-write approver
 * (owner / admin / space editor) then approves — which installs the wheel into
 * THIS workspace's per-space overlay (reusing PR2's installSpacePackages:
 * bwrap-isolated, wheels-only, fixed index, stdlib/preinstalled shadowing
 * guards) and resumes the parked job so the movement re-runs with the package
 * importable. The agent never installs anything itself.
 *
 * Gating (all runtime, so the tool def stays unconditional — see the
 * concurrency invariant in tools/index.ts):
 *   - `ctx.recordPackageRequest` unset  → feature unavailable for this run
 *     (disabled globally, or no resolvable space) → record nothing, proceed.
 *   - name already in `ctx.installedPackages` → already installed, just import.
 *   - `ctx.toolApprovalInteractive`     → park (pendingPackageApproval).
 *   - otherwise (subtask / headless)    → record auto_denied, proceed without.
 *
 * The spec is validated (parseAndValidateSpec + assertNotShadowing) HERE and
 * re-validated at approval time — the DB stores the canonical spec string.
 */

import { ToolDef } from '../../llm/openai-compat.js';
import type { ToolContext, ToolResult } from './core.js';
import { parseAndValidateSpec, assertNotShadowing } from '../python-packages.js';

const REQUEST_PACKAGE_DEF: ToolDef = {
  type: 'function',
  function: {
    name: 'RequestPackage',
    description:
      'preinstalled に無い Python ライブラリ(wheel)が必要なとき name（例: requests または requests==2.32.3）と理由(reason)を渡して申請する。ユーザーが応答できる実行では承認を求め、承認されればこのワークスペースに入り import できる。詳細は ReadToolDoc({ name: "RequestPackage" })。',
    parameters: {
      type: 'object',
      properties: {
        name: { type: 'string', description: 'パッケージ名。厳密固定は "name==version"（例: pandas==2.2.2）。バージョン省略可。' },
        reason: { type: 'string', description: 'なぜそのパッケージが必要か（具体的に）。' },
      },
      required: ['name', 'reason'],
    },
  },
};

export const TOOL_DEFS: Record<string, ToolDef> = {
  RequestPackage: REQUEST_PACKAGE_DEF,
};

export async function executeTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult | null> {
  if (name !== 'RequestPackage') return null;

  const pkgName = typeof input['name'] === 'string' ? input['name'].trim() : '';
  const reason = typeof input['reason'] === 'string' ? input['reason'].trim() : '';
  if (!pkgName) return { output: 'RequestPackage: name は必須です。', isError: true };
  if (!reason) return { output: 'RequestPackage: reason（なぜ必要か）は必須です。', isError: true };

  // Validate the spec (grammar + shadowing) up front. A HARD ERROR so the agent
  // does not mistake a rejected spec for a pending approval.
  let parsed;
  try {
    parsed = parseAndValidateSpec(pkgName);
    assertNotShadowing(parsed.normalizedName);
  } catch (e) {
    return {
      output:
        `"${pkgName}" は申請できません: ${(e as Error).message}。` +
        ` 厳密固定 "name==version" のみ・標準ライブラリや同梱パッケージと同名は不可です。`,
      isError: true,
    };
  }

  // Feature unavailable for this run (disabled globally, or no resolvable space).
  // recordPackageRequest is bound by the worker only when both hold. `?.` would
  // be a silent no-op, so gate explicitly and DON'T claim it was recorded.
  if (!ctx.recordPackageRequest) {
    return {
      output:
        `Python パッケージの追加はこの実行では利用できません。` +
        ` 標準ライブラリや同梱パッケージで代替するか、達成できない場合は complete({status:"needs_user_input", missing_info:"..."}) でユーザーに依頼してください。`,
      isError: false,
    };
  }

  // Already declined for THIS job → don't re-park (deny loop guard). The user
  // already said no; proceed without it.
  if (ctx.declinedPackages?.includes(parsed.normalizedName)) {
    return {
      output:
        `"${parsed.normalizedName}" の追加は既にこのタスクで却下されています。再申請せず、このパッケージ無しで進めてください（達成できない場合は complete({status:"needs_user_input"})）。`,
      isError: false,
    };
  }

  // Already installed in this workspace overlay → nothing to approve.
  // Version-aware: a pinned request needs an exact spec match; a bare-name
  // request is satisfied by any installed version. A pinned request for a
  // DIFFERENT version than the one already pinned cannot be satisfied (only one
  // version per package), so short-circuit WITHOUT parking (the approve path
  // couldn't install it anyway) and tell the agent to use the existing one.
  const sameName = (ctx.installedPackages ?? []).filter((p) => p.name === parsed.normalizedName);
  if (sameName.length > 0) {
    const requestIsPinned = parsed.spec.includes('==');
    const exact = sameName.some((p) => p.spec === parsed.spec);
    if (exact || !requestIsPinned) {
      return {
        output: `"${parsed.normalizedName}" は既にこのワークスペースにインストール済みです（${sameName[0].spec}）。そのまま import して使ってください。`,
        isError: false,
      };
    }
    return {
      output:
        `"${parsed.normalizedName}" は既に ${sameName[0].spec} でこのワークスペースに固定されています。` +
        ` 1 パッケージにつき 1 バージョンのみです。別バージョンが必要なら管理者が 設定 → Python で入れ替える必要があります。今は既存版で進めてください。`,
      isError: false,
    };
  }

  // Interactive run: pause for inline approval. On approve the wheel is
  // installed into the space overlay and the job resumes.
  if (ctx.toolApprovalInteractive) {
    ctx.recordPackageRequest({ spec: parsed.spec, normalizedName: parsed.normalizedName, reason, status: 'pending' });
    ctx.pendingPackageApproval = { spec: parsed.spec };
    return {
      output:
        `"${parsed.spec}" のインストール承認を待っています。` +
        ` 承認されればこのワークスペースに追加され import して続行でき、拒否されればそのパッケージ無しで進めることになります。`,
      isError: false,
    };
  }

  // Non-interactive (subtask / headless): record so the user finds it later,
  // and proceed without the package.
  ctx.recordPackageRequest({ spec: parsed.spec, normalizedName: parsed.normalizedName, reason, status: 'auto_denied' });
  return {
    output:
      `"${parsed.spec}" のインストール要求を記録しました（この実行では自動で承認できません）。` +
      ` このパッケージ無しで進めるか、達成できない場合は complete({status:"needs_user_input", missing_info:"..."}) でユーザーに依頼してください。`,
    isError: false,
  };
}
