// @ts-nocheck — route controller converted to .cts; handler req/res typed, full type-check deferred (thin glue over typed services).
import type { Req, Res, Next } from '../types/http';
const express = require('express');
const { standardizeError } = require('../middleware/errorFormatter.cts');
const router = express.Router();
const { getDb } = require('../db/database.cts');
const { buildTrackerRow, getCycleRange } = require('../services/statusService.cts');
const { getUserSettings } = require('../services/userSettings.cts');
const { accountingActiveSql } = require('../services/paymentAccountingService.cts');
const {
  feedUrlForToken,
  getActiveToken,
  getOrCreateToken,
  previewFeed,
  regenerateToken,
  revokeToken,
} = require('../services/calendarFeedService.cts');
const { localDateString } = require('../utils/dates.mts');
const { roundMoney, sumMoney, fromCents } = require('../utils/money.mts');

function clampDay(year, month, day) {
  const daysInMonth = new Date(year, month, 0).getDate();
  return Math.min(Math.max(parseInt(day || 1, 10), 1), daysInMonth);
}

function toDateString(year, month, day) {
  return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}

function emptyDay(year, month, day) {
  return {
    date: toDateString(year, month, day),
    day,
    bills_due: [],
    payments: [],
    status_summary: {
      due_count: 0,
      paid_count: 0,
      skipped_count: 0,
      missed_count: 0,
      total_due: 0,
      total_paid: 0,
    },
  };
}

function tokenPayload(req, tokenRow) {
  if (!tokenRow) {
    return {
      active: false,
      token: null,
      feed_url: null,
      created_at: null,
      last_used_at: null,
      revoked_at: null,
    };
  }
  return {
    active: !!tokenRow.active,
    token: tokenRow.token,
    feed_url: feedUrlForToken(req, tokenRow.token),
    created_at: tokenRow.created_at,
    last_used_at: tokenRow.last_used_at,
    revoked_at: tokenRow.revoked_at,
  };
}

// GET /api/calendar/feed — current user's calendar feed token and URL
router.get('/feed', (req: Req, res: Res) => {
  res.json(tokenPayload(req, getActiveToken(req.user.id)));
});

// POST /api/calendar/feed — create the feed token if one does not exist
router.post('/feed', (req: Req, res: Res) => {
  const token = getOrCreateToken(req.user.id);
  res.status(201).json(tokenPayload(req, token));
});

// POST /api/calendar/feed/regenerate — revoke old token and issue a new URL
router.post('/feed/regenerate', (req: Req, res: Res) => {
  const token = regenerateToken(req.user.id);
  res.json(tokenPayload(req, token));
});

// DELETE /api/calendar/feed — revoke the active feed URL
router.delete('/feed', (req: Req, res: Res) => {
  revokeToken(req.user.id);
  res.json(tokenPayload(req, null));
});

// GET /api/calendar/feed/preview — next generated events shown before subscribing
router.get('/feed/preview', (req: Req, res: Res) => {
  const limit = Math.min(Math.max(parseInt(req.query.limit || '10', 10) || 10, 1), 50);
  res.json({
    events: previewFeed(req.user.id, { limit }),
  });
});

// GET /api/calendar?year=2026&month=5
router.get('/', (req: Req, res: Res) => {
  const db = getDb();
  const now = new Date();
  const year = parseInt(req.query.year || now.getFullYear(), 10);
  const month = parseInt(req.query.month || now.getMonth() + 1, 10);

  if (isNaN(year) || year < 2000 || year > 2100) {
    return res
      .status(400)
      .json(
        standardizeError(
          'year must be a 4-digit integer between 2000 and 2100',
          'VALIDATION_ERROR',
          'year',
        ),
      );
  }
  if (isNaN(month) || month < 1 || month > 12) {
    return res
      .status(400)
      .json(
        standardizeError('month must be an integer between 1 and 12', 'VALIDATION_ERROR', 'month'),
      );
  }

  const today = localDateString(now);
  const userSettings = getUserSettings(req.user.id);
  const rowOptions = {
    gracePeriodDays: userSettings.grace_period_days,
    dueSoonDays: userSettings.due_soon_days,
  };
  const daysInMonth = new Date(year, month, 0).getDate();
  const { start, end } = getCycleRange(year, month);
  const days = Array.from({ length: daysInMonth }, (_, index) => emptyDay(year, month, index + 1));
  const dayByDate = new Map(days.map((day) => [day.date, day]));

  const bills = db
    .prepare(
      `
    SELECT b.*, c.name AS category_name
    FROM bills b
    LEFT JOIN categories c ON b.category_id = c.id AND c.deleted_at IS NULL
    WHERE b.active = 1 AND b.user_id = ? AND b.deleted_at IS NULL
    ORDER BY b.due_day ASC, b.name ASC
  `,
    )
    .all(req.user.id);

  const paymentsByBillStmt = db.prepare(`
    SELECT *
    FROM payments
    WHERE bill_id = ? AND paid_date BETWEEN ? AND ?
      AND deleted_at IS NULL
      AND ${accountingActiveSql()}
    ORDER BY paid_date DESC
  `);

  const monthlyStateStmt = db.prepare(`
    SELECT actual_amount, notes, is_skipped
    FROM monthly_bill_state
    WHERE bill_id = ? AND year = ? AND month = ?
  `);

  const payments = db
    .prepare(
      `
    SELECT
      p.id AS payment_id,
      p.bill_id,
      b.name AS bill_name,
      p.amount,
      p.paid_date,
      p.method,
      p.notes
    FROM payments p
    JOIN bills b ON p.bill_id = b.id
    WHERE b.user_id = ?
      AND b.deleted_at IS NULL
      AND p.paid_date BETWEEN ? AND ?
      AND p.deleted_at IS NULL
      AND ${accountingActiveSql('p')}
    ORDER BY p.paid_date ASC, b.name ASC
  `,
    )
    .all(req.user.id, start, end);

  for (const payment of payments) {
    const day = dayByDate.get(payment.paid_date);
    if (day) {
      const amount = fromCents(payment.amount);
      day.payments.push({
        payment_id: payment.payment_id,
        bill_id: payment.bill_id,
        bill_name: payment.bill_name,
        amount,
        paid_date: payment.paid_date,
        method: payment.method || null,
        notes: payment.notes || null,
      });
      day.status_summary.total_paid += amount || 0;
    }
  }

  const calendarBills = bills
    .map((bill) => {
      const billRange = getCycleRange(year, month, bill);
      if (!billRange) return null;

      const billPayments = paymentsByBillStmt.all(bill.id, billRange.start, billRange.end);
      const row = buildTrackerRow(bill, billPayments, year, month, today, rowOptions);
      if (!row) return null;

      const monthlyState = monthlyStateStmt.get(bill.id, year, month);
      const actualAmount = fromCents(monthlyState?.actual_amount);
      const isSkipped = !!monthlyState?.is_skipped;
      const effectiveAmount = actualAmount ?? row.expected_amount;
      const isPaidByThreshold = row.total_paid > 0 && row.total_paid >= effectiveAmount;
      const isAutodraft = row.status === 'autodraft';
      const status = isSkipped ? 'skipped' : isPaidByThreshold ? 'paid' : row.status;
      const isPaid = status === 'paid' || isAutodraft;

      return {
        bill_id: bill.id,
        name: bill.name,
        due_date: row.due_date,
        due_day: Number(row.due_date.slice(8, 10)),
        expected_amount: row.expected_amount,
        actual_amount: actualAmount,
        effective_amount: effectiveAmount,
        category_name: bill.category_name || null,
        is_paid: isPaid,
        is_skipped: isSkipped,
        paid_amount: row.total_paid || 0,
        status,
      };
    })
    .filter(Boolean);

  for (const bill of calendarBills) {
    const day = dayByDate.get(bill.due_date);
    if (!day) continue;

    day.bills_due.push(bill);
    day.status_summary.due_count += 1;
    if (bill.is_paid) day.status_summary.paid_count += 1;
    if (bill.is_skipped) day.status_summary.skipped_count += 1;
    if (!bill.is_paid && !bill.is_skipped && (bill.status === 'late' || bill.status === 'missed')) {
      day.status_summary.missed_count += 1;
    }
    if (!bill.is_skipped) day.status_summary.total_due += bill.effective_amount || 0;
  }

  const activeBills = calendarBills.filter((bill) => !bill.is_skipped);
  const expectedTotal = sumMoney(activeBills, (bill) => bill.effective_amount || 0);
  const paidTotal = sumMoney(activeBills, (bill) => bill.paid_amount || 0);
  const remainingTotal = Math.max(0, roundMoney(expectedTotal - paidTotal));
  const paidPercent =
    expectedTotal > 0 ? Math.min(100, Math.round((paidTotal / expectedTotal) * 100)) : 0;

  // Cent-exact: the per-day loops above accumulate floats; settle them here.
  for (const day of days) {
    day.status_summary.total_paid = roundMoney(day.status_summary.total_paid);
    day.status_summary.total_due = roundMoney(day.status_summary.total_due);
  }

  res.json({
    year,
    month,
    today,
    days,
    summary: {
      expected_total: expectedTotal,
      paid_total: paidTotal,
      remaining_total: remainingTotal,
      paid_percent: paidPercent,
      bill_count: activeBills.length,
      paid_count: activeBills.filter((bill) => bill.is_paid).length,
      skipped_count: calendarBills.filter((bill) => bill.is_skipped).length,
      missed_count: activeBills.filter((bill) => bill.status === 'late' || bill.status === 'missed')
        .length,
    },
  });
});

module.exports = router;
