import type {
  CrumbPlace,
  PlaceDraft,
  PlaceRef,
} from '@crumbs/core/schemas'
import {and, eq, isNotNull} from 'drizzle-orm'

import {db, schema, type DbOrTx} from '../db/client'

type PlaceRow = typeof schema.places.$inferSelect

const MERGE_CHASE_LIMIT = 10

export const toCrumbPlace = (row: PlaceRow): CrumbPlace => ({
  id: row.id,
  name: row.name,
  latitude: row.latitude,
  longitude: row.longitude,
  address: row.address,
  category: row.category,
  status: row.status,
})

// merged places redirect to their survivor; rows are never deleted
export const resolveMergedPlace = async (
  row: PlaceRow,
  executor: DbOrTx = db,
): Promise<PlaceRow> => {
  let current = row
  for (let i = 0; i < MERGE_CHASE_LIMIT; i++) {
    if (!current.mergedIntoId) return current
    const [next] = await executor
      .select()
      .from(schema.places)
      .where(eq(schema.places.id, current.mergedIntoId))
      .limit(1)
    if (!next) return current
    current = next
  }
  return current
}

export const getPlace = async (
  id: string,
  executor: DbOrTx = db,
): Promise<PlaceRow | null> => {
  const [row] = await executor
    .select()
    .from(schema.places)
    .where(eq(schema.places.id, id))
    .limit(1)
  return row ? resolveMergedPlace(row, executor) : null
}

const findByOsmKey = async (
  ref: PlaceRef,
): Promise<PlaceRow | null> => {
  if (!ref.osmType || !ref.osmId) return null
  const [row] = await db
    .select({place: schema.places})
    .from(schema.placeSources)
    .innerJoin(
      schema.places,
      eq(schema.places.id, schema.placeSources.placeId),
    )
    .where(
      and(
        eq(schema.placeSources.provider, ref.provider),
        eq(schema.placeSources.osmType, ref.osmType),
        eq(schema.placeSources.osmId, ref.osmId),
        isNotNull(schema.placeSources.osmId),
      ),
    )
    .limit(1)
  return row ? resolveMergedPlace(row.place) : null
}

// upsert a canonical place from a geocoder result; identity is ours,
// the provider key is stored as evidence and used only for dedup
export const resolvePlaceRef = async (
  ref: PlaceRef,
  {raw, queryText}: {raw?: unknown; queryText?: string} = {},
): Promise<PlaceRow> => {
  const existing = await findByOsmKey(ref)
  if (existing) return existing

  try {
    return await db.transaction(async (tx) => {
      const [place] = await tx
        .insert(schema.places)
        .values({
          name: ref.name,
          latitude: ref.coordinate.latitude,
          longitude: ref.coordinate.longitude,
          address: ref.address ?? null,
          category: ref.category ?? null,
          status: 'canonical',
        })
        .returning()
      if (!place) throw new Error('failed to create place')
      await tx.insert(schema.placeSources).values({
        placeId: place.id,
        provider: ref.provider,
        osmType: ref.osmType ?? null,
        osmId: ref.osmId ?? null,
        raw: raw ?? ref,
        queryText: queryText ?? null,
      })
      return place
    })
  } catch (err) {
    // concurrent save of the same place — the other writer won;
    // re-check the key regardless of how the driver surfaced the abort
    const winner = await findByOsmKey(ref)
    if (winner) return winner
    throw err
  }
}

// a place we couldn't geocode yet — fully usable, promotable later
export const createProvisionalPlace = async (
  draft: PlaceDraft,
  executor: DbOrTx = db,
): Promise<PlaceRow> => {
  const [place] = await executor
    .insert(schema.places)
    .values({
      name: draft.name,
      latitude: draft.latitude ?? null,
      longitude: draft.longitude ?? null,
      address: draft.address ?? null,
      status: 'provisional',
    })
    .returning()
  if (!place) throw new Error('failed to create place')
  return place
}
