/**
 * @module db/generationSegments
 * @description The generation-segment store — Stage-2 D1+D3+repacking's file
 * format (co-frozen 2026-07-19; design: the d1-d3-repacking spec).
 *
 * Packs CONSECUTIVE cold generations' record-sets (before-images + delta)
 * into append-once segment files with derived sidecar indexes, so history
 * scales in SEGMENTS (tens) instead of FILES-PER-GENERATION (hundreds of
 * thousands), and cold-open reads ONE manifest instead of listing the
 * backlog. Layout under `_generations/segments/`:
 *
 * - `seg-<firstGen, zero-padded 20>.bgs` — magic "BGS1", then one frame per
 *   generation: `u32 payloadLen | u32 crc32c | msgpack payload`. Payload is
 *   POSITIONAL: `[generation, timestamp, delta, records[], flags]` with
 *   records `[kindByte, id, record]`. `flags` reserves encoding evolution
 *   (bit 0 = compressed payload — v1 always 0; a future writer upgrade,
 *   never a format break). Sealed segments are IMMUTABLE — the fact log's
 *   own law, generalized.
 * - `seg-<firstGen>.idx` — DERIVED sidecar (msgpack): per-generation frame
 *   offsets (point reads = one ranged read, never a listing) + per-id
 *   generation postings (per-id chain rebuilds read only what they need).
 *   Corrupt/missing → rebuilt from its segment in one sequential read,
 *   loudly.
 * - `manifest.json` — the segment catalogue + `compactedBelow` (D3's
 *   horizon marker). Cold-open reads THIS; the packed backlog is never
 *   listed.
 *
 * D3 semantics carried here: bounded-retention reclaim drops WHOLE segments
 * at boundaries (O(1) per segment, no rewrite); under the archival profile
 * (`retention: 'all'`) nothing here is ever dropped — folding is the only
 * transform (re-representation, never deletion).
 */

import { encode as msgpackEncode, decode as msgpackDecode } from '@msgpack/msgpack'
import { crc32c } from '../utils/crc32c.js'
import type { FactLogStorage } from './factLog.js'
import { prodLog } from '../utils/logger.js'

/** Directory for segment files + manifest, under the generations prefix. */
export const SEGMENTS_PREFIX = '_generations/segments'

/** Target sealed-segment size (co-freeze proposal; tunable on evidence). */
export const SEGMENT_TARGET_BYTES = 64 * 1024 * 1024

const MAGIC = new TextEncoder().encode('BGS1')
const FRAME_PREFIX_BYTES = 8 // u32 payloadLen + u32 crc32c
const MANIFEST_PATH = `${SEGMENTS_PREFIX}/manifest.json`

/** One generation's fold input — exactly what the live tier holds for it. */
export interface FoldGeneration {
  generation: number
  timestamp: number
  /** The tx.json delta object, carried verbatim. */
  delta: unknown
  /** The before-image record-set (empty for record-less generations). */
  records: Array<{ kind: 'noun' | 'verb'; id: string; record: unknown }>
}

/** Manifest entry for one sealed segment. */
export interface SegmentMeta {
  file: string
  firstGeneration: number
  lastGeneration: number
  frames: number
  bytes: number
  /** crc32c of the full segment byte stream — the digest chain's link. */
  checksum: number
}

interface SegmentManifest {
  version: 1
  compactedBelow: number
  segments: SegmentMeta[]
}

interface SidecarIndex {
  version: 1
  /** [generation, frameOffset, frameLen] ascending by generation. */
  generations: Array<[number, number, number]>
  /** `${kindByte}:${id}` → ascending generations holding a record for it. */
  ids: Record<string, number[]>
}

const segmentFileName = (firstGeneration: number): string =>
  `seg-${String(firstGeneration).padStart(20, '0')}.bgs`
const sidecarFileName = (firstGeneration: number): string =>
  `seg-${String(firstGeneration).padStart(20, '0')}.idx`

/**
 * The generation-segment store. Owns the packed tier ONLY — the live
 * per-generation tier and the routing between tiers belong to
 * `GenerationStore`. All mutating entry points here are called under the
 * generation store's commit mutex.
 */
export class GenerationSegmentStore {
  private readonly storage: FactLogStorage
  private manifest: SegmentManifest = { version: 1, compactedBelow: 0, segments: [] }
  /** Sidecar cache — segments are immutable, so entries never invalidate. */
  private readonly sidecars = new Map<string, SidecarIndex>()

  constructor(storage: FactLogStorage) {
    this.storage = storage
  }

  /** Load the manifest (ONE read — never a directory listing). */
  async open(): Promise<void> {
    const raw = (await this.storage.readRawObject(MANIFEST_PATH)) as SegmentManifest | null
    if (raw) {
      if (raw.version !== 1) {
        throw new Error(
          `[GenerationSegments] manifest version ${String(raw.version)} is newer than this ` +
            `engine understands — refusing to serve partial history. Upgrade the engine.`
        )
      }
      this.manifest = raw
    }
  }

  /** The packed tier's catalogue (ascending, immutable snapshot). */
  segments(): readonly SegmentMeta[] {
    return this.manifest.segments
  }

  /** D3's horizon marker: generations below this were reclaimed (bounded profiles only). */
  compactedBelow(): number {
    return this.manifest.compactedBelow
  }

  /** The covering sealed segment for `gen`, or null if it lives outside the packed tier. */
  private coveringSegment(gen: number): SegmentMeta | null {
    // Manifest is ascending and ranges never overlap — binary search.
    const segs = this.manifest.segments
    let lo = 0
    let hi = segs.length - 1
    while (lo <= hi) {
      const mid = (lo + hi) >> 1
      const s = segs[mid]
      if (gen < s.firstGeneration) hi = mid - 1
      else if (gen > s.lastGeneration) lo = mid + 1
      else return s
    }
    return null
  }

  /** True when `gen` is packed (readable from this tier). */
  hasGeneration(gen: number): boolean {
    return this.coveringSegment(gen) !== null
  }

  /**
   * Fold consecutive generations into ONE new sealed segment + sidecar and
   * append it to the manifest atomically. Caller guarantees: `gens` is
   * ascending, contiguous with the packed tier (first = last packed + 1 when
   * segments exist), and already durable in the live tier. Crash between the
   * segment write and the caller's live-tier delete leaves a DUPLICATE
   * representation — resolved live-tier-wins by the reader; never a gap.
   */
  async fold(gens: FoldGeneration[]): Promise<SegmentMeta> {
    if (gens.length === 0) {
      throw new Error('[GenerationSegments] fold() requires at least one generation')
    }
    for (let i = 1; i < gens.length; i++) {
      if (gens[i].generation <= gens[i - 1].generation) {
        throw new Error('[GenerationSegments] fold() input must be strictly ascending')
      }
    }
    const last = this.manifest.segments[this.manifest.segments.length - 1]
    if (last && gens[0].generation <= last.lastGeneration) {
      throw new Error(
        `[GenerationSegments] fold() overlaps the packed tier: ${gens[0].generation} ≤ ` +
          `sealed ${last.lastGeneration} — segments are immutable, never rewritten`
      )
    }

    const first = gens[0].generation
    const file = segmentFileName(first)
    const sidecar: SidecarIndex = { version: 1, generations: [], ids: {} }

    // Encode all frames, tracking offsets for the sidecar.
    const parts: Uint8Array[] = [MAGIC]
    let offset = MAGIC.length
    for (const g of gens) {
      const payload = msgpackEncode([
        g.generation,
        g.timestamp,
        g.delta,
        g.records.map((r) => [r.kind === 'noun' ? 0 : 1, r.id, r.record]),
        0 // flags: v1 = uncompressed
      ])
      const frame = new Uint8Array(FRAME_PREFIX_BYTES + payload.length)
      const view = new DataView(frame.buffer)
      view.setUint32(0, payload.length, true)
      view.setUint32(4, crc32c(payload), true)
      frame.set(payload, FRAME_PREFIX_BYTES)
      sidecar.generations.push([g.generation, offset, frame.length])
      for (const r of g.records) {
        const key = `${r.kind === 'noun' ? 0 : 1}:${r.id}`
        ;(sidecar.ids[key] ??= []).push(g.generation)
      }
      parts.push(frame)
      offset += frame.length
    }
    const total = parts.reduce((n, p) => n + p.length, 0)
    const bytes = new Uint8Array(total)
    let at = 0
    for (const p of parts) {
      bytes.set(p, at)
      at += p.length
    }

    const meta: SegmentMeta = {
      file,
      firstGeneration: first,
      lastGeneration: gens[gens.length - 1].generation,
      frames: gens.length,
      bytes: total,
      checksum: crc32c(bytes)
    }

    // Durability order: segment + sidecar fsync'd BEFORE the manifest names
    // them (a crash before the manifest = invisible orphan files, harmless);
    // manifest last, atomically.
    const segPath = `${SEGMENTS_PREFIX}/${file}`
    const idxPath = `${SEGMENTS_PREFIX}/${sidecarFileName(first)}`
    await this.storage.writeRawBytes(segPath, bytes)
    await this.storage.writeRawBytes(idxPath, msgpackEncode(sidecar))
    await this.storage.syncRawObjects([segPath, idxPath])
    const next: SegmentManifest = {
      ...this.manifest,
      segments: [...this.manifest.segments, meta]
    }
    await this.storage.writeRawObject(MANIFEST_PATH, next)
    await this.storage.syncRawObjects([MANIFEST_PATH])
    this.manifest = next
    this.sidecars.set(file, sidecar)
    return meta
  }

  /** Load (or rebuild, loudly) a segment's sidecar. */
  private async sidecarFor(meta: SegmentMeta): Promise<SidecarIndex> {
    const cached = this.sidecars.get(meta.file)
    if (cached) return cached
    const idxPath = `${SEGMENTS_PREFIX}/${sidecarFileName(meta.firstGeneration)}`
    const raw = await this.storage.readRawBytes(idxPath)
    if (raw) {
      try {
        const idx = msgpackDecode(raw) as SidecarIndex
        if (idx.version === 1) {
          this.sidecars.set(meta.file, idx)
          return idx
        }
      } catch {
        // fall through to rebuild
      }
    }
    // Sidecars are DERIVED: rebuild from the segment, loudly — never serve
    // wrong offsets silently.
    prodLog.warn(
      `[GenerationSegments] sidecar for ${meta.file} missing or unreadable — rebuilding from the segment`
    )
    const rebuilt = await this.rebuildSidecar(meta)
    await this.storage.writeRawBytes(idxPath, msgpackEncode(rebuilt))
    this.sidecars.set(meta.file, rebuilt)
    return rebuilt
  }

  /** One sequential read of the segment → a fresh sidecar. Verifies every frame CRC. */
  private async rebuildSidecar(meta: SegmentMeta): Promise<SidecarIndex> {
    const frames = await this.readAllFrames(meta)
    const idx: SidecarIndex = { version: 1, generations: [], ids: {} }
    for (const f of frames) {
      idx.generations.push([f.generation, f.offset, f.frameLen])
      for (const r of f.records) {
        const key = `${r.kind === 'noun' ? 0 : 1}:${r.id}`
        ;(idx.ids[key] ??= []).push(f.generation)
      }
    }
    return idx
  }

  private decodeFrame(
    payload: Uint8Array
  ): { generation: number; timestamp: number; delta: unknown; records: FoldGeneration['records'] } {
    const [generation, timestamp, delta, rawRecords] = msgpackDecode(payload) as [
      number,
      number,
      unknown,
      Array<[number, string, unknown]>,
      number
    ]
    return {
      generation,
      timestamp,
      delta,
      records: rawRecords.map(([kindByte, id, record]) => ({
        kind: kindByte === 0 ? ('noun' as const) : ('verb' as const),
        id,
        record
      }))
    }
  }

  private async readAllFrames(meta: SegmentMeta): Promise<
    Array<ReturnType<GenerationSegmentStore['decodeFrame']> & { offset: number; frameLen: number }>
  > {
    const bytes = await this.storage.readRawBytes(`${SEGMENTS_PREFIX}/${meta.file}`)
    if (!bytes) {
      throw new Error(
        `[GenerationSegments] sealed segment ${meta.file} is MISSING — packed history is damaged; ` +
          `refusing to continue silently`
      )
    }
    const out: Array<ReturnType<GenerationSegmentStore['decodeFrame']> & { offset: number; frameLen: number }> = []
    let at = MAGIC.length
    const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
    while (at + FRAME_PREFIX_BYTES <= bytes.length) {
      const payloadLen = view.getUint32(at, true)
      const crc = view.getUint32(at + 4, true)
      const payload = bytes.subarray(at + FRAME_PREFIX_BYTES, at + FRAME_PREFIX_BYTES + payloadLen)
      if (payload.length !== payloadLen || crc32c(payload) !== crc) {
        throw new Error(
          `[GenerationSegments] frame CRC mismatch in ${meta.file} at offset ${at} — ` +
            `packed history is damaged; refusing to serve it`
        )
      }
      out.push({ ...this.decodeFrame(payload), offset: at, frameLen: FRAME_PREFIX_BYTES + payloadLen })
      at += FRAME_PREFIX_BYTES + payloadLen
    }
    return out
  }

  /** Read one packed generation's frame via its sidecar offset (one ranged read). */
  private async readFrame(
    gen: number
  ): Promise<ReturnType<GenerationSegmentStore['decodeFrame']> | null> {
    const meta = this.coveringSegment(gen)
    if (!meta) return null
    const idx = await this.sidecarFor(meta)
    // generations ascending → binary search.
    const gens = idx.generations
    let lo = 0
    let hi = gens.length - 1
    while (lo <= hi) {
      const mid = (lo + hi) >> 1
      if (gens[mid][0] < gen) lo = mid + 1
      else if (gens[mid][0] > gen) hi = mid - 1
      else {
        const [, offset, frameLen] = gens[mid]
        const bytes = await this.storage.readRawBytes(`${SEGMENTS_PREFIX}/${meta.file}`)
        if (!bytes) {
          throw new Error(`[GenerationSegments] sealed segment ${meta.file} is MISSING`)
        }
        const frame = bytes.subarray(offset, offset + frameLen)
        const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
        const payloadLen = view.getUint32(0, true)
        const crc = view.getUint32(4, true)
        const payload = frame.subarray(FRAME_PREFIX_BYTES, FRAME_PREFIX_BYTES + payloadLen)
        if (payload.length !== payloadLen || crc32c(payload) !== crc) {
          throw new Error(
            `[GenerationSegments] frame CRC mismatch for generation ${gen} in ${meta.file} — ` +
              `packed history is damaged; refusing to serve it`
          )
        }
        return this.decodeFrame(payload)
      }
    }
    // In the covering range but not present: the packed tier is dense by
    // construction (fold packs every generation it is handed, including
    // record-less ones) — absence inside a sealed range is damage.
    throw new Error(
      `[GenerationSegments] generation ${gen} is inside sealed segment ${meta.file}'s declared ` +
        `range but has no frame — packed history is damaged`
    )
  }

  /** The packed tier's delta for `gen` (null = not packed). */
  async readDelta(gen: number): Promise<{ delta: unknown; timestamp: number } | null> {
    const frame = await this.readFrame(gen)
    return frame ? { delta: frame.delta, timestamp: frame.timestamp } : null
  }

  /** The packed tier's full record-set for `gen` (null = not packed). */
  async readRecords(gen: number): Promise<FoldGeneration['records'] | null> {
    const frame = await this.readFrame(gen)
    return frame ? frame.records : null
  }

  /** One packed before-image (null = not packed OR no record for the id in that generation). */
  async readRecord(gen: number, kind: 'noun' | 'verb', id: string): Promise<unknown | null> {
    const frame = await this.readFrame(gen)
    if (!frame) return null
    const hit = frame.records.find((r) => r.kind === kind && r.id === id)
    return hit ? hit.record : null
  }

  /**
   * D3 reclaim: drop WHOLE segments whose lastGeneration < `belowGeneration`
   * and bump `compactedBelow`. Partial segments are never dropped — the
   * boundary waits. NEVER called under the archival profile (the caller
   * enforces retention semantics; this method only executes boundary drops).
   */
  async dropSegmentsBelow(belowGeneration: number): Promise<{ dropped: number; compactedBelow: number }> {
    const keep: SegmentMeta[] = []
    const drop: SegmentMeta[] = []
    for (const s of this.manifest.segments) {
      ;(s.lastGeneration < belowGeneration ? drop : keep).push(s)
    }
    if (drop.length === 0) {
      return { dropped: 0, compactedBelow: this.manifest.compactedBelow }
    }
    const compactedBelow = Math.max(
      this.manifest.compactedBelow,
      drop[drop.length - 1].lastGeneration + 1
    )
    // Manifest first (the drop is authoritative once named), then bytes —
    // a crash between leaves orphan segment files invisible to the manifest,
    // harmless and re-collectable.
    const next: SegmentManifest = { ...this.manifest, compactedBelow, segments: keep }
    await this.storage.writeRawObject(MANIFEST_PATH, next)
    await this.storage.syncRawObjects([MANIFEST_PATH])
    this.manifest = next
    for (const s of drop) {
      await this.storage.deleteRawObject(`${SEGMENTS_PREFIX}/${s.file}`)
      await this.storage.deleteRawObject(`${SEGMENTS_PREFIX}/${sidecarFileName(s.firstGeneration)}`)
      this.sidecars.delete(s.file)
    }
    return { dropped: drop.length, compactedBelow }
  }

  /**
   * D8 rider — the packed portion of `generationDigest(g)`: a deterministic
   * crc32c chain over sealed-segment checksums fully below `g`, plus the
   * frame CRC of `g`'s own frame when `g` is mid-segment. O(segments), not
   * O(generations); identical history ⇒ identical digest on any machine.
   * The live-tier portion is composed by the caller.
   */
  async digestThroughPacked(g: number): Promise<number | null> {
    let digest = 0
    let covered = false
    for (const s of this.manifest.segments) {
      if (s.lastGeneration <= g) {
        digest = crc32c(new TextEncoder().encode(`${digest}:${s.checksum}`))
        if (s.lastGeneration === g) covered = true
      } else if (s.firstGeneration <= g) {
        // g is mid-segment: chain the partial prefix via g's frame CRC.
        const frame = await this.readFrame(g)
        if (frame === null) return null
        const idx = await this.sidecarFor(s)
        const upTo = idx.generations.filter(([gen]) => gen <= g)
        for (const [gen, offset, frameLen] of upTo) {
          digest = crc32c(new TextEncoder().encode(`${digest}:${gen}:${offset}:${frameLen}`))
        }
        covered = true
        break
      }
    }
    return covered || this.manifest.segments.length > 0 ? digest : null
  }
}
