import {
  TRAIL_POSTS_PREVIEW,
  TrailPostSchema,
  type TrailPost,
} from '@crumbs/core/schemas'

import {trailDropped} from '@/lib/entity-sync'
import {isApiStatus, tolerateStatus, unwrap} from '@/lib/errors'
import {createKeyedPagedStore} from '@/lib/keyed-paged-resource'
import {optimisticTrailPost} from '@/lib/optimistic-trail'
import {pageParams} from '@/lib/paged-resource'
import {rpc} from '@/lib/rpc'
import {fromSchema} from '@/lib/snapshot-cache'
import {keyedPagedSnapshot} from '@/lib/snapshots'
import {uuid} from '@/lib/uuid'
import {authorOf, requireUser} from '@/stores/auth'
import {afterFlush, defineMutation} from '@/stores/outbox'
import {
  loadTrailDetail,
  patchTrailDetail,
  restoreTrail,
} from '@/stores/trails'

// a trail's message board. the full post history per trail for the see-all
// screen; the trail detail only embeds the latest few

const trailPostsPages = createKeyedPagedStore<TrailPost>(
  (trailId) => async (cursor) => {
    const {posts, nextCursor} = await unwrap(
      rpc.trails[':id'].posts.$get({
        param: {id: trailId},
        query: pageParams(cursor),
      }),
    )
    return {items: posts, nextCursor}
  },
  keyedPagedSnapshot('trail-posts', 1, fromSchema(TrailPostSchema)),
)

// outbox kinds, scoped by trail id so a post lands after its trail's queued
// create replays
const TRAIL_POST_CREATE = 'trail.post.create'
const TRAIL_POST_DELETE = 'trail.post.delete'

const patchDetailPosts = (
  trailId: string,
  fn: (posts: TrailPost[]) => TrailPost[],
): void =>
  patchTrailDetail(trailId, (d) => ({...d, posts: fn(d.posts)}))

// prepend an optimistic post to the trail's embedded preview and the see-all
// board, so a post created from either surface shows at once. both keep the
// client-minted id, so reconcilePost/the server backfill dedupe cleanly
const prependPost = (trailId: string, post: TrailPost): void => {
  patchDetailPosts(trailId, (posts) =>
    [post, ...posts].slice(0, TRAIL_POSTS_PREVIEW),
  )
  trailPostsPages.prependItems(trailId, [post], (p) => p.id)
}

// reconcile the optimistic post to the server copy (resolved facets, canonical
// createdAt) in the preview and any loaded see-all page
const reconcilePost = (trailId: string, post: TrailPost): void => {
  patchDetailPosts(trailId, (posts) =>
    posts.map((p) => (p.id === post.id ? post : p)),
  )
  trailPostsPages.mapItems((p) => (p.id === post.id ? post : p))
}

const removePost = (trailId: string, postId: string): void => {
  patchDetailPosts(trailId, (posts) =>
    posts.filter((p) => p.id !== postId),
  )
  trailPostsPages.removeItem((p) => p.id === postId)
}

export const createTrailPost = async (
  trailId: string,
  text: string,
): Promise<void> => {
  const user = requireUser()
  // mint the id so the optimistic post and the queued create share it; the
  // post shows at once (offline too) and the outbox replays the real create
  const id = uuid()
  const post = optimisticTrailPost(text, {
    id,
    author: authorOf(user),
    now: new Date().toISOString(),
  })
  prependPost(trailId, post)
  queueTrailPostCreate({
    scope: trailId,
    id,
    label: text,
    payload: {id, trailId, text},
  })
}

export const deleteTrailPost = async (
  trailId: string,
  postId: string,
): Promise<void> => {
  removePost(trailId, postId)
  queueTrailPostDelete({scope: trailId, payload: {trailId, postId}})
}

const queueTrailPostCreate = defineMutation<{
  id: string
  trailId: string
  text: string
}>(TRAIL_POST_CREATE, {
  run: async ({id, trailId, text}) => {
    try {
      const {post} = await unwrap(
        rpc.trails[':id'].posts.$post({
          param: {id: trailId},
          json: {id, text},
        }),
      )
      reconcilePost(trailId, post)
    } catch (err) {
      // the trail is gone — the post can never land, so drop its optimistic
      // copy instead of dead-lettering a permanent phantom
      if (isApiStatus(err, 404)) {
        removePost(trailId, id)
        return
      }
      throw err
    }
  },
  rollback: ({id, trailId}) => removePost(trailId, id),
})

const queueTrailPostDelete = defineMutation<{
  trailId: string
  postId: string
}>(TRAIL_POST_DELETE, {
  run: async ({trailId, postId}) => {
    await tolerateStatus(404, () =>
      unwrap(
        rpc.trails[':id'].posts[':postId'].$delete({
          param: {id: trailId, postId},
        }),
      ),
    )
    // backfill the embedded preview past the deleted post — coalesced so N
    // deletes in one trail refetch it once when the drain settles
    afterFlush(
      `detail:${trailId}`,
      () => void loadTrailDetail(trailId),
    )
  },
  // the post is still alive server-side; the detail refetch brings it back
  rollback: ({trailId}) => restoreTrail(trailId),
})

// a dropped trail takes its post pages with it
trailDropped.subscribe((id) => trailPostsPages.dropId(id))

export const trailPostsList = trailPostsPages
