import { sql } from 'drizzle-orm'
import { authorizeContentAction, type ContentPrincipal } from './authz.js'
import { canonicalContentMigrationHash } from './migrations/content-model.js'
import { resolveContentType, type ContentDefinitionExecutor, type ResolvedContentType } from './registry.js'
import { resolveContentStatus } from './status.js'
import { assertActiveContentTransaction, type ContentTransaction } from './schema.js'
import type { ContentVisibility } from './model.js'

export class ContentHierarchyError extends Error {
  override readonly name = 'ContentHierarchyError'
  constructor(readonly code: 'invalid-parent' | 'cycle' | 'stale-plan' | 'access-denied', readonly detail: string) {
    super(`content hierarchy ${code}: ${detail}`)
  }
}

interface HierarchyRow extends Record<string, unknown> {
  id: string
  type: string
  status: string
  visibility: ContentVisibility
  author: string
  parentId: string | null
  updatedAt: string | Date
  deletedAt: string | Date | null
  typeDefinitionRevision: number | string
  statusDefinitionRevision: number | string
}

function date(value: string | Date): Date {
  const parsed = value instanceof Date ? new Date(value.getTime()) : new Date(value)
  if (Number.isNaN(parsed.getTime())) throw new ContentHierarchyError('stale-plan', 'stored entry version is invalid')
  return parsed
}

async function row(tx: ContentDefinitionExecutor, id: string): Promise<HierarchyRow | undefined> {
  const rows = await tx.execute<HierarchyRow>(sql`SELECT id, type, status, visibility, author,
    parent_id AS "parentId", updated_at AS "updatedAt", deleted_at AS "deletedAt",
    type_definition_revision AS "typeDefinitionRevision", status_definition_revision AS "statusDefinitionRevision"
    FROM content_entries WHERE id = ${id}`)
  return rows[0]
}

export async function validateContentParent(
  tx: ContentDefinitionExecutor,
  input: { entryId?: string; type: ResolvedContentType; parentId: string | null },
): Promise<void> {
  assertActiveContentTransaction(tx as ContentTransaction)
  if (input.parentId === null) return
  if (!input.type.hierarchical) throw new ContentHierarchyError('invalid-parent', 'flat content types cannot have parents')
  const parent = await row(tx, input.parentId)
  if (!parent || parent.deletedAt !== null || parent.type !== input.type.key) {
    throw new ContentHierarchyError('invalid-parent', 'parent is unavailable or belongs to another type')
  }
  const seen = new Set<string>()
  let current: HierarchyRow | undefined = parent
  for (let depth = 0; current; depth++) {
    if (depth > 1_000) throw new ContentHierarchyError('cycle', 'hierarchy exceeds the maximum supported depth')
    if (input.entryId && current.id === input.entryId) throw new ContentHierarchyError('cycle', 'parent would create a cycle')
    if (seen.has(current.id)) throw new ContentHierarchyError('cycle', 'existing hierarchy contains a cycle')
    seen.add(current.id)
    if (current.parentId === null) break
    current = await row(tx, current.parentId)
    if (!current) throw new ContentHierarchyError('invalid-parent', 'ancestor chain is incomplete')
    if (current.type !== input.type.key || current.deletedAt !== null) {
      throw new ContentHierarchyError('invalid-parent', 'ancestor chain leaves the active type hierarchy')
    }
  }
}

export interface ContentPermanentDeleteItem { readonly id: string; readonly expectedUpdatedAt: Date }
export interface ContentPermanentDeletePlan {
  readonly token: string
  readonly selected: readonly ContentPermanentDeleteItem[]
  readonly reparent: readonly { childId: string; fromParentId: string; toParentId: string | null; expectedUpdatedAt: Date }[]
  readonly corpusVersion: string
  readonly authorizationVersion: string
}

function principalAuthorizationVersion(principal: ContentPrincipal): string {
  return [...principal.capabilities].sort().join('\u0000')
}

async function authorizeDeleteRow(tx: ContentDefinitionExecutor, principal: ContentPrincipal, candidate: HierarchyRow): Promise<void> {
  try {
    const [type, status] = await Promise.all([resolveContentType(tx, candidate.type), resolveContentStatus(tx, candidate.status)])
    if (Number(candidate.typeDefinitionRevision) !== type.revision || Number(candidate.statusDefinitionRevision) !== status.revision) throw new Error('definition revision mismatch')
    authorizeContentAction({ principal, type, status, operation: 'delete', entry: candidate })
  } catch {
    throw new ContentHierarchyError('access-denied', 'selected content is unavailable')
  }
}

async function authorizeEditRow(tx: ContentDefinitionExecutor, principal: ContentPrincipal, candidate: HierarchyRow): Promise<void> {
  try {
    const [type, status] = await Promise.all([resolveContentType(tx, candidate.type), resolveContentStatus(tx, candidate.status)])
    if (Number(candidate.typeDefinitionRevision) !== type.revision || Number(candidate.statusDefinitionRevision) !== status.revision) throw new Error('definition revision mismatch')
    authorizeContentAction({ principal, type, status, operation: 'edit', entry: candidate })
  } catch {
    throw new ContentHierarchyError('access-denied', 'selected content is unavailable')
  }
}

export async function preparePermanentContentDelete(
  tx: ContentTransaction,
  items: readonly ContentPermanentDeleteItem[],
  principal: ContentPrincipal,
): Promise<ContentPermanentDeletePlan> {
  assertActiveContentTransaction(tx)
  if (!Array.isArray(items) || items.length === 0 || items.length > 500) {
    throw new ContentHierarchyError('stale-plan', 'delete selection must contain 1..500 entries')
  }
  const ids = new Set<string>()
  const selectedRows = new Map<string, HierarchyRow>()
  for (const item of items) {
    if (!item || typeof item.id !== 'string' || !item.id || !(item.expectedUpdatedAt instanceof Date) || Number.isNaN(item.expectedUpdatedAt.getTime()) || ids.has(item.id)) {
      throw new ContentHierarchyError('stale-plan', 'delete selection is malformed or duplicated')
    }
    ids.add(item.id)
    const candidate = await row(tx, item.id)
    if (!candidate || candidate.deletedAt !== null || date(candidate.updatedAt).getTime() !== item.expectedUpdatedAt.getTime()) {
      throw new ContentHierarchyError('access-denied', 'selected content is unavailable')
    }
    await authorizeDeleteRow(tx, principal, candidate)
    selectedRows.set(item.id, candidate)
  }

  const reparent: Array<{ childId: string; fromParentId: string; toParentId: string | null; expectedUpdatedAt: Date }> = []
  const selectedSorted = [...selectedRows.values()].sort((a, b) => a.id.localeCompare(b.id))
  for (const parent of selectedSorted) {
    const children = await tx.execute<HierarchyRow>(sql`SELECT id, type, status, visibility, author,
      parent_id AS "parentId", updated_at AS "updatedAt", deleted_at AS "deletedAt",
    type_definition_revision AS "typeDefinitionRevision", status_definition_revision AS "statusDefinitionRevision"
      FROM content_entries WHERE parent_id = ${parent.id} ORDER BY id`)
    for (const child of children) {
      if (ids.has(child.id) || child.deletedAt !== null) continue
      let targetId = parent.parentId
      const ancestorSeen = new Set<string>()
      while (targetId !== null && ids.has(targetId)) {
        if (ancestorSeen.has(targetId)) throw new ContentHierarchyError('cycle', 'selected hierarchy contains a cycle')
        ancestorSeen.add(targetId)
        const ancestor = selectedRows.get(targetId) ?? await row(tx, targetId)
        if (!ancestor) throw new ContentHierarchyError('stale-plan', 'selected ancestor disappeared')
        targetId = ancestor.parentId
      }
      await authorizeEditRow(tx, principal, child)
      reparent.push({ childId: child.id, fromParentId: parent.id, toParentId: targetId, expectedUpdatedAt: date(child.updatedAt) })
    }
  }

  reparent.sort((a, b) => a.childId.localeCompare(b.childId))
  const selected = [...items].sort((a, b) => a.id.localeCompare(b.id)).map((item) => Object.freeze({ id: item.id, expectedUpdatedAt: new Date(item.expectedUpdatedAt) }))
  const corpusVersion = await canonicalContentMigrationHash({
    selected: selected.map((item) => ({ id: item.id, updatedAt: item.expectedUpdatedAt.toISOString(), parentId: selectedRows.get(item.id)?.parentId ?? null })),
    reparent: reparent.map((item) => ({ ...item, expectedUpdatedAt: item.expectedUpdatedAt.toISOString() })),
  })
  const authorizationVersion = await canonicalContentMigrationHash({ id: principal.id, system: principal.system === true, capabilities: principalAuthorizationVersion(principal) })
  const token = await canonicalContentMigrationHash({ corpusVersion, authorizationVersion, selected: selected.map((item) => ({ id: item.id, expectedUpdatedAt: item.expectedUpdatedAt.toISOString() })), reparent: reparent.map((item) => ({ ...item, expectedUpdatedAt: item.expectedUpdatedAt.toISOString() })) })
  return Object.freeze({ token, selected: Object.freeze(selected), reparent: Object.freeze(reparent.map((item) => Object.freeze({ ...item }))), corpusVersion, authorizationVersion })
}
