/**
 * In-memory adapter for the repository ports — the tested + v0.9-MVP-shipped default.
 *
 * INV8: lifecycle/business fields (account.status, host.status/revoked_at) are versioned via
 * append-only companion rows + an atomic single-row pointer swap. Each `swapStatus` mutation is
 * a synchronous critical section (no `await` inside) so a concurrent reader sees whole-old or
 * whole-new — never a torn mix. Prior snapshots stay readable from the `versions()` list.
 * High-frequency liveness (`last_seen`, route TTL) is advisory and NOT versioned (INV7).
 */
import type {
  AccountRecord,
  AccountStatus,
  AccountStatusVersionRow,
  HostRecord,
  HostStatus,
  HostStatusVersionRow,
  MeteringSampleRow,
  PairingCodeRecord,
  RouteEntry,
  SessionRecord,
} from '../model/records.js'
import type {
  AccountStore,
  AuditRow,
  AuditStore,
  CasOutcome,
  HostStore,
  MeteringStore,
  NodeRow,
  NodeStatus,
  NodeStore,
  PairingRow,
  PairingStore,
  RouteStore,
  SessionStore,
  Stores,
  SubdomainStore,
} from './ports.js'

interface AccountCell {
  record: AccountRecord
  version: number
  versions: AccountStatusVersionRow[]
}
interface HostCell {
  record: HostRecord
  version: number
  versions: HostStatusVersionRow[]
}

function memAccountStore(): AccountStore {
  const cells = new Map<string, AccountCell>()
  return {
    async insert(rec) {
      if (cells.has(rec.accountId)) throw new Error('duplicate accountId')
      cells.set(rec.accountId, {
        record: rec,
        version: 1,
        versions: [
          { accountId: rec.accountId, version: 1, status: rec.status, changedAt: rec.createdAt, changedBy: 'system' },
        ],
      })
    },
    async get(id) {
      return cells.get(id)?.record ?? null
    },
    async swapStatus(id, status, changedBy) {
      const cell = cells.get(id)
      if (cell === undefined) throw new Error('account not found')
      // --- atomic critical section (no await) ---
      const nextVersion = cell.version + 1
      const changedAt = new Date().toISOString()
      const nextRecord: AccountRecord = { ...cell.record, status }
      cell.versions.push({ accountId: id, version: nextVersion, status, changedAt, changedBy })
      cell.record = nextRecord
      cell.version = nextVersion
      // --- end critical section ---
      return nextRecord
    },
    async versions(id) {
      return (cells.get(id)?.versions ?? []).slice()
    },
  }
}

function memHostStore(): HostStore {
  const cells = new Map<string, HostCell>()
  const bySub = new Map<string, string>()
  return {
    async insert(rec) {
      if (cells.has(rec.hostId)) throw new Error('duplicate hostId')
      if (bySub.has(rec.subdomain)) throw new Error('duplicate subdomain')
      cells.set(rec.hostId, {
        record: rec,
        version: 1,
        versions: [
          {
            hostId: rec.hostId,
            version: 1,
            status: rec.status,
            revokedAt: rec.revokedAt,
            changedAt: rec.createdAt,
            changedBy: 'system',
          },
        ],
      })
      bySub.set(rec.subdomain, rec.hostId)
    },
    async get(id) {
      return cells.get(id)?.record ?? null
    },
    async getBySubdomain(sub) {
      const id = bySub.get(sub)
      return id === undefined ? null : (cells.get(id)?.record ?? null)
    },
    async listByAccount(accountId) {
      return [...cells.values()].filter((c) => c.record.accountId === accountId).map((c) => c.record)
    },
    async swapStatus(id, status, revokedAt, changedBy) {
      const cell = cells.get(id)
      if (cell === undefined) throw new Error('host not found')
      const nextVersion = cell.version + 1
      const changedAt = new Date().toISOString()
      const nextRecord: HostRecord = { ...cell.record, status, revokedAt }
      cell.versions.push({ hostId: id, version: nextVersion, status, revokedAt, changedAt, changedBy })
      cell.record = nextRecord
      cell.version = nextVersion
      return nextRecord
    },
    async touchLastSeen(id, ts) {
      const cell = cells.get(id)
      if (cell === undefined) return
      // Advisory cache — overwrite in place, NO version row (INV7).
      cell.record = { ...cell.record, lastSeen: ts }
    },
    async versions(id) {
      return (cells.get(id)?.versions ?? []).slice()
    },
  }
}

function memSessionStore(): SessionStore {
  const rows = new Map<string, SessionRecord>()
  return {
    async insert(rec) {
      if (rows.has(rec.sessionId)) throw new Error('duplicate sessionId')
      rows.set(rec.sessionId, rec)
    },
    async get(id) {
      return rows.get(id) ?? null
    },
  }
}

function memSubdomainStore(hosts: HostStore): SubdomainStore {
  const reserved = new Set<string>()
  return {
    async reserve(sub) {
      // Synchronous claim FIRST (no await before add) so two concurrent callers can't both win.
      if (reserved.has(sub)) return false
      reserved.add(sub)
      if ((await hosts.getBySubdomain(sub)) !== null) {
        reserved.delete(sub) // an already-bound host owns this label — release the optimistic claim
        return false
      }
      return true
    },
    async isTaken(sub) {
      return reserved.has(sub) || (await hosts.getBySubdomain(sub)) !== null
    },
  }
}

interface PairingCell {
  record: PairingCodeRecord
  attempts: number
}

function memPairingStore(): PairingStore {
  const cells = new Map<string, PairingCell>()
  return {
    async insert(rec) {
      if (cells.has(rec.codeHash)) throw new Error('duplicate code_hash')
      cells.set(rec.codeHash, { record: rec, attempts: 0 })
    },
    async get(codeHash): Promise<PairingRow | null> {
      const cell = cells.get(codeHash)
      return cell === undefined ? null : { record: cell.record, redeemAttempts: cell.attempts }
    },
    async casRedeem(codeHash, nowIso): Promise<CasOutcome> {
      const cell = cells.get(codeHash)
      if (cell === undefined) return 'unknown'
      // --- atomic critical section: CAS redeemedAt from null ---
      if (cell.record.redeemedAt !== null) return 'already_redeemed'
      cell.record = { ...cell.record, redeemedAt: nowIso }
      return 'ok'
      // --- end critical section ---
    },
    async registerFailure(codeHash) {
      const cell = cells.get(codeHash)
      if (cell === undefined) return 0
      cell.attempts += 1
      return cell.attempts
    },
  }
}

interface RouteCell {
  entry: RouteEntry
  expiresAtMs: number
}
function memRouteStore(): RouteStore {
  const cells = new Map<string, RouteCell>()
  const live = (id: string): RouteCell | null => {
    const c = cells.get(id)
    if (c === undefined) return null
    if (c.expiresAtMs <= Date.now()) {
      cells.delete(id) // fail closed: expired ⇒ offline (INV7)
      return null
    }
    return c
  }
  return {
    async set(hostId, entry, ttlSec) {
      cells.set(hostId, { entry, expiresAtMs: Date.now() + ttlSec * 1000 })
    },
    async refreshTtl(hostId, ttlSec) {
      const c = live(hostId)
      if (c === null) return false
      c.expiresAtMs = Date.now() + ttlSec * 1000
      return true
    },
    async get(hostId) {
      return live(hostId)?.entry ?? null
    },
    async delete(hostId) {
      cells.delete(hostId)
    },
    async listByNode(nodeId) {
      const out: { hostId: string; entry: RouteEntry }[] = []
      for (const hostId of [...cells.keys()]) {
        const c = live(hostId)
        if (c !== null && c.entry.relayNodeId === nodeId) out.push({ hostId, entry: c.entry })
      }
      return out
    },
  }
}

function memNodeStore(): NodeStore {
  const rows = new Map<string, NodeRow>()
  return {
    async upsert(row) {
      rows.set(row.nodeId, row)
    },
    async get(id) {
      return rows.get(id) ?? null
    },
    async setStatus(id, status: NodeStatus) {
      const r = rows.get(id)
      if (r === undefined) throw new Error('node not found')
      rows.set(id, { ...r, status })
    },
    async touch(id, ts) {
      const r = rows.get(id)
      if (r !== undefined) rows.set(id, { ...r, lastSeen: ts })
    },
    async delete(id) {
      rows.delete(id)
    },
  }
}

function memMeteringStore(): MeteringStore {
  const rows: MeteringSampleRow[] = []
  return {
    async append(row) {
      rows.push(row) // append-only
    },
    async query(accountId, fromIso, toIso) {
      const from = Date.parse(fromIso)
      const to = Date.parse(toIso)
      return rows.filter(
        (r) => r.accountId === accountId && Date.parse(r.sampledAt) >= from && Date.parse(r.sampledAt) <= to,
      )
    },
  }
}

function memAuditStore(): AuditStore {
  const rows: AuditRow[] = []
  return {
    async append(row) {
      rows.push(row) // append-only, no update/delete exposed (INV10)
    },
    async query(accountId, fromIso, toIso) {
      const from = Date.parse(fromIso)
      const to = Date.parse(toIso)
      return rows.filter(
        (r) => r.accountId === accountId && Date.parse(r.ts) >= from && Date.parse(r.ts) <= to,
      )
    },
  }
}

/** Build a full in-memory store set (all ports wired). */
export function createMemoryStores(): Stores {
  const hosts = memHostStore()
  return {
    accounts: memAccountStore(),
    hosts,
    sessions: memSessionStore(),
    subdomains: memSubdomainStore(hosts),
    pairing: memPairingStore(),
    routes: memRouteStore(),
    nodes: memNodeStore(),
    metering: memMeteringStore(),
    audit: memAuditStore(),
  }
}
