/**
 * Expense OCR correction routes — expense-ocr-correction-ux (wave-13).
 * Mounted on expenseRoutes in routes/expenses/index.ts.
 *
 * Routes:
 *   GET  /review             → NEEDS_REVIEW queue (expenses:read)
 *   POST /:id/reprocess      → re-queue OCR (expenses:write)
 *   POST /:id/approve        → OCR review approve (expenses:write)  — orthogonal to /approvals/:id/approve
 *   POST /:id/correct        → post-approval correction (expenses:write)
 *   POST /:id/void           → soft-delete via voided_at (expenses:write)
 *
 * NOTE: /review must be mounted BEFORE /:id in expenseRoutes to avoid id-capture.
 */
import { Hono } from 'hono'
import { z } from 'zod'
import type { AppEnv } from '../../types'
import { requirePermission } from '../../middleware/guards'
import {
  listReviewQueue,
  approveOcrExpense,
  approveExpense,
  isExpenseApprover,
  reprocessExpense,
  correctExpense,
  voidExpense,
} from '@zync/db/queries'
import { getExpenseById } from '@zync/expenses'
import { pushOverWebSocket } from '@zync/notifications'
import type { ExpenseProcessJob } from '../../queues/expense-process'
import { postExpenseReceipts } from '../../integrations/platform/inventory'

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

const reviewQuerySchema = z.object({
  cursor: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(100).default(20),
})

const approveOcrSchema = z.object({
  corrections: z.record(z.string(), z.string()).default({}),
})

const approveWorkflowSchema = z.object({
  note: z.string().max(1000).optional(),
})

const correctSchema = z.object({
  reason: z.string().min(1).max(2000),
  amount: z.string().optional(),
  category: z.string().optional(),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
  receiptR2Key: z.string().optional(),
})

const voidSchema = z.object({
  reason: z.string().min(1).max(2000),
})

function hasPermission(session: { permissions?: string[] } | undefined, permission: string): boolean {
  return !!session?.permissions?.includes(permission)
}

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

export const ocrCorrectionRoutes = new Hono<AppEnv>()

// ── GET /review ────────────────────────────────────────────────────────────────

ocrCorrectionRoutes.get('/review', requirePermission('expenses: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 = reviewQuerySchema.safeParse(raw)
  if (!parsed.success) {
    return c.json({ error: 'Invalid query', issues: parsed.error.issues }, 400)
  }

  const db = c.get('db')
  const result = await listReviewQueue(db, session.tid, {
    cursor: parsed.data.cursor,
    limit: parsed.data.limit,
  })

  return c.json(result, 200)
})

// ── POST /:id/reprocess ────────────────────────────────────────────────────────

ocrCorrectionRoutes.post('/:id/reprocess', requirePermission('expenses:write'), 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 id = c.req.param('id')

  try {
    const { r2Key, fileType } = await reprocessExpense(db, session.tid, id, session.sub)

    // Re-enqueue for OCR processing
    const job: ExpenseProcessJob = {
      type: 'expense.process',
      tenantId: session.tid,
      expenseId: id,
    }
    await c.env.EXPENSE_QUEUE.send(job)

    // Push realtime update (best-effort)
    await pushOverWebSocket(
      session.sub,
      session.tid,
      { type: 'expense.updated', titleKey: 'expense.updated', params: { expenseId: id } },
      c.env,
    ).catch(() => { /* best-effort */ })

    return c.json({ expenseId: id, status: 'NEEDS_REVIEW', r2Key, fileType }, 200)
  } catch (err) {
    const msg = (err as Error).message
    if (msg === 'Expense not found') return c.json({ error: msg }, 404)
    if (msg.includes('voided')) return c.json({ error: msg }, 422)
    throw err
  }
})

// ── POST /:id/approve (OCR review axis — orthogonal to /approvals/:id/approve) ─

ocrCorrectionRoutes.post('/:id/approve', 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 id = c.req.param('id')
  const expense = await getExpenseById(db, session.tid, id)
  if (!expense) {
    return c.json({ error: 'Expense not found' }, 404)
  }

  if (expense.status === 'NEEDS_REVIEW') {
    if (!hasPermission(session, 'expenses:write')) {
      return c.json({ error: 'Forbidden' }, 403)
    }

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

    try {
      const updated = await approveOcrExpense(db, {
        tenantId: session.tid,
        expenseId: id,
        userId: session.sub,
        corrections: parsed.data.corrections,
      })
      await postExpenseReceipts(db as never, updated)

      await pushOverWebSocket(
        session.sub,
        session.tid,
        { type: 'expense.updated', titleKey: 'expense.updated', params: { expenseId: id } },
        c.env,
      ).catch(() => { /* best-effort */ })

      return c.json(updated, 200)
    } catch (err) {
      const msg = (err as Error).message
      if (msg === 'Expense not found') return c.json({ error: msg }, 404)
      if (msg.includes('NEEDS_REVIEW') || msg.includes('voided')) return c.json({ error: msg }, 422)
      throw err
    }
  }

  if (expense.approvalStatus !== 'pending') {
    return c.json({ error: 'Expense not found or not in pending status' }, 409)
  }

  if (!hasPermission(session, 'expenses:approve')) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const approver = await isExpenseApprover({
    tenantId: session.tid,
    userId: session.sub,
    db,
  })
  if (!approver) {
    return c.json({ error: 'Not an authorized approver' }, 403)
  }

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

  try {
    const updated = await approveExpense({
      tenantId: session.tid,
      expenseId: id,
      approverUserId: session.sub,
      note: parsed.data.note,
      db,
    })
    const finalized = await getExpenseById(db, session.tid, id)
    if (finalized) {
      await postExpenseReceipts(db as never, {
        id: finalized.id,
        tenantId: finalized.tenantId,
        amount: finalized.amount,
        vatAmount: finalized.vatAmount,
        processedAt: finalized.processedAt?.toISOString() ?? null,
        updatedAt: finalized.updatedAt.toISOString(),
        createdAt: finalized.createdAt.toISOString(),
        sourceMetadata: (finalized.sourceMetadata as Record<string, unknown> | null) ?? null,
      })
    }

    await pushOverWebSocket(
      session.sub,
      session.tid,
      { type: 'expense.updated', titleKey: 'expense.updated', params: { expenseId: id } },
      c.env,
    ).catch(() => { /* best-effort */ })

    return c.json(updated, 200)
  } catch (err) {
    const msg = (err as Error).message
    return c.json({ error: msg }, 409)
  }
})

// ── POST /:id/correct ──────────────────────────────────────────────────────────

ocrCorrectionRoutes.post('/:id/correct', requirePermission('expenses: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 = correctSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

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

  try {
    const updated = await correctExpense(db, {
      tenantId: session.tid,
      expenseId: id,
      userId: session.sub,
      reason: parsed.data.reason,
      amount: parsed.data.amount,
      category: parsed.data.category,
      date: parsed.data.date,
      receiptR2Key: parsed.data.receiptR2Key,
    })

    await pushOverWebSocket(
      session.sub,
      session.tid,
      { type: 'expense.updated', titleKey: 'expense.updated', params: { expenseId: id } },
      c.env,
    ).catch(() => { /* best-effort */ })

    return c.json(updated, 200)
  } catch (err) {
    const msg = (err as Error).message
    if (msg === 'Expense not found') return c.json({ error: msg }, 404)
    if (msg.includes('approved') || msg.includes('voided')) return c.json({ error: msg }, 422)
    throw err
  }
})

// ── POST /:id/void ─────────────────────────────────────────────────────────────

ocrCorrectionRoutes.post('/:id/void', requirePermission('expenses: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 = voidSchema.safeParse(body)
  if (!parsed.success) {
    return c.json({ error: 'Validation failed', issues: parsed.error.issues }, 400)
  }

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

  try {
    await voidExpense(db, session.tid, id, session.sub, parsed.data.reason)

    await pushOverWebSocket(
      session.sub,
      session.tid,
      { type: 'expense.updated', titleKey: 'expense.updated', params: { expenseId: id } },
      c.env,
    ).catch(() => { /* best-effort */ })

    return c.json({ expenseId: id, voided: true }, 200)
  } catch (err) {
    const msg = (err as Error).message
    if (msg === 'Expense not found') return c.json({ error: msg }, 404)
    if (msg.includes('already voided')) return c.json({ error: msg }, 422)
    throw err
  }
})
