/** ACP streaming and projection settings derived from config. */
import type { AcpSessionUpdateTag } from "@openclaw/acp-core/runtime/types";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { clampPositiveInteger, resolveEffectiveBlockStreamingConfig } from "./block-streaming.js";

const DEFAULT_ACP_STREAM_COALESCE_IDLE_MS = 350;
const DEFAULT_ACP_STREAM_MAX_CHUNK_CHARS = 1800;
const DEFAULT_ACP_REPEAT_SUPPRESSION = true;
const DEFAULT_ACP_DELIVERY_MODE = "final_only";
const DEFAULT_ACP_HIDDEN_BOUNDARY_SEPARATOR = "paragraph";
const DEFAULT_ACP_HIDDEN_BOUNDARY_SEPARATOR_LIVE = "space";
const DEFAULT_ACP_MAX_OUTPUT_CHARS = 24_000;
const DEFAULT_ACP_MAX_SESSION_UPDATE_CHARS = 320;

const ACP_TAG_VISIBILITY_DEFAULTS: Record<AcpSessionUpdateTag, boolean> = {
  agent_message_chunk: true,
  tool_call: false,
  tool_call_update: false,
  usage_update: false,
  available_commands_update: false,
  current_mode_update: false,
  config_option_update: false,
  session_info_update: false,
  plan: false,
  agent_thought_chunk: false,
};

/** ACP delivery strategy for projected assistant output. */
export type AcpDeliveryMode = "live" | "final_only";
export type AcpHiddenBoundarySeparator = "none" | "space" | "newline" | "paragraph";

/** Normalized ACP projection settings consumed by stream projectors. */
export type AcpProjectionSettings = {
  deliveryMode: AcpDeliveryMode;
  hiddenBoundarySeparator: AcpHiddenBoundarySeparator;
  repeatSuppression: boolean;
  maxOutputChars: number;
  maxSessionUpdateChars: number;
  tagVisibility: Partial<Record<AcpSessionUpdateTag, boolean>>;
};

function clampBoolean(value: unknown, fallback: boolean): boolean {
  return typeof value === "boolean" ? value : fallback;
}

function resolveAcpDeliveryMode(value: unknown): AcpDeliveryMode {
  if (value === "live" || value === "final_only") {
    return value;
  }
  return DEFAULT_ACP_DELIVERY_MODE;
}

function resolveAcpHiddenBoundarySeparator(
  value: unknown,
  fallback: AcpHiddenBoundarySeparator,
): AcpHiddenBoundarySeparator {
  if (value === "none" || value === "space" || value === "newline" || value === "paragraph") {
    return value;
  }
  return fallback;
}

function resolveAcpStreamCoalesceIdleMs(cfg: OpenClawConfig): number {
  return clampPositiveInteger(
    cfg.acp?.stream?.coalesceIdleMs,
    DEFAULT_ACP_STREAM_COALESCE_IDLE_MS,
    {
      min: 0,
      max: 5_000,
    },
  );
}

function resolveAcpStreamMaxChunkChars(cfg: OpenClawConfig): number {
  return clampPositiveInteger(cfg.acp?.stream?.maxChunkChars, DEFAULT_ACP_STREAM_MAX_CHUNK_CHARS, {
    min: 50,
    max: 4_000,
  });
}

/** Resolves ACP projection settings with bounded defaults. */
export function resolveAcpProjectionSettings(cfg: OpenClawConfig): AcpProjectionSettings {
  const stream = cfg.acp?.stream;
  const deliveryMode = resolveAcpDeliveryMode(stream?.deliveryMode);
  const hiddenBoundaryFallback: AcpHiddenBoundarySeparator =
    deliveryMode === "live"
      ? DEFAULT_ACP_HIDDEN_BOUNDARY_SEPARATOR_LIVE
      : DEFAULT_ACP_HIDDEN_BOUNDARY_SEPARATOR;
  return {
    deliveryMode,
    hiddenBoundarySeparator: resolveAcpHiddenBoundarySeparator(
      stream?.hiddenBoundarySeparator,
      hiddenBoundaryFallback,
    ),
    repeatSuppression: clampBoolean(stream?.repeatSuppression, DEFAULT_ACP_REPEAT_SUPPRESSION),
    maxOutputChars: clampPositiveInteger(stream?.maxOutputChars, DEFAULT_ACP_MAX_OUTPUT_CHARS, {
      min: 1,
      max: 500_000,
    }),
    maxSessionUpdateChars: clampPositiveInteger(
      stream?.maxSessionUpdateChars,
      DEFAULT_ACP_MAX_SESSION_UPDATE_CHARS,
      {
        min: 64,
        max: 8_000,
      },
    ),
    tagVisibility: stream?.tagVisibility ?? {},
  };
}

/** Resolves ACP streaming chunk/coalescing settings. */
export function resolveAcpStreamingConfig(params: {
  cfg: OpenClawConfig;
  provider?: string;
  accountId?: string;
  deliveryMode?: AcpDeliveryMode;
}) {
  const resolved = resolveEffectiveBlockStreamingConfig({
    cfg: params.cfg,
    provider: params.provider,
    accountId: params.accountId,
    maxChunkChars: resolveAcpStreamMaxChunkChars(params.cfg),
    coalesceIdleMs: resolveAcpStreamCoalesceIdleMs(params.cfg),
  });

  // In live mode, ACP text deltas should flush promptly and never be held
  // behind large generic min-char thresholds.
  if (params.deliveryMode === "live") {
    return {
      chunking: {
        ...resolved.chunking,
        minChars: 1,
      },
      coalescing: {
        ...resolved.coalescing,
        minChars: 1,
        // ACP delta streams already carry spacing/newlines; preserve exact text.
        joiner: "",
      },
    };
  }

  return resolved;
}

export function isAcpTagVisible(settings: AcpProjectionSettings, tag: string | undefined): boolean {
  if (!tag) {
    return true;
  }
  const override = settings.tagVisibility[tag as AcpSessionUpdateTag];
  if (typeof override === "boolean") {
    return override;
  }
  if (Object.hasOwn(ACP_TAG_VISIBILITY_DEFAULTS, tag)) {
    return ACP_TAG_VISIBILITY_DEFAULTS[tag as AcpSessionUpdateTag];
  }
  return true;
}
