// ST-BME: UI 状态工厂、纯工具函数
// 此模块中的函数均不依赖 index.js 模块级可变状态，
// 可被 index.js 及其他模块安全导入。
import { sanitizePlannerMessageText } from "../runtime/planner-tag-utils.js";
import { AUTHORITY_DIAGNOSTICS_MANIFEST_LIMIT } from "../maintenance/authority-diagnostics-bundle.js";
import { createAuthorityUpgradeState } from "../runtime/authority-upgrade-state.js";

// ═══════════════════════════════════════════════════════════
// 常量
// ═══════════════════════════════════════════════════════════

export const BATCH_STAGE_ORDER = ["core", "structural", "semantic", "finalize"];
export const BATCH_STAGE_SEVERITY = {
  success: 0,
  partial: 1,
  failed: 2,
};

// ═══════════════════════════════════════════════════════════
// UI 状态工厂
// ═══════════════════════════════════════════════════════════

export function createUiStatus(text = "待命", meta = "", level = "idle") {
  return {
    text: String(text || "待命"),
    meta: String(meta || ""),
    level,
    updatedAt: Date.now(),
  };
}

export function createGraphPersistenceState() {
  return {
    loadState: "no-chat",
    chatId: "",
    reason: "当前尚未进入聊天",
    attemptIndex: 0,
    revision: 0,
    lastPersistedRevision: 0,
    queuedPersistRevision: 0,
    queuedPersistChatId: "",
    queuedPersistMode: "",
    queuedPersistRotateIntegrity: false,
    queuedPersistReason: "",
    shadowSnapshotUsed: false,
    shadowSnapshotRevision: 0,
    shadowSnapshotUpdatedAt: "",
    shadowSnapshotReason: "",
    lastPersistReason: "",
    lastPersistMode: "",
    metadataIntegrity: "",
    writesBlocked: false,
    pendingPersist: false,
    lastAcceptedRevision: 0,
    acceptedStorageTier: "none",
    hostProfile: "generic-st",
    chatStateTarget: null,
    primaryStorageTier: "indexeddb",
    cacheStorageTier: "none",
    cacheMirrorState: "idle",
    cacheLag: 0,
    lightweightHostMode: false,
    persistDiagnosticTier: "none",
    acceptedBy: "none",
    lastRecoverableStorageTier: "none",
    persistMismatchReason: "",
    commitMarker: null,
    lukerSidecarFormatVersion: 0,
    lukerManifestRevision: 0,
    lukerJournalDepth: 0,
    lukerJournalBytes: 0,
    lukerCheckpointRevision: 0,
    projectionState: {
      runtime: {
        status: "idle",
        updatedAt: 0,
        reason: "",
      },
      persistent: {
        status: "idle",
        updatedAt: 0,
        reason: "",
      },
    },
    lastHookPhase: "",
    lastRequestRescanReason: "",
    lastIgnoredMutationEvent: "",
    lastIgnoredMutationReason: "",
    lastChatStateConflict: null,
    lastBranchInheritResult: null,
    restoreLock: {
      active: false,
      depth: 0,
      source: "",
      reason: "",
      startedAt: 0,
    },
    storagePrimary: "indexeddb",
    storageMode: "indexeddb",
    resolvedLocalStore: "indexeddb:indexeddb",
    authority: {
      enabledMode: "auto",
      baseUrl: "/api/plugins/authority",
      installed: false,
      healthy: false,
      sessionReady: false,
      permissionReady: false,
      minimumFeatureSetReady: false,
      serverPrimaryReady: false,
      storagePrimaryReady: false,
      triviumPrimaryReady: false,
      jobsReady: false,
      blobReady: false,
      features: [],
      missingFeatures: [],
      reason: "not-probed",
      lastError: "",
      endpoint: "",
      status: 0,
      latencyMs: 0,
      lastProbeAt: 0,
      updatedAt: "",
    },
    authorityBrowserState: {
      mode: "minimal",
      serverRevision: 0,
      serverIntegrity: "",
      lastProbeAt: 0,
      lastCommitAt: 0,
      lastError: "",
      offlineQueueBytes: 0,
      offlineQueueItems: 0,
      offlineQueueOverflow: false,
      offlineQueueOverflowReason: "",
      updatedAt: "",
    },
    authorityInstalled: false,
    authorityHealthy: false,
    authorityServerPrimaryReady: false,
    authorityStoragePrimaryReady: false,
    authorityTriviumPrimaryReady: false,
    authorityBlobReady: false,
    authorityBrowserCacheMode: "minimal",
    authorityOfflineQueueBytes: 0,
    authorityOfflineQueueItems: 0,
    authorityDegradedReason: "",
    authorityUpgradeState: createAuthorityUpgradeState(),
    authorityUpgradeMode: "standalone",
    authorityUpgradeText: "纯前端模式",
    authorityUpgradeMeta: "未检测到可用服务端增强，BME 将继续本地运行",
    authorityUpgradeLevel: "idle",
    authorityUpgradeReady: false,
    authorityMigrationState: "idle",
    authorityMigrationSource: "",
    authorityMigrationRevision: 0,
    authorityMigrationLastError: "",
    lastAuthorityMigrationResult: null,
    authorityJobsReady: false,
    authorityBmeProtocolVersion: 0,
    authorityBmeVectorManifestReady: false,
    authorityBmeVectorApplyReady: false,
    authorityBmeVectorApplyJobsReady: false,
    authorityBmeServerEmbeddingProbeReady: false,
    authorityBmeCandidateSearchReady: false,
    authorityJobQueueState: "idle",
    authorityLastJob: null,
    authorityLastJobId: "",
    authorityLastJobKind: "",
    authorityLastJobStatus: "",
    authorityLastJobProgress: 0,
    authorityLastJobError: "",
    authorityLastJobUpdatedAt: "",
    authorityJobTrackingMode: "idle",
    authorityJobTrackingReason: "",
    authorityJobTrackingUpdatedAt: "",
    authorityRecentJobs: [],
    authorityRecentJobsUpdatedAt: "",
    authorityRecentJobsError: "",
    authorityRecentJobsNextCursor: "",
    authorityRecentJobsHasMore: false,
    authorityBlobState: "idle",
    authorityLastBlobEvent: null,
    authorityLastBlobAction: "",
    authorityLastBlobBackend: "",
    authorityLastBlobPath: "",
    authorityLastBlobReason: "",
    authorityLastBlobError: "",
    authorityLastBlobUpdatedAt: "",
    authorityBlobCheckpointPath: "",
    authorityBlobCheckpointRevision: 0,
    authorityBlobCheckpointUpdatedAt: "",
    authorityConsistencyState: "idle",
    authorityConsistencyAudit: null,
    authorityConsistencyUpdatedAt: "",
    authorityConsistencyError: "",
    authorityCheckpointRestoreState: "idle",
    authorityCheckpointRestoreResult: null,
    authorityCheckpointRestoreUpdatedAt: "",
    authorityCheckpointRestoreError: "",
    authorityRepairState: "idle",
    authorityRepairResult: null,
    authorityRepairUpdatedAt: "",
    authorityRepairError: "",
    authorityPerformanceBaseline: null,
    authorityPerformanceBaselineComparison: null,
    authorityPerformanceBaselineUpdatedAt: "",
    authorityPerformanceBaselineReason: "",
    authorityDiagnosticsBundlePath: "",
    authorityDiagnosticsBundleReason: "",
    authorityDiagnosticsBundleUpdatedAt: "",
    authorityDiagnosticsBundleSize: 0,
    authorityDiagnosticsManifestPath: "",
    authorityDiagnosticsArtifacts: [],
    authorityDiagnosticsArtifactsUpdatedAt: "",
    authorityDiagnosticsArtifactsError: "",
    authorityDiagnosticsRetentionLimit: AUTHORITY_DIAGNOSTICS_MANIFEST_LIMIT,
    authorityDiagnosticsLastPrunedCount: 0,
    authorityDiagnosticsLastPrunedAt: "",
    authorityDiagnosticsLastPruneError: "",
    localStoreFormatVersion: 1,
    localStoreMigrationState: "idle",
    backgroundMaintenance: {
      state: "idle",
      queued: 0,
      activeId: "",
      activeName: "",
      completed: 0,
      failed: 0,
      dropped: 0,
      lastTask: null,
      updatedAt: 0,
    },
    opfsWriteLockState: {
      active: false,
      queueDepth: 0,
      lastReason: "",
      updatedAt: 0,
    },
    opfsWalDepth: 0,
    opfsPendingBytes: 0,
    opfsCompactionState: null,
    runtimeGraphReadable: false,
    remoteSyncFormatVersion: 1,
    dbReady: false,
    indexedDbRevision: 0,
    indexedDbLastError: "",
    syncState: "idle",
    lastSyncUploadedAt: 0,
    lastSyncDownloadedAt: 0,
    lastSyncedRevision: 0,
    lastBackupUploadedAt: 0,
    lastBackupRestoredAt: 0,
    lastBackupRollbackAt: 0,
    lastBackupFilename: "",
    syncDirty: false,
    syncDirtyReason: "",
    lastSyncError: "",
    dualWriteLastResult: null,
    persistDelta: null,
    persistObservability: {
      totalSamples: 0,
      byPath: {},
      byReason: {},
      byPathReason: {},
      lastPathKey: "",
      lastReasonKey: "",
      lastPathReasonKey: "",
      lastRecordedAt: "",
    },
    loadDiagnostics: null,
    updatedAt: new Date().toISOString(),
  };
}

export function createRecallInputRecord(overrides = {}) {
  return {
    text: "",
    hash: "",
    messageId: null,
    source: "",
    at: 0,
    ...overrides,
  };
}

export function createRecallRunResult(status = "completed", extra = {}) {
  const normalizedStatus = String(status || "skipped").trim() || "skipped";
  return {
    ok: normalizedStatus === "completed",
    didRecall: normalizedStatus === "completed",
    status: normalizedStatus,
    ...extra,
  };
}

// ═══════════════════════════════════════════════════════════
// 批次状态
// ═══════════════════════════════════════════════════════════

export function createBatchStageStatus(stage, consistency = "strong") {
  return {
    stage,
    outcome: "success",
    consistency,
    warnings: [],
    errors: [],
    artifacts: [],
  };
}

/**
 * @param {object} opts
 * @param {number[]} opts.processedRange
 * @param {number}   opts.extractionCountBefore
 * @param {number}   [opts.extractionCountAfter] — 如未提供，fallback 为 extractionCountBefore
 */
export function createBatchStatusSkeleton({
  processedRange,
  extractionCountBefore,
  extractionCountAfter,
}) {
  const countBefore = Number.isFinite(extractionCountBefore)
    ? extractionCountBefore
    : 0;
  const countAfter = Number.isFinite(extractionCountAfter)
    ? extractionCountAfter
    : countBefore;
  return {
    model: "layered-batch-v1",
    processedRange: Array.isArray(processedRange)
      ? [...processedRange]
      : [-1, -1],
    extractionCountBefore: countBefore,
    extractionCountAfter: countAfter,
    stages: {
      core: createBatchStageStatus("core", "strong"),
      structural: createBatchStageStatus("structural", "weak"),
      semantic: createBatchStageStatus("semantic", "weak"),
      finalize: createBatchStageStatus("finalize", "strong"),
    },
    outcome: "success",
    consistency: "strong",
    completed: false,
    persistence: null,
    historyAdvanceAllowed: false,
    backgroundMaintenanceState: "",
    backgroundMaintenanceQueued: false,
    backgroundMaintenanceMode: "",
    backgroundMaintenanceTasks: [],
    backgroundMaintenanceQueue: null,
    backgroundVectorSyncQueued: false,
    backgroundVectorSyncMode: "",
    backgroundVectorSyncTaskId: "",
    backgroundVectorSyncState: "",
    backgroundVectorSyncQueue: null,
    warnings: [],
    errors: [],
  };
}

export function setBatchStageOutcome(status, stage, outcome, message = "") {
  const stageStatus = status?.stages?.[stage];
  if (!stageStatus) return;
  const nextSeverity = BATCH_STAGE_SEVERITY[outcome] ?? 0;
  const previousSeverity = BATCH_STAGE_SEVERITY[stageStatus.outcome] ?? 0;
  if (nextSeverity >= previousSeverity) {
    stageStatus.outcome = outcome;
  }
  if (!message) return;
  if (outcome === "failed") {
    stageStatus.errors.push(message);
  } else if (outcome === "partial") {
    stageStatus.warnings.push(message);
  }
}

export function pushBatchStageArtifact(status, stage, artifact) {
  const stageStatus = status?.stages?.[stage];
  if (!stageStatus || !artifact) return;
  if (!stageStatus.artifacts.includes(artifact)) {
    stageStatus.artifacts.push(artifact);
  }
}

/**
 * @param {object} status
 * @param {number} [currentExtractionCount] — 传入调用方的 extractionCount
 */
export function finalizeBatchStatus(status, currentExtractionCount) {
  const stages = status?.stages || {};
  const structuralOutcome = stages.structural?.outcome || "success";
  const semanticOutcome = stages.semantic?.outcome || "success";
  const finalizeOutcome = stages.finalize?.outcome || "failed";
  const outcomeList = BATCH_STAGE_ORDER.map(
    (stage) => stages[stage]?.outcome || "success",
  );

  if (finalizeOutcome !== "success") {
    status.outcome = "failed";
  } else if (outcomeList.includes("failed")) {
    status.outcome = "failed";
  } else if (structuralOutcome === "partial" || semanticOutcome === "partial") {
    status.outcome = "partial";
  } else {
    status.outcome = "success";
  }

  status.consistency =
    finalizeOutcome === "success" &&
    stages.core?.outcome === "success" &&
    stages.structural?.outcome === "success"
      ? "strong"
      : "weak";
  status.completed = finalizeOutcome === "success";
  if (Number.isFinite(currentExtractionCount)) {
    status.extractionCountAfter = currentExtractionCount;
  }
  status.warnings = BATCH_STAGE_ORDER.flatMap(
    (stage) => stages[stage]?.warnings || [],
  );
  status.errors = BATCH_STAGE_ORDER.flatMap(
    (stage) => stages[stage]?.errors || [],
  );
  return status;
}

// ═══════════════════════════════════════════════════════════
// 纯映射 / 纯变换
// ═══════════════════════════════════════════════════════════

export function normalizeStageNoticeLevel(level = "info") {
  if (level === "running" || level === "idle") return "info";
  if (level === "success" || level === "warning" || level === "error") {
    return level;
  }
  return "info";
}

export function getStageNoticeTitle(stage) {
  switch (stage) {
    case "extraction":
      return "ST-BME 提取";
    case "vector":
      return "ST-BME 向量";
    case "recall":
      return "ST-BME 召回";
    case "history":
      return "ST-BME 历史恢复";
    default:
      return "ST-BME";
  }
}

export function getStageNoticeDuration(level = "info") {
  switch (level) {
    case "error":
      return 6000;
    case "warning":
      return 5000;
    case "success":
      return 3000;
    default:
      return 3200;
  }
}

export function getRecallHookLabel(hookName = "") {
  switch (hookName) {
    case "GENERATION_AFTER_COMMANDS":
      return "hook GENERATION_AFTER_COMMANDS";
    case "GENERATE_BEFORE_COMBINE_PROMPTS":
      return "hook GENERATE_BEFORE_COMBINE_PROMPTS";
    default:
      return "";
  }
}

export function getGenerationRecallHookStateFromResult(result) {
  const status = String(result?.status || "").trim();
  switch (status) {
    case "completed":
      return "completed";
    case "failed":
      return "failed";
    case "aborted":
    case "superseded":
      return "aborted";
    default:
      return "skipped";
  }
}

export function isTerminalGenerationRecallHookState(state = "") {
  return ["completed", "failed", "aborted", "skipped"].includes(
    String(state || ""),
  );
}

export function shouldRunRecallForTransaction(transaction, hookName) {
  if (!hookName) return true;
  if (!transaction) return true;

  const hookStates = transaction.hookStates || {};
  const currentHookState = hookStates[hookName];
  if (
    currentHookState === "running" ||
    isTerminalGenerationRecallHookState(currentHookState)
  ) {
    return false;
  }

  const peerHookName =
    hookName === "GENERATION_AFTER_COMMANDS"
      ? "GENERATE_BEFORE_COMBINE_PROMPTS"
      : hookName === "GENERATE_BEFORE_COMBINE_PROMPTS"
        ? "GENERATION_AFTER_COMMANDS"
        : "";

  if (!peerHookName) return true;

  const peerHookState = hookStates[peerHookName];
  if (
    peerHookState === "running" ||
    isTerminalGenerationRecallHookState(peerHookState)
  ) {
    return false;
  }

  return true;
}

export function formatRecallContextLine(message) {
  return `[${message.is_user ? "user" : "assistant"}]: ${sanitizePlannerMessageText(message)}`;
}

// ═══════════════════════════════════════════════════════════
// 文本 / 数值 工具
// ═══════════════════════════════════════════════════════════

export function normalizeRecallInputText(value) {
  return String(value ?? "")
    .replace(/\r\n/g, "\n")
    .trim();
}

export function isTrivialUserInput(text) {
  const normalizedText = normalizeRecallInputText(text);
  if (!normalizedText) {
    return {
      trivial: true,
      reason: "empty",
      normalizedText,
    };
  }

  if (normalizedText.startsWith("/")) {
    return {
      trivial: true,
      reason: "slash-command",
      normalizedText,
    };
  }

  return {
    trivial: false,
    reason: "",
    normalizedText,
  };
}

export function hashRecallInput(text) {
  let hash = 0;
  const normalized = normalizeRecallInputText(text);
  for (let index = 0; index < normalized.length; index++) {
    hash = (hash * 31 + normalized.charCodeAt(index)) >>> 0;
  }
  return normalized ? String(hash) : "";
}

export function isFreshRecallInputRecord(record, ttlMs = 60000) {
  return Boolean(
    record?.text &&
    record.at &&
    Date.now() - record.at <= ttlMs,
  );
}

export function clampInt(value, fallback, min = 0, max = Number.MAX_SAFE_INTEGER) {
  const num = Number.parseInt(value, 10);
  if (!Number.isFinite(num)) return fallback;
  return Math.min(max, Math.max(min, num));
}

export function clampFloat(value, fallback, min = 0, max = 1) {
  const num = Number.parseFloat(value);
  if (!Number.isFinite(num)) return fallback;
  return Math.min(max, Math.max(min, num));
}
