import { isAbsolute } from "node:path";
import type { AcpSessionRuntimeOptions, SessionAcpMeta } from "../../config/sessions/types.js";
import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js";
import { normalizeText } from "../normalize-text.js";
import { AcpRuntimeError } from "../runtime/errors.js";

export { normalizeText } from "../normalize-text.js";

const MAX_RUNTIME_MODE_LENGTH = 64;
const MAX_MODEL_LENGTH = 200;
const MAX_PERMISSION_PROFILE_LENGTH = 80;
const MAX_CWD_LENGTH = 4096;
const MIN_TIMEOUT_SECONDS = 1;
const MAX_TIMEOUT_SECONDS = 24 * 60 * 60;
const MAX_BACKEND_OPTION_KEY_LENGTH = 64;
const MAX_BACKEND_OPTION_VALUE_LENGTH = 512;
const MAX_BACKEND_EXTRAS = 32;

const SAFE_OPTION_KEY_RE = /^[a-z0-9][a-z0-9._:-]*$/i;

function failInvalidOption(message: string): never {
  throw new AcpRuntimeError("ACP_INVALID_RUNTIME_OPTION", message);
}

function validateNoControlChars(value: string, field: string): string {
  for (let i = 0; i < value.length; i += 1) {
    const code = value.charCodeAt(i);
    if (code < 32 || code === 127) {
      failInvalidOption(`${field} must not include control characters.`);
    }
  }
  return value;
}

function validateBoundedText(params: { value: unknown; field: string; maxLength: number }): string {
  const normalized = normalizeText(params.value);
  if (!normalized) {
    failInvalidOption(`${params.field} must not be empty.`);
  }
  if (normalized.length > params.maxLength) {
    failInvalidOption(`${params.field} must be at most ${params.maxLength} characters.`);
  }
  return validateNoControlChars(normalized, params.field);
}

function validateBackendOptionKey(rawKey: unknown): string {
  const key = validateBoundedText({
    value: rawKey,
    field: "ACP config key",
    maxLength: MAX_BACKEND_OPTION_KEY_LENGTH,
  });
  if (!SAFE_OPTION_KEY_RE.test(key)) {
    failInvalidOption(
      "ACP config key must use letters, numbers, dots, colons, underscores, or dashes.",
    );
  }
  return key;
}

function validateBackendOptionValue(rawValue: unknown): string {
  return validateBoundedText({
    value: rawValue,
    field: "ACP config value",
    maxLength: MAX_BACKEND_OPTION_VALUE_LENGTH,
  });
}

export function validateRuntimeModeInput(rawMode: unknown): string {
  return validateBoundedText({
    value: rawMode,
    field: "Runtime mode",
    maxLength: MAX_RUNTIME_MODE_LENGTH,
  });
}

export function validateRuntimeModelInput(rawModel: unknown): string {
  return validateBoundedText({
    value: rawModel,
    field: "Model id",
    maxLength: MAX_MODEL_LENGTH,
  });
}

export function validateRuntimePermissionProfileInput(rawProfile: unknown): string {
  return validateBoundedText({
    value: rawProfile,
    field: "Permission profile",
    maxLength: MAX_PERMISSION_PROFILE_LENGTH,
  });
}

export function validateRuntimeCwdInput(rawCwd: unknown): string {
  const cwd = validateBoundedText({
    value: rawCwd,
    field: "Working directory",
    maxLength: MAX_CWD_LENGTH,
  });
  if (!isAbsolute(cwd)) {
    failInvalidOption(`Working directory must be an absolute path. Received "${cwd}".`);
  }
  return cwd;
}

export function validateRuntimeTimeoutSecondsInput(rawTimeout: unknown): number {
  if (typeof rawTimeout !== "number" || !Number.isFinite(rawTimeout)) {
    failInvalidOption("Timeout must be a positive integer in seconds.");
  }
  const timeout = Math.round(rawTimeout);
  if (timeout < MIN_TIMEOUT_SECONDS || timeout > MAX_TIMEOUT_SECONDS) {
    failInvalidOption(
      `Timeout must be between ${MIN_TIMEOUT_SECONDS} and ${MAX_TIMEOUT_SECONDS} seconds.`,
    );
  }
  return timeout;
}

export function parseRuntimeTimeoutSecondsInput(rawTimeout: unknown): number {
  const normalized = normalizeText(rawTimeout);
  if (!normalized || !/^\d+$/.test(normalized)) {
    failInvalidOption("Timeout must be a positive integer in seconds.");
  }
  return validateRuntimeTimeoutSecondsInput(Number.parseInt(normalized, 10));
}

export function validateRuntimeConfigOptionInput(
  rawKey: unknown,
  rawValue: unknown,
): {
  key: string;
  value: string;
} {
  return {
    key: validateBackendOptionKey(rawKey),
    value: validateBackendOptionValue(rawValue),
  };
}

export function validateRuntimeOptionPatch(
  patch: Partial<AcpSessionRuntimeOptions> | undefined,
): Partial<AcpSessionRuntimeOptions> {
  if (!patch) {
    return {};
  }
  const rawPatch = patch as Record<string, unknown>;
  const allowedKeys = new Set([
    "runtimeMode",
    "model",
    "cwd",
    "permissionProfile",
    "timeoutSeconds",
    "backendExtras",
  ]);
  for (const key of Object.keys(rawPatch)) {
    if (!allowedKeys.has(key)) {
      failInvalidOption(`Unknown runtime option "${key}".`);
    }
  }

  const next: Partial<AcpSessionRuntimeOptions> = {};
  if (Object.hasOwn(rawPatch, "runtimeMode")) {
    if (rawPatch.runtimeMode === undefined) {
      next.runtimeMode = undefined;
    } else {
      next.runtimeMode = validateRuntimeModeInput(rawPatch.runtimeMode);
    }
  }
  if (Object.hasOwn(rawPatch, "model")) {
    if (rawPatch.model === undefined) {
      next.model = undefined;
    } else {
      next.model = validateRuntimeModelInput(rawPatch.model);
    }
  }
  if (Object.hasOwn(rawPatch, "cwd")) {
    if (rawPatch.cwd === undefined) {
      next.cwd = undefined;
    } else {
      next.cwd = validateRuntimeCwdInput(rawPatch.cwd);
    }
  }
  if (Object.hasOwn(rawPatch, "permissionProfile")) {
    if (rawPatch.permissionProfile === undefined) {
      next.permissionProfile = undefined;
    } else {
      next.permissionProfile = validateRuntimePermissionProfileInput(rawPatch.permissionProfile);
    }
  }
  if (Object.hasOwn(rawPatch, "timeoutSeconds")) {
    if (rawPatch.timeoutSeconds === undefined) {
      next.timeoutSeconds = undefined;
    } else {
      next.timeoutSeconds = validateRuntimeTimeoutSecondsInput(rawPatch.timeoutSeconds);
    }
  }
  if (Object.hasOwn(rawPatch, "backendExtras")) {
    const rawExtras = rawPatch.backendExtras;
    if (rawExtras === undefined) {
      next.backendExtras = undefined;
    } else if (!rawExtras || typeof rawExtras !== "object" || Array.isArray(rawExtras)) {
      failInvalidOption("Backend extras must be a key/value object.");
    } else {
      const entries = Object.entries(rawExtras);
      if (entries.length > MAX_BACKEND_EXTRAS) {
        failInvalidOption(`Backend extras must include at most ${MAX_BACKEND_EXTRAS} entries.`);
      }
      const extras: Record<string, string> = {};
      for (const [entryKey, entryValue] of entries) {
        const { key, value } = validateRuntimeConfigOptionInput(entryKey, entryValue);
        extras[key] = value;
      }
      next.backendExtras = Object.keys(extras).length > 0 ? extras : undefined;
    }
  }

  return next;
}

export function normalizeRuntimeOptions(
  options: AcpSessionRuntimeOptions | undefined,
): AcpSessionRuntimeOptions {
  const runtimeMode = normalizeText(options?.runtimeMode);
  const model = normalizeText(options?.model);
  const cwd = normalizeText(options?.cwd);
  const permissionProfile = normalizeText(options?.permissionProfile);
  let timeoutSeconds: number | undefined;
  if (typeof options?.timeoutSeconds === "number" && Number.isFinite(options.timeoutSeconds)) {
    const rounded = Math.round(options.timeoutSeconds);
    if (rounded > 0) {
      timeoutSeconds = rounded;
    }
  }
  const backendExtrasEntries = Object.entries(options?.backendExtras ?? {})
    .map(([key, value]) => [normalizeText(key), normalizeText(value)] as const)
    .filter(([key, value]) => Boolean(key && value)) as Array<[string, string]>;
  const backendExtras =
    backendExtrasEntries.length > 0 ? Object.fromEntries(backendExtrasEntries) : undefined;
  return {
    ...(runtimeMode ? { runtimeMode } : {}),
    ...(model ? { model } : {}),
    ...(cwd ? { cwd } : {}),
    ...(permissionProfile ? { permissionProfile } : {}),
    ...(typeof timeoutSeconds === "number" ? { timeoutSeconds } : {}),
    ...(backendExtras ? { backendExtras } : {}),
  };
}

export function mergeRuntimeOptions(params: {
  current?: AcpSessionRuntimeOptions;
  patch?: Partial<AcpSessionRuntimeOptions>;
}): AcpSessionRuntimeOptions {
  const current = normalizeRuntimeOptions(params.current);
  const patch = normalizeRuntimeOptions(validateRuntimeOptionPatch(params.patch));
  const mergedExtras = {
    ...current.backendExtras,
    ...patch.backendExtras,
  };
  return normalizeRuntimeOptions({
    ...current,
    ...patch,
    ...(Object.keys(mergedExtras).length > 0 ? { backendExtras: mergedExtras } : {}),
  });
}

export function resolveRuntimeOptionsFromMeta(meta: SessionAcpMeta): AcpSessionRuntimeOptions {
  const normalized = normalizeRuntimeOptions(meta.runtimeOptions);
  if (normalized.cwd || !meta.cwd) {
    return normalized;
  }
  return normalizeRuntimeOptions({
    ...normalized,
    cwd: meta.cwd,
  });
}

export function runtimeOptionsEqual(
  a: AcpSessionRuntimeOptions | undefined,
  b: AcpSessionRuntimeOptions | undefined,
): boolean {
  return JSON.stringify(normalizeRuntimeOptions(a)) === JSON.stringify(normalizeRuntimeOptions(b));
}

export function buildRuntimeControlSignature(options: AcpSessionRuntimeOptions): string {
  const normalized = normalizeRuntimeOptions(options);
  const extras = Object.entries(normalized.backendExtras ?? {}).toSorted(([a], [b]) =>
    a.localeCompare(b),
  );
  return JSON.stringify({
    runtimeMode: normalized.runtimeMode ?? null,
    model: normalized.model ?? null,
    permissionProfile: normalized.permissionProfile ?? null,
    timeoutSeconds: normalized.timeoutSeconds ?? null,
    backendExtras: extras,
  });
}

export function buildRuntimeConfigOptionPairs(
  options: AcpSessionRuntimeOptions,
): Array<[string, string]> {
  const normalized = normalizeRuntimeOptions(options);
  const pairs = new Map<string, string>();
  if (normalized.model) {
    pairs.set("model", normalized.model);
  }
  if (normalized.permissionProfile) {
    pairs.set("approval_policy", normalized.permissionProfile);
  }
  if (typeof normalized.timeoutSeconds === "number") {
    pairs.set("timeout", String(normalized.timeoutSeconds));
  }
  for (const [key, value] of Object.entries(normalized.backendExtras ?? {})) {
    if (!pairs.has(key)) {
      pairs.set(key, value);
    }
  }
  return [...pairs.entries()];
}

export function inferRuntimeOptionPatchFromConfigOption(
  key: string,
  value: string,
): Partial<AcpSessionRuntimeOptions> {
  const validated = validateRuntimeConfigOptionInput(key, value);
  const normalizedKey = normalizeLowercaseStringOrEmpty(validated.key);
  if (normalizedKey === "model") {
    return { model: validateRuntimeModelInput(validated.value) };
  }
  if (
    normalizedKey === "approval_policy" ||
    normalizedKey === "permission_profile" ||
    normalizedKey === "permissions"
  ) {
    return { permissionProfile: validateRuntimePermissionProfileInput(validated.value) };
  }
  if (normalizedKey === "timeout" || normalizedKey === "timeout_seconds") {
    return { timeoutSeconds: parseRuntimeTimeoutSecondsInput(validated.value) };
  }
  if (normalizedKey === "cwd") {
    return { cwd: validateRuntimeCwdInput(validated.value) };
  }
  return {
    backendExtras: {
      [validated.key]: validated.value,
    },
  };
}
