/**
 * Page hierarchy resolution for the Pages tree view.
 *
 * Parent resolution strategy (in priority order):
 *   1. `parentPageId` — explicit link, used when it points at a page that
 *      exists in the set (and isn't the page itself).
 *   2. Path-prefix derivation — for rows without a usable `parentPageId`,
 *      the longest existing path prefix wins: "/about/team" becomes a child
 *      of the page whose path is "/about". Missing intermediate levels are
 *      skipped ("/a/b/c" with no "/a/b" attaches to "/a"). The root path
 *      "/" is never treated as a prefix parent, so top-level pages stay
 *      siblings of the homepage.
 *
 * Safety guarantees:
 *   - Orphans (parent id not in the set) become roots.
 *   - Cycles (a → b → a) are broken deterministically: walking up from each
 *     page in input order, the edge that closes a cycle is severed and that
 *     page becomes a root. Every page appears in the tree exactly once.
 *   - Sibling ordering is stable: title (locale-aware), then path.
 *
 * Pure functions — no network, no React — so the view and the unit tests
 * share one implementation.
 */
import type { Page } from './pages-service.js'

export interface PageTreeNode {
  page: Page
  children: PageTreeNode[]
}

/** One visible row of the flattened tree table. */
export interface PageTreeRow {
  page: Page
  depth: number
  hasChildren: boolean
}

function normalizePath(path: string): string {
  let p = path.trim()
  if (!p.startsWith('/')) p = `/${p}`
  p = p.replace(/\/{2,}/g, '/')
  if (p.length > 1) p = p.replace(/\/+$/, '')
  return p
}

/**
 * Longest existing path-prefix parent, or null. Only prefixes with at
 * least one segment count — "/" (the homepage) is never a derived parent.
 */
function derivePathParent(page: Page, idByPath: Map<string, string>): string | null {
  const segments = normalizePath(page.path).split('/').filter(Boolean)
  for (let i = segments.length - 1; i >= 1; i--) {
    const prefix = `/${segments.slice(0, i).join('/')}`
    const candidate = idByPath.get(prefix)
    if (candidate !== undefined && candidate !== page.id) return candidate
  }
  return null
}

function compareSiblings(a: PageTreeNode, b: PageTreeNode): number {
  return (
    a.page.title.localeCompare(b.page.title) ||
    a.page.path.localeCompare(b.page.path) ||
    a.page.id.localeCompare(b.page.id)
  )
}

/** Build the parent/child hierarchy for the tree view. Returns root nodes. */
export function buildPageTree(pages: Page[]): PageTreeNode[] {
  const byId = new Map<string, Page>()
  for (const p of pages) byId.set(p.id, p)

  // First occurrence wins on duplicate paths, keeping resolution stable
  // with respect to input order.
  const idByPath = new Map<string, string>()
  for (const p of pages) {
    const norm = normalizePath(p.path)
    if (!idByPath.has(norm)) idByPath.set(norm, p.id)
  }

  const parentOf = new Map<string, string | null>()
  for (const p of pages) {
    const explicit =
      p.parentPageId && p.parentPageId !== p.id && byId.has(p.parentPageId) ? p.parentPageId : null
    parentOf.set(p.id, explicit ?? derivePathParent(p, idByPath))
  }

  // Cycle guard: walk each page's ancestor chain; the edge that revisits a
  // seen node is severed, rooting that page. Deterministic in input order.
  for (const p of pages) {
    const seen = new Set<string>([p.id])
    let cur = p.id
    for (;;) {
      const parent = parentOf.get(cur) ?? null
      if (parent === null) break
      if (seen.has(parent)) {
        parentOf.set(cur, null)
        break
      }
      seen.add(parent)
      cur = parent
    }
  }

  const nodeById = new Map<string, PageTreeNode>()
  for (const p of pages) nodeById.set(p.id, { page: p, children: [] })

  const roots: PageTreeNode[] = []
  for (const p of pages) {
    const node = nodeById.get(p.id)!
    const parentId = parentOf.get(p.id) ?? null
    const parentNode = parentId !== null ? nodeById.get(parentId) : undefined
    if (parentNode) parentNode.children.push(node)
    else roots.push(node)
  }

  const sortRecursive = (nodes: PageTreeNode[]) => {
    nodes.sort(compareSiblings)
    for (const n of nodes) sortRecursive(n.children)
  }
  sortRecursive(roots)
  return roots
}

/**
 * Flatten the tree into visible rows: children render only when their
 * parent's id is in `expanded`.
 */
export function flattenPageTree(nodes: PageTreeNode[], expanded: Set<string>): PageTreeRow[] {
  const rows: PageTreeRow[] = []
  const walk = (list: PageTreeNode[], depth: number) => {
    for (const node of list) {
      const hasChildren = node.children.length > 0
      rows.push({ page: node.page, depth, hasChildren })
      if (hasChildren && expanded.has(node.page.id)) walk(node.children, depth + 1)
    }
  }
  walk(nodes, 0)
  return rows
}

/** Default expand state: every root node that has children starts open. */
export function defaultExpandedIds(roots: PageTreeNode[]): Set<string> {
  return new Set(roots.filter((n) => n.children.length > 0).map((n) => n.page.id))
}

/** Ids of every page in `id`'s subtree (excluding `id` itself). */
export function collectDescendantIds(roots: PageTreeNode[], id: string): Set<string> {
  const out = new Set<string>()
  const collect = (node: PageTreeNode) => {
    for (const child of node.children) {
      out.add(child.page.id)
      collect(child)
    }
  }
  const find = (nodes: PageTreeNode[]): PageTreeNode | undefined => {
    for (const n of nodes) {
      if (n.page.id === id) return n
      const hit = find(n.children)
      if (hit) return hit
    }
    return undefined
  }
  const node = find(roots)
  if (node) collect(node)
  return out
}

export type ReparentCheck = { ok: true } | { ok: false; reason: string }

/**
 * Validate a drag-reparent: `sourceId` becomes a child of `targetId`
 * (or a root when `targetId` is null). Rejects self-drops, drops into the
 * page's own subtree (would create a cycle), and no-op moves.
 */
export function checkReparent(
  roots: PageTreeNode[],
  sourceId: string,
  targetId: string | null,
): ReparentCheck {
  if (targetId === sourceId) {
    return { ok: false, reason: 'A page cannot be its own parent' }
  }
  if (targetId !== null && collectDescendantIds(roots, sourceId).has(targetId)) {
    return { ok: false, reason: 'Cannot move a page into its own sub-pages' }
  }
  const parentOf = (nodes: PageTreeNode[], parent: string | null): string | null | undefined => {
    for (const n of nodes) {
      if (n.page.id === sourceId) return parent
      const hit = parentOf(n.children, n.page.id)
      if (hit !== undefined) return hit
    }
    return undefined
  }
  if ((parentOf(roots, null) ?? null) === targetId) {
    return { ok: false, reason: 'Page is already there' }
  }
  return { ok: true }
}
