import { ensureContextEnginesInitialized } from "../../context-engine/init.js";
import {
  resolveContextEngine,
  resolveContextEngineOwnerPluginId,
} from "../../context-engine/registry.js";
import type { ContextEngineRuntimeContext } from "../../context-engine/types.js";
import {
  captureCompactionCheckpointSnapshotAsync,
  cleanupCompactionCheckpointSnapshot,
  persistSessionCompactionCheckpoint,
  readSessionLeafIdFromTranscriptAsync,
  resolveSessionCompactionCheckpointReason,
  type CapturedCompactionCheckpointSnapshot,
} from "../../gateway/session-compaction-checkpoints.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { resolveUserPath } from "../../utils.js";
import { resolveAgentDir, resolveSessionAgentIds } from "../agent-scope.js";
import { resolveContextWindowInfo } from "../context-window-guard.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
import {
  maybeCompactAgentHarnessSession,
  resolveAgentHarnessPolicy,
} from "../harness/selection.js";
import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js";
import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
import type { CompactEmbeddedPiSessionParams } from "./compact.types.js";
import { asCompactionHookRunner, runPostCompactionSideEffects } from "./compaction-hooks.js";
import {
  buildEmbeddedCompactionRuntimeContext,
  resolveEmbeddedCompactionTarget,
} from "./compaction-runtime-context.js";
import {
  rotateTranscriptFileAfterCompaction,
  shouldRotateCompactionTranscript,
} from "./compaction-successor-transcript.js";
import { resolveContextEngineCapabilities } from "./context-engine-capabilities.js";
import { runContextEngineMaintenance } from "./context-engine-maintenance.js";
import { resolveGlobalLane, resolveSessionLane } from "./lanes.js";
import { log } from "./logger.js";
import { readPiModelContextTokens } from "./model-context-tokens.js";
import { resolveModelAsync } from "./model.js";
import type { EmbeddedPiCompactResult } from "./types.js";

/**
 * Compacts a session with lane queueing (session lane + global lane).
 * Use this from outside a lane context. If already inside a lane, use
 * `compactEmbeddedPiSessionDirect` to avoid deadlocks.
 */
export async function compactEmbeddedPiSession(
  params: CompactEmbeddedPiSessionParams,
): Promise<EmbeddedPiCompactResult> {
  ensureRuntimePluginsLoaded({
    config: params.config,
    workspaceDir: params.workspaceDir,
    allowGatewaySubagentBinding: params.allowGatewaySubagentBinding,
  });
  ensureContextEnginesInitialized();
  const agentIds = resolveSessionAgentIds({
    sessionKey: params.sessionKey,
    config: params.config,
  });
  const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, agentIds.sessionAgentId);
  const resolvedWorkspaceDir = resolveUserPath(params.workspaceDir);
  const contextEngine = await resolveContextEngine(params.config, {
    agentDir,
    workspaceDir: resolvedWorkspaceDir,
  });
  let contextTokenBudget = params.contextTokenBudget;
  if (!contextTokenBudget || !Number.isFinite(contextTokenBudget) || contextTokenBudget <= 0) {
    const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({
      config: params.config,
      provider: params.provider,
      modelId: params.model,
      authProfileId: params.authProfileId,
      defaultProvider: DEFAULT_PROVIDER,
      defaultModel: DEFAULT_MODEL,
    });
    const ceProvider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER;
    const ceModelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL;
    const { model: ceModel } = await resolveModelAsync(
      ceProvider,
      ceModelId,
      agentDir,
      params.config,
    );
    const ceRuntimeModel = ceModel as ProviderRuntimeModel | undefined;
    const ceHarnessPolicy = resolveAgentHarnessPolicy({
      provider: ceProvider,
      modelId: ceModelId,
      config: params.config,
      agentId: agentIds.sessionAgentId,
      sessionKey: params.sessionKey,
    });
    contextTokenBudget = resolveContextWindowInfo({
      cfg: params.config,
      provider: resolveContextConfigProviderForRuntime({
        provider: ceProvider,
        runtimeId: ceHarnessPolicy.runtime,
      }),
      modelId: ceModelId,
      modelContextTokens: readPiModelContextTokens(ceModel),
      modelContextWindow: ceRuntimeModel?.contextWindow,
      defaultTokens: DEFAULT_CONTEXT_TOKENS,
    }).tokens;
  }
  const contextEngineRuntimeContext = buildCompactionContextEngineRuntimeContext({
    params,
    agentDir,
    contextTokenBudget,
    contextEnginePluginId: resolveContextEngineOwnerPluginId(contextEngine),
  });
  const harnessResult = await maybeCompactAgentHarnessSession({
    ...params,
    contextEngine,
    contextTokenBudget,
    contextEngineRuntimeContext,
  });
  if (harnessResult) {
    await contextEngine.dispose?.();
    return harnessResult;
  }
  const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
  const globalLane = resolveGlobalLane(params.lane);
  const enqueueGlobal =
    params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
  return enqueueCommandInLane(sessionLane, () =>
    enqueueGlobal(async () => {
      let checkpointSnapshot: CapturedCompactionCheckpointSnapshot | null = null;
      let checkpointSnapshotRetained = false;
      try {
        // When the context engine owns compaction, its compact() implementation
        // bypasses compactEmbeddedPiSessionDirect (which fires the hooks internally).
        // Fire before_compaction / after_compaction hooks here so plugin subscribers
        // are notified regardless of which engine is active.
        const engineOwnsCompaction = contextEngine.info.ownsCompaction === true;
        checkpointSnapshot = engineOwnsCompaction
          ? await captureCompactionCheckpointSnapshotAsync({
              sessionFile: params.sessionFile,
            })
          : null;
        const hookRunner = engineOwnsCompaction
          ? asCompactionHookRunner(getGlobalHookRunner())
          : null;
        const hookSessionKey = params.sessionKey?.trim() || params.sessionId;
        const { sessionAgentId } = resolveSessionAgentIds({
          sessionKey: params.sessionKey,
          config: params.config,
        });
        const resolvedMessageProvider = params.messageChannel ?? params.messageProvider;
        const hookCtx = {
          sessionId: params.sessionId,
          agentId: sessionAgentId,
          sessionKey: hookSessionKey,
          workspaceDir: resolvedWorkspaceDir,
          messageProvider: resolvedMessageProvider,
        };
        const runtimeContext = contextEngineRuntimeContext;
        // Engine-owned compaction doesn't load the transcript at this level, so
        // message counts are unavailable. We pass sessionFile so hook subscribers
        // can read the transcript themselves if they need exact counts.
        if (hookRunner?.hasHooks?.("before_compaction") && hookRunner.runBeforeCompaction) {
          try {
            await hookRunner.runBeforeCompaction(
              {
                messageCount: -1,
                sessionFile: params.sessionFile,
              },
              hookCtx,
            );
          } catch (err) {
            log.warn("before_compaction hook failed", {
              errorMessage: formatErrorMessage(err),
            });
          }
        }
        const result = await contextEngine.compact({
          sessionId: params.sessionId,
          sessionKey: params.sessionKey,
          sessionFile: params.sessionFile,
          tokenBudget: contextTokenBudget,
          currentTokenCount: params.currentTokenCount,
          compactionTarget: params.trigger === "manual" ? "threshold" : "budget",
          customInstructions: params.customInstructions,
          force: params.trigger === "manual",
          runtimeContext,
        });
        const delegatedSessionId = result.result?.sessionId;
        const delegatedSessionFile = result.result?.sessionFile;
        const delegatedRotatedTranscript =
          (typeof delegatedSessionId === "string" && delegatedSessionId !== params.sessionId) ||
          (typeof delegatedSessionFile === "string" && delegatedSessionFile !== params.sessionFile);
        let postCompactionSessionId = delegatedSessionId ?? params.sessionId;
        let postCompactionSessionFile = delegatedSessionFile ?? params.sessionFile;
        let postCompactionLeafId: string | undefined;
        if (result.ok && result.compacted) {
          if (shouldRotateCompactionTranscript(params.config) && !delegatedRotatedTranscript) {
            try {
              const rotation = await rotateTranscriptFileAfterCompaction({
                sessionFile: params.sessionFile,
              });
              if (rotation.rotated) {
                postCompactionSessionId = rotation.sessionId ?? postCompactionSessionId;
                postCompactionSessionFile = rotation.sessionFile ?? postCompactionSessionFile;
                postCompactionLeafId = rotation.leafId;
                log.info(
                  `[compaction] rotated active transcript after context-engine compaction ` +
                    `(sessionKey=${params.sessionKey ?? params.sessionId})`,
                );
              }
            } catch (err) {
              log.warn("failed to rotate compacted transcript", {
                errorMessage: formatErrorMessage(err),
              });
            }
          }
          if (params.config && params.sessionKey && checkpointSnapshot) {
            try {
              const postLeafId =
                postCompactionLeafId ??
                (await readSessionLeafIdFromTranscriptAsync(postCompactionSessionFile)) ??
                undefined;
              const storedCheckpoint = await persistSessionCompactionCheckpoint({
                cfg: params.config,
                sessionKey: params.sessionKey,
                sessionId: postCompactionSessionId,
                reason: resolveSessionCompactionCheckpointReason({
                  trigger: params.trigger,
                }),
                snapshot: checkpointSnapshot,
                summary: result.result?.summary,
                firstKeptEntryId: result.result?.firstKeptEntryId,
                tokensBefore: result.result?.tokensBefore,
                tokensAfter: result.result?.tokensAfter,
                postSessionFile: postCompactionSessionFile,
                postLeafId,
                postEntryId: postLeafId,
              });
              checkpointSnapshotRetained = storedCheckpoint !== null;
            } catch (err) {
              log.warn("failed to persist compaction checkpoint", {
                errorMessage: formatErrorMessage(err),
              });
            }
          }
          await runContextEngineMaintenance({
            contextEngine,
            sessionId: postCompactionSessionId,
            sessionKey: params.sessionKey,
            sessionFile: postCompactionSessionFile,
            reason: "compaction",
            runtimeContext,
            config: params.config,
          });
        }
        if (engineOwnsCompaction && result.ok && result.compacted) {
          await runPostCompactionSideEffects({
            config: params.config,
            sessionKey: params.sessionKey,
            sessionFile: postCompactionSessionFile,
          });
        }
        if (
          result.ok &&
          result.compacted &&
          hookRunner?.hasHooks?.("after_compaction") &&
          hookRunner.runAfterCompaction
        ) {
          try {
            const afterHookCtx = {
              ...hookCtx,
              sessionId: postCompactionSessionId,
            };
            await hookRunner.runAfterCompaction(
              {
                messageCount: -1,
                compactedCount: -1,
                tokenCount: result.result?.tokensAfter,
                sessionFile: postCompactionSessionFile,
              },
              afterHookCtx,
            );
          } catch (err) {
            log.warn("after_compaction hook failed", {
              errorMessage: formatErrorMessage(err),
            });
          }
        }
        return {
          ok: result.ok,
          compacted: result.compacted,
          reason: result.reason,
          result: result.result
            ? {
                summary: result.result.summary ?? "",
                firstKeptEntryId: result.result.firstKeptEntryId ?? "",
                tokensBefore: result.result.tokensBefore,
                tokensAfter: result.result.tokensAfter,
                details: result.result.details,
                ...(postCompactionSessionId !== params.sessionId
                  ? { sessionId: postCompactionSessionId }
                  : {}),
                ...(postCompactionSessionFile !== params.sessionFile
                  ? { sessionFile: postCompactionSessionFile }
                  : {}),
              }
            : undefined,
        };
      } finally {
        if (!checkpointSnapshotRetained) {
          await cleanupCompactionCheckpointSnapshot(checkpointSnapshot);
        }
        await contextEngine.dispose?.();
      }
    }),
  );
}

function buildCompactionContextEngineRuntimeContext(params: {
  params: CompactEmbeddedPiSessionParams;
  agentDir: string;
  contextEnginePluginId?: string;
  contextTokenBudget?: number;
}): ContextEngineRuntimeContext {
  const { sessionAgentId } = resolveSessionAgentIds({
    sessionKey: params.params.sessionKey,
    config: params.params.config,
  });
  return {
    ...params.params,
    ...buildEmbeddedCompactionRuntimeContext({
      sessionKey: params.params.sessionKey,
      messageChannel: params.params.messageChannel,
      messageProvider: params.params.messageProvider,
      agentAccountId: params.params.agentAccountId,
      currentChannelId: params.params.currentChannelId,
      currentThreadTs: params.params.currentThreadTs,
      currentMessageId: params.params.currentMessageId,
      authProfileId: params.params.authProfileId,
      workspaceDir: params.params.workspaceDir,
      agentDir: params.agentDir,
      config: params.params.config,
      skillsSnapshot: params.params.skillsSnapshot,
      senderIsOwner: params.params.senderIsOwner,
      senderId: params.params.senderId,
      provider: params.params.provider,
      modelId: params.params.model,
      modelFallbacksOverride: params.params.modelFallbacksOverride,
      thinkLevel: params.params.thinkLevel,
      reasoningLevel: params.params.reasoningLevel,
      bashElevated: params.params.bashElevated,
      extraSystemPrompt: params.params.extraSystemPrompt,
      sourceReplyDeliveryMode: params.params.sourceReplyDeliveryMode,
      ownerNumbers: params.params.ownerNumbers,
    }),
    ...resolveContextEngineCapabilities({
      config: params.params.config,
      sessionKey: params.params.sessionKey,
      agentId: sessionAgentId,
      contextEnginePluginId: params.contextEnginePluginId,
      purpose: "context-engine.compaction",
    }),
    tokenBudget: params.contextTokenBudget,
    currentTokenCount: params.params.currentTokenCount,
  };
}
