/**
 * Time management routes — time-management.
 * Mounted at /api/time in apps/zync-api/src/routes/index.ts (via manifest).
 *
 * Authenticated routes run behind authMiddleware + requireModuleEnabled('time_management').
 * Magic-link GET is unauthenticated (token carries authority).
 *
 * Routes:
 *   GET    /                         → list entries (paginated, filterable)
 *   GET    /active                   → current running entry or JSON null
 *   GET    /summary                  → weekly summary ?week=YYYY-WNN
 *   GET    /magic                    → consume magic link (UNAUTHENTICATED)
 *   POST   /start                    → start timer
 *   POST   /:id/stop                 → stop running timer
 *   POST   /beacon                   → sendBeacon stop (tab close)
 *   POST   /                         → manual log entry
 *   POST   /magic                    → create magic link
 *   PATCH  /:id                      → edit entry
 *   DELETE /:id                      → hard delete
 */
import { Hono } from 'hono'
import { z } from 'zod'
import type { AppEnv } from '../types'
import { authMiddleware } from '../middleware/auth'
import { requireModuleEnabled } from '../middleware/require-module-enabled'
import { requirePermission } from '../middleware/guards'
import {
  getActiveEntry,
  startEntry,
  stopEntry,
  listEntries,
  logManualEntry,
  updateEntry,
  deleteEntry,
  getWeekSummary,
  getTimeEntryById,
  insertTimerMagicLinkToken,
  findTimerMagicLinkByHash,
  consumeTimerMagicLink,
  getTaskProjectId,
  getTaskTitle,
  getUserEmailById,
} from '@zync/time'
import {
  createDb,
  evaluateBudgetAlert,
  createNotification,
  getTask,
  getActiveTenantMemberByEmail,
  getMembershipView,
  assertTenantOwnsProject,
  assertTenantOwnsTask,
  listUnbilledTimeEntries,
  invalidTenantReferenceBody,
} from '@zync/db/queries'
import type { TenantId, UserId } from '@zync/types'
import { guardTimerMagicLinkTenantMember } from '../lib/timer-magic-link-guards'
import {
  generateOpaqueToken,
  hashToken,
  timingSafeEqual,
} from '@zync/auth'
import { sendEmail } from '@zync/notifications'
import { enqueueTimerWebhook } from '../lib/time-webhooks'
import type { TimeEntry } from '@zync/types'

// ── Zod schemas ────────────────────────────────────────────────────────────────

const startTimerSchema = z.object({
  projectId: z.string().uuid(),
  taskId: z.string().uuid().nullable().optional(),
  description: z.string().max(2000).nullable().optional(),
  source: z.enum(['manual', 'auto']).optional().default('manual'),
})

const manualEntrySchema = z
  .object({
    projectId: z.string().uuid(),
    taskId: z.string().uuid().nullable().optional(),
    startedAt: z.string().datetime(),
    stoppedAt: z.string().datetime(),
    description: z.string().max(2000).nullable().optional(),
    billable: z.boolean().optional(),
  })
  .refine((v) => new Date(v.stoppedAt) > new Date(v.startedAt), {
    message: 'stoppedAt must be after startedAt',
  })

const patchEntrySchema = z
  .object({
    projectId: z.string().uuid().optional(),
    taskId: z.string().uuid().nullable().optional(),
    startedAt: z.string().datetime().optional(),
    stoppedAt: z.string().datetime().optional(),
    description: z.string().max(2000).nullable().optional(),
    billable: z.boolean().optional(),
  })
  .refine(
    (v) => {
      if (v.startedAt && v.stoppedAt) return new Date(v.stoppedAt) > new Date(v.startedAt)
      return true
    },
    { message: 'stoppedAt must be after startedAt' },
  )

const listQuerySchema = z.object({
  from: z.string().optional(),
  to: z.string().optional(),
  user: z.string().uuid().optional(),
  projectId: z.string().uuid().optional(),
  customerId: z.string().uuid().optional(),
  taskId: z.string().uuid().optional(),
  unbilled: z.coerce.boolean().optional().default(false),
  cursor: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(100).default(50),
})

const summaryQuerySchema = z.object({
  week: z.string().regex(/^\d{4}-W\d{2}$/, 'week must be YYYY-WNN'),
  user: z.string().uuid().optional(),
})

const beaconSchema = z.object({
  entryId: z.string().uuid(),
})

const createMagicLinkSchema = z.object({
  taskId: z.string().uuid(),
  recipientEmail: z.string().email().optional(),
})

// ── Router ─────────────────────────────────────────────────────────────────────

export const timeRoutes = new Hono<AppEnv>()

// ── GET /magic (UNAUTHENTICATED — token is the authority) ─────────────────────

timeRoutes.get('/magic', async (c) => {
  const tokenParam = c.req.query('token')
  if (!tokenParam) {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  const db = createDb(c.env)

  // Hash the token for lookup
  const tokenHash = await hashToken(tokenParam)
  const tokenRow = await findTimerMagicLinkByHash(db, tokenHash)

  if (!tokenRow) {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  // Timing-safe comparison of plaintext token
  if (!tokenRow.token || !timingSafeEqual(tokenParam, tokenRow.token)) {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  if (tokenRow.purpose !== 'timer') {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  if (tokenRow.expiresAt < new Date()) {
    return c.redirect('https://app.zync.is/time-track?magic_error=expired')
  }

  // Atomically mark used — guard against replay race
  const consumed = await consumeTimerMagicLink(db, tokenHash)
  if (!consumed) {
    return c.redirect('https://app.zync.is/time-track?magic_error=used')
  }

  if (!tokenRow.userId) {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  const recipientMembership = await getMembershipView(
    db,
    tokenRow.userId as UserId,
    tokenRow.tenantId as TenantId,
  )
  const consumeGuard = guardTimerMagicLinkTenantMember(recipientMembership)
  if (!consumeGuard.ok) {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  // Derive projectId from task
  let projectId: string | null = null
  if (tokenRow.taskId) {
    projectId = await getTaskProjectId(db, tokenRow.tenantId, tokenRow.taskId)
  }

  if (!projectId) {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  // Start timer server-side using token.user_id
  let newEntry: TimeEntry
  try {
    const { entry, stoppedEntry } = await startEntry(
      db,
      tokenRow.tenantId,
      tokenRow.userId,
      {
        projectId,
        taskId: tokenRow.taskId ?? null,
        source: 'magic_link',
      },
    )
    newEntry = entry

    // Enqueue webhooks
    if (stoppedEntry) {
      await enqueueTimerWebhook(c.env, tokenRow.tenantId, {
        event: 'timer.stopped',
        payload: {
          entryId: stoppedEntry.id,
          userId: stoppedEntry.userId,
          taskId: stoppedEntry.taskId,
          projectId: stoppedEntry.projectId,
          startedAt: stoppedEntry.startedAt,
          stoppedAt: stoppedEntry.stoppedAt!,
          durationSeconds: stoppedEntry.durationSeconds,
        },
      })
    }

    await enqueueTimerWebhook(c.env, tokenRow.tenantId, {
      event: 'timer.started',
      payload: {
        entryId: newEntry.id,
        userId: newEntry.userId,
        taskId: newEntry.taskId,
        projectId: newEntry.projectId,
        startedAt: newEntry.startedAt,
        source: newEntry.source,
      },
    })
  } catch {
    return c.redirect('https://app.zync.is/time-track?magic_error=invalid')
  }

  return c.redirect(`https://app.zync.is/time-track?started=${newEntry.id}`)
})

// ── Authenticated routes ───────────────────────────────────────────────────────

timeRoutes.use('*', authMiddleware)
timeRoutes.use('*', requireModuleEnabled('time_management'))

// ── GET /api/time ─────────────────────────────────────────────────────────────

timeRoutes.get('/', requirePermission('time:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const url = new URL(c.req.url)
  const raw = Object.fromEntries(url.searchParams.entries())
  const parsed = listQuerySchema.safeParse(raw)
  if (!parsed.success) {
    return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 400)
  }

  const { from, to, user, projectId, customerId, taskId, unbilled, cursor, limit } = parsed.data

  // Requesting another user's entries requires time:read_all
  const targetUserId = user ?? session.sub
  if (user && user !== session.sub) {
    if (!session.permissions?.includes('time:read_all')) {
      return c.json({ error: 'Forbidden' }, 403)
    }
  }

  const db = c.get('db')
  if (unbilled) {
    const result = await listUnbilledTimeEntries(db, {
      tenantId: session.tid,
      projectId,
      customerId,
      userId: targetUserId,
      dateFrom: from,
      dateTo: to,
      cursor,
      limit,
    })

    return c.json(
      {
        items: result.entries,
        nextCursor: result.nextCursor,
        total: result.entries.length,
      },
      200,
    )
  }

  const result = await listEntries(db, session.tid, {
    from,
    to,
    userId: targetUserId,
    projectId,
    taskId,
    cursor,
    limit,
  })

  return c.json(result, 200)
})

// ── GET /api/time/active ──────────────────────────────────────────────────────

timeRoutes.get('/active', requirePermission('time:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const db = c.get('db')
  const entry = await getActiveEntry(db, session.tid, session.sub)

  if (!entry) return c.json(null, 200)

  return c.json(entry, 200)
})

// ── GET /api/time/summary ─────────────────────────────────────────────────────

timeRoutes.get('/summary', requirePermission('time:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const url = new URL(c.req.url)
  const raw = Object.fromEntries(url.searchParams.entries())
  const parsed = summaryQuerySchema.safeParse(raw)
  if (!parsed.success) {
    return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 400)
  }

  const { week, user } = parsed.data

  const targetUserId = user ?? session.sub
  if (user && user !== session.sub) {
    if (!session.permissions?.includes('time:read_all')) {
      return c.json({ error: 'Forbidden' }, 403)
    }
  }

  const db = c.get('db')
  const summary = await getWeekSummary(db, session.tid, week, targetUserId)
  return c.json(summary, 200)
})

// ── POST /api/time/start ──────────────────────────────────────────────────────

timeRoutes.post('/start', requirePermission('time:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const body = await c.req.json().catch(() => null)

  const parsed = startTimerSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')
  if (!(await assertTenantOwnsProject(db, session.tid, parsed.data.projectId))) {
    return c.json(invalidTenantReferenceBody('projectId'), 400)
  }
  if (!(await assertTenantOwnsTask(db, session.tid, parsed.data.taskId ?? null))) {
    return c.json(invalidTenantReferenceBody('taskId'), 400)
  }
  const { entry, stoppedEntry } = await startEntry(db, session.tid, session.sub, {
    projectId: parsed.data.projectId,
    taskId: parsed.data.taskId ?? null,
    description: parsed.data.description ?? null,
    source: parsed.data.source,
  })

  // Enqueue webhooks
  if (stoppedEntry) {
    await enqueueTimerWebhook(c.env, session.tid, {
      event: 'timer.stopped',
      payload: {
        entryId: stoppedEntry.id,
        userId: stoppedEntry.userId,
        taskId: stoppedEntry.taskId,
        projectId: stoppedEntry.projectId,
        startedAt: stoppedEntry.startedAt,
        stoppedAt: stoppedEntry.stoppedAt!,
        durationSeconds: stoppedEntry.durationSeconds,
      },
    })
    if (stoppedEntry.projectId) {
      c.executionCtx.waitUntil(
        evaluateBudgetAlert(db, session.tid, stoppedEntry.projectId, { createNotif: createNotification }),
      )
    }
  }

  await enqueueTimerWebhook(c.env, session.tid, {
    event: 'timer.started',
    payload: {
      entryId: entry.id,
      userId: entry.userId,
      taskId: entry.taskId,
      projectId: entry.projectId,
      startedAt: entry.startedAt,
      source: entry.source,
    },
  })

  return c.json(entry, 201)
})

// ── POST /api/time/:id/stop ───────────────────────────────────────────────────

timeRoutes.post('/:id/stop', requirePermission('time:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const entryId = c.req.param('id')

  const db = c.get('db')

  // Optional body: { stoppedAt?: ISO string } for idle-discard (stop at idle-onset)
  const StopBodySchema = z.object({
    stoppedAt: z.string().datetime().optional(),
  }).optional()

  let stopBody: { stoppedAt?: string } | undefined
  try {
    const rawBody = await c.req.text()
    if (rawBody) {
      stopBody = StopBodySchema.parse(JSON.parse(rawBody)) ?? undefined
    }
  } catch {
    // ignore parse errors — use defaults
  }

  // Check ownership — editing another user's entry requires time:write_all
  const existing = await getTimeEntryById(db, session.tid, entryId)
  if (!existing) {
    return c.json({ error: 'Not found' }, 404)
  }
  if (existing.userId !== session.sub && !session.permissions?.includes('time:write_all')) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const stopOptions = stopBody?.stoppedAt
    ? { stoppedAt: new Date(stopBody.stoppedAt) }
    : undefined

  const entry = await stopEntry(db, session.tid, entryId, stopOptions)

  // Emit timer.auto_paused when stopped at idle-onset (idle-discard), else timer.stopped
  const webhookEvent = stopBody?.stoppedAt ? 'timer.auto_paused' : 'timer.stopped'

  await enqueueTimerWebhook(c.env, session.tid, {
    event: webhookEvent,
    payload: {
      entryId: entry.id,
      userId: entry.userId,
      taskId: entry.taskId,
      projectId: entry.projectId,
      startedAt: entry.startedAt,
      stoppedAt: entry.stoppedAt!,
      durationSeconds: entry.durationSeconds,
    },
  })

  if (entry.projectId) {
    c.executionCtx.waitUntil(
      evaluateBudgetAlert(db, session.tid, entry.projectId, { createNotif: createNotification }),
    )
  }

  return c.json(entry, 200)
})

// ── POST /api/time/beacon ─────────────────────────────────────────────────────
// Handles navigator.sendBeacon (Content-Type: text/plain or application/json)

timeRoutes.post('/beacon', requirePermission('time:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  // sendBeacon may send Content-Type text/plain or application/json; read the
  // raw text and JSON.parse it so both content-types are handled uniformly.
  let body: unknown
  try {
    body = JSON.parse(await c.req.text())
  } catch {
    return c.json({ error: 'Invalid body' }, 400)
  }

  const parsed = beaconSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')

  // Check ownership — stopping another user's entry requires time:write_all
  const existing = await getTimeEntryById(db, session.tid, parsed.data.entryId)
  if (!existing) {
    return c.body(null, 204)
  }
  if (existing.userId !== session.sub && !session.permissions?.includes('time:write_all')) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  // Idempotent — no error if already stopped
  try {
    const entry = await stopEntry(db, session.tid, parsed.data.entryId)
    await enqueueTimerWebhook(c.env, session.tid, {
      event: 'timer.stopped',
      payload: {
        entryId: entry.id,
        userId: entry.userId,
        taskId: entry.taskId,
        projectId: entry.projectId,
        startedAt: entry.startedAt,
        stoppedAt: entry.stoppedAt!,
        durationSeconds: entry.durationSeconds,
      },
    })
    if (entry.projectId) {
      c.executionCtx.waitUntil(
        evaluateBudgetAlert(db, session.tid, entry.projectId, { createNotif: createNotification }),
      )
    }
  } catch (err: unknown) {
    // If already stopped or not found, treat as success (idempotent)
    const e = err as { status?: number }
    if (e?.status !== 404) {
      throw err
    }
  }

  return new Response(null, { status: 204 })
})

// ── POST /api/time (manual log) ───────────────────────────────────────────────

timeRoutes.post('/', requirePermission('time:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const body = await c.req.json().catch(() => null)

  const parsed = manualEntrySchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')
  if (!(await assertTenantOwnsProject(db, session.tid, parsed.data.projectId))) {
    return c.json(invalidTenantReferenceBody('projectId'), 400)
  }
  if (!(await assertTenantOwnsTask(db, session.tid, parsed.data.taskId ?? null))) {
    return c.json(invalidTenantReferenceBody('taskId'), 400)
  }
  const entry = await logManualEntry(db, session.tid, session.sub, {
    projectId: parsed.data.projectId,
    taskId: parsed.data.taskId ?? null,
    startedAt: parsed.data.startedAt,
    stoppedAt: parsed.data.stoppedAt,
    description: parsed.data.description ?? null,
    billable: parsed.data.billable ?? true,
  })

  // wave-6: fire-and-forget budget alert evaluation (project-hourly-budget)
  if (entry.projectId) {
    c.executionCtx.waitUntil(
      evaluateBudgetAlert(db, session.tid, entry.projectId, { createNotif: createNotification }),
    )
  }

  return c.json(entry, 201)
})

// ── POST /api/time/magic ──────────────────────────────────────────────────────

timeRoutes.post('/magic', requirePermission('tasks:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  // Rate-limit via RATE_LIMITER_WEBHOOK
  const rateLimitKey = `${session.tid}:${session.sub}:magic-link`
  const { success: rateLimitOk } = await (async () => { try { const _rl = await c.env.RATE_LIMITER_WEBHOOK?.limit({ key: rateLimitKey }); return _rl ?? { success: true }; } catch { return { success: true }; } })()
  if (!rateLimitOk) {
    return c.json({ error: 'Rate limit exceeded' }, 429)
  }

  const body = await c.req.json().catch(() => null)

  const parsed = createMagicLinkSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')

  // Resolve recipient userId and email
  let recipientUserId: string
  let recipientEmail: string

  if (!parsed.data.recipientEmail) {
    // Default: send to caller
    recipientUserId = session.sub
    const callerMembership = await getMembershipView(
      db,
      session.sub as UserId,
      session.tid as TenantId,
    )
    const callerGuard = guardTimerMagicLinkTenantMember(callerMembership)
    if (!callerGuard.ok) {
      return c.json({ error: callerGuard.error }, callerGuard.status)
    }
    const callerEmail = await getUserEmailById(db, session.sub)
    if (!callerEmail) {
      return c.json({ error: 'Could not resolve recipient email' }, 400)
    }
    recipientEmail = callerEmail
  } else {
    // Look up active tenant member by email
    const memberUser = await getActiveTenantMemberByEmail(
      db,
      session.tid,
      parsed.data.recipientEmail,
    )
    if (!memberUser) {
      return c.json({ error: 'Recipient not found in this workspace' }, 404)
    }
    recipientUserId = memberUser.id
    recipientEmail = memberUser.email
  }

  const task = await getTask(db, session.tid, parsed.data.taskId)
  if (!task) {
    return c.json({ error: 'Not found' }, 404)
  }

  // Get task title for email subject
  const taskTitle = (await getTaskTitle(db, session.tid, parsed.data.taskId)) ?? 'Task'

  // Generate token
  const plainToken = await generateOpaqueToken()
  const tokenHash = await hashToken(plainToken)
  const expiresAt = new Date(Date.now() + 48 * 60 * 60 * 1000) // 48h

  await insertTimerMagicLinkToken(db, {
    tenantId: session.tid,
    userId: recipientUserId,
    taskId: parsed.data.taskId,
    token: plainToken,
    tokenHash,
    expiresAt,
  })

  const link = `https://app.zync.is/magic/time?token=${plainToken}`

  // Determine locale — default he-IL per IL-first policy
  const locale: 'he-IL' | 'en-US' = 'he-IL'

  await sendEmail(
    {
      to: recipientEmail,
      templateKey: locale === 'he-IL' ? 'timer-magic-link' : 'timer-magic-link',
      vars: {
        subject: `Start timer: ${taskTitle}`,
        title: `Start timer: ${taskTitle}`,
        body: locale === 'he-IL'
          ? `לחץ על הכפתור להפעלת טיימר עבור "${taskTitle}"`
          : `Click the button to start a timer for "${taskTitle}"`,
        link,
        taskTitle,
      },
      locale,
    },
    c.env,
  )

  // Never echo the raw token in the response
  return c.json({ sent: true }, 201)
})

// ── PATCH /api/time/:id ───────────────────────────────────────────────────────

timeRoutes.patch('/:id', requirePermission('time:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const entryId = c.req.param('id')

  const body = await c.req.json().catch(() => null)

  const parsed = patchEntrySchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')

  // Check ownership
  const existing = await getTimeEntryById(db, session.tid, entryId)
  if (!existing) {
    return c.json({ error: 'Not found' }, 404)
  }
  if (existing.userId !== session.sub && !session.permissions?.includes('time:write_all')) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  if (parsed.data.projectId !== undefined) {
    if (!(await assertTenantOwnsProject(db, session.tid, parsed.data.projectId))) {
      return c.json(invalidTenantReferenceBody('projectId'), 400)
    }
  }
  if (parsed.data.taskId !== undefined) {
    if (!(await assertTenantOwnsTask(db, session.tid, parsed.data.taskId))) {
      return c.json(invalidTenantReferenceBody('taskId'), 400)
    }
  }

  const entry = await updateEntry(db, session.tid, entryId, {
    projectId: parsed.data.projectId,
    taskId: parsed.data.taskId,
    startedAt: parsed.data.startedAt,
    stoppedAt: parsed.data.stoppedAt,
    description: parsed.data.description,
    billable: parsed.data.billable,
  })

  // wave-6: fire-and-forget budget alert evaluation (project-hourly-budget)
  const projectIdForAlert = entry.projectId ?? existing.projectId
  if (projectIdForAlert) {
    c.executionCtx.waitUntil(
      evaluateBudgetAlert(db, session.tid, projectIdForAlert, { createNotif: createNotification }),
    )
  }

  return c.json(entry, 200)
})

// ── DELETE /api/time/:id ──────────────────────────────────────────────────────

timeRoutes.delete('/:id', requirePermission('time:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

  const entryId = c.req.param('id')
  const db = c.get('db')

  // Check ownership
  const existing = await getTimeEntryById(db, session.tid, entryId)
  if (!existing) {
    return c.json({ error: 'Not found' }, 404)
  }
  if (existing.userId !== session.sub && !session.permissions?.includes('time:write_all')) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  await deleteEntry(db, session.tid, entryId)
  return new Response(null, { status: 204 })
})
