import type { Db } from '../types/db';
const { monthKey } = require('../utils/dates.mts');
const { toCents, fromCents } = require('../utils/money.mts') as typeof import('../utils/money.mts');

type Row = Record<string, any>;
type FieldResult<T> = { value?: T; error?: string };

const VALID_VISIBILITY = ['default', 'all', 'ranges', 'none'];

const TEMPLATE_FIELDS = [
  'name',
  'category_id',
  'due_day',
  'override_due_date',
  'bucket',
  'expected_amount',
  'interest_rate',
  'billing_cycle',
  'cycle_type',
  'cycle_day',
  'autopay_enabled',
  'autodraft_status',
  'auto_mark_paid',
  'website',
  'username',
  'account_info',
  'has_2fa',
  'notes',
  'current_balance',
  'minimum_payment',
  'snowball_order',
  'snowball_include',
  'snowball_exempt',
  'history_visibility',
  'is_subscription',
  'subscription_type',
  'reminder_days_before',
  'subscription_source',
  'subscription_detected_at',
];

function hasText(value: unknown): boolean {
  return typeof value === 'string' && value.trim().length > 0;
}

function isDebtBill(bill: Row): boolean {
  const category = String(bill.category_name || '').toLowerCase();
  return (
    Number(bill.current_balance) > 0 ||
    bill.minimum_payment != null ||
    ['credit card', 'credit cards', 'loan', 'loans', 'debt'].some((token) =>
      category.includes(token),
    )
  );
}

function billAuditIssue(bill: Row, field: string, severity: string, suggestion: string) {
  return {
    bill_id: bill.id,
    bill_name: bill.name,
    field,
    severity,
    suggestion,
  };
}

function sanitizeTemplateData(data: Row = {}): Row {
  return TEMPLATE_FIELDS.reduce((out: Row, field: string) => {
    if (data[field] !== undefined) out[field] = data[field];
    return out;
  }, {} as Row);
}

function parseTemplateData(raw: any): Row {
  try {
    return sanitizeTemplateData(JSON.parse(raw || '{}'));
  } catch {
    return {};
  }
}

function categoryBelongsToUser(db: Db, categoryId: any, userId: number): boolean {
  if (!categoryId) return true;
  return !!db
    .prepare('SELECT id FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
    .get(categoryId, userId);
}

/**
 * Converts a bill row's integer-cents money columns to dollars for API responses.
 */
function serializeBill(bill: Row | null | undefined): any {
  if (!bill) return bill;
  return {
    ...bill,
    expected_amount: fromCents(bill.expected_amount),
    current_balance: fromCents(bill.current_balance),
    minimum_payment: fromCents(bill.minimum_payment),
  };
}

function insertBill(db: Db, userId: number, normalized: Row): Row {
  const result = db
    .prepare(
      `
    INSERT INTO bills
      (user_id, name, category_id, due_day, override_due_date, bucket, expected_amount,
       interest_rate, billing_cycle, autopay_enabled, autodraft_status, auto_mark_paid, website, username,
       account_info, has_2fa, notes, history_visibility, active, cycle_type, cycle_day,
       current_balance, minimum_payment, snowball_order, snowball_include, snowball_exempt,
       is_subscription, subscription_type, reminder_days_before, subscription_source, subscription_detected_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `,
    )
    .run(
      userId,
      normalized.name,
      normalized.category_id,
      normalized.due_day,
      normalized.override_due_date,
      normalized.bucket,
      normalized.expected_amount,
      normalized.interest_rate,
      normalized.billing_cycle,
      normalized.autopay_enabled,
      normalized.autodraft_status,
      normalized.auto_mark_paid,
      normalized.website,
      normalized.username,
      normalized.account_info,
      normalized.has_2fa,
      normalized.notes,
      normalized.history_visibility,
      normalized.cycle_type,
      normalized.cycle_day,
      normalized.current_balance,
      normalized.minimum_payment,
      normalized.snowball_order,
      normalized.snowball_include,
      normalized.snowball_exempt,
      normalized.is_subscription,
      normalized.subscription_type,
      normalized.reminder_days_before,
      normalized.subscription_source,
      normalized.subscription_detected_at,
    );

  return db.prepare('SELECT * FROM bills WHERE id = ?').get(result.lastInsertRowid);
}

function auditBillsForUser(db: Db, userId: number, includeInactive = false) {
  const bills = db
    .prepare(
      `
    SELECT b.id, b.name, b.category_id, b.due_day, b.active, b.autopay_enabled,
           b.website, b.username, b.account_info, b.current_balance,
           b.minimum_payment, b.interest_rate, 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.user_id = ?
      AND b.deleted_at IS NULL
      ${includeInactive ? '' : 'AND b.active = 1'}
    ORDER BY b.active DESC, b.due_day ASC, b.name ASC
  `,
    )
    .all(userId);

  const auditedBills = bills.map((bill: Row) => {
    const issues: any[] = [];
    const dueDay = Number(bill.due_day);
    const debt = isDebtBill(bill);
    const balance = Number(bill.current_balance);

    if (!Number.isInteger(dueDay) || dueDay < 1 || dueDay > 31) {
      issues.push(
        billAuditIssue(
          bill,
          'due_day',
          'error',
          'Add a due day between 1 and 31 so tracker periods and reminders can place this bill correctly.',
        ),
      );
    }
    if (!bill.category_id || !bill.category_name) {
      issues.push(
        billAuditIssue(
          bill,
          'category_id',
          'warning',
          'Choose a category so summaries, filters, and snowball debt detection stay accurate.',
        ),
      );
    }
    if (debt && !(Number(bill.minimum_payment) > 0)) {
      issues.push(
        billAuditIssue(
          bill,
          'minimum_payment',
          'error',
          'Add the required minimum payment so debt snowball projections can calculate payoff order and dates.',
        ),
      );
    }
    if (bill.autopay_enabled && !hasText(bill.website) && !hasText(bill.account_info)) {
      issues.push(
        billAuditIssue(
          bill,
          'autopay_enabled',
          'warning',
          'Add a website or account note so autopay bills still have enough reference information when something needs attention.',
        ),
      );
    }
    if (Number.isFinite(balance) && balance > 0 && bill.interest_rate == null) {
      issues.push(
        billAuditIssue(
          bill,
          'interest_rate',
          'warning',
          'Add the APR so snowball and amortization estimates include interest instead of assuming 0%.',
        ),
      );
    }

    return {
      id: bill.id,
      name: bill.name,
      active: !!bill.active,
      category_name: bill.category_name,
      due_day: bill.due_day,
      is_debt: debt,
      issues,
    };
  });

  const issues = auditedBills.flatMap((bill: Row) => bill.issues);
  return {
    bills: auditedBills.filter((bill: Row) => bill.issues.length > 0),
    summary: {
      audited_bills: bills.length,
      issue_count: issues.length,
      error_count: issues.filter((item: Row) => item.severity === 'error').length,
      warning_count: issues.filter((item: Row) => item.severity === 'warning').length,
    },
  };
}

// Helper function to get default cycle day based on cycle type
function getDefaultCycleDay(cycleType: any): string {
  switch (cycleType) {
    case 'monthly':
      return '1'; // 1st of the month
    case 'weekly':
      return 'monday'; // Monday
    case 'biweekly':
      return 'monday'; // Monday
    case 'quarterly':
      return '1'; // January/first quarter cycle
    case 'annual':
      return '1'; // January
    default:
      return '1';
  }
}

// Validate cycle_day based on cycle_type
function validateCycleDay(cycleType: any, cycleDay: any): FieldResult<string> {
  if (cycleDay === undefined || cycleDay === null) return { value: getDefaultCycleDay(cycleType) };
  const ct = cycleType || 'monthly';
  switch (ct) {
    case 'monthly': {
      const d = Number(cycleDay);
      if (!Number.isInteger(d) || d < 1 || d > 31)
        return { error: 'monthly cycle_day must be 1-31' };
      return { value: String(d) };
    }
    case 'weekly':
    case 'biweekly': {
      const days = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
      if (!days.includes(String(cycleDay).toLowerCase()))
        return { error: 'weekly/biweekly cycle_day must be a valid day name' };
      return { value: String(cycleDay).toLowerCase() };
    }
    case 'quarterly':
    case 'annual': {
      const month = Number(cycleDay);
      if (!Number.isInteger(month) || month < 1 || month > 12)
        return { error: 'quarterly/annual cycle_day must be a month number 1-12' };
      return { value: String(month) };
    }
    default:
      return { value: getDefaultCycleDay(ct) };
  }
}

function parseDueDay(value: any): FieldResult<number> {
  const day = Number(value);
  if (!Number.isInteger(day) || day < 1 || day > 31) {
    return { error: 'due_day must be an integer between 1 and 31' };
  }
  return { value: day };
}

function parseInterestRate(value: any): FieldResult<number | null> {
  if (value === undefined) return { value: undefined };
  if (value === null) return { value: null };
  if (typeof value === 'string' && value.trim() === '') return { value: null };

  const rate = Number(value);
  if (!Number.isFinite(rate) || rate < 0 || rate > 100) {
    return { error: 'interest_rate must be a number between 0 and 100, or null' };
  }
  return { value: rate };
}

function getValidCycleTypes(): string[] {
  return ['monthly', 'weekly', 'biweekly', 'quarterly', 'annual'];
}

function cycleTypeFromBillingCycle(billingCycle: any): string {
  const value = String(billingCycle || '').toLowerCase();
  if (value === 'quarterly') return 'quarterly';
  if (value === 'annually' || value === 'annual') return 'annual';
  return 'monthly';
}

function billingCycleForCycleType(cycleType: any): string {
  const value = String(cycleType || '').toLowerCase();
  if (value === 'quarterly') return 'quarterly';
  if (value === 'annual') return 'annually';
  if (value === 'weekly' || value === 'biweekly') return 'irregular';
  return 'monthly';
}

/**
 * Validates and normalizes bill data for creation/update.
 */
function validateBillData(data: Row, existingBill: Row | null = null) {
  const errors: any[] = [];
  const normalized: Row = {};

  const validCycleTypes = getValidCycleTypes();

  // name is required; fall back to existing value on partial updates
  const nameVal = data.name !== undefined ? data.name : existingBill?.name;
  if (!nameVal) {
    errors.push({ field: 'name', message: 'name is required' });
  }
  normalized.name = nameVal || null;

  // due_day is required; fall back to existing value on partial updates
  if (data.due_day === undefined || data.due_day === null) {
    if (existingBill?.due_day != null) {
      normalized.due_day = existingBill.due_day;
    } else {
      errors.push({ field: 'due_day', message: 'due_day is required' });
    }
  } else {
    const dueResult = parseDueDay(data.due_day);
    if (dueResult.error) {
      errors.push({ field: 'due_day', message: dueResult.error });
    } else {
      normalized.due_day = dueResult.value;
    }
  }

  // category_id validation
  normalized.category_id =
    data.category_id !== undefined ? data.category_id || null : existingBill?.category_id || null;

  // override_due_date
  normalized.override_due_date =
    data.override_due_date !== undefined
      ? data.override_due_date || null
      : existingBill?.override_due_date || null;

  // expected_amount (stored as integer cents). Validate: reject non-numeric,
  // negative, or absurd values (kept well under Number.MAX_SAFE_INTEGER cents).
  const MAX_EXPECTED_CENTS = 100_000_000_00; // $100,000,000
  if (data.expected_amount === undefined) {
    normalized.expected_amount = existingBill?.expected_amount || 0;
  } else if (data.expected_amount === null || data.expected_amount === '') {
    normalized.expected_amount = 0;
  } else {
    const cents = toCents(data.expected_amount);
    if (!Number.isInteger(cents) || cents! < 0 || cents! > MAX_EXPECTED_CENTS) {
      errors.push({
        field: 'expected_amount',
        message: 'expected_amount must be a number between 0 and 100000000',
      });
    } else {
      normalized.expected_amount = cents;
    }
  }

  // interest_rate
  if (data.interest_rate !== undefined) {
    const parsedInterest = parseInterestRate(data.interest_rate);
    if (parsedInterest.error) {
      errors.push({ field: 'interest_rate', message: parsedInterest.error });
    } else {
      normalized.interest_rate = parsedInterest.value ?? null;
    }
  } else {
    normalized.interest_rate = existingBill?.interest_rate ?? null;
  }

  // autopay_enabled
  normalized.autopay_enabled =
    data.autopay_enabled !== undefined
      ? data.autopay_enabled
        ? 1
        : 0
      : existingBill?.autopay_enabled || 0;

  // autodraft_status
  normalized.autodraft_status =
    data.autodraft_status !== undefined
      ? data.autodraft_status || 'none'
      : existingBill?.autodraft_status || 'none';

  // auto_mark_paid
  normalized.auto_mark_paid =
    data.auto_mark_paid !== undefined
      ? data.auto_mark_paid
        ? 1
        : 0
      : existingBill?.auto_mark_paid || 0;

  // website
  normalized.website =
    data.website !== undefined ? data.website || null : existingBill?.website || null;

  // username
  normalized.username =
    data.username !== undefined ? data.username || null : existingBill?.username || null;

  // account_info
  normalized.account_info =
    data.account_info !== undefined
      ? data.account_info || null
      : existingBill?.account_info || null;

  // has_2fa
  normalized.has_2fa =
    data.has_2fa !== undefined ? (data.has_2fa ? 1 : 0) : existingBill?.has_2fa || 0;

  // notes
  normalized.notes = data.notes !== undefined ? data.notes || null : existingBill?.notes || null;

  // active
  normalized.active = data.active !== undefined ? (data.active ? 1 : 0) : existingBill?.active || 1;

  // history_visibility
  const nextVisibility =
    data.history_visibility !== undefined
      ? data.history_visibility
      : existingBill?.history_visibility || 'default';
  if (!VALID_VISIBILITY.includes(nextVisibility)) {
    errors.push({
      field: 'history_visibility',
      message: `history_visibility must be one of: ${VALID_VISIBILITY.join(', ')}`,
    });
  }
  normalized.history_visibility = nextVisibility;

  // cycle_type is canonical. billing_cycle is derived for legacy display/import/export compatibility.
  const submittedCycleType = data.cycle_type !== undefined ? data.cycle_type : undefined;
  const fallbackCycleType =
    existingBill?.cycle_type ||
    cycleTypeFromBillingCycle(data.billing_cycle ?? existingBill?.billing_cycle);
  let nextCycleType = submittedCycleType ?? fallbackCycleType ?? 'monthly';
  let nextCycleDay = existingBill?.cycle_day || getDefaultCycleDay(nextCycleType);

  if (submittedCycleType !== undefined) {
    if (!validCycleTypes.includes(submittedCycleType)) {
      errors.push({
        field: 'cycle_type',
        message: `cycle_type must be one of: ${validCycleTypes.join(', ')}`,
      });
    } else {
      nextCycleType = submittedCycleType;
    }
  }

  const cycleDayInput = data.cycle_day !== undefined ? data.cycle_day : nextCycleDay;
  let cycleDayResult = validateCycleDay(nextCycleType, cycleDayInput);
  if (
    cycleDayResult.error &&
    data.cycle_day === undefined &&
    ['quarterly', 'annual'].includes(nextCycleType)
  ) {
    cycleDayResult = validateCycleDay(nextCycleType, getDefaultCycleDay(nextCycleType));
  }
  if (cycleDayResult.error) {
    errors.push({ field: 'cycle_day', message: cycleDayResult.error });
  } else {
    nextCycleDay = cycleDayResult.value;
  }
  normalized.cycle_type = nextCycleType;
  normalized.cycle_day = nextCycleDay;
  normalized.billing_cycle = billingCycleForCycleType(nextCycleType);

  // Calculate bucket based on due_day
  normalized.bucket = normalized.due_day <= 14 ? '1st' : '15th';

  // current_balance — outstanding debt balance, stored as integer cents (nullable)
  if (data.current_balance !== undefined) {
    if (data.current_balance === null || data.current_balance === '') {
      normalized.current_balance = null;
    } else {
      const cb = toCents(data.current_balance);
      if (!Number.isInteger(cb) || cb! < 0) {
        errors.push({
          field: 'current_balance',
          message: 'current_balance must be a non-negative number',
        });
      } else {
        normalized.current_balance = cb;
      }
    }
  } else {
    normalized.current_balance = existingBill?.current_balance ?? null;
  }

  // minimum_payment — required minimum payment for debt, stored as integer cents (nullable)
  if (data.minimum_payment !== undefined) {
    if (data.minimum_payment === null || data.minimum_payment === '') {
      normalized.minimum_payment = null;
    } else {
      const mp = toCents(data.minimum_payment);
      if (!Number.isInteger(mp) || mp! < 0) {
        errors.push({
          field: 'minimum_payment',
          message: 'minimum_payment must be a non-negative number',
        });
      } else {
        normalized.minimum_payment = mp;
      }
    }
  } else {
    normalized.minimum_payment = existingBill?.minimum_payment ?? null;
  }

  // snowball_order — drag position on snowball page (nullable integer)
  if (data.snowball_order !== undefined) {
    if (data.snowball_order === null || data.snowball_order === '') {
      normalized.snowball_order = null;
    } else {
      const so = parseInt(data.snowball_order, 10);
      if (!Number.isInteger(so) || so < 0) {
        errors.push({
          field: 'snowball_order',
          message: 'snowball_order must be a non-negative integer',
        });
      } else {
        normalized.snowball_order = so;
      }
    }
  } else {
    normalized.snowball_order = existingBill?.snowball_order ?? null;
  }

  // snowball_include — manual override to force bill onto snowball page
  normalized.snowball_include =
    data.snowball_include !== undefined
      ? data.snowball_include
        ? 1
        : 0
      : (existingBill?.snowball_include ?? 0);

  // snowball_exempt — manual override to hide an auto-detected debt-like bill
  normalized.snowball_exempt =
    data.snowball_exempt !== undefined
      ? data.snowball_exempt
        ? 1
        : 0
      : (existingBill?.snowball_exempt ?? 0);

  normalized.is_subscription =
    data.is_subscription !== undefined
      ? data.is_subscription
        ? 1
        : 0
      : (existingBill?.is_subscription ?? 0);

  normalized.subscription_type =
    data.subscription_type !== undefined
      ? data.subscription_type
        ? String(data.subscription_type).trim().slice(0, 64)
        : null
      : (existingBill?.subscription_type ?? null);

  if (data.reminder_days_before !== undefined) {
    const days = Number(data.reminder_days_before);
    if (!Number.isInteger(days) || days < 0 || days > 30) {
      errors.push({
        field: 'reminder_days_before',
        message: 'reminder_days_before must be between 0 and 30',
      });
    } else {
      normalized.reminder_days_before = days;
    }
  } else {
    normalized.reminder_days_before = existingBill?.reminder_days_before ?? 3;
  }

  normalized.subscription_source =
    data.subscription_source !== undefined
      ? data.subscription_source
        ? String(data.subscription_source).trim().slice(0, 32)
        : 'manual'
      : existingBill?.subscription_source || 'manual';

  normalized.subscription_detected_at =
    data.subscription_detected_at !== undefined
      ? data.subscription_detected_at || null
      : (existingBill?.subscription_detected_at ?? null);

  return {
    errors,
    normalized: {
      ...normalized,
      name: normalized.name || null,
      due_day: normalized.due_day || null,
    },
  };
}

/**
 * Validates cycle_day for a given cycle_type without requiring the full bill data.
 */
function validateCycleDayOnly(cycleType: any, cycleDay: any) {
  return validateCycleDay(cycleType, cycleDay);
}

// Computes how a payment affects a debt bill's current_balance.
function computeBalanceDelta(bill: Row, paymentAmount: any) {
  const bal = Number(bill.current_balance); // cents
  const rate = Number(bill.interest_rate) || 0; // percent
  const amt = Number(paymentAmount); // cents

  if (!Number.isFinite(bal) || bal <= 0) return null;
  if (!Number.isFinite(amt) || amt <= 0) return null;

  const currentMonth = monthKey(); // "YYYY-MM" (local time)
  const applyInterest = rate > 0 && bill.interest_accrued_month !== currentMonth;
  const interestDelta = applyInterest ? Math.round((bal * rate) / 100 / 12) : 0; // cents

  const raw = bal + interestDelta - amt; // cents, exact integer arithmetic
  const newBalance = Math.max(0, raw);
  const delta = newBalance - bal;

  return {
    new_balance: newBalance,
    balance_delta: delta,
    interest_delta: applyInterest ? interestDelta : null,
    interest_accrued_month: applyInterest ? currentMonth : null,
  };
}

// Updates current_balance (and interest_accrued_month when interest was charged) after a payment.
function applyBalanceDelta(db: Db, billId: any, balCalc: any): void {
  if (!balCalc) return;
  db.prepare(
    `
    UPDATE bills
    SET current_balance        = ?,
        interest_accrued_month = COALESCE(?, interest_accrued_month),
        updated_at             = datetime('now')
    WHERE id = ?
  `,
  ).run(balCalc.new_balance, balCalc.interest_accrued_month, billId);
}

module.exports = {
  VALID_VISIBILITY,
  TEMPLATE_FIELDS,
  auditBillsForUser,
  billingCycleForCycleType,
  categoryBelongsToUser,
  cycleTypeFromBillingCycle,
  getValidCycleTypes,
  getDefaultCycleDay,
  insertBill,
  serializeBill,
  parseTemplateData,
  validateCycleDay,
  parseDueDay,
  parseInterestRate,
  sanitizeTemplateData,
  validateBillData,
  validateCycleDayOnly,
  computeBalanceDelta,
  applyBalanceDelta,
};
