import { sql, type SQLWrapper } from 'drizzle-orm'
import type { ContentSchemaValue } from './schema.js'
import { canonicalContentMigrationHash } from './migrations/content-model.js'
import type {
  ContentDefinitionAuthorization,
  ContentDefinitionEffects,
  ContentDefinitionEvent,
} from './definition-lifecycle.js'
import type { ContentPrincipal } from './authz.js'

export type ContentFeature =
  | 'title'
  | 'editor'
  | 'blocks'
  | 'author'
  | 'featuredImage'
  | 'excerpt'
  | 'comments'
  | 'revisions'
  | 'pageAttributes'
  | 'postFormats'
  | 'customFields'
  | 'trackbacks'

export interface ContentTypeLabels {
  name: string
  singularName: string
  menuName: string
  nameAdminBar: string
  addNew: string
  addNewItem: string
  editItem: string
  newItem: string
  viewItem: string
  viewItems: string
  searchItems: string
  notFound: string
  notFoundInTrash: string
  parentItemColon: string
  allItems: string
  archives: string
  attributes: string
  insertIntoItem: string
  uploadedToThisItem: string
  featuredImage: string
  setFeaturedImage: string
  removeFeaturedImage: string
  useFeaturedImage: string
  filterItemsList: string
  filterByDate: string
  itemsListNavigation: string
  itemsList: string
  itemPublished: string
  itemPublishedPrivately: string
  itemRevertedToDraft: string
  itemScheduled: string
  itemUpdated: string
  itemLink: string
  itemLinkDescription: string
}

export interface ContentTypeCapabilities {
  manageType: string
  migrateAll: string
  manageTerms: string
  create: string
  read: string
  readPrivate: string
  readProtected: string
  editOwn: string
  editOthers: string
  editPrivate: string
  editPublished: string
  publish: string
  deleteOwn: string
  deleteOthers: string
  deletePrivate: string
  deletePublished: string
}

export interface ContentTypeRewrite {
  slug: string
  withFront?: boolean
  feeds?: boolean
  pages?: boolean
}

export type ContentTemplateValue =
  | string
  | number
  | boolean
  | null
  | readonly ContentTemplateValue[]
  | { readonly [key: string]: ContentTemplateValue }

export interface ContentTemplateBlock {
  type: string
  attributes?: Readonly<Record<string, ContentTemplateValue>>
  children?: readonly ContentTemplateBlock[]
}

export interface ContentTypeDefinition {
  key: string
  labels: ContentTypeLabels
  description?: string
  public: boolean
  hierarchical: boolean
  excludeFromSearch: boolean
  publiclyQueryable: boolean
  showUi: boolean
  showInMenu: boolean | string
  showInNavMenus: boolean
  showInAdminBar: boolean
  menuPosition?: number
  menuIcon?: string
  capabilities: ContentTypeCapabilities
  supports: readonly ContentFeature[]
  taxonomies: readonly string[]
  hasArchive: boolean | string
  rewrite: false | ContentTypeRewrite
  queryVariable: false | string
  canExport: boolean
  deleteWithAuthor: boolean | null
  rest: false | { base: string; namespace: string; revisions: boolean; autosaves: boolean }
  defaultTemplateKey: string
  template?: readonly ContentTemplateBlock[]
  templateLock?: false | 'insert' | 'all' | 'contentOnly'
  statusKeys?: readonly string[]
  active: boolean
  position?: number
}

export type CodeContentType = ContentTypeDefinition & { readonly origin: 'code' }
export interface ResolvedContentType extends ContentTypeDefinition {
  readonly origin: 'code' | 'db'
  readonly version: number
  readonly revision: number
  readonly canonicalHash: string
  readonly shadowedDbVersion?: number
}
export type ContentTypePatch = Partial<Omit<ContentTypeDefinition, 'key' | 'active'>>

export type ContentDefinitionExecutor = {
  execute<Row extends Record<string, unknown> = Record<string, unknown>>(query: SQLWrapper): Promise<readonly Row[]>
}

export type ContentDefinitionErrorCode =
  | 'invalid-definition'
  | 'definition-conflict'
  | 'definition-not-found'
  | 'stale-definition'
  | 'code-definition-owned'
  | 'operation-conflict'
  | 'invalid-receipt'
  | 'stale-impact'
  | 'backup-required'
  | 'backup-mismatch'
  | 'strategy-conflict'
  | 'authorization-drift'

export class ContentDefinitionError extends Error {
  override readonly name = 'ContentDefinitionError'

  constructor(
    readonly code: ContentDefinitionErrorCode,
    readonly detail: string,
    readonly field?: string,
  ) {
    super(`content definition ${code}: ${detail}`)
  }
}

export function isContentDefinitionError(error: unknown): error is ContentDefinitionError {
  return error instanceof ContentDefinitionError
}

const TYPE_KEY = /^[a-z][a-z0-9_-]{0,63}$/
const CAPABILITY_KEYS = [
  'manageType', 'migrateAll', 'manageTerms', 'create', 'read', 'readPrivate', 'readProtected',
  'editOwn', 'editOthers', 'editPrivate', 'editPublished', 'publish', 'deleteOwn', 'deleteOthers',
  'deletePrivate', 'deletePublished',
] as const satisfies readonly (keyof ContentTypeCapabilities)[]
const LABEL_KEYS = [
  'name', 'singularName', 'menuName', 'nameAdminBar', 'addNew', 'addNewItem', 'editItem', 'newItem',
  'viewItem', 'viewItems', 'searchItems', 'notFound', 'notFoundInTrash', 'parentItemColon', 'allItems',
  'archives', 'attributes', 'insertIntoItem', 'uploadedToThisItem', 'featuredImage', 'setFeaturedImage',
  'removeFeaturedImage', 'useFeaturedImage', 'filterItemsList', 'filterByDate', 'itemsListNavigation',
  'itemsList', 'itemPublished', 'itemPublishedPrivately', 'itemRevertedToDraft', 'itemScheduled',
  'itemUpdated', 'itemLink', 'itemLinkDescription',
] as const satisfies readonly (keyof ContentTypeLabels)[]
const FEATURE_SET = new Set<ContentFeature>([
  'title', 'editor', 'blocks', 'author', 'featuredImage', 'excerpt', 'comments', 'revisions',
  'pageAttributes', 'postFormats', 'customFields', 'trackbacks',
])

function invalid(detail: string, field?: string): never {
  throw new ContentDefinitionError('invalid-definition', detail, field)
}

function bounded(value: unknown, field: string, max = 256): string {
  if (typeof value !== 'string') invalid('must be a string', field)
  const normalized = value.trim()
  if (normalized.length === 0 || normalized.length > max) invalid(`must contain 1..${max} characters`, field)
  return normalized
}

function finitePosition(value: unknown, field: string): number | undefined {
  if (value === undefined) return undefined
  if (typeof value !== 'number' || !Number.isSafeInteger(value)) invalid('must be a safe integer', field)
  return value
}

function uniqueSorted(values: readonly string[], field: string): readonly string[] {
  const normalized = values.map((value, index) => bounded(value, `${field}[${index}]`, 128))
  return Object.freeze([...new Set(normalized)].sort())
}

function normalizeTemplateValue(value: ContentTemplateValue, path: string, depth = 0): ContentTemplateValue {
  if (depth > 32) invalid('template nesting exceeds 32 levels', path)
  if (value === null || typeof value === 'string' || typeof value === 'boolean') return value
  if (typeof value === 'number') {
    if (!Number.isFinite(value)) invalid('template numbers must be finite', path)
    return value
  }
  if (Array.isArray(value)) return Object.freeze(value.map((item, index) => normalizeTemplateValue(item, `${path}[${index}]`, depth + 1)))
  if (typeof value === 'object') {
    const out: Record<string, ContentTemplateValue> = {}
    for (const key of Object.keys(value).sort()) {
      out[key] = normalizeTemplateValue((value as Record<string, ContentTemplateValue>)[key]!, `${path}.${key}`, depth + 1)
    }
    return Object.freeze(out)
  }
  invalid('unsupported template value', path)
}

function normalizeTemplateBlock(block: ContentTemplateBlock, path: string, depth = 0): ContentTemplateBlock {
  if (depth > 32) invalid('template nesting exceeds 32 levels', path)
  const type = bounded(block.type, `${path}.type`, 128)
  const attributes = block.attributes
    ? Object.freeze(Object.fromEntries(Object.keys(block.attributes).sort().map((key) => [key, normalizeTemplateValue(block.attributes![key]!, `${path}.attributes.${key}`)])))
    : undefined
  const children = block.children
    ? Object.freeze(block.children.map((child, index) => normalizeTemplateBlock(child, `${path}.children[${index}]`, depth + 1)))
    : undefined
  return Object.freeze({ type, ...(attributes ? { attributes } : {}), ...(children ? { children } : {}) })
}

export function normalizeContentTypeDefinition(definition: ContentTypeDefinition): ContentTypeDefinition {
  if (!TYPE_KEY.test(definition.key)) invalid('must be a stable lowercase machine key', 'key')
  const labels = Object.fromEntries(LABEL_KEYS.map((key) => [key, bounded(definition.labels?.[key], `labels.${key}`, 256)])) as unknown as ContentTypeLabels
  const capabilities = Object.fromEntries(CAPABILITY_KEYS.map((key) => [key, bounded(definition.capabilities?.[key], `capabilities.${key}`, 128)])) as unknown as ContentTypeCapabilities
  if (!Array.isArray(definition.supports)) invalid('must be an array', 'supports')
  for (const [index, feature] of definition.supports.entries()) {
    if (!FEATURE_SET.has(feature)) invalid(`unknown feature ${String(feature)}`, `supports[${index}]`)
  }
  const supports = Object.freeze([...new Set(definition.supports)].sort()) as readonly ContentFeature[]
  const taxonomies = uniqueSorted(definition.taxonomies ?? [], 'taxonomies')
  const statusKeys = definition.statusKeys === undefined ? undefined : uniqueSorted(definition.statusKeys, 'statusKeys')
  const defaultTemplateKey = bounded(definition.defaultTemplateKey, 'defaultTemplateKey', 128)

  if (definition.hasArchive !== false && !definition.publiclyQueryable) invalid('archive requires publiclyQueryable', 'hasArchive')
  if (definition.rewrite !== false && !definition.publiclyQueryable) invalid('rewrite requires publiclyQueryable', 'rewrite')
  if (definition.template !== undefined && !supports.includes('blocks')) invalid('template requires blocks support', 'template')
  if (!definition.hierarchical && supports.includes('pageAttributes')) {
    // pageAttributes remains useful for menu order/template; only parent assignment is hierarchy-gated.
  }
  if (definition.rest !== false) {
    if (definition.rest.revisions && !supports.includes('revisions')) invalid('REST revisions require revisions support', 'rest.revisions')
    if (definition.rest.autosaves && (!supports.includes('revisions') || !supports.includes('editor'))) {
      invalid('REST autosaves require revisions and editor support', 'rest.autosaves')
    }
  }

  const rewrite = definition.rewrite === false ? false : Object.freeze({
    slug: bounded(definition.rewrite.slug, 'rewrite.slug', 128),
    ...(definition.rewrite.withFront === undefined ? {} : { withFront: Boolean(definition.rewrite.withFront) }),
    ...(definition.rewrite.feeds === undefined ? {} : { feeds: Boolean(definition.rewrite.feeds) }),
    ...(definition.rewrite.pages === undefined ? {} : { pages: Boolean(definition.rewrite.pages) }),
  })
  const rest = definition.rest === false ? false : Object.freeze({
    base: bounded(definition.rest.base, 'rest.base', 128),
    namespace: bounded(definition.rest.namespace, 'rest.namespace', 128),
    revisions: Boolean(definition.rest.revisions),
    autosaves: Boolean(definition.rest.autosaves),
  })
  const template = definition.template === undefined
    ? undefined
    : Object.freeze(definition.template.map((block, index) => normalizeTemplateBlock(block, `template[${index}]`)))

  return Object.freeze({
    key: definition.key,
    labels: Object.freeze(labels),
    ...(definition.description === undefined ? {} : { description: bounded(definition.description, 'description', 4096) }),
    public: Boolean(definition.public),
    hierarchical: Boolean(definition.hierarchical),
    excludeFromSearch: Boolean(definition.excludeFromSearch),
    publiclyQueryable: Boolean(definition.publiclyQueryable),
    showUi: Boolean(definition.showUi),
    showInMenu: typeof definition.showInMenu === 'string' ? bounded(definition.showInMenu, 'showInMenu', 128) : Boolean(definition.showInMenu),
    showInNavMenus: Boolean(definition.showInNavMenus),
    showInAdminBar: Boolean(definition.showInAdminBar),
    ...(finitePosition(definition.menuPosition, 'menuPosition') === undefined ? {} : { menuPosition: definition.menuPosition }),
    ...(definition.menuIcon === undefined ? {} : { menuIcon: bounded(definition.menuIcon, 'menuIcon', 256) }),
    capabilities: Object.freeze(capabilities),
    supports,
    taxonomies,
    hasArchive: typeof definition.hasArchive === 'string' ? bounded(definition.hasArchive, 'hasArchive', 128) : Boolean(definition.hasArchive),
    rewrite,
    queryVariable: typeof definition.queryVariable === 'string' ? bounded(definition.queryVariable, 'queryVariable', 128) : false,
    canExport: Boolean(definition.canExport),
    deleteWithAuthor: definition.deleteWithAuthor === null ? null : Boolean(definition.deleteWithAuthor),
    rest,
    defaultTemplateKey,
    ...(template ? { template } : {}),
    ...(definition.templateLock === undefined ? {} : { templateLock: definition.templateLock }),
    ...(statusKeys ? { statusKeys } : {}),
    active: Boolean(definition.active),
    ...(finitePosition(definition.position, 'position') === undefined ? {} : { position: definition.position }),
  })
}

export function defineContentType(definition: ContentTypeDefinition): CodeContentType {
  return Object.freeze({ ...normalizeContentTypeDefinition(definition), origin: 'code' as const })
}

function parseDefinition(value: unknown): ContentTypeDefinition {
  const parsed = typeof value === 'string' ? JSON.parse(value) as unknown : value
  return normalizeContentTypeDefinition(parsed as ContentTypeDefinition)
}

function bool(value: unknown): boolean {
  return value === true || value === 1 || value === '1'
}

interface DefinitionRow extends Record<string, unknown> {
  key: string
  origin: string
  version: number | string
  currentRevision: number | string
  canonicalHash: string
  definition: unknown
  active: boolean | number | string
  shadowedDbVersion: number | string | null
}

async function definitionRows(db: ContentDefinitionExecutor, kind: 'type' | 'status', key?: string): Promise<readonly DefinitionRow[]> {
  const table = kind === 'type' ? 'content_type_definitions' : 'content_status_definitions'
  const where = key === undefined ? sql.raw('') : sql` WHERE key = ${key}`
  return db.execute<DefinitionRow>(sql`SELECT key, origin, version, current_revision AS "currentRevision", canonical_hash AS "canonicalHash", definition, active, shadowed_db_version AS "shadowedDbVersion" FROM ${sql.raw(table)}${where}`)
}

export async function resolveContentTypes(
  db: ContentDefinitionExecutor,
  opts: { codeTypes?: readonly CodeContentType[] } = {},
): Promise<ResolvedContentType[]> {
  const rows = await definitionRows(db, 'type')
  const code = new Map((opts.codeTypes ?? []).map((definition) => [definition.key, defineContentType(definition)]))
  const dbRows = new Map(rows.map((row) => [row.key, row]))
  const resolved: ResolvedContentType[] = []

  for (const definition of code.values()) {
    const collision = dbRows.get(definition.key)
    const canonicalHash = await canonicalContentMigrationHash(definition)
    resolved.push(Object.freeze({
      ...definition,
      version: collision?.origin === 'code' ? Number(collision.version) : 1,
      revision: collision?.origin === 'code' ? Number(collision.currentRevision) : 1,
      canonicalHash,
      ...(collision && collision.origin !== 'code' ? { shadowedDbVersion: Number(collision.version) } : {}),
    }))
  }

  for (const row of rows) {
    if (code.has(row.key)) continue
    const definition = parseDefinition(row.definition)
    resolved.push(Object.freeze({
      ...definition,
      active: bool(row.active),
      origin: row.origin === 'code' ? 'code' : 'db',
      version: Number(row.version),
      revision: Number(row.currentRevision),
      canonicalHash: row.canonicalHash,
      ...(row.shadowedDbVersion === null ? {} : { shadowedDbVersion: Number(row.shadowedDbVersion) }),
    }))
  }

  return resolved.sort((left, right) =>
    (left.position ?? 0) - (right.position ?? 0)
      || left.labels.name.localeCompare(right.labels.name)
      || left.key.localeCompare(right.key))
}

export async function resolveContentType(
  db: ContentDefinitionExecutor,
  key: string,
  opts: { codeTypes?: readonly CodeContentType[] } = {},
): Promise<ResolvedContentType> {
  const found = (await resolveContentTypes(db, opts)).find((definition) => definition.key === key)
  if (!found) throw new ContentDefinitionError('definition-not-found', 'content type is not registered')
  return found
}

export interface DefinitionReceiptPayload {
  requestHash: string
  principalScope: string
  result: ContentSchemaValue
}

export function principalScope(principal: ContentPrincipal): string {
  return `${principal.tenantId ?? ''}:${principal.id}`
}

export async function readDefinitionReceipt(db: ContentDefinitionExecutor, operationId: string): Promise<DefinitionReceiptPayload | undefined> {
  const rows = await db.execute<{ payload: unknown }>(sql`SELECT payload FROM content_lifecycle_journal WHERE operation_id = ${operationId}`)
  const raw = rows[0]?.payload
  if (raw === undefined) return undefined
  const payload = typeof raw === 'string' ? JSON.parse(raw) as DefinitionReceiptPayload : raw as DefinitionReceiptPayload
  if (!payload || typeof payload.requestHash !== 'string' || typeof payload.principalScope !== 'string') {
    throw new ContentDefinitionError('invalid-receipt', 'operation receipt is malformed')
  }
  return payload
}

export async function writeDefinitionReceipt(
  db: ContentDefinitionExecutor,
  input: { operationId: string; kind: string; payload: DefinitionReceiptPayload },
): Promise<void> {
  const now = new Date().toISOString()
  await db.execute(sql`INSERT INTO content_lifecycle_journal (id, operation_id, kind, state, payload, created_at, updated_at)
    VALUES (${crypto.randomUUID()}, ${input.operationId}, ${input.kind}, 'committed', ${JSON.stringify(input.payload)}, ${now}, ${now})`)
}

export async function writeDefinitionVersion(
  db: ContentDefinitionExecutor,
  input: { kind: 'type' | 'status'; key: string; revision: number; canonicalHash: string; definition: ContentSchemaValue; origin: 'code' | 'db' | 'import' },
): Promise<void> {
  const now = new Date().toISOString()
  await db.execute(sql`INSERT INTO content_definition_versions (definition_kind, definition_key, revision, canonical_hash, definition, origin, created_at)
    VALUES (${input.kind}, ${input.key}, ${input.revision}, ${input.canonicalHash}, ${JSON.stringify(input.definition)}, ${input.origin}, ${now})`)
}

export async function createContentType(
  db: ContentDefinitionExecutor,
  principal: ContentPrincipal,
  input: { definition: ContentTypeDefinition; operationId: string },
  authorization: ContentDefinitionAuthorization,
  effects: ContentDefinitionEffects,
  opts: { codeTypes?: readonly CodeContentType[] } = {},
): Promise<ResolvedContentType> {
  const normalized = normalizeContentTypeDefinition({ ...input.definition, active: true })
  const { policyVersion } = await authorization.assert(db, principal, 'createType', normalized.key)
  if ((opts.codeTypes ?? []).some((definition) => definition.key === normalized.key)) {
    throw new ContentDefinitionError('definition-conflict', 'database definition cannot shadow a code-owned key', 'key')
  }
  const requestHash = await canonicalContentMigrationHash({ definition: normalized, operationId: input.operationId })
  const scope = principalScope(principal)
  const replay = await readDefinitionReceipt(db, input.operationId)
  if (replay) {
    if (replay.requestHash !== requestHash || replay.principalScope !== scope) {
      throw new ContentDefinitionError('operation-conflict', 'operation id was already used for different bytes or scope')
    }
    return Object.freeze(replay.result as unknown as ResolvedContentType)
  }
  if ((await definitionRows(db, 'type', normalized.key)).length !== 0) {
    throw new ContentDefinitionError('definition-conflict', 'content type key already exists', 'key')
  }

  const canonicalHash = await canonicalContentMigrationHash(normalized)
  const now = new Date().toISOString()
  await db.execute(sql`INSERT INTO content_type_definitions (key, origin, version, active, current_revision, canonical_hash, definition, created_at, updated_at)
    VALUES (${normalized.key}, 'db', 1, ${true}, 1, ${canonicalHash}, ${JSON.stringify(normalized)}, ${now}, ${now})`)
  await writeDefinitionVersion(db, { kind: 'type', key: normalized.key, revision: 1, canonicalHash, definition: normalized as unknown as ContentSchemaValue, origin: 'db' })
  const result: ResolvedContentType = Object.freeze({ ...normalized, origin: 'db', version: 1, revision: 1, canonicalHash })
  await writeDefinitionReceipt(db, { operationId: input.operationId, kind: 'definition:create:type', payload: { requestHash, principalScope: scope, result: result as unknown as ContentSchemaValue } })
  const event: ContentDefinitionEvent = {
    id: crypto.randomUUID(), version: 1, kind: 'dbDefinitionCreated', operationId: input.operationId,
    principalId: principal.id, definitionKind: 'type', definitionKey: normalized.key,
    definitionHash: canonicalHash, revision: 1, authorizationPolicyVersion: policyVersion,
  }
  await effects.audit.record(db, event)
  await effects.outbox.enqueue(db, event)
  return result
}

export const __contentRegistryInternals = Object.freeze({
  definitionRows,
  parseDefinition,
  bool,
})
