import { db } from '@/lib/db'
import { slugify } from '@/lib/utils'
import type { Prisma } from '@prisma/client'

// Recompute path = "Root > Child > Grandchild" for a location and every
// descendant. The seed row joins its parent (if any) so a renamed/moved
// non-root location inherits the parent's prefix; otherwise we'd lose it.
export async function recomputePaths(
  tx: Prisma.TransactionClient,
  rootId: string,
): Promise<void> {
  await tx.$executeRaw`
    WITH RECURSIVE tree AS (
      SELECT l.id, l.name, l."parentId",
             CASE
               WHEN p.path IS NULL THEN l.name
               ELSE p.path || ' > ' || l.name
             END AS path
      FROM "Location" l
      LEFT JOIN "Location" p ON p.id = l."parentId"
      WHERE l.id = ${rootId}
      UNION ALL
      SELECT c.id, c.name, c."parentId", t.path || ' > ' || c.name
      FROM "Location" c
      JOIN tree t ON c."parentId" = t.id
    )
    UPDATE "Location" l
    SET path = tree.path
    FROM tree
    WHERE l.id = tree.id
  `
}

export async function createLocation(input: {
  name: string
  parentId: string | null
  notes?: string | null
}) {
  const name = input.name.trim()
  if (!name) throw new Error('Name is required')
  const slug = slugify(name)
  return db.$transaction(async tx => {
    const parent = input.parentId
      ? await tx.location.findUnique({ where: { id: input.parentId } })
      : null
    const path = parent ? `${parent.path} > ${name}` : name
    const created = await tx.location.create({
      data: {
        name,
        slug,
        parentId: input.parentId,
        notes: input.notes ?? null,
        path,
      },
    })
    return created
  })
}

export async function renameLocation(id: string, name: string) {
  return db.$transaction(async tx => {
    await tx.location.update({
      where: { id },
      data: { name: name.trim(), slug: slugify(name) },
    })
    await recomputePaths(tx, id)
    await rebuildSearchTextInSubtree(tx, id)
  })
}

// Items inherit location.path into their searchText, so any rename/move of a
// location must rebuild searchText for every item below it.
async function rebuildSearchTextInSubtree(
  tx: Prisma.TransactionClient,
  rootId: string,
) {
  const root = await tx.location.findUnique({ where: { id: rootId } })
  if (!root) return
  const subtree = await tx.location.findMany({
    where: {
      OR: [{ id: rootId }, { path: { startsWith: root.path + ' > ' } }],
    },
    select: { id: true },
  })
  const items = await tx.item.findMany({
    where: { locationId: { in: subtree.map(s => s.id) } },
    include: { tags: true, location: true },
  })
  for (const it of items) {
    const parts = [
      it.name, it.description, it.category, it.brand,
      it.serialNumber, it.notes, it.caption, it.location.path,
      it.tags.map(t => t.name).join(' '),
    ].filter(Boolean)
    await tx.item.update({
      where: { id: it.id },
      data: { searchText: parts.join(' ').toLowerCase() },
    })
  }
}

export async function moveLocation(id: string, newParentId: string | null) {
  return db.$transaction(async tx => {
    if (newParentId === id) throw new Error('Cannot move into itself')
    if (newParentId) {
      const target = await tx.location.findUnique({
        where: { id: newParentId },
        select: { path: true },
      })
      const moving = await tx.location.findUnique({
        where: { id },
        select: { path: true },
      })
      if (target && moving) {
        if (target.path === moving.path || target.path.startsWith(moving.path + ' > ')) {
          throw new Error('Cannot move into own descendant')
        }
      }
    }
    await tx.location.update({
      where: { id },
      data: { parentId: newParentId },
    })
    await recomputePaths(tx, id)
    await rebuildSearchTextInSubtree(tx, id)
  })
}

export async function deleteLocation(id: string) {
  // Reparent children and detach items, then delete.
  return db.$transaction(async tx => {
    const target = await tx.location.findUnique({ where: { id } })
    if (!target) return
    if (await tx.item.count({ where: { locationId: id } }) > 0) {
      throw new Error('Location has items — move them first')
    }
    if (await tx.location.count({ where: { parentId: id } }) > 0) {
      throw new Error('Location has children — move or delete them first')
    }
    await tx.location.delete({ where: { id } })
  })
}

// Parse a freeform location string into ordered segments. Accepts ">", "/",
// and " - " as separators, in any combination — so all of these resolve to
// the same chain: "Garage > Middle shelf > 4th level", "garage/middle shelf/4th level",
// "Garage - middle shelf - 4th level". Multi-word names are preserved.
export function parseLocationPath(input: string): string[] {
  return input
    .split(/\s*[>/]\s*|\s+-\s+/)
    .map(s => s.trim())
    .filter(s => s.length > 0)
}

// Find-or-create the entire chain in order, parented appropriately. Returns
// the leaf id. Matching is case-insensitive on slug at each level, so
// "garage" and "Garage" reuse the same node.
export async function resolveLocationPath(input: string): Promise<{ id: string; path: string } | null> {
  const segments = parseLocationPath(input)
  if (segments.length === 0) return null
  let parentId: string | null = null
  let parentPath = ''
  let leafId = ''
  for (const segment of segments) {
    const slug = slugify(segment)
    if (!slug) continue
    // Explicit annotations on both Prisma calls so TS doesn't fall into a
    // circular inference loop: `parentId` is a let that gets reassigned from
    // these results, and without explicit types it can't decide their shape.
    const existing: { id: string; path: string } | null = await db.location.findFirst({
      where: { parentId, slug },
      select: { id: true, path: true },
    })
    if (existing) {
      parentId = existing.id
      parentPath = existing.path
      leafId = existing.id
    } else {
      const newPath = parentPath ? `${parentPath} > ${segment}` : segment
      const created: { id: string; path: string } = await db.location.create({
        data: { name: segment, slug, parentId, path: newPath },
        select: { id: true, path: true },
      })
      parentId = created.id
      parentPath = created.path
      leafId = created.id
    }
  }
  if (!leafId) return null
  return { id: leafId, path: parentPath }
}

export async function listLocationTree() {
  const rows = await db.location.findMany({
    orderBy: [{ path: 'asc' }],
    select: {
      id: true,
      name: true,
      parentId: true,
      path: true,
      _count: { select: { items: true, children: true } },
    },
  })
  return rows
}
