export type ContentStatus = string

export type ContentVisibility = 'public' | 'private' | 'members'

export interface TermRef {
  id: string
  taxonomy: string
  slug: string
  name: string
  parentId: string | null
  depth: number
}

export interface ContentMediaRef {
  id: string
  kind?: string
}

export type ContentTemplateKey = string
export type ContentCommentStatus = 'open' | 'closed'
export type ContentPingStatus = 'open' | 'closed'

/**
 * Final entry columns introduced by the lossless content-model migration.
 * Canonical public entry shape for the registry-aware lifecycle.
 */
export interface ContentEntryCompletion {
  parentId: string | null
  menuOrder: number
  templateKey: ContentTemplateKey | null
  excerpt: string
  featuredMedia: ContentMediaRef | null
  commentStatus: ContentCommentStatus
  pingStatus: ContentPingStatus
  passwordProtected: boolean
  sticky: boolean
  format: string | null
  deletedAt: Date | null
  lastEditedBy: string
  typeDefinitionRevision: number
  statusDefinitionRevision: number
}

export type ContentEntry = {
  id: string
  slug: string
  type: string
  title: string
  body: string
  status: ContentStatus
  visibility: ContentVisibility
  publishedAt: Date | null
  author: string
  terms: TermRef[]
  createdAt: Date
  updatedAt: Date
} & ContentEntryCompletion

/** Backward-compatible alias retained while downstream packages migrate to the final row name. */
export type FinalContentEntry = ContentEntry

export interface ContentTemplateDescriptor {
  key: ContentTemplateKey
  label: string
  typeKeys: readonly string[]
  active?: boolean
  version?: string
}

export interface ContentTemplateRegistry {
  readonly version: string
  resolve(typeKey: string, key: ContentTemplateKey): ContentTemplateDescriptor | undefined
  list(typeKey: string): readonly ContentTemplateDescriptor[]
}

export interface ContentWriteInput {
  slug: string
  type: string
  title: string
  body: string
  excerpt?: string
  visibility?: ContentVisibility
  author?: string
  termIds?: string[]
  parentId?: string | null
  menuOrder?: number
  templateKey?: ContentTemplateKey | null
  featuredMedia?: ContentMediaRef | null
  commentStatus?: ContentCommentStatus
  pingStatus?: ContentPingStatus
  sticky?: boolean
  format?: string | null
}
export type ContentUpdatePatch = Partial<Omit<ContentWriteInput, 'type'>>

export interface ContentPasswordAdapter {
  hash(password: string): Promise<{ credential: string; version: string }>
  verify(password: string, credential: string): Promise<boolean>
  needsRehash?(credential: string): Promise<boolean> | boolean
  issueProof(entryId: string, credentialVersion: string): Promise<string>
  verifyProof(entryId: string, credentialVersion: string, proof: string): Promise<boolean>
}

export type ProtectedContentRead =
  | { access: 'granted'; entry: ContentEntry }
  | { access: 'passwordRequired'; entry: Omit<ContentEntry, 'body'> }

export type ContentSchemaSnapshot = Readonly<Record<string, unknown>>

export interface ContentRevision {
  id: string
  entryId: string
  seq: number
  title: string
  body: string
  slug: string
  type: string
  termIds: string[]
  snapshot?: ContentSchemaSnapshot
  editor: string
  createdAt: Date
}

export interface RevisionListQuery {
  /** keyset cursor: return revisions with seq < this. Omit for newest page. */
  before?: number
  /** page size, clamped (default 20, max 100 — match comments/notifications clamp). */
  limit?: number
}

export interface RevisionPage {
  revisions: ContentRevision[]
  /** seq to pass as `before` for the next page; null when exhausted. */
  nextCursor: number | null
}

/** Author-supplied write input. `id` present => update an existing entry; absent => create. */
export type ContentInput = {
  id?: string
  slug: string
  type: string
  title: string
  body: string
  visibility?: ContentVisibility
  termIds?: string[]
}

export type NormalizedContentInput = {
  id: string | null
  slug: string
  type: string
  title: string
  body: string
  visibility: ContentVisibility | null
  termIds: string[] | undefined
}

/** Typed, contextful boundary error — carries the offending field + why (coding-standard §4). */
export class ContentValidationError extends Error {
  override readonly name = 'ContentValidationError'
  constructor(
    readonly field: string,
    readonly detail: string,
  ) {
    super(`content input invalid: ${field} — ${detail}`)
  }
}

/**
 * Host-injected HTML sanitizer — REQUIRED on `put`; no default no-op (XSS hard floor).
 * The module owns the contract + enforcement; the host injects a runtime-appropriate engine
 * (DOMPurify-class). Mirrors the `comments` module's `Sanitize` seam.
 */
export type Sanitize = (raw: string) => string

/** Thrown when a write reaches the store without a usable `sanitize` function (fail-closed). */
export class ContentSanitizationError extends Error {
  override readonly name = 'ContentSanitizationError'
  constructor(message: string) {
    super(message)
  }
}

const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
const MAX_SLUG = 200
const MAX_TYPE = 64
const MAX_TITLE = 300
const VISIBILITY_LITERALS: ContentVisibility[] = ['public', 'private', 'members']
export const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i

function normTermIds(raw: unknown): string[] | undefined {
  if (raw === undefined) return undefined
  if (!Array.isArray(raw)) throw new ContentValidationError('termIds', 'must be an array of UUID strings')
  const out: string[] = []
  for (const id of raw) {
    if (typeof id !== 'string' || !UUID_RE.test(id)) {
      throw new ContentValidationError('termIds', 'must contain only well-formed UUID strings')
    }
    out.push(id)
  }
  return [...new Set(out)]
}

function normVisibility(raw: unknown, isUpdate: boolean): ContentVisibility | null {
  if (raw === undefined || raw === null) return isUpdate ? null : 'public'
  if (typeof raw !== 'string' || !VISIBILITY_LITERALS.includes(raw as ContentVisibility)) {
    throw new ContentValidationError('visibility', 'must be public|private|members')
  }
  return raw as ContentVisibility
}

/**
 * Validator-agnostic boundary normalization (north-star: never bundle a validator).
 * Hand-rolled required-field + format checks; the adopter's own validator runs at the route.
 */
export function normalizeContentInput(raw: ContentInput): NormalizedContentInput {
  if (typeof raw !== 'object' || raw === null) throw new ContentValidationError('input', 'must be an object')

  const slug = String(raw.slug ?? '').trim()
  if (!SLUG_RE.test(slug)) throw new ContentValidationError('slug', 'must be lowercase kebab-case [a-z0-9-]')
  if (slug.length > MAX_SLUG) throw new ContentValidationError('slug', `exceeds ${MAX_SLUG} chars`)

  const type = String(raw.type ?? '').trim()
  if (!type) throw new ContentValidationError('type', 'required')
  if (type.length > MAX_TYPE) throw new ContentValidationError('type', `exceeds ${MAX_TYPE} chars`)

  const title = String(raw.title ?? '').trim()
  if (!title) throw new ContentValidationError('title', 'required')
  if (title.length > MAX_TITLE) throw new ContentValidationError('title', `exceeds ${MAX_TITLE} chars`)

  const body = typeof raw.body === 'string' ? raw.body : ''

  const id = raw.id !== undefined && raw.id !== null ? String(raw.id).trim() : null
  if (id !== null && id.length === 0) throw new ContentValidationError('id', 'must be non-empty when present')

  return {
    id,
    slug,
    type,
    title,
    body,
    visibility: normVisibility(raw.visibility, id !== null),
    termIds: normTermIds(raw.termIds),
  }
}
