import {
  AddCrumbToTrailSchema,
  CreateTrailSchema,
  ListTrailsQuerySchema,
  MoveTrailCrumbSchema,
  UpdateTrailSchema,
} from '@crumbs/core/schemas'
import {and, eq} from 'drizzle-orm'
import {Hono} from 'hono'
import {HTTPException} from 'hono/http-exception'
import {z} from 'zod'

import {type GuardedVars} from '../auth/middleware'
import {db, schema} from '../db/client'
import {
  removeReplacedImage,
  storeUploadedImage,
} from '../lib/blob-image'
import {resolveClientIdReplay} from '../lib/idempotency'
import {trailCoverLimiter} from '../lib/rate-limit'
import {firstOr404} from '../lib/require'
import {processCrumbImage, trailCoverKey} from '../lib/source-image'
import {uuidParams, validate} from '../lib/validate'
import {
  appendTrailCrumb,
  reorderTrailCrumb,
  suggestTrailForCrumb,
} from '../services/trail-crumbs'
import {notifyTrailMembers} from '../services/trail-notifications'
import {
  getTrailDetail,
  listQuickTrails,
  listTrailSummaries,
  loadTrailSummariesByIds,
  requireOwnerOrRowAuthor,
  requireOwnTrailCover,
  requireTrailMember,
  requireTrailViewer,
  toTrailSummary,
} from '../services/trails'
import {requireViewableCrumb} from '../services/visibility'
import trailPosts from './trail-posts'
import trailSocial from './trail-social'

// at most one activity push per trail per member per day
const notifyTrailActivity = (
  trail: {id: string; name: string; authorId: string},
  actor: {id: string; handle: string},
  crumbName: string,
) =>
  notifyTrailMembers({
    trail,
    actorId: actor.id,
    body: `@${actor.handle} added ${crumbName}`,
    throttle: {
      key: `trail-activity:${trail.id}:${new Date().toISOString().slice(0, 10)}`,
      ttlSeconds: 60 * 60 * 24,
    },
  })

const MAX_TRAIL_COVER_UPLOAD = 10 * 1024 * 1024

const getTrailSummaryById = async (
  viewerId: string,
  trailId: string,
) => {
  const summaries = await loadTrailSummariesByIds(viewerId, [trailId])
  const trail = summaries.get(trailId)
  if (!trail) throw new HTTPException(404, {message: 'not_found'})
  return trail
}

const trails = new Hono<{Variables: GuardedVars}>()
  // social routes (invites, join, pins, reactions) register first so their
  // literal paths aren't shadowed by /:id below
  .route('/', trailSocial)
  .route('/', trailPosts)
  .post('/', validate('json', CreateTrailSchema), async (c) => {
    const user = c.get('user')
    const {id, name, crumbId} = c.req.valid('json')

    // any crumb the creator can see may seed the trail, not just their own
    if (crumbId) await requireViewableCrumb(crumbId, user.id)

    const inserted = await db.transaction(async (tx) => {
      // a client-minted id makes the create idempotent under replay
      const [trail] = await tx
        .insert(schema.trails)
        .values({...(id ? {id} : {}), authorId: user.id, name})
        .onConflictDoNothing({target: schema.trails.id})
        .returning()
      if (!trail) return null
      if (crumbId)
        await tx.insert(schema.trailCrumbs).values({
          trailId: trail.id,
          crumbId,
          addedBy: user.id,
          position: 0,
        })
      return trail
    })

    if (inserted)
      return c.json({
        trail: toTrailSummary(
          inserted,
          {handle: user.handle, avatarRef: user.avatarRef},
          crumbId ? 1 : 0,
        ),
      })

    // the client id already exists: an idempotent replay of this author's
    // create (return its current summary), or a forged id we must reject,
    // never leaking another user's trail
    const existing = await resolveClientIdReplay(
      id,
      async (tid) => {
        const [row] = await db
          .select({
            id: schema.trails.id,
            authorId: schema.trails.authorId,
          })
          .from(schema.trails)
          .where(eq(schema.trails.id, tid))
          .limit(1)
        return row
      },
      (row) => row.authorId === user.id,
    )
    return c.json({
      trail: await getTrailSummaryById(user.id, existing.id),
    })
  })
  .get('/', validate('query', ListTrailsQuerySchema), async (c) => {
    const user = c.get('user')
    const {crumbId, limit, cursor} = c.req.valid('query')
    const {trails, nextCursor} = await listTrailSummaries({
      subjectId: user.id,
      viewerId: user.id,
      crumbId,
      includeShared: true,
      limit,
      cursor,
    })
    return c.json({trails, nextCursor})
  })
  // bounded pinned/shared strip for home quick-access
  .get('/quick', async (c) => {
    const user = c.get('user')
    return c.json({trails: await listQuickTrails(user.id)})
  })
  // programmatic auto-file suggestion: member trails whose crumbs share
  // tags with this one, best match first
  .get(
    '/suggest',
    validate('query', z.object({crumbId: z.uuid()})),
    async (c) => {
      const me = c.get('user')
      const {crumbId} = c.req.valid('query')

      return c.json({
        suggestion: await suggestTrailForCrumb(me.id, crumbId),
      })
    },
  )
  .get('/:id', uuidParams('id'), async (c) => {
    const user = c.get('user')
    const {id} = c.req.valid('param')
    const {row, role} = await requireTrailViewer(id, user.id)
    return c.json({trail: await getTrailDetail(row, role, user.id)})
  })
  .patch(
    '/:id',
    uuidParams('id'),
    validate('json', UpdateTrailSchema),
    async (c) => {
      const user = c.get('user')
      const {id} = c.req.valid('param')
      const {name, description} = c.req.valid('json')
      firstOr404(
        await db
          .update(schema.trails)
          .set({
            ...(name !== undefined && {name}),
            // an empty description clears it
            ...(description !== undefined && {
              description: description || null,
            }),
            updatedAt: new Date(),
          })
          .where(
            and(
              eq(schema.trails.id, id),
              eq(schema.trails.authorId, user.id),
            ),
          )
          .returning({id: schema.trails.id}),
      )
      // return the updated summary so the client's replayed edit can
      // reconcile its optimistic copy to the server truth on ack
      return c.json({trail: await getTrailSummaryById(user.id, id)})
    },
  )
  // manual trail cover; reuses the crumb image pipeline (re-encode,
  // exif strip) so uploaded bytes are never trusted or served as-is
  .put(
    '/:id/cover',
    trailCoverLimiter,
    uuidParams('id'),
    async (c) => {
      const user = c.get('user')
      const {id} = c.req.valid('param')
      const existing = await requireOwnTrailCover(id, user.id)

      const ref = await storeUploadedImage(c.req, {
        max: MAX_TRAIL_COVER_UPLOAD,
        process: processCrumbImage,
        key: (hash) => trailCoverKey(id, hash),
      })
      await db
        .update(schema.trails)
        .set({coverImageRef: ref, updatedAt: new Date()})
        .where(eq(schema.trails.id, id))

      await removeReplacedImage(existing.coverImageRef, ref)
      return c.json({trail: await getTrailSummaryById(user.id, id)})
    },
  )
  .delete('/:id/cover', uuidParams('id'), async (c) => {
    const user = c.get('user')
    const {id} = c.req.valid('param')
    const existing = await requireOwnTrailCover(id, user.id)

    await db
      .update(schema.trails)
      .set({coverImageRef: null, updatedAt: new Date()})
      .where(eq(schema.trails.id, id))

    await removeReplacedImage(existing.coverImageRef, null)
    return c.json({trail: await getTrailSummaryById(user.id, id)})
  })
  .delete('/:id', uuidParams('id'), async (c) => {
    const user = c.get('user')
    const {id} = c.req.valid('param')
    firstOr404(
      await db
        .delete(schema.trails)
        .where(
          and(
            eq(schema.trails.id, id),
            eq(schema.trails.authorId, user.id),
          ),
        )
        .returning({id: schema.trails.id}),
    )
    return c.json({ok: true})
  })
  .post(
    '/:id/crumbs',
    uuidParams('id'),
    validate('json', AddCrumbToTrailSchema),
    async (c) => {
      const user = c.get('user')
      const {id} = c.req.valid('param')
      const {crumbId} = c.req.valid('json')

      const {trail} = await requireTrailMember(id, user.id)
      // members may file any crumb they can see, not only their own
      const crumb = await requireViewableCrumb(crumbId, user.id)

      const nextPosition = await appendTrailCrumb(
        trail.id,
        crumbId,
        user.id,
      )

      notifyTrailActivity(
        trail,
        {id: user.id, handle: user.handle},
        crumb.name,
      ).catch((err: unknown) =>
        console.error('trail activity push failed', {
          trailId: trail.id,
          err,
        }),
      )

      return c.json({ok: true, position: nextPosition})
    },
  )
  .put(
    '/:id/crumbs/:crumbId/position',
    uuidParams('id', 'crumbId'),
    validate('json', MoveTrailCrumbSchema),
    async (c) => {
      const user = c.get('user')
      const {id, crumbId} = c.req.valid('param')
      const {position} = c.req.valid('json')

      const {trail} = await requireTrailMember(id, user.id)
      await reorderTrailCrumb(trail.id, crumbId, position)

      return c.json({ok: true})
    },
  )
  .delete(
    '/:id/crumbs/:crumbId',
    uuidParams('id', 'crumbId'),
    async (c) => {
      const user = c.get('user')
      const {id, crumbId} = c.req.valid('param')
      const {role} = await requireTrailMember(id, user.id)

      // a collaborator may pull a crumb they filed; the owner may pull any
      await requireOwnerOrRowAuthor(role, user.id, async () => {
        const [row] = await db
          .select({addedBy: schema.trailCrumbs.addedBy})
          .from(schema.trailCrumbs)
          .where(
            and(
              eq(schema.trailCrumbs.trailId, id),
              eq(schema.trailCrumbs.crumbId, crumbId),
            ),
          )
          .limit(1)
        return row?.addedBy
      })

      firstOr404(
        await db
          .delete(schema.trailCrumbs)
          .where(
            and(
              eq(schema.trailCrumbs.trailId, id),
              eq(schema.trailCrumbs.crumbId, crumbId),
            ),
          )
          .returning({crumbId: schema.trailCrumbs.crumbId}),
      )
      return c.json({ok: true})
    },
  )

export default trails
