/**
 * T6 — per-tenant subdomain assignment. The subdomain is the browser-origin isolation boundary
 * (EXPLORE §3, INV1): a malformed/duplicate label must never collapse two tenants into one
 * origin. Validation is RFC-1123-label strict; assignment is an atomic single-winner reserve.
 */
import type { SubdomainStore } from '../store/ports.js'
import type { AuditWriter } from '../audit/log.js'
import { noopAuditWriter } from '../audit/log.js'
import { nowIso } from '../util/ids.js'

/** Names that must never become a tenant subdomain (host-header / infra confusion). */
export const RESERVED_SUBDOMAINS: ReadonlySet<string> = new Set([
  'www',
  'api',
  'admin',
  'app',
  'relay',
  'term',
  'root',
  'localhost',
  '_',
])

const LABEL_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/

/** Lowercase, strip characters invalid in an RFC-1123 label; collapse to a candidate label. */
export function normalizeSubdomain(raw: string): string {
  return raw
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9-]/g, '')
    .replace(/^-+/, '')
    .replace(/-+$/, '')
    .slice(0, 63)
}

/** 1–63 chars, [a-z0-9-], not leading/trailing '-', not reserved. */
export function isValidSubdomain(raw: string): boolean {
  if (raw.length < 1 || raw.length > 63) return false
  if (!LABEL_RE.test(raw)) return false
  if (RESERVED_SUBDOMAINS.has(raw)) return false
  return true
}

/** 'alice' + 'term.example.com' → 'alice.term.example.com'. */
export function assembleFqdn(subdomain: string, baseDomain: string): string {
  return `${subdomain}.${baseDomain}`
}

const MAX_COLLISION_RETRIES = 5

export interface SubdomainAssigner {
  assignSubdomain(accountId: string, requested?: string): Promise<string>
}

export interface SubdomainAssignerDeps {
  readonly subdomains: SubdomainStore
  readonly audit?: AuditWriter
  readonly actor?: string
  /** Deterministic suffix source (test seam); defaults to random 4-hex. */
  readonly suffix?: () => string
}

export function createSubdomainAssigner(deps: SubdomainAssignerDeps): SubdomainAssigner {
  const audit = deps.audit ?? noopAuditWriter()
  const actor = deps.actor ?? 'system'
  const suffix = deps.suffix ?? (() => Math.floor(Math.random() * 0xffff).toString(16).padStart(4, '0'))
  return {
    async assignSubdomain(accountId, requested) {
      const base = requested !== undefined ? normalizeSubdomain(requested) : `h${suffix()}`
      if (!isValidSubdomain(base)) {
        throw new Error(`invalid subdomain: ${requested ?? base}`)
      }
      // A requested name that collides is NEVER silently reassigned to someone else's tenant.
      // We try the exact name, then deterministic '-<suffix>' variants, else 409-equivalent throw.
      const candidates = [base, ...Array.from({ length: MAX_COLLISION_RETRIES }, () => `${base}-${suffix()}`)]
      for (const candidate of candidates) {
        if (!isValidSubdomain(candidate)) continue
        // eslint-disable-next-line no-await-in-loop
        if (await deps.subdomains.reserve(candidate)) {
          await audit.writeAuditEvent({
            action: 'subdomain.assign',
            principalId: actor,
            accountId,
            hostId: null,
            ts: nowIso(),
            meta: { subdomain: candidate },
          })
          return candidate
        }
      }
      throw new Error(`subdomain collision: ${base} is taken (409)`)
    },
  }
}
