/**
 * Expense-to-invoice-line bridge routes — expense-to-invoice-line (P052).
 * Mounted at /api/expenses in apps/zync-api/src/routes/index.ts (sub-mount).
 *
 * Routes:
 *   GET  /billable               → list billable (unbilled COMPLETED) expenses
 *   GET  /:id/draft-invoices     → DRAFT invoices for the expense's customer
 *   POST /:id/add-to-invoice     → add expense as a line on an existing DRAFT invoice
 *
 * Auth: authMiddleware + requireModuleEnabled('expenses').
 * The POST route requires both invoices:write and expenses:read.
 */
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 {
  listBillableExpenses,
  getExpenseForInvoicing,
  listDraftInvoicesForCustomer,
  addExpenseToInvoiceLine,
} from '@zync/db/queries'

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

const billableQuerySchema = z.object({
  projectId: z.string().uuid().optional(),
  cursor: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(200).optional(),
})

const addToInvoiceBodySchema = z.object({
  invoiceId: z.string().uuid(),
  // Optional overrides — default values are sourced from the expense record
  description: z.string().max(500).optional(),
  amount: z
    .string()
    .regex(/^\d+(\.\d{1,2})?$/, 'amount must be a positive decimal with up to 2 decimal places')
    .optional(),
})

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

export const expenseInvoiceRoutes = new Hono<AppEnv>()

expenseInvoiceRoutes.use('*', authMiddleware)
expenseInvoiceRoutes.use('*', requireModuleEnabled('expenses'))

// ── GET /api/expenses/billable ────────────────────────────────────────────────

expenseInvoiceRoutes.get(
  '/billable',
  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 = billableQuerySchema.safeParse(raw)
    if (!parsed.success) {
      return c.json({ error: 'Invalid query parameters', issues: parsed.error.issues }, 400)
    }

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

    return c.json(result, 200)
  },
)

// ── GET /api/expenses/:id/draft-invoices ──────────────────────────────────────
//
// Returns DRAFT invoices that share the same customer as the given expense.
// Used to populate the "Add to existing invoice" modal selector.

expenseInvoiceRoutes.get(
  '/:id/draft-invoices',
  requirePermission('expenses:read'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

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

    const expense = await getExpenseForInvoicing(db, session.tid, expenseId)
    if (!expense) {
      return c.json({ error: 'Expense not found' }, 404)
    }

    if (!expense.customerId) {
      return c.json({ error: 'Expense has no associated customer (via project)' }, 422)
    }

    const draftInvoices = await listDraftInvoicesForCustomer(
      db,
      session.tid,
      expense.customerId,
    )

    return c.json({ expense, draftInvoices }, 200)
  },
)

// ── POST /api/expenses/:id/add-to-invoice ─────────────────────────────────────

expenseInvoiceRoutes.post(
  '/:id/add-to-invoice',
  requirePermission('expenses:read'),  // also checked: invoices:write below
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

    // Require both invoices:write and expenses:read (read already checked above)
    if (!session.permissions?.includes('invoices:write')) {
      return c.json({ error: 'Forbidden — requires invoices:write' }, 403)
    }

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

    const db = c.get('db')

    // Load and validate the expense
    const expense = await getExpenseForInvoicing(db, session.tid, expenseId)
    if (!expense) {
      return c.json({ error: 'Expense not found' }, 404)
    }

    if (expense.billedAt !== null) {
      return c.json(
        { error: 'Expense is already billed', invoiceId: expense.invoiceId },
        409,
      )
    }

    if (!expense.projectId) {
      return c.json({ error: 'Expense must be linked to a project to be invoiced' }, 422)
    }

    if (!expense.customerId) {
      return c.json({ error: 'Expense project has no associated customer' }, 422)
    }

    // Use caller-provided description/amount or fall back to expense defaults
    const description =
      parsed.data.description ?? expense.description ?? 'Expense'
    const amount = parsed.data.amount ?? expense.amount

    if (!amount || amount === '0') {
      return c.json({ error: 'Expense has no amount — provide amount in request body' }, 422)
    }

    let result: Awaited<ReturnType<typeof addExpenseToInvoiceLine>>
    try {
      result = await db.transaction(async (tx) => {
        return addExpenseToInvoiceLine(tx, {
          expenseId,
          invoiceId: parsed.data.invoiceId,
          description,
          amount,
          tenantId: session.tid!,
        })
      })
    } catch (err) {
      // Distinguish FK violation (invoice not found / not DRAFT) from other errors
      const msg = err instanceof Error ? err.message : String(err)
      if (msg.includes('foreign key') || msg.includes('fk_') || msg.includes('invoice_lines_invoice_id')) {
        return c.json({ error: 'Invoice not found or not editable' }, 404)
      }
      throw err
    }

    return c.json(result, 201)
  },
)
