'use strict';

import type { Db } from '../types/db';

const { getSetting } = require('../db/database.cts');

// Dynamic better-sqlite3 bill/payment rows — columns read individually.
type Row = Record<string, any>;

interface DateRange {
  start: string;
  end: string;
}
interface StatusOptions {
  gracePeriodDays?: string | number | null;
  dueSoonDays?: string | number | null;
}

// A bill's month is "settled" when its status is paid or autodraft (assumed paid
// via autopay). Single source of truth so the ~scattered inline
// `status === 'paid' || status === 'autodraft'` checks don't drift.
const PAID_STATUSES = Object.freeze(['paid', 'autodraft']);
function isPaidStatus(status: string | null | undefined): boolean {
  return status === 'paid' || status === 'autodraft';
}

const WEEKDAY_INDEX: Record<string, number> = {
  sunday: 0,
  monday: 1,
  tuesday: 2,
  wednesday: 3,
  thursday: 4,
  friday: 5,
  saturday: 6,
};

const BIWEEKLY_ANCHOR = new Date(Date.UTC(2000, 0, 3)); // Monday

function resolveGracePeriodDays(value?: string | number | null): number {
  const parsed = parseInt(value ?? getSetting('grace_period_days') ?? '5', 10);
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : 5;
}

// How many days before the due date a bill is flagged "due soon". Configurable
// like the grace period; clamped to a sane 0–31 with a fallback of 3.
function resolveDueSoonDays(value?: string | number | null): number {
  const parsed = parseInt(value ?? getSetting('due_soon_days') ?? '3', 10);
  if (!Number.isInteger(parsed) || parsed < 0) return 3;
  return Math.min(parsed, 31);
}

function pad(value: number | string): string {
  return String(value).padStart(2, '0');
}

const { fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');
// paymentValidation.cts is CommonJS (module.exports =); its members aren't
// namespace properties of `typeof import(...)`, so require plain (→ any). Only
// used in a .map(), so the loss of the branded signature here is immaterial.
const { serializePayment } = require('./paymentValidation.cts');

function dateString(year: number, month: number, day: number): string {
  return `${year}-${pad(month)}-${pad(day)}`;
}

function dateFromString(value: string): Date {
  const [year, month, day] = String(value).split('-').map(Number);
  return new Date(Date.UTC(year, month - 1, day));
}

function addDays(date: Date, days: number): Date {
  const next = new Date(date);
  next.setUTCDate(next.getUTCDate() + days);
  return next;
}

function toDateString(date: Date): string {
  return dateString(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
}

function daysInMonth(year: number, month: number): number {
  return new Date(Date.UTC(year, month, 0)).getUTCDate();
}

function clampDay(year: number, month: number, day: any): number {
  const parsed = parseInt(day, 10);
  const safeDay = Number.isInteger(parsed) ? parsed : 1;
  return Math.min(Math.max(safeDay, 1), daysInMonth(year, month));
}

function normalizeCycleType(bill: Row = {}): string {
  const value = String(bill.cycle_type || bill.billing_cycle || 'monthly').toLowerCase();
  if (value === 'annually') return 'annual';
  if (['monthly', 'weekly', 'biweekly', 'quarterly', 'annual'].includes(value)) return value;
  return 'monthly';
}

function parseCycleMonth(value: any, fallback = 1): number {
  const parsed = parseInt(value, 10);
  if (Number.isInteger(parsed) && parsed >= 1 && parsed <= 12) return parsed;
  return fallback;
}

function parseWeekday(value: any, fallback = 'monday'): number {
  const normalized = String(value || fallback)
    .trim()
    .toLowerCase();
  return WEEKDAY_INDEX[normalized] ?? WEEKDAY_INDEX[fallback];
}

function firstWeekdayInMonth(year: number, month: number, weekdayIndex: number): Date {
  const first = new Date(Date.UTC(year, month - 1, 1));
  const offset = (weekdayIndex - first.getUTCDay() + 7) % 7;
  return addDays(first, offset);
}

function firstBiweeklyDateInMonth(year: number, month: number, weekdayIndex: number): Date | null {
  const start = new Date(Date.UTC(year, month - 1, 1));
  const end = new Date(Date.UTC(year, month, 0));
  const weekdayAnchor = addDays(
    BIWEEKLY_ANCHOR,
    (weekdayIndex - BIWEEKLY_ANCHOR.getUTCDay() + 7) % 7,
  );
  let cursor = firstWeekdayInMonth(year, month, weekdayIndex);

  while (cursor <= end) {
    const diffDays = Math.round((cursor.getTime() - weekdayAnchor.getTime()) / 86400000);
    if (diffDays % 14 === 0) return cursor;
    cursor = addDays(cursor, 7);
  }

  return null;
}

function monthMatchesQuarterlyCycle(month: number, cycleStartMonth: number): boolean {
  return (((month - cycleStartMonth) % 3) + 3) % 3 === 0;
}

/**
 * Resolves the due date for a bill in a given year/month.
 * Returns null when the bill's cycle does not occur in the requested month.
 * Legacy override_due_date values are intentionally ignored by the current
 * product behavior.
 */
function resolveDueDate(bill: Row, year: number, month: number): string | null {
  const cycleType = normalizeCycleType(bill);

  if (cycleType === 'weekly') {
    return toDateString(firstWeekdayInMonth(year, month, parseWeekday(bill.cycle_day)));
  }

  if (cycleType === 'biweekly') {
    const due = firstBiweeklyDateInMonth(year, month, parseWeekday(bill.cycle_day));
    return due ? toDateString(due) : null;
  }

  if (cycleType === 'quarterly') {
    if (!monthMatchesQuarterlyCycle(month, parseCycleMonth(bill.cycle_day, 1))) return null;
    return dateString(year, month, clampDay(year, month, bill.due_day));
  }

  if (cycleType === 'annual') {
    if (month !== parseCycleMonth(bill.cycle_day, 1)) return null;
    return dateString(year, month, clampDay(year, month, bill.due_day));
  }

  const day = clampDay(year, month, bill.due_day);
  return dateString(year, month, day);
}

/**
 * Auto-assigns bucket from due_day: 1–14 → '1st', 15+ → '15th'
 */
function resolveBucket(bill: Row): string {
  if (bill.bucket) return bill.bucket;
  return bill.due_day <= 14 ? '1st' : '15th';
}

/**
 * Computes the payment cycle start/end for a bill in a given month.
 * For monthly bills the cycle is the calendar month.
 */
function getCalendarMonthRange(year: number, month: number): DateRange {
  const start = dateString(year, month, 1);
  const end = dateString(year, month, daysInMonth(year, month));
  return { start, end };
}

/**
 * Computes the payment cycle start/end for a bill in a given month.
 * Without a bill argument this preserves the historical calendar-month range.
 */
function getCycleRange(year: number, month: number, bill: Row | null = null): DateRange | null {
  if (!bill) return getCalendarMonthRange(year, month);

  const dueDate = resolveDueDate(bill, year, month);
  if (!dueDate) return null;

  const cycleType = normalizeCycleType(bill);
  if (cycleType === 'weekly') {
    const start = dateFromString(dueDate);
    return { start: dueDate, end: toDateString(addDays(start, 6)) };
  }
  if (cycleType === 'biweekly') {
    const start = dateFromString(dueDate);
    return { start: dueDate, end: toDateString(addDays(start, 13)) };
  }
  if (cycleType === 'quarterly') {
    const startMonth = month;
    const endDate = new Date(Date.UTC(year, startMonth - 1 + 3, 0));
    return { start: dateString(year, startMonth, 1), end: toDateString(endDate) };
  }
  if (cycleType === 'annual') {
    return { start: dateString(year, 1, 1), end: dateString(year, 12, 31) };
  }

  return getCalendarMonthRange(year, month);
}

/**
 * Returns status for a bill given its payments and due date.
 *
 * Statuses:
 *  paid       — has a non-deleted payment in this billing cycle
 *             — OR total paid >= expected_amount (fully settled)
 *  autodraft  — autopay_enabled and assumed_paid (no confirmed payment yet)
 *  upcoming   — due_date in the future
 *  due_soon   — due within 3 days
 *  late       — past due, within grace period
 *  missed     — past grace period, unpaid
 */
function calculateStatus(
  bill: Row,
  payments: Row[],
  dueDate: string | null,
  today: string,
  options: StatusOptions = {},
): string {
  if (!dueDate) return 'inactive_cycle';

  const gracePeriodDays = resolveGracePeriodDays(options.gracePeriodDays);
  const dueSoonDays = resolveDueSoonDays(options.dueSoonDays);
  const safePayments = Array.isArray(payments) ? payments : [];
  const expectedAmount = Number(bill.expected_amount) || 0;
  const totalPaid = safePayments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0);

  if (totalPaid >= expectedAmount) return 'paid';

  if (bill.autopay_enabled && bill.autodraft_status === 'assumed_paid') {
    return 'autodraft';
  }

  const due = new Date(dueDate + 'T00:00:00');
  const todayDate = new Date(today + 'T00:00:00');
  const diffDays = Math.floor((due.getTime() - todayDate.getTime()) / 86400000);

  if (diffDays > dueSoonDays) return 'upcoming';
  if (diffDays >= 0) return 'due_soon';
  if (Math.abs(diffDays) <= gracePeriodDays) return 'late';
  return 'missed';
}

/**
 * Builds a full tracker row for a bill in a given month.
 */
function buildTrackerRow(
  bill: Row,
  payments: Row[],
  year: number,
  month: number,
  todayStr: string,
  options: StatusOptions = {},
): Row | null {
  const dueDate = resolveDueDate(bill, year, month);
  if (!dueDate) return null;

  const bucket = resolveBucket(bill);
  const safePayments = Array.isArray(payments) ? payments : [];
  const status = calculateStatus(bill, safePayments, dueDate, todayStr, options);
  const expectedAmount = Number(bill.expected_amount) || 0;
  const totalPaid = safePayments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0);
  const hasPayment = safePayments.length > 0;
  const isSettled = isPaidStatus(status);
  const paidTowardDue = Math.min(totalPaid, expectedAmount);
  const overpaidAmount = Math.max(totalPaid - expectedAmount, 0);
  const rawBalance = expectedAmount - totalPaid;
  const balance = isSettled ? 0 : Math.max(rawBalance, 0);
  const lastPayment = hasPayment
    ? [...safePayments].sort((a, b) => b.paid_date.localeCompare(a.paid_date))[0]
    : null;

  return {
    id: bill.id,
    name: bill.name,
    category_id: bill.category_id,
    category_name: bill.category_name || null,
    due_date: dueDate,
    due_day: bill.due_day,
    bucket,
    expected_amount: fromCents(expectedAmount),
    notes: bill.notes || null, // Bill-level notes (always available)
    total_paid: fromCents(totalPaid),
    paid_toward_due: fromCents(paidTowardDue),
    overpaid_amount: fromCents(overpaidAmount),
    balance: fromCents(balance),
    has_payment: hasPayment,
    is_settled: isSettled,
    last_paid_date: lastPayment ? lastPayment.paid_date : null,
    last_payment_amount: lastPayment ? fromCents(lastPayment.amount) : null,
    status,
    autopay_enabled: !!bill.autopay_enabled,
    autodraft_status: bill.autodraft_status,
    auto_mark_paid: !!bill.auto_mark_paid,
    billing_cycle: bill.billing_cycle,
    cycle_type: normalizeCycleType(bill),
    cycle_day: bill.cycle_day,
    current_balance: bill.current_balance != null ? fromCents(bill.current_balance) : null,
    minimum_payment: bill.minimum_payment != null ? fromCents(bill.minimum_payment) : null,
    interest_rate: bill.interest_rate ?? null,
    is_subscription: !!bill.is_subscription,
    has_2fa: !!bill.has_2fa,
    has_merchant_rule: !!bill.has_merchant_rule,
    has_linked_transactions: !!bill.has_linked_transactions,
    website: bill.website || null,
    autopay_verified_at: bill.autopay_verified_at ?? null,
    inactive_reason: bill.inactive_reason ?? null,
    inactivated_at: bill.inactivated_at ?? null,
    sparkline: bill.sparkline ?? null,
    autopay_stats: bill.autopay_stats ?? null,
    payments: safePayments.map(serializePayment),
  };
}

module.exports = {
  buildTrackerRow,
  calculateStatus,
  getCalendarMonthRange,
  getCycleRange,
  isPaidStatus,
  normalizeCycleType,
  PAID_STATUSES,
  resolveBucket,
  resolveDueDate,
  resolveGracePeriodDays,
  resolveDueSoonDays,
};
