/**
 * Scheduled reports cron handler — scheduled-reports (wave-15).
 *
 * Called directly from the scheduled() handler in index.ts (no HTTP hop).
 * Runs every 15 minutes; processes all due report schedules.
 *
 * Flow per due schedule:
 *  1. Check creator still has reports:export_external permission → disable + alert if not
 *  2. computeReportPeriod → ReportPeriod
 *  3. generateReport → ReportData
 *  4. renderToFile → RenderedFile
 *  5. For each recipient: resolve email → sendReportEmail
 *  6. touchScheduleRun (update last_run_at, compute next_run_at)
 *  7. logAuditEvent
 */
import type { Env } from '@zync/types'
import {
  createDb,
  listDueSchedules,
  getReportScheduleById,
  touchScheduleRun,
  deactivateSchedule,
  createNotification,
  logAuditEvent,
  getMembership,
  getPermissionsForRole,
  getUserEmailById,
} from '@zync/db/queries'
import type { ReportScheduleRow, ReportRecipient } from '@zync/db/queries'
import { computeReportPeriod, computeNextRun } from '../lib/schedule-math'
import { generateReport, ReportTypeUnavailableError } from '../lib/report-generators/index'
import { renderToFile } from '../lib/report-renderers'
import { sendReportEmail } from '../lib/send-report-email'

const DEFAULT_TZ = 'Asia/Jerusalem'

async function hasPermission(
  db: ReturnType<typeof createDb>,
  tenantId: string,
  userId: string,
  permKey: string,
): Promise<boolean> {
  try {
    // Cast string → branded types — safe here since values come from DB rows
    const membership = await getMembership(
      db,
      userId as Parameters<typeof getMembership>[1],
      tenantId as Parameters<typeof getMembership>[2],
    )
    if (!membership) return false
    const perms = await getPermissionsForRole(db, membership.roleId)
    return perms.includes(permKey)
  } catch {
    return false
  }
}

async function disableScheduleAndAlert(
  db: ReturnType<typeof createDb>,
  schedule: ReportScheduleRow,
  reason: 'creator_lost_access' | 'report_type_unavailable',
  env: Env,
): Promise<void> {
  await deactivateSchedule(db, schedule.tenantId, schedule.id)

  const titleKey =
    reason === 'creator_lost_access'
      ? 'notifications.report_schedule_disabled_access'
      : 'notifications.report_schedule_disabled_unavailable'

  // Notify the creator (only if creator is set)
  if (schedule.createdBy) {
    await createNotification(db, {
      tenantId: schedule.tenantId,
      userId: schedule.createdBy,
      type: 'report_schedule_disabled',
      titleKey,
      params: { scheduleName: schedule.name },
      entityType: 'report_schedule',
      entityId: schedule.id,
    })
  }

  await logAuditEvent(
    { env },
    {
      tenantId: schedule.tenantId,
      userId: schedule.createdBy ?? undefined,
      eventType: 'report_schedule.disabled',
      entityType: 'report_schedule',
      entityId: schedule.id,
      metadata: { reason, scheduleName: schedule.name },
    },
  )
}

async function resolveRecipientEmail(
  db: ReturnType<typeof createDb>,
  recipient: ReportRecipient,
): Promise<string | null> {
  if (recipient.type === 'email') {
    return recipient.email ?? null
  }
  if (recipient.type === 'user' && recipient.user_id) {
    return getUserEmailById(db, recipient.user_id)
  }
  return null
}

async function processSchedule(
  db: ReturnType<typeof createDb>,
  schedule: ReportScheduleRow,
  now: Date,
  env: Env,
): Promise<void> {
  // 1. Permission check on creator
  const creatorHasPermission = schedule.createdBy
    ? await hasPermission(db, schedule.tenantId, schedule.createdBy, 'reports:export_external')
    : false
  if (!creatorHasPermission) {
    await disableScheduleAndAlert(db, schedule, 'creator_lost_access', env)
    return
  }

  // 2. Determine tenant timezone (default to IL)
  const tz = DEFAULT_TZ

  // 3. Compute report period
  const period = computeReportPeriod(schedule, now, tz)

  // 4. Generate report data
  let data
  try {
    data = await generateReport(schedule, period, db, env)
  } catch (err) {
    if (err instanceof ReportTypeUnavailableError) {
      await disableScheduleAndAlert(db, schedule, 'report_type_unavailable', env)
      return
    }
    throw err
  }

  // 5. Render to file
  const format = 'xlsx' as const
  const file = await renderToFile(data, format)

  // 6. Send to all recipients
  const recipients = (schedule.recipients ?? []) as ReportRecipient[]
  for (const recipient of recipients) {
    const email = await resolveRecipientEmail(db, recipient)
    if (!email) continue
    try {
      await sendReportEmail(email, file, schedule, period, env)
    } catch (err) {
      console.error('Failed to send report email', {
        scheduleId: schedule.id,
        email,
        err,
      })
      // Continue to next recipient — don't fail the whole batch
    }
  }

  // 7. Touch run timestamps
  const nextRunAt = computeNextRun(schedule, now, tz)
  await touchScheduleRun(db, schedule.tenantId, schedule.id, now, nextRunAt)

  // 8. Audit log
  await logAuditEvent(
    { env },
    {
      tenantId: schedule.tenantId,
      userId: schedule.createdBy ?? undefined,
      eventType: 'report_schedule.executed',
      entityType: 'report_schedule',
      entityId: schedule.id,
      metadata: {
        scheduleName: schedule.name,
        reportType: schedule.reportType,
        period: `${period.from}/${period.to}`,
        recipientCount: recipients.length,
      },
    },
  )
}

/**
 * One-off run for a specific schedule (dispatched via queue).
 * Does NOT update next_run_at — only executes generate/render/send.
 */
export async function runScheduledReportById(
  scheduleId: string,
  tenantId: string,
  env: Env,
): Promise<void> {
  const db = createDb(env)
  const now = new Date()

  const schedule = await getReportScheduleById(db, tenantId, scheduleId)
  if (!schedule || !schedule.isActive) return

  const tz = DEFAULT_TZ
  const period = computeReportPeriod(schedule, now, tz)

  const data = await generateReport(schedule, period, db, env)
  const format = 'xlsx' as const
  const file = await renderToFile(data, format)

  const recipients = (schedule.recipients ?? []) as ReportRecipient[]
  for (const recipient of recipients) {
    const email = await resolveRecipientEmail(db, recipient)
    if (!email) continue
    try {
      await sendReportEmail(email, file, schedule, period, env)
    } catch (err) {
      console.error('Failed to send one-off report email', { scheduleId, email, err })
    }
  }

  await logAuditEvent(
    { env },
    {
      tenantId: schedule.tenantId,
      userId: schedule.createdBy ?? undefined,
      eventType: 'report_schedule.one_off_executed',
      entityType: 'report_schedule',
      entityId: schedule.id,
      metadata: { scheduleName: schedule.name, reportType: schedule.reportType },
    },
  )
}

/**
 * Main cron entry point. Called directly from the scheduled() handler.
 * Processes all due report schedules, isolating failures per schedule.
 */
export async function runScheduledReports(env: Env): Promise<void> {
  const db = createDb(env)
  const now = new Date()

  const dueSchedules = await listDueSchedules(db, now)
  if (dueSchedules.length === 0) return

  for (const schedule of dueSchedules) {
    try {
      await processSchedule(db, schedule, now, env)
    } catch (err) {
      console.error('Scheduled report execution failed', {
        scheduleId: schedule.id,
        tenantId: schedule.tenantId,
        err,
      })
      // Continue to next schedule — don't block others on one failure
    }
  }
}
