/**
 * Task CRUD + bulk routes — tasks-board-engine.
 *
 * Mounted at /api/tasks (behind authMiddleware + requireModuleEnabled('tasks')).
 *
 * GET    /              list tasks (filterable, cursor-paginated)
 * POST   /              create task
 * PATCH  /:id           update task (status+position move, assignee change)
 * DELETE /:id           delete task
 * POST   /bulk          bulk status update
 *
 * Permissions:
 *   tasks:read   — list / get
 *   tasks:write  — create / update (also required for status/position moves)
 *   tasks:delete — delete
 *   tasks:assign — required additionally when assignee_id changes
 */
import { Hono } from 'hono'
import type { AppEnv } from '../types'
import { isOwnerOrAdminRole } from '../lib/system-roles'
import { requirePermission } from '../middleware/guards'
import {
  listTasks,
  getTask,
  createTask,
  updateTask,
  deleteTask,
  bulkUpdateStatus,
  recordTaskAudit,
  recordTaskSystemMessage,
  getTaskStatusById,
  getUnsatisfiedBlockers,
  maybeEnqueueTaskStatusInvoice,
  enqueueTaskStatusInvoicesForBulk,
  getProjectIdsForTasks,
  bumpProjectRevision,
  assertTenantOwnsProject,
  assertTenantOwnsTaskStatus,
  assertActiveTenantAssignee,
  invalidTenantReferenceBody,
} from '@zync/db/queries'
import { publishRealtimeEvent } from '@zync/realtime/server'
import { validateTaskDescriptionJson } from '../lib/sanitize-comment'
import {
  createTaskSchema,
  updateTaskSchema,
  bulkUpdateSchema,
  taskFiltersSchema,
  parseMultiParam,
} from '../schemas/tasks'
import type { TaskFilters } from '@zync/types'

export const tasksRoute = new Hono<AppEnv>()

// GET /api/tasks
tasksRoute.get('/', requirePermission('tasks: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 rawQuery = Object.fromEntries(url.searchParams.entries())
  const parsed = taskFiltersSchema.safeParse(rawQuery)
  if (!parsed.success) {
    return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 422)
  }

  const q = parsed.data

  const filters: TaskFilters = {
    project: q.project,
    status: parseMultiParam(q.status),
    priority: parseMultiParam(q.priority),
    assignee: q.assignee,
    source: parseMultiParam(q.source),
    dueFrom: q.due_from,
    dueTo: q.due_to,
    labels: parseMultiParam(q.labels),
    q: q.q,
  }

  const db = c.get('db')
  const result = await listTasks(db, session.tid, filters, q.cursor, q.limit)
  return c.json({ rows: result.rows, nextCursor: result.nextCursor }, 200)
})

// GET /api/tasks/:id
tasksRoute.get('/:id', requirePermission('tasks: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 task = await getTask(db, session.tid, c.req.param('id'))
  if (!task) {
    return c.json({ error: 'Not found' }, 404)
  }

  return c.json(task, 200)
})

// POST /api/tasks
tasksRoute.post('/', requirePermission('tasks: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 = createTaskSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 422)
  }

  // Ensure reporter_id defaults to session user if not provided (or override)
  const input = {
    ...parsed.data,
    reporter_id: parsed.data.reporter_id ?? session.sub,
  }

  const db = c.get('db')

  if (!(await assertTenantOwnsProject(db, session.tid, input.project_id))) {
    return c.json(invalidTenantReferenceBody('project_id'), 400)
  }
  if (!(await assertTenantOwnsTaskStatus(db, session.tid, input.status_id))) {
    return c.json(invalidTenantReferenceBody('status_id'), 400)
  }
  if (!(await assertActiveTenantAssignee(db, session.tid, input.assignee_id))) {
    return c.json(invalidTenantReferenceBody('assignee_id'), 400)
  }
  if (!(await assertActiveTenantAssignee(db, session.tid, input.reporter_id))) {
    return c.json(invalidTenantReferenceBody('reporter_id'), 400)
  }

  const task = await createTask(db, session.tid, input)
  if (task.project_id) {
    await bumpProjectRevision(c.env, session.tid, task.project_id)
  }
  return c.json(task, 201)
})

// PATCH /api/tasks/:id
tasksRoute.patch('/:id', requirePermission('tasks: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 = updateTaskSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 422)
  }

  // If assignee_id is being changed, additionally require tasks:assign
  if (
    parsed.data.assignee_id !== undefined &&
    !session.permissions.includes('tasks:assign')
  ) {
    return c.json({ error: 'Forbidden: tasks:assign required to change assignee' }, 403)
  }

  const db = c.get('db')
  const taskId = c.req.param('id')
  const existingTask = await getTask(db, session.tid, taskId)
  if (!existingTask) {
    return c.json({ error: 'Not found' }, 404)
  }

  if (
    parsed.data.description !== undefined &&
    parsed.data.description !== null &&
    !validateTaskDescriptionJson(parsed.data.description)
  ) {
    return c.json(
      { error: 'Invalid request', issues: [{ path: ['description'], message: 'Invalid Tiptap document' }] },
      422,
    )
  }

  if (parsed.data.project_id !== undefined) {
    if (!(await assertTenantOwnsProject(db, session.tid, parsed.data.project_id))) {
      return c.json(invalidTenantReferenceBody('project_id'), 400)
    }
  }
  if (parsed.data.status_id !== undefined) {
    if (!(await assertTenantOwnsTaskStatus(db, session.tid, parsed.data.status_id))) {
      return c.json(invalidTenantReferenceBody('status_id'), 400)
    }
  }
  if (parsed.data.assignee_id !== undefined) {
    if (!(await assertActiveTenantAssignee(db, session.tid, parsed.data.assignee_id))) {
      return c.json(invalidTenantReferenceBody('assignee_id'), 400)
    }
  }

  // task-dependencies: block a status move while blockers are unsatisfied.
  // OWNER/ADMIN may force it through with `override: true`.
  let overriddenBlockers: string[] = []
  if (parsed.data.status_id !== undefined) {
    const blockers = await getUnsatisfiedBlockers(db, session.tid, taskId)
    if (blockers.length > 0) {
      const isPrivileged = isOwnerOrAdminRole(session.role)
      if (!isPrivileged || parsed.data.override !== true) {
        return c.json({ error: 'task_blocked', blockers }, 409)
      }
      overriddenBlockers = blockers.map((b) => b.id)
    }
  }

  const task = await updateTask(db, session.tid, taskId, parsed.data)

  const taskUpdates: Promise<unknown>[] = []

  if (parsed.data.status_id !== undefined && parsed.data.status_id !== existingTask.status_id) {
    const [fromStatus, toStatus] = await Promise.all([
      getTaskStatusById(db, session.tid, existingTask.status_id),
      getTaskStatusById(db, session.tid, parsed.data.status_id),
    ])
    const previousValue = {
      id: existingTask.status_id,
      name: fromStatus?.name ?? existingTask.status_id,
    }
    const newValue = {
      id: task.status_id,
      name: toStatus?.name ?? task.status_id,
    }
    taskUpdates.push(
      recordTaskAudit(db, {
        tenantId: session.tid,
        taskId,
        actorId: session.sub,
        action: 'status_changed',
        previousValue,
        newValue,
      }),
      recordTaskSystemMessage(db, {
        tenantId: session.tid,
        taskId,
        text: `Status changed from ${previousValue.name} to ${newValue.name} by ${session.sub}`,
      }),
    )
  }

  if (parsed.data.assignee_id !== undefined && parsed.data.assignee_id !== existingTask.assignee_id) {
    taskUpdates.push(
      recordTaskAudit(db, {
        tenantId: session.tid,
        taskId,
        actorId: session.sub,
        action: 'assigned',
        previousValue: existingTask.assignee_id,
        newValue: task.assignee_id,
      }),
      recordTaskSystemMessage(db, {
        tenantId: session.tid,
        taskId,
        text: task.assignee_id
          ? `Assigned to ${task.assignee_id} by ${session.sub}`
          : `Assignment cleared by ${session.sub}`,
      }),
    )
  }

  if (parsed.data.priority !== undefined && parsed.data.priority !== existingTask.priority) {
    taskUpdates.push(
      recordTaskAudit(db, {
        tenantId: session.tid,
        taskId,
        actorId: session.sub,
        action: 'priority_changed',
        previousValue: existingTask.priority,
        newValue: task.priority,
      }),
      recordTaskSystemMessage(db, {
        tenantId: session.tid,
        taskId,
        text: `Priority changed from ${existingTask.priority} to ${task.priority} by ${session.sub}`,
      }),
    )
  }

  if (parsed.data.due_date !== undefined && parsed.data.due_date !== existingTask.due_date) {
    taskUpdates.push(
      recordTaskAudit(db, {
        tenantId: session.tid,
        taskId,
        actorId: session.sub,
        action: 'due_date_changed',
        previousValue: existingTask.due_date,
        newValue: task.due_date,
      }),
      recordTaskSystemMessage(db, {
        tenantId: session.tid,
        taskId,
        text: task.due_date
          ? `Due date changed from ${existingTask.due_date ?? 'none'} to ${task.due_date} by ${session.sub}`
          : `Due date cleared by ${session.sub}`,
      }),
    )
  }

  if (parsed.data.description !== undefined && parsed.data.description !== existingTask.description) {
    taskUpdates.push(
      recordTaskAudit(db, {
        tenantId: session.tid,
        taskId,
        actorId: session.sub,
        action: 'edited',
        previousValue: null,
        newValue: null,
      }),
    )
  }

  if (parsed.data.title !== undefined && parsed.data.title !== existingTask.title) {
    taskUpdates.push(
      recordTaskAudit(db, {
        tenantId: session.tid,
        taskId,
        actorId: session.sub,
        action: 'title_changed',
        previousValue: existingTask.title,
        newValue: task.title,
      }),
    )
  }

  const affectedProjectIds = [
    ...new Set(
      [existingTask.project_id, task.project_id].filter(
        (projectId): projectId is string => projectId !== null,
      ),
    ),
  ]
  for (const projectId of affectedProjectIds) {
    await bumpProjectRevision(c.env, session.tid, projectId)
  }

  if (parsed.data.status_id !== undefined) {
    try {
      await maybeEnqueueTaskStatusInvoice(
        db,
        session.tid,
        taskId,
        parsed.data.status_id,
        c.env.QUEUE,
      )
    } catch (err) {
      console.error('[tasks] task-status invoice enqueue failed:', err)
    }
  }

  // Task-detail audit trail + system messages (non-fatal, best-effort).
  try {
    await Promise.all(taskUpdates)
  } catch {
    // Non-fatal — task-detail audit/system-message best-effort
  }

  // Real-time fan-out (non-fatal). task.updated → tenant; task.assigned →
  // user-scoped (only the new assignee, never broadcast to the whole tenant).
  try {
    await publishRealtimeEvent(c.env.REALTIME_QUEUE, {
      type: 'task.updated',
      tenantId: session.tid,
      payload: { taskId, changes: parsed.data as Record<string, unknown> },
    })
    if (parsed.data.status_id && parsed.data.status_id !== existingTask.status_id) {
      await publishRealtimeEvent(c.env.REALTIME_QUEUE, {
        type: 'task.status_changed',
        tenantId: session.tid,
        payload: {
          taskId,
          boardId: task.project_id ?? existingTask.project_id ?? '',
          fromStatus: existingTask.status_id,
          toStatus: task.status_id,
          movedByUserId: session.sub,
        },
      })
    }
    if (parsed.data.assignee_id) {
      await publishRealtimeEvent(c.env.REALTIME_QUEUE, {
        type: 'task.assigned',
        tenantId: session.tid,
        targetUserId: parsed.data.assignee_id,
        payload: {
          taskId,
          assignedToUserId: parsed.data.assignee_id,
          assignedByUserId: session.sub,
        },
      })
    }
  } catch {
    // Non-fatal — clients refetch on reconnect
  }

  if (overriddenBlockers.length > 0) {
    return c.json({ ...task, _meta: { overridden: true, overridden_blockers: overriddenBlockers } }, 200)
  }
  return c.json(task, 200)
})

// DELETE /api/tasks/:id
tasksRoute.delete('/:id', requirePermission('tasks:delete'), 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 task = await getTask(db, session.tid, c.req.param('id'))
  await deleteTask(db, session.tid, c.req.param('id'))
  if (task?.project_id) {
    await bumpProjectRevision(c.env, session.tid, task.project_id)
  }
  return c.body(null, 204)
})

// POST /api/tasks/bulk
tasksRoute.post('/bulk', requirePermission('tasks: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 = bulkUpdateSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Invalid request', issues: parsed.error.issues }, 422)
  }

  const db = c.get('db')

  if (!(await assertTenantOwnsTaskStatus(db, session.tid, parsed.data.statusId))) {
    return c.json(invalidTenantReferenceBody('status_id'), 400)
  }

  const count = await bulkUpdateStatus(
    db,
    session.tid,
    parsed.data.taskIds,
    parsed.data.statusId,
  )
  const projectIds = await getProjectIdsForTasks(db, session.tid, parsed.data.taskIds)
  for (const projectId of projectIds) {
    await bumpProjectRevision(c.env, session.tid, projectId)
  }

  try {
    await enqueueTaskStatusInvoicesForBulk(
      db,
      session.tid,
      parsed.data.taskIds,
      parsed.data.statusId,
      c.env.QUEUE,
    )
  } catch (err) {
    console.error('[tasks] bulk task-status invoice enqueue failed:', err)
  }

  return c.json({ updated: count }, 200)
})
