import { roleFromAgentId } from "../_internal/report-reconcile.ts";
import {
  invokeGeminiWorkspaceTool,
  isGeminiWorkspaceTool,
} from "./geminiWorkspaceTools.ts";
import {
  invokeFcopToolOnce,
  resolveOneShotPythonBin,
} from "./FcopMcpOneShot.ts";
import {
  GoogleToolAuditLogger,
  classifyGoogleToolLayer,
  hashGoogleToolArgs,
} from "./GoogleToolAuditLogger.ts";
import {
  buildGoogleVisionToolDeclarations,
  invokeGoogleVisionTool,
  isGoogleVisionTool,
} from "./googleVisionTools.ts";
import { applyPmHeavyToolWrapper } from "./pmHeavyToolWrapper.ts";
import { normalizeWriteReportTaskIdPrefix } from "./geminiWriteReportTaskIdGuard.ts";
import { normalizeRelationList } from "../ledger/frontmatter.ts";
import {
  invokePmRuntimeControlTool,
  isPmRuntimeControlTool,
} from "../pm/PmRuntimeControlTools.ts";
import {
  invokeRuntimeTaskControlTool,
  isRuntimeTaskControlTool,
} from "../scheduler/RuntimeTaskControlTools.ts";
import {
  evaluateNativeOperationBoundary,
  OPERATION_APPROVAL_REQUIRED,
  UniversalApprovalStore,
} from "../approval/index.ts";

export type GoogleToolCallRouterInput = {
  projectRoot: string;
  agentId: string;
  toolName: string;
  args: Record<string, unknown>;
  callId: string;
  useFcopOneShot: boolean;
  mcpClient?: {
    callTool?: (name: string, args: Record<string, unknown>) => Promise<unknown>;
  };
  pythonBin?: string;
  auditLogger?: GoogleToolAuditLogger;
  traceId?: string;
  runId?: string;
  sessionId?: string;
  taskId?: string;
  threadKey?: string;
  /** Full session prompt for PM heavy-tool gating (patrol vs Hot Path). */
  promptRoutingText?: string;
  turnIndex?: number;
  visionClient?: { models?: { generateContent?: (req: unknown) => Promise<unknown> } };
  visionModel?: string;
};

export type GoogleToolCallRouterResult = {
  ok: boolean;
  output?: string;
  error?: string;
  code?: string;
  approval_id?: string;
  operation_digest?: string;
  operation_fingerprint?: string;
  operation_outcome?: Record<string, unknown>;
};

const globalAuditLogger = new GoogleToolAuditLogger();

export function getDefaultGoogleToolAuditLogger(): GoogleToolAuditLogger {
  return globalAuditLogger;
}

export { buildGoogleVisionToolDeclarations };

export async function routeGoogleToolCall(
  input: GoogleToolCallRouterInput,
): Promise<GoogleToolCallRouterResult> {
  const startedAt = new Date().toISOString();
  const t0 = Date.now();
  const role = roleFromAgentId(input.agentId);
  const layer = classifyGoogleToolLayer(input.toolName);
  const logger = input.auditLogger ?? globalAuditLogger;

  const operationBoundary = await evaluateNativeOperationBoundary({
    toolName: input.toolName,
    args: input.args,
    projectRoot: input.projectRoot,
    projectId:
      input.projectRoot.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ||
      "project",
    agentId: input.agentId,
    ...(input.sessionId ? { sessionId: input.sessionId } : {}),
    ...(input.taskId ? { taskId: input.taskId } : {}),
    ...(input.threadKey ? { threadKey: input.threadKey } : {}),
  });
  if (operationBoundary.decision !== "ALLOW") {
    let code = operationBoundary.decision === "ROLE_CAPABILITY_DENIED"
      ? "ROLE_CAPABILITY_DENIED"
      : operationBoundary.decision === "TOOL_REQUEST_INVALID"
        ? "TOOL_REQUEST_INVALID"
        : OPERATION_APPROVAL_REQUIRED;
    let error = operationBoundary.decision === "REQUIRE_APPROVAL"
      ? "operation approval is required"
      : operationBoundary.reason;
    let approvalId = "";
    let operationDigest = "";
    let operationFingerprint = "";
    let operationOutcome: Record<string, unknown> | undefined;
    if (operationBoundary.decision === "REQUIRE_APPROVAL") {
      const store = new UniversalApprovalStore(input.projectRoot);
      const created = store.createPending(operationBoundary.input);
      if (created.outcome === "APPROVAL_REQUIRED") {
        store.deliverAgentNotice(created.notice);
        store.validateWaitingProjection(created.notice);
        approvalId = created.approval.approval_id;
        operationDigest = created.approval.operation_digest;
        operationFingerprint = created.notice.operation_fingerprint;
        operationOutcome = {
          approval_id: created.approval.approval_id,
          approval_status: created.approval.status,
          operation_executed: false,
          agent_notice_delivered: true,
          project_id: created.notice.project_id,
          task_id: created.notice.task_id,
          thread_key: created.notice.thread_key,
          role: created.notice.role,
          operation_fingerprint: created.notice.operation_fingerprint,
          rule_ids: created.notice.rule_ids,
        };
        error = JSON.stringify(created.notice);
      } else if (created.outcome === "APPROVAL_CREATION_PENDING") {
        code = created.code;
        error = JSON.stringify({
          code: created.code,
          operation_executed: false,
          request_id: created.request_id,
          operation_fingerprint: created.operation_fingerprint,
          required_agent_action: "WAIT_FOR_APPROVAL_RECORD",
        });
        operationFingerprint = created.operation_fingerprint;
        operationOutcome = {
          operation_executed: false,
          request_id: created.request_id,
          retry_path: created.retry_path,
          error: created.error,
        };
      } else {
        code = "ALLOW";
      }
    }
    if (code === "ALLOW") {
      // A trusted frontend confirmation can resolve inside the universal
      // store; continue through the same router without manufacturing a wait.
    } else {
    const result: GoogleToolCallRouterResult = {
      ok: false,
      error,
      code,
      ...(approvalId ? { approval_id: approvalId } : {}),
      ...(operationDigest ? { operation_digest: operationDigest } : {}),
      ...(operationFingerprint ? { operation_fingerprint: operationFingerprint } : {}),
      ...(operationOutcome ? { operation_outcome: operationOutcome } : {}),
    };
    logger.log({
      agentId: input.agentId,
      toolName: input.toolName,
      ok: false,
      durationMs: Date.now() - t0,
      errorCode: result.code,
      summary: result.error,
      trace_id: input.traceId,
      run_id: input.runId,
      session_id: input.sessionId,
      task_id: input.taskId,
      role,
      tool_layer: layer,
      args_hash: hashGoogleToolArgs(input.args),
      started_at: startedAt,
      finished_at: new Date().toISOString(),
      status: "rejected",
      turn_index: input.turnIndex,
    });
    return result;
    }
  }

  const pmHeavy = applyPmHeavyToolWrapper(role, input.toolName, input.args, {
    taskId: input.taskId,
    promptRoutingText: input.promptRoutingText,
    agentId: input.agentId,
  });
  const relationNormalizedArgs =
    /^(?:write_task|write_report)$/i.test(input.toolName)
      ? {
          ...pmHeavy.args,
          ...(pmHeavy.args.references !== undefined
            ? { references: normalizeRelationList(pmHeavy.args.references) }
            : {}),
          ...(pmHeavy.args.depends_on !== undefined
            ? { depends_on: normalizeRelationList(pmHeavy.args.depends_on) }
            : {}),
        }
      : pmHeavy.args;
  const reportTaskRaw = String(relationNormalizedArgs.task_id ?? input.taskId ?? "");
  const reportTaskId =
    normalizeWriteReportTaskIdPrefix(reportTaskRaw) || reportTaskRaw.trim();
  const suppliedEvidenceRefs = Array.isArray(pmHeavy.args.evidence_refs)
    ? pmHeavy.args.evidence_refs.map(String).filter(Boolean)
    : [];
  const effectiveArgs =
    input.toolName === "write_report"
      ? {
          ...relationNormalizedArgs,
          ...(input.sessionId ? { session_id: input.sessionId } : {}),
          ...(input.runId ? { run_id: input.runId } : {}),
          runtime_bound: Boolean(input.sessionId),
          agent_id: input.agentId,
          caller_role: input.agentId,
          ...(input.threadKey ? { thread_key: input.threadKey } : {}),
          source: "runtime_tool_call",
          evidence_refs: [
            ...new Set([
              ...suppliedEvidenceRefs,
              input.sessionId,
              input.runId,
            ].filter((value): value is string => Boolean(value))),
          ],
          ...(reportTaskId ? { task_id: reportTaskId } : {}),
        }
      : relationNormalizedArgs;

  if (!pmHeavy.allowed && pmHeavy.skipMessage) {
    logger.log({
      agentId: input.agentId,
      toolName: input.toolName,
      ok: true,
      durationMs: Date.now() - t0,
      summary: pmHeavy.skipMessage.slice(0, 200),
      trace_id: input.traceId,
      run_id: input.runId,
      session_id: input.sessionId,
      task_id: input.taskId,
      role,
      tool_layer: layer,
      args_hash: hashGoogleToolArgs(effectiveArgs),
      started_at: startedAt,
      finished_at: new Date().toISOString(),
      status: "skipped",
      turn_index: input.turnIndex,
    });
    return { ok: true, output: pmHeavy.skipMessage };
  }

  try {
    let output: string;

    if (isPmRuntimeControlTool(input.toolName)) {
      output = JSON.stringify(
        await invokePmRuntimeControlTool({
          toolName: input.toolName,
          args: effectiveArgs,
          agentId: input.agentId,
          sessionId: input.sessionId,
        }),
      );
    } else if (isRuntimeTaskControlTool(input.toolName)) {
      output = JSON.stringify(
        await invokeRuntimeTaskControlTool({
          toolName: input.toolName,
          args: effectiveArgs,
          agentId: input.agentId,
          sessionId: input.sessionId ?? "",
        }),
      );
    } else if (isGoogleVisionTool(input.toolName)) {
      output = await invokeGoogleVisionTool(
        input.projectRoot,
        input.toolName,
        input.args,
        {
          visionClient: input.visionClient,
          visionModel: input.visionModel,
        },
      );
    } else if (isGeminiWorkspaceTool(input.toolName)) {
      output = await invokeGeminiWorkspaceTool(
        input.projectRoot,
        input.toolName,
        input.args,
      );
    } else if (input.useFcopOneShot) {
      const bin =
        input.pythonBin ??
        resolveOneShotPythonBin({
          command: process.env.PYTHON_BIN || "python",
          env: process.env as Record<string, string | undefined>,
        });
      output = await invokeFcopToolOnce(
        bin,
        input.projectRoot,
        input.toolName,
        effectiveArgs,
      );
    } else if (input.mcpClient?.callTool) {
      const raw = await input.mcpClient.callTool(input.toolName, effectiveArgs);
      output =
        typeof raw === "string"
          ? raw
          : JSON.stringify(raw ?? { ok: true, result: raw });
    } else {
      throw new Error(
        `No execution path for tool ${input.toolName} (one-shot off, no MCP client)`,
      );
    }

    logger.log({
      agentId: input.agentId,
      toolName: input.toolName,
      ok: true,
      durationMs: Date.now() - t0,
      summary: output.slice(0, 200),
      trace_id: input.traceId,
      run_id: input.runId,
      session_id: input.sessionId,
      task_id: input.taskId,
      role,
      tool_layer: layer,
      args_hash: hashGoogleToolArgs(effectiveArgs),
      started_at: startedAt,
      finished_at: new Date().toISOString(),
      status: "ok",
      turn_index: input.turnIndex,
    });

    return { ok: true, output };
  } catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    const code =
      err instanceof Error && "code" in err
        ? String((err as Error & { code?: string }).code)
        : "TOOL_FAILED";

    logger.log({
      agentId: input.agentId,
      toolName: input.toolName,
      ok: false,
      durationMs: Date.now() - t0,
      errorCode: code,
      summary: message,
      trace_id: input.traceId,
      run_id: input.runId,
      session_id: input.sessionId,
      task_id: input.taskId,
      role,
      tool_layer: layer,
      args_hash: hashGoogleToolArgs(effectiveArgs),
      started_at: startedAt,
      finished_at: new Date().toISOString(),
      status: "failed",
      turn_index: input.turnIndex,
    });

    return { ok: false, error: message, code };
  }
}
