import type { CatalogPartType, HiveCatalog, HiveFacts, HiveLevelFact, HivePart, HiveState, HiveSystemDto, } from "./types"; const UNBUILT_FRAME_TYPES = new Set(["NONE", "FOUNDATION", "DIVIDER"]); /** Catalog of a system as the API returns it (all part types, also inactive). */ export function catalogFromSystem(system: HiveSystemDto): HiveCatalog { const rows: CatalogPartType[] = system.partTypes.map((row) => ({ id: row.id, kind: row.kind, name: row.name, lengthMm: row.lengthMm, widthMm: row.widthMm, heightMm: row.heightMm, frameSlots: row.frameSlots, stateSchema: row.stateSchema, active: row.active, })); return { systemId: system.id, partTypes: new Map(rows.map((row) => [row.id, row])) }; } function allParts(state: HiveState): HivePart[] { return state.stack.flatMap((part) => [part, ...(part.inserts ?? [])]); } function levelsOf(part: HivePart, catalog: HiveCatalog): HiveLevelFact[] { const type = catalog.partTypes.get(part.partTypeId); if (!type) return []; return type.stateSchema.flatMap((field) => { if (field.kind !== "level") return []; const raw = part.state[field.key]; return [ { partId: part.id, key: field.key, label: field.label, unit: field.unit, value: typeof raw === "number" ? raw : 0, max: field.max, ...(field.feedType ? { feedType: field.feedType } : {}), }, ]; }); } /** Pure derivation of display facts (spec 5.3), same as the backend. */ export function deriveHiveFacts(state: HiveState, catalog: HiveCatalog): HiveFacts { const parts = allParts(state); const kindOf = (part: HivePart) => catalog.partTypes.get(part.partTypeId)?.kind; const frames = state.stack .filter((part) => kindOf(part) === "BOX") .flatMap((part) => part.frames ?? []); const evaporators = parts.filter((part) => kindOf(part) === "EVAPORATOR"); const acid = evaporators.reduce((sum, part) => { const value = part.state.acidMl; return sum + (typeof value === "number" ? value : 0); }, 0); return { boxCount: state.stack.filter((part) => kindOf(part) === "BOX").length, broodFrames: frames.filter((f) => f.type === "BROOD").length, honeyFrames: frames.filter((f) => f.type === "HONEY").length, builtFrames: frames.filter((f) => !UNBUILT_FRAME_TYPES.has(f.type)).length, levels: parts.flatMap((part) => levelsOf(part, catalog)), hasFloorInsert: parts.some((part) => kindOf(part) === "FLOOR_INSERT"), evaporatorAcidMl: evaporators.length > 0 ? acid : null, }; }