/**
 * Audit log routes — tenant-audit-log spec.
 *
 * GET  /api/audit-log         — filtered, cursor-paginated list
 * GET  /api/audit-log/export  — streamed CSV (capped at 10k rows)
 *
 * Access control:
 *   - Default (settings view):  OWNER / ADMIN only → 403 for others.
 *   - ?scope=reports:            MEMBER allowed, but API enforces own-actions
 *                                 only (WHERE user_id = session.sub).
 *                                 CONTRACTOR / CLIENT_PORTAL always 403.
 *
 * Role check uses session.role — canonical values from foundation-auth-rbac seed:
 *   'OWNER' | 'ADMIN' | 'MEMBER' | 'CONTRACTOR' | …
 */
import { Hono } from 'hono'
import { z } from 'zod'
import { authMiddleware } from '../middleware/auth'
import { listAuditLog, fetchAuditLogForExport } from '@zync/db/queries'
import type { AppEnv } from '../types'
import {
  isContractorRole,
  isOwnerOrAdminRole,
  MEMBER_ROLE,
} from '../lib/system-roles'

function isAuditDateInput(value: string): boolean {
  if (/^\d+$/.test(value)) return true
  if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return true
  return Number.isFinite(Date.parse(value))
}

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

const AuditLogQuerySchema = z.object({
  from: z.string().refine(isAuditDateInput, 'Expected ISO date/time or unix timestamp').optional(),
  to: z.string().refine(isAuditDateInput, 'Expected ISO date/time or unix timestamp').optional(),
  user_id: z.string().uuid().optional(),
  event_type: z.string().optional(),
  entity_type: z.string().optional(),
  cursor: z.string().optional(),
  limit: z.coerce.number().int().min(1).max(200).default(50),
  q: z.string().optional(),
  entity_q: z.string().optional(),
  scope: z.enum(['settings', 'reports']).optional(),
})

const AuditLogExportSchema = z.object({
  from: z.string().refine(isAuditDateInput, 'Expected ISO date/time or unix timestamp').optional(),
  to: z.string().refine(isAuditDateInput, 'Expected ISO date/time or unix timestamp').optional(),
  user_id: z.string().uuid().optional(),
  event_type: z.string().optional(),
  entity_type: z.string().optional(),
  q: z.string().optional(),
  entity_q: z.string().optional(),
  scope: z.enum(['settings', 'reports']).optional(),
})

// ── Role helpers ──────────────────────────────────────────────────────────────

type AuditAccessResult =
  | { allowed: true; forceUserId: string | undefined }
  | { allowed: false; status: 403 }

function checkAuditAccess(
  session: { role?: string; sub: string },
  scope: 'settings' | 'reports',
): AuditAccessResult {
  const role = session.role ?? MEMBER_ROLE

  // CONTRACTOR and CLIENT_PORTAL never allowed
  if (role === 'client_portal' || isContractorRole(role)) {
    return { allowed: false, status: 403 }
  }

  if (scope === 'settings') {
    // Settings view: OWNER/ADMIN only
    if (!isOwnerOrAdminRole(role)) {
      return { allowed: false, status: 403 }
    }
    return { allowed: true, forceUserId: undefined }
  }

  // Reports view: OWNER/ADMIN see all; MEMBER sees own only
  if (isOwnerOrAdminRole(role)) {
    return { allowed: true, forceUserId: undefined }
  }
  // MEMBER — enforce own-actions-only server-side
  return { allowed: true, forceUserId: session.sub }
}

function canExportAuditLog(role: string | null | undefined): boolean {
  return isOwnerOrAdminRole(role)
}

// ── CSV helpers ───────────────────────────────────────────────────────────────

/**
 * CSV-escape a single field value (RFC 4180) + neutralize spreadsheet formula
 * injection: a cell beginning with = + - @ TAB or CR is evaluated as a formula
 * by Excel/Sheets, so prefix those with a single quote before quoting.
 */
function csvField(value: string | null | undefined): string {
  let s = value ?? ''
  if (/^[=+\-@\t\r]/.test(s)) {
    s = `'${s}`
  }
  if (s.includes('"') || s.includes(',') || s.includes('\n') || s.includes('\r')) {
    return `"${s.replace(/"/g, '""')}"`
  }
  return s
}

function csvRow(fields: (string | null | undefined)[]): string {
  return fields.map(csvField).join(',') + '\r\n'
}

/** Format Unix seconds as DD/MM/YYYY HH:mm (UTC, tenant locale approximation). */
function formatDateTime(unixSeconds: number): string {
  const d = new Date(unixSeconds * 1000)
  const dd = String(d.getUTCDate()).padStart(2, '0')
  const mm = String(d.getUTCMonth() + 1).padStart(2, '0')
  const yyyy = d.getUTCFullYear()
  const hh = String(d.getUTCHours()).padStart(2, '0')
  const min = String(d.getUTCMinutes()).padStart(2, '0')
  return `${dd}/${mm}/${yyyy} ${hh}:${min}`
}

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

export const auditLogRoutes = new Hono<AppEnv>()

auditLogRoutes.use('*', authMiddleware)

// GET /api/audit-log
auditLogRoutes.get('/', 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)
  const parsed = AuditLogQuerySchema.safeParse(rawQuery)
  if (!parsed.success) {
    return c.json({ error: 'Invalid query parameters', issues: parsed.error.issues }, 400)
  }

  const { scope = 'settings', cursor, limit, from, to, user_id, event_type, entity_type, q, entity_q } = parsed.data

  const access = checkAuditAccess(
    { role: (session as { role?: string }).role, sub: session.sub },
    scope,
  )
  if (!access.allowed) {
    return c.json({ error: 'Forbidden' }, 403)
  }
  if (!canExportAuditLog((session as { role?: string }).role)) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const db = c.get('db')
  const page = await listAuditLog(db, session.tid, {
    userId: access.forceUserId ?? user_id,
    eventType: event_type,
    entityType: entity_type,
    from,
    to,
    cursor,
    limit,
    q,
    entityQ: entity_q,
  })

  return c.json(page, 200)
})

// GET /api/audit-log/export
auditLogRoutes.get('/export', 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)
  const parsed = AuditLogExportSchema.safeParse(rawQuery)
  if (!parsed.success) {
    return c.json({ error: 'Invalid query parameters', issues: parsed.error.issues }, 400)
  }

  const { scope = 'settings', from, to, user_id, event_type, entity_type, q, entity_q } = parsed.data

  const access = checkAuditAccess(
    { role: (session as { role?: string }).role, sub: session.sub },
    scope,
  )
  if (!access.allowed) {
    return c.json({ error: 'Forbidden' }, 403)
  }

  const db = c.get('db')
  const rows = await fetchAuditLogForExport(db, session.tid, {
    userId: access.forceUserId ?? user_id,
    eventType: event_type,
    entityType: entity_type,
    from,
    to,
    q,
    entityQ: entity_q,
  })

  // Build CSV in a ReadableStream
  const CSV_HEADER = csvRow(['Date', 'Actor', 'Email', 'Event', 'Entity Type', 'Entity', 'Details', 'IP Address'])

  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue(new TextEncoder().encode(CSV_HEADER))
      for (const row of rows) {
        const details = row.metadata
          ? Object.entries(row.metadata)
              .map(([k, v]) => `${k}: ${String(v)}`)
              .join('; ')
          : ''
        const line = csvRow([
          formatDateTime(row.created_at),
          row.actor_name ?? 'System',
          row.actor_email,
          row.event_type,
          row.entity_type,
          row.entity_label,
          details,
          row.ip_address,
        ])
        controller.enqueue(new TextEncoder().encode(line))
      }
      controller.close()
    },
  })

  // from/to are ISO-date-validated above; sanitize defensively so no CR/LF or
  // quote can ever reach the Content-Disposition header (header injection).
  const safe = (s: string) => s.replace(/[^A-Za-z0-9._-]/g, '')
  const fromLabel = safe(from ?? 'all')
  const toLabel = safe(to ?? 'now')
  const filename = `audit-log-${fromLabel}-${toLabel}.csv`

  return new Response(stream, {
    status: 200,
    headers: {
      'Content-Type': 'text/csv; charset=utf-8',
      'Content-Disposition': `attachment; filename="${filename}"`,
    },
  })
})
