'use strict';

const cron = require('node-cron');
const { getDb } = require('../db/database');
const { buildTrackerRow, getCycleRange } = require('../services/statusService');
const { pruneExpiredSessions } = require('../services/authService');
const { pruneExpiredChallenges: pruneWebAuthnChallenges } = require('../services/webauthnService');
const { runNotifications, runDriftNotifications } = require('../services/notificationService');
const { runAllCleanup } = require('../services/cleanupService');
const {
  markWorkerError,
  markWorkerStarted,
  markWorkerSuccess,
} = require('../services/statusRuntime');
const { localDateString, localDateStringDaysAgo } = require('../utils/dates.mts');

const DAILY_CRON_HOUR = 6;

function nextDailyRunIso(from = new Date()) {
  const next = new Date(from);
  next.setHours(DAILY_CRON_HOUR, 0, 0, 0);
  if (next <= from) next.setDate(next.getDate() + 1);
  return next.toISOString();
}

async function runDailyTasks() {
  const db = getDb();
  const now = new Date();
  const year = now.getFullYear();
  const month = now.getMonth() + 1;
  // Local date — keep consistent with year/month above and with the client's
  // notion of "today". toISOString() would give the UTC date, which can be a
  // different calendar day and caused autopay marking a day early/late.
  const todayStr = localDateString(now);

  const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all();

  if (bills.length > 0) {
    // Batch-fetch payments for all active bills from the last 90 days to avoid
    // one query per bill. Different billing cycles (monthly/quarterly/annual) all
    // fit inside a 90-day window for the current month's due-date checks.
    const billIds      = bills.map(b => b.id);
    const placeholders = billIds.map(() => '?').join(',');
    const windowStart  = localDateStringDaysAgo(90, now);

    let allPayments = [];
    try {
      allPayments = db.prepare(`
        SELECT * FROM payments
        WHERE bill_id IN (${placeholders})
          AND paid_date >= ?
          AND deleted_at IS NULL
      `).all(...billIds, windowStart);
    } catch (err) {
      console.error('[worker] Failed to batch-fetch payments:', err.message);
    }

    // Group payments by bill_id in memory
    const paymentsByBill = new Map();
    for (const p of allPayments) {
      if (!paymentsByBill.has(p.bill_id)) paymentsByBill.set(p.bill_id, []);
      paymentsByBill.get(p.bill_id).push(p);
    }

    // Prepare autopay update statement once outside the loop
    const markAutopay = db.prepare(
      "UPDATE bills SET autodraft_status = 'assumed_paid', updated_at = datetime('now') WHERE id = ?"
    );

    for (const bill of bills) {
      const range = getCycleRange(year, month, bill);
      if (!range) continue; // bill does not apply this cycle

      // Filter pre-fetched payments to the bill's cycle range
      const payments = (paymentsByBill.get(bill.id) || []).filter(
        p => p.paid_date >= range.start && p.paid_date <= range.end
      );

      const row = buildTrackerRow(bill, payments, year, month, todayStr);
      if (!row) continue;

      // Auto-mark autopay bills as assumed_paid on due date
      if (
        bill.autopay_enabled &&
        bill.autodraft_status === 'pending' &&
        todayStr >= row.due_date
      ) {
        try {
          markAutopay.run(bill.id);
        } catch (err) {
          console.error(`[worker] Failed to mark autopay for bill ${bill.id}:`, err.message);
        }
      }
    }
  }

  pruneExpiredSessions();
  pruneWebAuthnChallenges(db);

  await runNotifications().catch(err => {
    console.error('[worker] Notification error (non-fatal):', err.message);
  });

  await runDriftNotifications().catch(err => {
    console.error('[worker] Drift notification error (non-fatal):', err.message);
  });

  await runAllCleanup().catch(err => {
    console.error('[worker] Cleanup error (non-fatal):', err.message);
  });

  markWorkerSuccess(nextDailyRunIso());
  console.log(`[worker] Daily tasks ran at ${todayStr}`);
}

function start() {
  markWorkerStarted(nextDailyRunIso());

  // Run once at startup
  runDailyTasks().catch(err => {
    markWorkerError(err, nextDailyRunIso());
    console.error('[worker] Startup task error:', err);
  });

  // Run every day at 6:00 AM
  cron.schedule('0 6 * * *', () => {
    runDailyTasks().catch(err => {
      markWorkerError(err, nextDailyRunIso());
      console.error('[worker] Daily task error:', err);
    });
  });
}

module.exports = { start, runDailyTasks, nextDailyRunIso };
