import {randomBytes, randomUUID} from 'node:crypto'

import {pickTagEmoji} from '@crumbs/core/emojis'
import {resolveMentionFacets} from '@crumbs/core/mentions'
import {eq, inArray} from 'drizzle-orm'

import {upsertUserByEmail} from '../auth/users'
import {getOrCreateConversation} from '../services/dms'
import {getOrCreateEvent} from '../services/events'
import {createProvisionalPlace} from '../services/places'
import {db, schema} from './client'
import {resetUserData, seedTrail, setHandle} from './dev-seed'

// idempotent local-dev seed for `me@seed.crumbs.test`: a lived-in collection
// of *things* — films, recipes, trips, books, buys — plus real stockholm
// spots for the map, upcoming events (an exhibition, a gig, a supper club),
// and a few crumbs imported from a link or a screengrab. it's woven into
// shared trails with a partner, complete with member posts (@mentions),
// share links, reactions, read state, and a couple of dm threads. re-running
// wipes prior seed state (by email / place + event name / shared-by) and
// rebuilds it.

const seed = async () => {
  const users = await ensureUsers()
  const ids: Record<UserKey, string> = {
    me: users.me.id,
    mossy: users.mossy.id,
    toasty: users.toasty.id,
    breezy: users.breezy.id,
    fizzy: users.fizzy.id,
    dreamy: users.dreamy.id,
  }

  await resetSeed(Object.values(ids))

  const placeList = await seedPlaces()
  const placesByName = Object.fromEntries(
    placeList.map((p) => [p.name, p.id]),
  )
  const eventsByKey = await seedEvents(placesByName)
  const sourcesByKey = await seedSources(ids.me)
  const refs = {placesByName, eventsByKey, sourcesByKey}

  const me = await seedCrumbsFor(ids.me, PRIMARY_SPECS, refs, 0)
  const mossy = await seedCrumbsFor(ids.mossy, MOSSY_SPECS, refs, 300)
  const toasty = await seedCrumbsFor(
    ids.toasty,
    TOASTY_SPECS,
    refs,
    400,
  )
  await seedCrumbsFor(ids.breezy, BREEZY_SPECS, refs, 500)

  await seedFriendships(ids)
  await seedTrails(ids, {me, mossy, toasty})
  await seedConversations(ids)

  console.log('seeded:')
  console.log(`  primary: ${users.me.handle} <${users.me.email}>`)
  console.log(
    `  crumbs:  ${me.ids.length} (you) — films, recipes, trips, buys,`,
  )
  console.log(
    `           3 events, 3 imported (link + screengrab) + stockholm spots`,
  )
  console.log(
    `  trails:  movies to watch (pinned + shared), date night (shared),`,
  )
  console.log(
    `           to cook, someday trips, södermalm favourites`,
  )
  console.log(
    `  social:  member posts w/ @mentions, 1 active + 1 revoked share link,`,
  )
  console.log(`           reactions, read state, 2 dm threads`)
  console.log(
    `  collab:  1 shared by mossy (accepted), 1 invite from toasty (pending)`,
  )
  console.log(`\nlog in locally:`)
  console.log(
    `  curl -s localhost:4000/dev/login -H 'content-type: application/json' \\`,
  )
  console.log(`    -d '{"email":"${users.me.email}"}'`)
}

// --- users ------------------------------------------------------------------

type UserKey =
  | 'me'
  | 'mossy'
  | 'toasty'
  | 'breezy'
  | 'fizzy'
  | 'dreamy'

const SEED_USERS: Record<UserKey, {email: string; handle: string}> = {
  me: {email: 'me@seed.crumbs.test', handle: 'sunny-otter'},
  mossy: {
    email: 'mossy-matcha@seed.crumbs.test',
    handle: 'mossy-matcha',
  },
  toasty: {
    email: 'toasty-bagel@seed.crumbs.test',
    handle: 'toasty-bagel',
  },
  breezy: {
    email: 'breezy-cove@seed.crumbs.test',
    handle: 'breezy-cove',
  },
  fizzy: {
    email: 'fizzy-pickle@seed.crumbs.test',
    handle: 'fizzy-pickle',
  },
  dreamy: {
    email: 'dreamy-clover@seed.crumbs.test',
    handle: 'dreamy-clover',
  },
}

type SeedUser = {id: string; email: string; handle: string}

const ensureUsers = async (): Promise<Record<UserKey, SeedUser>> => {
  const keys = Object.keys(SEED_USERS) as UserKey[]
  const result = {} as Record<UserKey, SeedUser>
  await Promise.all(
    keys.map(async (key) => {
      const {email, handle} = SEED_USERS[key]
      const user = await upsertUserByEmail(email)
      await setHandle(user.id, handle)
      result[key] = {id: user.id, email: user.email, handle}
    }),
  )
  return result
}

const memberSet = (ids: Record<UserKey, string>, keys: UserKey[]) =>
  keys.map((k) => ({id: ids[k], handle: SEED_USERS[k].handle}))

// --- time -------------------------------------------------------------------

const HOUR = 3_600_000
const DAY = 86_400_000

// fixed timeline so the feed (and its keyset cursor) is reproducible
const CREATED_BASE = new Date('2026-06-15T12:00:00.000Z').getTime()

// plans + events anchor to the current week's monday so "coming up" stays
// populated; re-runs within the same week are still reproducible
const planBase = () => {
  const monday = new Date()
  monday.setUTCHours(18, 0, 0, 0)
  monday.setUTCDate(
    monday.getUTCDate() - ((monday.getUTCDay() + 6) % 7),
  )
  return monday.getTime()
}
const PLAN_BASE = planBase()

// dm + read-state timestamps read as "recent" relative to now
const NOW = Date.now()
const hoursAgo = (h: number) => new Date(NOW - h * HOUR)
const daysAgo = (d: number) => new Date(NOW - d * DAY)

// --- places -----------------------------------------------------------------

type SeedPlace = {
  name: string
  category: string
  latitude: number
  longitude: number
  address: string
}

// real stockholm spots, mostly södermalm. `museum` / `music_venue` entries
// are venues for events rather than saved places, so they get no place-crumb
const PLACES: SeedPlace[] = [
  {
    name: 'Drop Coffee',
    category: 'cafe',
    latitude: 59.3178,
    longitude: 18.0621,
    address: 'Wollmar Yxkullsgatan 10, Södermalm',
  },
  {
    name: 'Pom & Flora',
    category: 'brunch',
    latitude: 59.3126,
    longitude: 18.0806,
    address: 'Bondegatan 44, Södermalm',
  },
  {
    name: 'Greasy Spoon',
    category: 'brunch',
    latitude: 59.3143,
    longitude: 18.0806,
    address: 'Tjärhovsgatan 19, Södermalm',
  },
  {
    name: 'Urban Deli Nytorget',
    category: 'deli',
    latitude: 59.3115,
    longitude: 18.0778,
    address: 'Nytorget 4, Södermalm',
  },
  {
    name: 'Pelikan',
    category: 'restaurant',
    latitude: 59.3083,
    longitude: 18.0776,
    address: 'Blekingegatan 40, Södermalm',
  },
  {
    name: 'Woodstockholm',
    category: 'restaurant',
    latitude: 59.3188,
    longitude: 18.0745,
    address: 'Mosebacke Torg 9, Södermalm',
  },
  {
    name: 'Bar Agrikultur',
    category: 'wine_bar',
    latitude: 59.3116,
    longitude: 18.0785,
    address: 'Brännkyrkagatan 89, Södermalm',
  },
  {
    name: 'Omnipollos Hatt',
    category: 'bar',
    latitude: 59.3196,
    longitude: 18.0726,
    address: 'Hökens Gata 1A, Södermalm',
  },
  {
    name: 'Häktet',
    category: 'bar',
    latitude: 59.317,
    longitude: 18.0555,
    address: 'Hornsgatan 82, Södermalm',
  },
  {
    name: 'Söderbokhandeln',
    category: 'bookshop',
    latitude: 59.3151,
    longitude: 18.073,
    address: 'Götgatan 37, Södermalm',
  },
  {
    name: 'Nizza',
    category: 'restaurant',
    latitude: 59.3137,
    longitude: 18.0792,
    address: 'Åsögatan 171, Södermalm',
  },
  {
    name: 'Taameya',
    category: 'restaurant',
    latitude: 59.3134,
    longitude: 18.0808,
    address: 'Folkungagatan 122, Södermalm',
  },
  {
    name: 'Konst-ig',
    category: 'bookshop',
    latitude: 59.314,
    longitude: 18.0748,
    address: 'Åsögatan 124, Södermalm',
  },
  // pat's place / lu: category guessed — adjust if off
  {
    name: 'Svedjan',
    category: 'cafe',
    latitude: 59.3129,
    longitude: 18.0765,
    address: 'Skånegatan 71, Södermalm',
  },
  {
    name: "Pat's Place",
    category: 'restaurant',
    latitude: 59.3155,
    longitude: 18.0812,
    address: 'Bondegatan 20, Södermalm',
  },
  {
    name: 'Lu',
    category: 'restaurant',
    latitude: 59.3162,
    longitude: 18.0702,
    address: 'Götgatan 78, Södermalm',
  },
  {
    name: 'Mikkeller',
    category: 'bar',
    latitude: 59.3388,
    longitude: 18.0668,
    address: 'Brunnsgatan 33, Norrmalm',
  },
  {
    name: 'Café Pascal',
    category: 'cafe',
    latitude: 59.3452,
    longitude: 18.0526,
    address: 'Norrtullsgatan 4, Vasastan',
  },
  {
    name: 'Lilla Ego',
    category: 'restaurant',
    latitude: 59.3437,
    longitude: 18.0432,
    address: 'Västmannagatan 69, Vasastan',
  },
  {
    name: 'Centralbadet',
    category: 'sauna',
    latitude: 59.3382,
    longitude: 18.0596,
    address: 'Drottninggatan 88, Norrmalm',
  },
  {
    name: 'Sturehof',
    category: 'restaurant',
    latitude: 59.336,
    longitude: 18.073,
    address: 'Stureplan 2, Östermalm',
  },
  {
    name: 'Rosendals Trädgård',
    category: 'cafe',
    latitude: 59.3247,
    longitude: 18.112,
    address: 'Rosendalsterrassen 12, Djurgården',
  },
  {
    name: 'Fotografiska',
    category: 'museum',
    latitude: 59.3186,
    longitude: 18.0847,
    address: 'Stadsgårdshamnen 22, Södermalm',
  },
  {
    name: 'Debaser',
    category: 'music_venue',
    latitude: 59.3155,
    longitude: 18.03,
    address: 'Hornstulls Strand 4, Södermalm',
  },
]

const PLACE_NAMES = PLACES.map((p) => p.name)
const PLACE_BY_NAME = Object.fromEntries(
  PLACES.map((p) => [p.name, p]),
)

// event venues carry occurrences, not saved spots — no place-crumb for them
const CRUMB_PLACES = PLACES.filter(
  (p) => p.category !== 'museum' && p.category !== 'music_venue',
)

// loose chips by category so located saves still read nicely
const placeTags = (category: string): string[] => {
  const byCategory: Record<string, string[]> = {
    cafe: ['coffee', 'cozy'],
    brunch: ['brunch'],
    deli: ['lunch', 'deli'],
    restaurant: ['eat', 'dinner'],
    wine_bar: ['natural-wine', 'date-night'],
    bar: ['drinks', 'late-night'],
    bookshop: ['bookshop'],
    sauna: ['sauna', 'wellness'],
  }
  return byCategory[category] ?? ['stockholm']
}

const seedPlaces = async (): Promise<{id: string; name: string}[]> =>
  Promise.all(
    PLACES.map(async (p) => {
      const place = await createProvisionalPlace({
        name: p.name,
        address: p.address,
        latitude: p.latitude,
        longitude: p.longitude,
      })
      // category isn't part of a draft; backfill it so place chips read nicely
      await db
        .update(schema.places)
        .set({category: p.category})
        .where(eq(schema.places.id, place.id))
      return {id: place.id, name: p.name}
    }),
  )

// --- events -----------------------------------------------------------------

type EventKey = 'maier' | 'feverray' | 'supper'

// upcoming occurrences: an exhibition and a gig at real venues, plus a
// venue-less supper club (exercises events' nullable place). names are used
// for reset, so keep them distinct from the crumb labels
const EVENT_SPECS: Record<
  EventKey,
  {
    name: string
    placeName: string | null
    inDays: number
    url: string | null
  }
> = {
  maier: {
    name: 'Vivian Maier — Unseen Work',
    placeName: 'Fotografiska',
    inDays: 9,
    url: 'https://www.fotografiska.com/sto/',
  },
  feverray: {
    name: 'Fever Ray — Radical Romantics',
    placeName: 'Debaser',
    inDays: 18,
    url: 'https://www.debaser.se/',
  },
  supper: {
    name: 'Cardamom supper club vol. 4',
    placeName: null,
    inDays: 25,
    url: null,
  },
}

const EVENT_NAMES = Object.values(EVENT_SPECS).map((e) => e.name)

const seedEvents = async (
  placesByName: Record<string, string>,
): Promise<Record<EventKey, string>> => {
  const keys = Object.keys(EVENT_SPECS) as EventKey[]
  const entries = await Promise.all(
    keys.map(async (key) => {
      const spec = EVENT_SPECS[key]
      const startsAt = new Date(PLAN_BASE + spec.inDays * DAY)
      const event = await getOrCreateEvent({
        name: spec.name,
        placeId: spec.placeName
          ? (placesByName[spec.placeName] ?? null)
          : null,
        startsAt,
        // a gig ends the same night; the exhibition/supper club are open-ended
        endsAt:
          key === 'feverray'
            ? new Date(startsAt.getTime() + 3 * HOUR)
            : null,
        url: spec.url,
      })
      return [key, event.id] as const
    }),
  )
  return Object.fromEntries(entries) as Record<EventKey, string>
}

// --- sources ----------------------------------------------------------------

type SourceKey = 'cookies' | 'watchlist'

// shared provenance for imported crumbs: a saved reel link, and a screengrab
// of a friend's watch list that fans out into two crumbs
const seedSources = async (
  sharedById: string,
): Promise<Record<SourceKey, string>> => {
  const [link] = await db
    .insert(schema.sources)
    .values({
      kind: 'link',
      url: 'https://www.instagram.com/reel/seed-miso-cookies/',
      sharedById,
    })
    .returning({id: schema.sources.id})
  const [grab] = await db
    .insert(schema.sources)
    .values({
      kind: 'screengrab',
      assetRef: 'seed/screengrab-watchlist.jpg',
      sharedById,
    })
    .returning({id: schema.sources.id})
  if (!link || !grab) throw new Error('failed to seed sources')
  return {cookies: link.id, watchlist: grab.id}
}

// --- crumbs -----------------------------------------------------------------

const STATUSES = ['saved', 'planned', 'done'] as const

// a saved thing — located when `placeName` is set, tied to a date via
// `eventKey`, or loose (a film, recipe, trip…). `source` + `sourceKey` mark
// an imported crumb. status/plannedInDays override the default timeline for a
// handful we want surfaced under "coming up"
type CrumbSpec = {
  name: string
  tags: string[]
  note?: string
  placeName?: string
  eventKey?: EventKey
  sourceKey?: SourceKey
  source?: (typeof schema.crumbs.$inferInsert)['source']
  status?: (typeof STATUSES)[number]
  plannedInDays?: number
}

type CrumbRefs = {
  placesByName: Record<string, string>
  eventsByKey: Record<EventKey, string>
  sourcesByKey: Record<SourceKey, string>
}

// loose, place-less saves — the heart of the reframe
const MOVIES: CrumbSpec[] = [
  {name: '🎬 Dune: Part Two', tags: ['movie', 'sci-fi']},
  {
    name: '🎬 Past Lives',
    tags: ['movie', 'date-night'],
    note: "heard it's a quiet heartbreaker",
    status: 'planned',
    plannedInDays: 8,
  },
  {name: '🎬 Everything Everywhere All at Once', tags: ['movie']},
  {name: '🎬 Poor Things', tags: ['movie']},
  {name: '🎬 The Holdovers', tags: ['movie', 'cozy']},
  {name: '🎬 Anatomy of a Fall', tags: ['movie']},
  {name: '🎬 Aftersun', tags: ['movie']},
  {
    name: '📺 The Bear',
    tags: ['show'],
    note: 'everyone says start from season one',
    status: 'planned',
    plannedInDays: 11,
  },
  {name: '📺 Severance', tags: ['show', 'sci-fi']},
  {name: '📺 Shōgun', tags: ['show']},
]

const KITCHEN: CrumbSpec[] = [
  {name: '🍳 Marry me chicken', tags: ['recipe', 'cook']},
  {name: '🍝 Miso butter pasta', tags: ['recipe', 'cook']},
  {name: '🥘 Weeknight shakshuka', tags: ['recipe', 'cook']},
  {
    name: '🍪 Gochujang caramel cookies',
    tags: ['recipe', 'sweet-tooth'],
  },
  {name: '🥗 Crispy gnocchi salad', tags: ['recipe', 'cook']},
  {name: '🍜 15-minute dan dan noodles', tags: ['recipe', 'cook']},
]

const TRIPS: CrumbSpec[] = [
  {name: '✈️ Weekend in Lisbon', tags: ['travel', 'someday']},
  {name: '🏝️ Cabin in the archipelago', tags: ['travel', 'someday']},
  {name: '🚂 Day trip to Uppsala', tags: ['travel', 'someday']},
  {name: '🌌 Northern lights in Abisko', tags: ['travel', 'someday']},
  {
    name: '✈️ Long weekend in Copenhagen',
    tags: ['travel', 'someday'],
  },
]

const READS: CrumbSpec[] = [
  {
    name: '📖 Tomorrow, and Tomorrow, and Tomorrow',
    tags: ['book', 'read'],
  },
  {name: '📖 The Wager', tags: ['book', 'read']},
  {name: '📖 Babel', tags: ['book', 'read']},
  {name: '🎧 Brat — Charli XCX', tags: ['listen']},
  {name: '🎙️ The Rest Is History', tags: ['listen', 'podcast']},
  {
    name: '📰 The case for slow productivity',
    tags: ['read', 'article'],
  },
]

const BUYS: CrumbSpec[] = [
  {name: '🛍️ Stanley quencher', tags: ['buy']},
  {name: '🪴 Lego orchid set', tags: ['buy', 'gift']},
  {name: '🎧 Sony WH-1000XM5', tags: ['buy']},
  {name: '👟 New running shoes', tags: ['buy']},
]

// date-anchored saves — each targets an event, so no place (the venue lives
// on the event). status planned; the date comes from the occurrence
const EVENTS: CrumbSpec[] = [
  {
    name: '🖼️ Vivian Maier at Fotografiska',
    tags: ['exhibition', 'date-night'],
    eventKey: 'maier',
    status: 'planned',
  },
  {
    name: '🎤 Fever Ray at Debaser',
    tags: ['gig', 'music'],
    eventKey: 'feverray',
    note: 'grab tickets before they go',
    status: 'planned',
  },
  {
    name: '🍮 Cardamom supper club',
    tags: ['supper-club', 'eat'],
    eventKey: 'supper',
    status: 'planned',
  },
]

// imported saves: a reel link, and a screengrab that yielded two shows
const IMPORTS: CrumbSpec[] = [
  {
    name: '🍪 Miso brown butter cookies',
    tags: ['recipe', 'sweet-tooth'],
    source: 'link',
    sourceKey: 'cookies',
    note: 'saved from that reel',
  },
  {
    name: '📺 Slow Horses',
    tags: ['show'],
    source: 'screengrab',
    sourceKey: 'watchlist',
  },
  {
    name: '📺 The Diplomat',
    tags: ['show'],
    source: 'screengrab',
    sourceKey: 'watchlist',
  },
]

const placeSpec = (p: SeedPlace): CrumbSpec => ({
  name: p.name,
  tags: placeTags(p.category),
  placeName: p.name,
})

// my collection: curated loose things (so trails can pick them by name),
// upcoming events, a few imports, then every located spot
const PRIMARY_SPECS: CrumbSpec[] = [
  ...MOVIES,
  ...KITCHEN,
  ...TRIPS,
  ...READS,
  ...BUYS,
  ...EVENTS,
  ...IMPORTS,
  ...CRUMB_PLACES.map(placeSpec),
]

// the partner: a few films of their own (to share into our watchlist) and
// some local spots
const MOSSY_SPECS: CrumbSpec[] = [
  {name: '🎬 La La Land', tags: ['movie', 'date-night']},
  {name: '🎬 Challengers', tags: ['movie']},
  {name: '📺 Baby Reindeer', tags: ['show']},
  placeSpec(PLACE_BY_NAME['Drop Coffee']!),
  placeSpec(PLACE_BY_NAME['Café Pascal']!),
  placeSpec(PLACE_BY_NAME['Rosendals Trädgård']!),
]

// a friend who runs the brunch tour: brunch spots + a film
const TOASTY_SPECS: CrumbSpec[] = [
  {name: '🎬 The Substance', tags: ['movie']},
  placeSpec(PLACE_BY_NAME['Pom & Flora']!),
  placeSpec(PLACE_BY_NAME['Greasy Spoon']!),
  placeSpec(PLACE_BY_NAME['Urban Deli Nytorget']!),
  placeSpec(PLACE_BY_NAME['Café Pascal']!),
  placeSpec(PLACE_BY_NAME['Rosendals Trädgård']!),
]

// another friend: a generic mix so "shared by" and search have variety
const BREEZY_SPECS: CrumbSpec[] = [
  {name: '📺 Ripley', tags: ['show']},
  {name: '📖 The Wager', tags: ['book', 'read']},
  placeSpec(PLACE_BY_NAME['Sturehof']!),
  placeSpec(PLACE_BY_NAME['Lilla Ego']!),
  placeSpec(PLACE_BY_NAME['Omnipollos Hatt']!),
]

const NOTES = [
  'best cortado in söder ☕',
  'go before the brunch rush',
  'natural wine list is unreal 🍷',
  'get the meatballs, obviously',
  'tiny and lovely, get there early',
  'window seats face the water 🌊',
  'their cardamom bun is the one 🥐',
  'cash only, tucked down a courtyard',
  'sauna then a cold plunge',
  'takeaway is perfect for a night in',
]

const statusFor = (
  n: number,
): {status: (typeof STATUSES)[number]; plannedFor: Date | null} => {
  const r = n % 20
  if (r < 12) return {status: 'saved', plannedFor: null}
  if (r < 17)
    return {
      status: 'planned',
      plannedFor: new Date(PLAN_BASE + (n % 14) * DAY),
    }
  return {status: 'done', plannedFor: null}
}

// returns crumb ids in build order plus a name→id lookup so trails can be
// composed from specific crumbs (and the same crumb reused across trails)
const seedCrumbsFor = async (
  authorId: string,
  specs: CrumbSpec[],
  refs: CrumbRefs,
  offset: number,
): Promise<{ids: string[]; byName: Record<string, string>}> => {
  const built = specs.map((spec, i) => {
    const n = offset + i
    const timing = spec.status
      ? {
          status: spec.status,
          plannedFor:
            spec.plannedInDays != null
              ? new Date(PLAN_BASE + spec.plannedInDays * DAY)
              : null,
        }
      : statusFor(n)
    return {
      values: {
        id: randomUUID(),
        authorId,
        name: spec.name,
        note:
          spec.note ??
          (spec.placeName && i % 6 === 0
            ? NOTES[n % NOTES.length]!
            : null),
        placeId: spec.placeName
          ? (refs.placesByName[spec.placeName] ?? null)
          : null,
        eventId: spec.eventKey
          ? (refs.eventsByKey[spec.eventKey] ?? null)
          : null,
        sourceId: spec.sourceKey
          ? (refs.sourcesByKey[spec.sourceKey] ?? null)
          : null,
        source: spec.source ?? 'manual',
        status: timing.status,
        plannedFor: timing.plannedFor,
        createdAt: new Date(CREATED_BASE - i * 6 * HOUR),
      },
      tags: spec.tags,
    }
  })

  await db.insert(schema.crumbs).values(built.map((b) => b.values))
  await seedCrumbTags(
    built.map((b) => ({crumbId: b.values.id, tags: b.tags})),
  )

  const byName: Record<string, string> = {}
  for (const b of built)
    if (!(b.values.name in byName))
      byName[b.values.name] = b.values.id

  return {ids: built.map((b) => b.values.id), byName}
}

// batched tagging: upsert the tag vocabulary once, then link every crumb in
// one insert instead of a round trip per crumb
const seedCrumbTags = async (
  links: {crumbId: string; tags: string[]}[],
) => {
  const names = [...new Set(links.flatMap((l) => l.tags))]
  if (!names.length) return
  await db
    .insert(schema.tags)
    .values(names.map((name) => ({name, emoji: pickTagEmoji(name)})))
    .onConflictDoNothing({target: schema.tags.name})
  const rows = await db
    .select({id: schema.tags.id, name: schema.tags.name})
    .from(schema.tags)
    .where(inArray(schema.tags.name, names))
  const idByName = new Map(rows.map((r) => [r.name, r.id]))
  const crumbTagRows = links.flatMap((l) =>
    [...new Set(l.tags)].flatMap((name) => {
      const tagId = idByName.get(name)
      return tagId ? [{crumbId: l.crumbId, tagId}] : []
    }),
  )
  if (crumbTagRows.length)
    await db.insert(schema.crumbTags).values(crumbTagRows)
}

// --- friendships ------------------------------------------------------------

const seedFriendships = async (ids: Record<UserKey, string>) => {
  await db.insert(schema.friendships).values([
    {requesterId: ids.me, addresseeId: ids.mossy, status: 'accepted'},
    {
      requesterId: ids.toasty,
      addresseeId: ids.me,
      status: 'accepted',
    },
    {
      requesterId: ids.me,
      addresseeId: ids.breezy,
      status: 'accepted',
    },
    {requesterId: ids.fizzy, addresseeId: ids.me, status: 'pending'},
    {requesterId: ids.me, addresseeId: ids.dreamy, status: 'pending'},
  ])
}

// --- trails -----------------------------------------------------------------

type SeededCrumbs = {ids: string[]; byName: Record<string, string>}

const seedTrails = async (
  ids: Record<UserKey, string>,
  crumbs: {
    me: SeededCrumbs
    mossy: SeededCrumbs
    toasty: SeededCrumbs
  },
) => {
  const {me, mossy, toasty} = crumbs

  // the hero shared watchlist: i own it, my partner collaborates and has
  // thrown in a couple of their own picks
  const movies = await seedTrail(ids.me, '🎬 Movies to watch', [
    ...MOVIES.map((m) => me.byName[m.name]!),
    mossy.byName['🎬 La La Land']!,
    mossy.byName['🎬 Challengers']!,
  ])
  await addCollaborator(movies, ids.mossy, ids.me)
  await seedShareLink(movies, ids.me)
  const movieMembers = memberSet(ids, ['me', 'mossy'])
  await seedTrailPosts(movies, [
    {
      authorId: ids.mossy,
      text: '@sunny-otter past lives friday? 🍷',
      members: movieMembers,
      hoursAgo: 26,
    },
    {
      authorId: ids.me,
      text: '@mossy-matcha yes!! booking the sofa 🛋️',
      members: movieMembers,
      hoursAgo: 24,
    },
  ])

  // a mixed collection — a place (dinner + wine), an event, and a film
  const dateNight = await seedTrail(ids.me, '🍝 Date night', [
    me.byName['Pelikan']!,
    me.byName['Bar Agrikultur']!,
    me.byName['🖼️ Vivian Maier at Fotografiska']!,
    me.byName['🎬 Past Lives']!,
  ])
  await addCollaborator(dateNight, ids.mossy, ids.me)
  await seedTrailPosts(dateNight, [
    {
      authorId: ids.me,
      text: '@mossy-matcha pelikan then back for the film?',
      members: memberSet(ids, ['me', 'mossy']),
      hoursAgo: 40,
    },
  ])

  await seedTrail(
    ids.me,
    '🍳 To cook',
    KITCHEN.map((k) => me.byName[k.name]!),
  )
  await seedTrail(
    ids.me,
    '✈️ Someday trips',
    TRIPS.map((tr) => me.byName[tr.name]!),
  )

  // a place-heavy trail; carries a revoked share link (a tombstone)
  const soder = await seedTrail(
    ids.me,
    '☕ Södermalm favourites',
    SODER_FAVOURITES.map((name) => me.byName[name]!),
  )
  await seedShareLink(soder, ids.me, true)

  // a trail mossy owns and has accepted me onto
  const cozy = await seedTrail(ids.mossy, 'Cozy nights in 🍵', [
    mossy.byName['📺 Baby Reindeer']!,
    mossy.byName['Drop Coffee']!,
  ])
  await addCollaborator(cozy, ids.me, ids.mossy)
  await seedTrailPosts(cozy, [
    {
      authorId: ids.mossy,
      text: '@sunny-otter added baby reindeer, brace yourself 😅',
      members: memberSet(ids, ['mossy', 'me']),
      hoursAgo: 30,
    },
  ])

  // a trail toasty owns with an open invite waiting for me
  await seedTrail(
    ids.toasty,
    'Söder brunch tour 🥯',
    BRUNCH_TOUR.map((name) => toasty.byName[name]).filter(
      (id): id is string => id != null,
    ),
  ).then((brunch) =>
    addCollaborator(brunch, ids.me, ids.toasty, 'pending'),
  )

  // pin the hero watchlist. read state: the watchlist + cozy trail both have
  // posts newer than my last view (unseen activity); date night is caught up
  await db.insert(schema.trailUserStates).values([
    {
      trailId: movies,
      userId: ids.me,
      pinnedAt: new Date(),
      lastViewedAt: daysAgo(3),
    },
    {trailId: dateNight, userId: ids.me, lastViewedAt: hoursAgo(1)},
    {trailId: cozy, userId: ids.me, lastViewedAt: daysAgo(2)},
  ])

  // a couple of reactions so the shared watchlist feels alive
  await db.insert(schema.crumbReactions).values([
    {
      trailId: movies,
      crumbId: me.byName['🎬 Dune: Part Two']!,
      userId: ids.mossy,
      emoji: '🍿',
    },
    {
      trailId: movies,
      crumbId: mossy.byName['🎬 La La Land']!,
      userId: ids.me,
      emoji: '❤️',
    },
  ])
}

const SODER_FAVOURITES = [
  'Drop Coffee',
  'Pom & Flora',
  'Greasy Spoon',
  'Urban Deli Nytorget',
  'Pelikan',
  'Woodstockholm',
  'Bar Agrikultur',
  'Omnipollos Hatt',
  'Häktet',
  'Söderbokhandeln',
  'Nizza',
  'Konst-ig',
]

const BRUNCH_TOUR = [
  'Pom & Flora',
  'Greasy Spoon',
  'Urban Deli Nytorget',
  'Café Pascal',
  'Rosendals Trädgård',
]

const addCollaborator = async (
  trailId: string,
  userId: string,
  inviterId: string,
  status: 'accepted' | 'pending' = 'accepted',
) => {
  await db
    .insert(schema.trailCollaborators)
    .values({trailId, userId, inviterId, status})
}

const seedShareLink = async (
  trailId: string,
  createdBy: string,
  revoked = false,
) => {
  await db.insert(schema.trailShareLinks).values({
    trailId,
    token: randomBytes(24).toString('base64url'),
    createdBy,
    revokedAt: revoked ? new Date() : null,
  })
}

type PostSpec = {
  authorId: string
  text: string
  members: {id: string; handle: string}[]
  hoursAgo: number
}

// resolve @mentions to member ids at publish time, exactly like the route
const seedTrailPosts = async (trailId: string, posts: PostSpec[]) => {
  await db.insert(schema.trailPosts).values(
    posts.map((p) => ({
      trailId,
      authorId: p.authorId,
      text: p.text,
      facets: resolveMentionFacets(p.text, p.members),
      createdAt: hoursAgo(p.hoursAgo),
    })),
  )
}

// --- dms --------------------------------------------------------------------

type DmSpec = {from: 'me' | 'peer'; text: string; hoursAgo: number}

const seedConversations = async (ids: Record<UserKey, string>) => {
  // caught-up thread with my partner
  await seedConversation(ids.me, ids.mossy, true, [
    {
      from: 'peer',
      text: 'they added past lives to the watchlist 👀',
      hoursAgo: 27,
    },
    {from: 'me', text: 'i saw!! friday?', hoursAgo: 26},
    {from: 'peer', text: "perfect, i'll bring wine 🍷", hoursAgo: 25},
  ])
  // a friend's last message still unread — shows an inbox badge
  await seedConversation(ids.me, ids.toasty, false, [
    {from: 'me', text: 'brunch sunday?', hoursAgo: 6},
    {
      from: 'peer',
      text: 'always. greasy spoon at 11? 🥯',
      hoursAgo: 5,
    },
  ])
}

const seedConversation = async (
  meId: string,
  peerId: string,
  readByMe: boolean,
  msgs: DmSpec[],
) => {
  const conversation = await getOrCreateConversation(meId, peerId)
  const rows = msgs.map((m) => ({
    conversationId: conversation.id,
    senderId: m.from === 'me' ? meId : peerId,
    text: m.text,
    createdAt: hoursAgo(m.hoursAgo),
  }))
  await db.insert(schema.messages).values(rows)

  const lastAt = rows[rows.length - 1]!.createdAt
  const meIsA = conversation.userAId === meId
  // read up to just past the last message when caught up, else leave unread
  const readAt = readByMe ? new Date(lastAt.getTime() + 1000) : null
  await db
    .update(schema.conversations)
    .set({
      lastMessageAt: lastAt,
      ...(meIsA
        ? {userALastReadAt: readAt}
        : {userBLastReadAt: readAt}),
    })
    .where(eq(schema.conversations.id, conversation.id))
}

// --- reset ------------------------------------------------------------------

const resetSeed = async (userIds: string[]) => {
  await resetUserData(userIds)
  // crumbs are gone, so the source/event/place restrict + set-null fks are
  // satisfied; drop events before places since events reference them
  await db
    .delete(schema.sources)
    .where(inArray(schema.sources.sharedById, userIds))
  await db
    .delete(schema.events)
    .where(inArray(schema.events.name, EVENT_NAMES))
  await db
    .delete(schema.places)
    .where(inArray(schema.places.name, PLACE_NAMES))
}

try {
  await seed()
} finally {
  await db.$client.end()
}
