/**
 * Contractors router — contractor-payouts (P049, wave 7).
 * Mounted at /api/contractors in apps/zync-api/src/routes/index.ts.
 *
 * Routes:
 *   GET    /                              → list contractors
 *   POST   /                              → create contractor
 *   GET    /withholding-report            → Form 856 annual report
 *   GET    /withholding-report/xlsx       → Excel download
 *   GET    /:id                           → contractor detail
 *   PATCH  /:id                           → update contractor
 *   DELETE /:id                           → deactivate contractor
 *   GET    /:id/assignments               → project assignments
 *   POST   /:id/assignments               → assign to project
 *   PATCH  /:id/assignments/:aid          → update assignment rate/role
 *   DELETE /:id/assignments/:aid          → unassign
 *   GET    /:id/time                      → time entries (filterable by period)
 *   GET    /:id/bills                     → payout bills
 *   POST   /:id/bills                     → generate draft bill
 *   PATCH  /:id/bills/:bid                → update bill
 *   POST   /:id/bills/:bid/void           → void bill
 *   GET    /:id/withholding-certs         → certificate history
 *   POST   /:id/withholding-certs         → record new certificate
 */
import { Hono } from 'hono'
import type { AppEnv } from '../../types'
import { authMiddleware } from '../../middleware/auth'
import { requirePermission } from '../../middleware/guards'
import { requireModuleEnabled } from '../../middleware/require-module-enabled'
import {
  listContractors,
  getContractor,
  createContractor,
  updateContractor,
  deactivateContractor,
  listAssignments,
  createAssignment,
  updateAssignment,
  deleteAssignment,
  listContractorTimeEntries,
  listPayoutBills,
  getPayoutBillWithLines,
  generatePayoutBillDraft,
  updatePayoutBill,
  voidPayoutBill,
  getWithholdingReport,
  listWithholdingCertificates,
  addWithholdingCertificate,
  ContractorNotFoundError,
  PayoutBillNotFoundError,
  PayoutBillConflictError,
  createContractorSchema,
  updateContractorSchema,
  listContractorsSchema,
  createAssignmentSchema,
  updateAssignmentSchema,
  generateBillSchema,
  updateBillSchema,
  voidBillSchema,
  listTimeForContractorSchema,
  withholdingReportSchema,
  assertTenantOwnsProject,
  invalidTenantReferenceBody,
} from '@zync/db/queries'
import { z } from 'zod'
import { contractorPortalInviteRoutes } from '../contractors-portal-invite'
import { buildForm857Xlsx, buildWithholdingReportXlsx } from '../../lib/contractor-payout-exports'
import { sendEmail } from '@zync/notifications'
import {
  applyFieldPermissions,
  applyFieldPermissionsToPage,
  attachReadOnlyMeta,
  getFieldPermissionContext,
} from '../field-permissions/enforcement'

export const contractorRoutes = new Hono<AppEnv>()

contractorRoutes.use('*', authMiddleware)
contractorRoutes.use('*', requireModuleEnabled('contractor_payouts'))

// contractor-portal (wave 8): staff portal invite routes
contractorRoutes.route('/', contractorPortalInviteRoutes)

// ── GET /api/contractors ──────────────────────────────────────────────────────

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

  const rawQuery = Object.fromEntries(new URL(c.req.url).searchParams)
  if (typeof rawQuery.project_id === 'string' && rawQuery.projectId === undefined) {
    rawQuery.projectId = rawQuery.project_id
  }

  const query = listContractorsSchema.safeParse(rawQuery)
  if (!query.success) {
    return c.json({ error: 'Invalid query', issues: query.error.issues }, 400)
  }

  const db = c.get('db')
  const result = await listContractors(db, session.tid, query.data)
  const { role, rules } = await getFieldPermissionContext(db, session)
  return c.json(applyFieldPermissionsToPage(result, 'contractor', role, rules))
})

// ── POST /api/contractors ─────────────────────────────────────────────────────

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

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

  const db = c.get('db')
  const contractor = await createContractor(db, session.tid, parsed.data)
  return c.json(contractor, 201)
})

// ── GET /api/contractors/withholding-report ───────────────────────────────────
// Must come before /:id to avoid route shadowing

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

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

  const db = c.get('db')
  const report = await getWithholdingReport(db, session.tid, query.data.year)
  return c.json(report)
})

// ── GET /api/contractors/withholding-report/xlsx ──────────────────────────────

contractorRoutes.get('/withholding-report/xlsx', requirePermission('payouts:read'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

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

  const db = c.get('db')
  const report = await getWithholdingReport(db, session.tid, query.data.year)
  const bytes = await buildWithholdingReportXlsx(report)
  return new Response(bytes, {
    status: 200,
    headers: {
      'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      'Content-Disposition': `attachment; filename="withholding-report-${query.data.year}.xlsx"`,
      'Content-Length': String(bytes.byteLength),
      'X-Content-Type-Options': 'nosniff',
    },
  })
})

contractorRoutes.get(
  '/withholding-report/:contractorId/form-857',
  requirePermission('payouts:read'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

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

    const db = c.get('db')
    const report = await getWithholdingReport(db, session.tid, query.data.year)
    try {
      const bytes = await buildForm857Xlsx(report, c.req.param('contractorId'))
      return new Response(bytes, {
        status: 200,
        headers: {
          'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
          'Content-Disposition': `attachment; filename="form-857-${query.data.year}-${c.req.param('contractorId')}.xlsx"`,
          'Content-Length': String(bytes.byteLength),
          'X-Content-Type-Options': 'nosniff',
        },
      })
    } catch {
      return c.json({ error: 'Not found' }, 404)
    }
  },
)

// ── GET /api/contractors/:id ──────────────────────────────────────────────────

contractorRoutes.get('/:id', requirePermission('payouts: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')
  try {
    const contractor = await getContractor(db, session.tid, c.req.param('id'))
    const { role, rules } = await getFieldPermissionContext(db, session)
    const filtered = applyFieldPermissions(contractor, 'contractor', role, rules)
    return c.json(attachReadOnlyMeta(filtered.data, filtered.readOnly))
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── PATCH /api/contractors/:id ────────────────────────────────────────────────

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

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

  const db = c.get('db')
  try {
    const updated = await updateContractor(db, session.tid, c.req.param('id'), parsed.data)
    return c.json(updated)
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── DELETE /api/contractors/:id ───────────────────────────────────────────────

contractorRoutes.delete('/:id', requirePermission('payouts: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')
  try {
    const updated = await deactivateContractor(db, session.tid, c.req.param('id'))
    return c.json(updated)
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── GET /api/contractors/:id/assignments ──────────────────────────────────────

contractorRoutes.get('/:id/assignments', requirePermission('payouts: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')
  try {
    const assignments = await listAssignments(db, session.tid, c.req.param('id'))
    return c.json({ items: assignments })
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── POST /api/contractors/:id/assignments ─────────────────────────────────────

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

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

  const db = c.get('db')
  try {
    if (!(await assertTenantOwnsProject(db, session.tid, parsed.data.projectId))) {
      return c.json(invalidTenantReferenceBody('project_id'), 400)
    }

    const assignment = await createAssignment(db, session.tid, c.req.param('id'), parsed.data)
    return c.json(assignment, 201)
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── PATCH /api/contractors/:id/assignments/:aid ───────────────────────────────

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

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

  const db = c.get('db')
  try {
    const updated = await updateAssignment(
      db,
      session.tid,
      c.req.param('id'),
      c.req.param('aid'),
      parsed.data,
    )
    return c.json(updated)
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── DELETE /api/contractors/:id/assignments/:aid ──────────────────────────────

contractorRoutes.delete(
  '/:id/assignments/:aid',
  requirePermission('payouts: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')
    await deleteAssignment(db, session.tid, c.req.param('id'), c.req.param('aid'))
    return c.json({ success: true })
  },
)

// ── GET /api/contractors/:id/time ─────────────────────────────────────────────

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

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

  const db = c.get('db')
  try {
    const result = await listContractorTimeEntries(db, session.tid, c.req.param('id'), query.data)
    return c.json(result)
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── GET /api/contractors/:id/bills ────────────────────────────────────────────

contractorRoutes.get('/:id/bills', requirePermission('payouts: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 params = {
    cursor: url.searchParams.get('cursor') ?? undefined,
    limit: url.searchParams.get('limit') ? Number(url.searchParams.get('limit')) : 50,
  }

  const db = c.get('db')
  try {
    const result = await listPayoutBills(db, session.tid, c.req.param('id'), params)
    return c.json(result)
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

contractorRoutes.get('/:id/bills/:bid', requirePermission('payouts: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')
  try {
    const result = await getPayoutBillWithLines(db, session.tid, c.req.param('id'), c.req.param('bid'))
    return c.json(result)
  } catch (err) {
    if (err instanceof ContractorNotFoundError || err instanceof PayoutBillNotFoundError) {
      return c.json({ error: 'Not found' }, 404)
    }
    throw err
  }
})

// ── POST /api/contractors/:id/bills ───────────────────────────────────────────

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

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

  const db = c.get('db')
  try {
    const result = await generatePayoutBillDraft(
      db,
      session.tid,
      c.req.param('id'),
      session.sub,
      parsed.data,
    )
    return c.json(
      {
        ...result,
        warning: result.usedStatutoryDefault
          ? "No withholding certificate on file — applying statutory default rate of 30%."
          : undefined,
      },
      201,
    )
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── PATCH /api/contractors/:id/bills/:bid ─────────────────────────────────────

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

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

  const db = c.get('db')
  try {
    const result = await updatePayoutBill(
      db,
      session.tid,
      c.req.param('id'),
      c.req.param('bid'),
      parsed.data,
      session.sub,
    )
    if (parsed.data.status === 'SENT') {
      const contractor = await getContractor(db, session.tid, c.req.param('id'))
      if (contractor.email) {
        await sendEmail(
          {
            to: contractor.email,
            locale: 'he-IL',
            templateKey: 'invoice-sent',
            vars: {
              subject: `Payout bill ${result.bill.periodStart} - ${result.bill.periodEnd}`,
              heading: 'Payout bill ready for review',
              summary: `Gross ${result.bill.amount} ${result.bill.currency}, net ${result.bill.netAmount ?? result.bill.amount} ${result.bill.currency}`,
            } as Record<string, string>,
          },
          c.env,
        )
      }
    }
    return c.json(result)
  } catch (err) {
    if (err instanceof ContractorNotFoundError || err instanceof PayoutBillNotFoundError)
      return c.json({ error: 'Not found' }, 404)
    if (err instanceof PayoutBillConflictError) return c.json({ error: err.message }, 409)
    throw err
  }
})

// ── POST /api/contractors/:id/bills/:bid/void ─────────────────────────────────

contractorRoutes.post('/:id/bills/:bid/void', requirePermission('payouts:write'), async (c) => {
  const session = c.get('session')
  if (!session || session.type !== 'user' || !session.tid || !session.sub) {
    return c.json({ error: 'Unauthorized' }, 401)
  }

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

  const db = c.get('db')
  try {
    const bill = await voidPayoutBill(
      db,
      session.tid,
      c.req.param('id'),
      c.req.param('bid'),
      session.sub,
      parsed.data.reason,
    )
    return c.json(bill)
  } catch (err) {
    if (err instanceof ContractorNotFoundError || err instanceof PayoutBillNotFoundError)
      return c.json({ error: 'Not found' }, 404)
    if (err instanceof PayoutBillConflictError) {
      if (err.message.includes('PAID')) return c.json({ error: 'cannot_void_paid' }, 409)
      return c.json({ error: err.message }, 409)
    }
    throw err
  }
})

contractorRoutes.get('/:id/certificate', requirePermission('payouts: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')
  try {
    const contractor = await getContractor(db, session.tid, c.req.param('id'))
    if (!contractor.withholdingCertificateR2Key) {
      return c.json({ error: 'Not found' }, 404)
    }
    const object = await c.env.STORAGE.get(contractor.withholdingCertificateR2Key)
    if (!object) return c.json({ error: 'Not found' }, 404)
    return new Response(object.body, {
      status: 200,
      headers: {
        'Content-Type': object.httpMetadata?.contentType ?? 'application/pdf',
        'Content-Disposition': 'attachment; filename="withholding-certificate.pdf"',
        'X-Content-Type-Options': 'nosniff',
      },
    })
  } catch (err) {
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

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

  let formData: FormData
  try {
    formData = await c.req.formData()
  } catch {
    return c.json({ error: 'Expected multipart/form-data' }, 400)
  }

  const file = formData.get('file')
  if (!(file && typeof file === 'object' && 'arrayBuffer' in file)) {
    return c.json({ error: 'file is required' }, 400)
  }
  const upload = file as File
  if (upload.type !== 'application/pdf') {
    return c.json({ error: 'Only PDF certificates are supported' }, 400)
  }

  const key = `${session.tid}/contractors/${c.req.param('id')}/withholding-certificate-${Date.now()}.pdf`
  await c.env.STORAGE.put(key, await upload.arrayBuffer(), {
    httpMetadata: { contentType: 'application/pdf' },
  })

  const db = c.get('db')
  try {
    const contractor = await updateContractor(db, session.tid, c.req.param('id'), {
      withholdingCertificateR2Key: key,
    })
    return c.json({ key, contractor }, 201)
  } catch (err) {
    await c.env.STORAGE.delete(key).catch(() => {})
    if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
    throw err
  }
})

// ── GET /api/contractors/:id/withholding-certs ────────────────────────────────

contractorRoutes.get(
  '/:id/withholding-certs',
  requirePermission('payouts: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')
    try {
      const certs = await listWithholdingCertificates(db, session.tid, c.req.param('id'))
      return c.json({ items: certs })
    } catch (err) {
      if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
      throw err
    }
  },
)

// ── POST /api/contractors/:id/withholding-certs ───────────────────────────────

const addCertSchema = z.object({
  certificateNumber: z.string().min(1).max(100),
  taxYear: z.string().regex(/^\d{4}$/),
  withholdingRate: z.string().regex(/^(0(\.\d{1,4})?|1(\.0{1,4})?)$/),
  expiryDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  r2Key: z.string().max(500).optional().nullable(),
})

contractorRoutes.post(
  '/:id/withholding-certs',
  requirePermission('payouts:write'),
  async (c) => {
    const session = c.get('session')
    if (!session || session.type !== 'user' || !session.tid || !session.sub) {
      return c.json({ error: 'Unauthorized' }, 401)
    }

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

    const db = c.get('db')
    try {
      const cert = await addWithholdingCertificate(
        db,
        session.tid,
        c.req.param('id'),
        session.sub,
        parsed.data,
      )
      return c.json(cert, 201)
    } catch (err) {
      if (err instanceof ContractorNotFoundError) return c.json({ error: 'Not found' }, 404)
      throw err
    }
  },
)
