// @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 router = express.Router();
const { getDb } = require('../db/database.cts');
const { standardizeError } = require('../middleware/errorFormatter.cts');
const { calculateSnowball, calculateAvalanche } = require('../services/snowballService.cts');
const { calculateMinimumOnly, debtAprSnapshot } = require('../services/aprService.cts');
const { serializeBill } = require('../services/billsService.cts');
const { toCents, fromCents } = require('../utils/money.mts');

const DEBT_LIKE_CLAUSES = `(
  b.snowball_include = 1
  OR (
    COALESCE(b.snowball_exempt, 0) = 0
    AND (
      LOWER(c.name) LIKE '%credit%'
      OR LOWER(c.name) LIKE '%loan%'
      OR LOWER(c.name) LIKE '%debt%'
      OR LOWER(c.name) LIKE '%mortgage%'
      OR LOWER(c.name) LIKE '%housing%'
    )
  )
)`;

function isRamseyMode(userId) {
  const db = getDb();
  const row = db
    .prepare(
      `
    SELECT value
    FROM user_settings
    WHERE user_id = ? AND key = 'snowball_ramsey_mode'
  `,
    )
    .get(userId);
  return row ? row.value !== 'false' && row.value !== '0' : true;
}

function getUserBoolSetting(userId, key, fallback = false) {
  const db = getDb();
  const row = db
    .prepare(
      `
    SELECT value
    FROM user_settings
    WHERE user_id = ? AND key = ?
  `,
    )
    .get(userId, key);
  if (!row) return fallback;
  return row.value === 'true' || row.value === '1';
}

function upsertUserSetting(db, userId, key, value) {
  db.prepare(
    `
    INSERT INTO user_settings (user_id, key, value, updated_at)
    VALUES (?, ?, ?, datetime('now'))
    ON CONFLICT(user_id, key) DO UPDATE SET
      value = excluded.value,
      updated_at = datetime('now')
  `,
  ).run(userId, key, String(value));
}

function getDebtQuery(ramseyMode) {
  const orderBy = ramseyMode
    ? `
    CASE WHEN b.current_balance IS NULL THEN 1 ELSE 0 END ASC,
    b.current_balance ASC,
    LOWER(b.name) ASC,
    b.id ASC`
    : `
    CASE WHEN b.snowball_order IS NULL THEN 1 ELSE 0 END ASC,
    b.snowball_order ASC,
    CASE WHEN b.current_balance IS NULL THEN 1 ELSE 0 END ASC,
    b.current_balance ASC`;

  return `
  SELECT b.*, c.name AS category_name
  FROM bills b
  LEFT JOIN categories c ON b.category_id = c.id AND c.user_id = b.user_id AND c.deleted_at IS NULL
  WHERE b.user_id = ?
    AND b.active = 1
    AND b.deleted_at IS NULL
    AND ${DEBT_LIKE_CLAUSES}
  ORDER BY${orderBy}
  `;
}

function getDebtBills(userId, ramseyMode) {
  const db = getDb();
  const mode = ramseyMode !== undefined ? ramseyMode : isRamseyMode(userId);
  return db.prepare(getDebtQuery(mode)).all(userId);
}

// GET /api/snowball — server-filtered debt bills, pre-sorted by snowball_order
router.get('/', (req: Req, res: Res) => {
  const ramseyMode = isRamseyMode(req.user.id);
  res.json(getDebtBills(req.user.id, ramseyMode).map(serializeBill));
});

// GET /api/snowball/settings — extra monthly payment for this user
router.get('/settings', (req: Req, res: Res) => {
  const db = getDb();
  const user = db.prepare('SELECT snowball_extra_payment FROM users WHERE id = ?').get(req.user.id);
  res.json({
    extra_payment: fromCents(user?.snowball_extra_payment ?? 0),
    ramsey_mode: isRamseyMode(req.user.id),
    ready_current_on_bills: getUserBoolSetting(req.user.id, 'snowball_ready_current_on_bills'),
    ready_emergency_fund: getUserBoolSetting(req.user.id, 'snowball_ready_emergency_fund'),
  });
});

// PATCH /api/snowball/settings — save extra monthly payment
router.patch('/settings', (req: Req, res: Res) => {
  const { extra_payment, ramsey_mode, ready_current_on_bills, ready_emergency_fund } = req.body;
  let val = 0;

  if (extra_payment !== undefined && extra_payment !== null && extra_payment !== '') {
    val = parseFloat(extra_payment);
    if (!Number.isFinite(val) || val < 0) {
      return res
        .status(400)
        .json(
          standardizeError(
            'extra_payment must be a non-negative number',
            'VALIDATION_ERROR',
            'extra_payment',
          ),
        );
    }
  }

  const db = getDb();
  const save = db.transaction(() => {
    if (extra_payment !== undefined) {
      db.prepare('UPDATE users SET snowball_extra_payment = ? WHERE id = ?').run(
        toCents(val),
        req.user.id,
      );
    }

    if (ramsey_mode !== undefined) {
      upsertUserSetting(db, req.user.id, 'snowball_ramsey_mode', ramsey_mode ? 'true' : 'false');
    }

    if (ready_current_on_bills !== undefined) {
      upsertUserSetting(
        db,
        req.user.id,
        'snowball_ready_current_on_bills',
        ready_current_on_bills ? 'true' : 'false',
      );
    }

    if (ready_emergency_fund !== undefined) {
      upsertUserSetting(
        db,
        req.user.id,
        'snowball_ready_emergency_fund',
        ready_emergency_fund ? 'true' : 'false',
      );
    }
  });
  save();

  const user = db.prepare('SELECT snowball_extra_payment FROM users WHERE id = ?').get(req.user.id);
  res.json({
    extra_payment: fromCents(user?.snowball_extra_payment ?? 0),
    // Use body value when ramsey_mode was just saved; fall back to DB read if not in request
    ramsey_mode: ramsey_mode !== undefined ? !!ramsey_mode : isRamseyMode(req.user.id),
    ready_current_on_bills: getUserBoolSetting(req.user.id, 'snowball_ready_current_on_bills'),
    ready_emergency_fund: getUserBoolSetting(req.user.id, 'snowball_ready_emergency_fund'),
  });
});

// GET /api/snowball/projection — snowball, avalanche, minimum-only projections
// Each debt result is enriched with current APR metrics (monthly interest, etc.)
router.get('/projection', (req: Req, res: Res) => {
  const db = getDb();

  const ramseyMode = isRamseyMode(req.user.id);
  const bills = getDebtBills(req.user.id, ramseyMode);
  const user = db.prepare('SELECT snowball_extra_payment FROM users WHERE id = ?').get(req.user.id);

  // Money fields on `bills` are stored as integer cents; the snowball/APR math
  // and the API response are dollar-denominated, so convert before computing.
  const billsForMath = bills.map((b) => ({
    ...b,
    current_balance: fromCents(b.current_balance),
    minimum_payment: fromCents(b.minimum_payment),
  }));

  // Allow an optional ?extra=N override so the client can preview an unsaved
  // extra payment without a round-trip save. Falls back to the stored value.
  const queryExtra = req.query.extra !== undefined ? parseFloat(req.query.extra) : NaN;
  const extra =
    Number.isFinite(queryExtra) && queryExtra >= 0
      ? queryExtra
      : fromCents(user?.snowball_extra_payment ?? 0);

  // Build a lookup of APR snapshots keyed by bill id (computed once from current balances)
  const aprByBill = {};
  for (const b of billsForMath) {
    const snap = debtAprSnapshot(b);
    if (snap) aprByBill[b.id] = snap;
  }

  // Enrich each debt result with its APR snapshot
  function enrich(projection) {
    return {
      ...projection,
      debts: projection.debts.map((d) => ({
        ...d,
        apr_snapshot: aprByBill[d.id] ?? null,
      })),
    };
  }

  const now = new Date();
  const snowball = enrich(calculateSnowball(billsForMath, extra, now));
  const avalanche = enrich(calculateAvalanche(billsForMath, extra, now));
  const minimum_only = enrich(calculateMinimumOnly(billsForMath, now));

  // Comparison: what does the snowball save vs just paying minimums?
  const comparison = buildComparison(snowball, minimum_only);

  res.json({ snowball, avalanche, minimum_only, comparison });
});

// Build a summary comparing snowball to the minimum-only baseline
function buildComparison(snowball, minimum_only) {
  const sbMonths = snowball.months_to_freedom;
  const moMonths = minimum_only.months_to_freedom;
  const sbInterest = snowball.total_interest_paid;
  const moInterest = minimum_only.total_interest_paid;

  if (!sbMonths || !moMonths) return null;

  const months_saved = moMonths - sbMonths;
  const interest_saved = Math.round((moInterest - sbInterest) * 100) / 100;
  const years_saved = +(months_saved / 12).toFixed(1);

  return {
    months_saved,
    years_saved,
    interest_saved,
    minimum_only_months: moMonths,
    minimum_only_interest: moInterest,
    minimum_only_payoff: minimum_only.payoff_display,
    snowball_months: sbMonths,
    snowball_interest: sbInterest,
    snowball_payoff: snowball.payoff_display,
  };
}

// PATCH /api/snowball/order — batch-save snowball_order positions
router.patch('/order', (req: Req, res: Res) => {
  const items = req.body;
  if (!Array.isArray(items)) {
    return res
      .status(400)
      .json(standardizeError('Request body must be an array', 'VALIDATION_ERROR'));
  }
  if (items.length === 0) {
    return res.json({ success: true, updated: 0 });
  }

  // Validate every row before touching the DB — no silent skips
  const parsed = [];
  for (let i = 0; i < items.length; i++) {
    const row = items[i];
    const id = parseInt(row?.id, 10);
    const order = parseInt(row?.snowball_order, 10);
    if (!Number.isInteger(id) || id <= 0) {
      return res
        .status(400)
        .json(
          standardizeError(
            `Item at index ${i} has an invalid id: ${JSON.stringify(row?.id)}`,
            'VALIDATION_ERROR',
          ),
        );
    }
    if (!Number.isInteger(order) || order < 0) {
      return res
        .status(400)
        .json(
          standardizeError(
            `Item at index ${i} has an invalid snowball_order: ${JSON.stringify(row?.snowball_order)}`,
            'VALIDATION_ERROR',
          ),
        );
    }
    parsed.push({ id, order });
  }

  const db = getDb();
  const userId = req.user.id;
  const update = db.prepare(
    'UPDATE bills SET snowball_order = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
  );

  db.transaction(() => {
    for (const { id, order } of parsed) {
      update.run(order, id, userId);
    }
  })();

  res.json({ success: true, updated: parsed.length });
});

// ── Snowball Plan helpers ─────────────────────────────────────────────────────

function enrichPlanWithProgress(db, plan) {
  let snapshot;
  try {
    snapshot = JSON.parse(plan.plan_snapshot);
  } catch {
    snapshot = null;
  }

  // Fetch every snapshot bill in one query (user-scoped), not one-per-debt.
  const snapDebts = snapshot?.debts ?? [];
  const billIds = [...new Set(snapDebts.map((d) => d.bill_id).filter(Boolean))];
  const billsById = {};
  if (billIds.length) {
    const placeholders = billIds.map(() => '?').join(',');
    for (const b of db
      .prepare(
        `SELECT id, current_balance, name, deleted_at FROM bills WHERE id IN (${placeholders}) AND user_id = ?`,
      )
      .all(...billIds, plan.user_id)) {
      billsById[b.id] = b;
    }
  }

  const currentDebts = snapDebts.map((d) => {
    const bill = billsById[d.bill_id];
    const currentBalance = bill && !bill.deleted_at ? fromCents(bill.current_balance) : null;
    const startingBalance = d.starting_balance ?? 0;
    const progressPct =
      startingBalance > 0 && currentBalance !== null
        ? Math.min(
            100,
            Math.max(0, Math.round(((startingBalance - currentBalance) / startingBalance) * 100)),
          )
        : null;
    return {
      bill_id: d.bill_id,
      name: d.name,
      current_balance: currentBalance,
      starting_balance: startingBalance,
      progress_pct: progressPct,
      deleted: !!bill?.deleted_at,
    };
  });

  const startedMs = plan.started_at ? new Date(plan.started_at).getTime() : Date.now();
  const monthsElapsed = Math.floor((Date.now() - startedMs) / (1000 * 60 * 60 * 24 * 30));

  return {
    ...plan,
    extra_payment: fromCents(plan.extra_payment),
    plan_snapshot: snapshot,
    months_elapsed: monthsElapsed,
    current_debts: currentDebts,
  };
}

// POST /api/snowball/plans — start a new snowball plan
router.post('/plans', (req: Req, res: Res) => {
  try {
    const db = getDb();
    const userId = req.user.id;
    const { name, method, notes } = req.body;

    const planName =
      typeof name === 'string' && name.trim() ? name.trim().slice(0, 100) : 'Snowball Plan';
    const planMethod = ['snowball', 'avalanche', 'custom'].includes(method) ? method : 'snowball';

    const ramseyMode = isRamseyMode(userId);
    const debts = getDebtBills(userId, ramseyMode);
    const activeDebts = debts.filter((b) => (b.current_balance ?? 0) > 0);
    if (activeDebts.length === 0) {
      return res
        .status(400)
        .json(
          standardizeError(
            'No debts with a balance found. Add a balance to at least one bill.',
            'NO_DEBTS',
          ),
        );
    }

    // Money fields on `debts` are stored as integer cents; the snowball/APR
    // math and plan_snapshot are dollar-denominated, so convert before computing.
    const debtsForMath = debts.map((b) => ({
      ...b,
      current_balance: fromCents(b.current_balance),
      minimum_payment: fromCents(b.minimum_payment),
    }));

    const user = db.prepare('SELECT snowball_extra_payment FROM users WHERE id = ?').get(userId);
    const extraCents = user?.snowball_extra_payment ?? 0;
    const extra = fromCents(extraCents);
    const now = new Date();

    const snowball =
      planMethod === 'avalanche'
        ? calculateAvalanche(debtsForMath, extra, now)
        : calculateSnowball(debtsForMath, extra, now);
    const minOnly = calculateMinimumOnly(debtsForMath, now);
    const interestSaved = Math.max(
      0,
      Math.round(((minOnly.total_interest_paid ?? 0) - (snowball.total_interest_paid ?? 0)) * 100) /
        100,
    );

    const debtSnaps = debtsForMath.map((b, i) => {
      const proj = snowball.debts?.find((d) => d.id === b.id);
      return {
        bill_id: b.id,
        name: b.name,
        starting_balance: b.current_balance ?? 0,
        minimum_payment: b.minimum_payment ?? 0,
        interest_rate: b.interest_rate ?? 0,
        projected_payoff_month: proj?.payoff_month ?? null,
        projected_payoff_date: proj?.payoff_date ?? null,
        projected_total_interest: proj?.total_interest ?? null,
        order: i,
      };
    });

    const planSnapshot = JSON.stringify({
      projected_payoff_date: snowball.payoff_date ?? null,
      projected_months: snowball.months_to_freedom ?? null,
      projected_total_interest: snowball.total_interest_paid ?? null,
      minimum_only_months: minOnly.months_to_freedom ?? null,
      interest_saved: interestSaved,
      debts: debtSnaps,
    });

    // Abandon any existing active/paused plan first
    db.prepare(
      `
      UPDATE snowball_plans SET status = 'abandoned', updated_at = datetime('now')
      WHERE user_id = ? AND status IN ('active', 'paused')
    `,
    ).run(userId);

    const result = db
      .prepare(
        `
      INSERT INTO snowball_plans (user_id, name, method, status, extra_payment, plan_snapshot, notes, started_at, created_at, updated_at)
      VALUES (?, ?, ?, 'active', ?, ?, ?, datetime('now'), datetime('now'), datetime('now'))
    `,
      )
      .run(userId, planName, planMethod, extraCents, planSnapshot, notes || null);

    const plan = db
      .prepare('SELECT * FROM snowball_plans WHERE id = ?')
      .get(result.lastInsertRowid);
    res.status(201).json(enrichPlanWithProgress(db, plan));
  } catch (err) {
    console.error('[snowball plans] POST error:', err.message);
    res.status(500).json(standardizeError('Failed to start plan', 'PLAN_CREATE_ERROR'));
  }
});

// GET /api/snowball/plans — list all plans for user
router.get('/plans', (req: Req, res: Res) => {
  try {
    const db = getDb();
    const plans = db
      .prepare(
        `
      SELECT * FROM snowball_plans WHERE user_id = ? ORDER BY created_at DESC
    `,
      )
      .all(req.user.id);
    res.json({ plans: plans.map((p) => enrichPlanWithProgress(db, p)) });
  } catch (err) {
    console.error('[snowball plans] GET /plans error:', err.message);
    res.status(500).json(standardizeError('Failed to load plans', 'PLAN_LIST_ERROR'));
  }
});

// GET /api/snowball/plans/active — return the active or paused plan (or null)
router.get('/plans/active', (req: Req, res: Res) => {
  try {
    const db = getDb();
    const plan = db
      .prepare(
        `
      SELECT * FROM snowball_plans
      WHERE user_id = ? AND status IN ('active', 'paused')
      ORDER BY created_at DESC LIMIT 1
    `,
      )
      .get(req.user.id);
    res.json(plan ? enrichPlanWithProgress(db, plan) : null);
  } catch (err) {
    console.error('[snowball plans] GET /plans/active error:', err.message);
    res.status(500).json(standardizeError('Failed to load active plan', 'PLAN_ACTIVE_ERROR'));
  }
});

// PATCH /api/snowball/plans/:id — update name or notes
router.patch('/plans/:id', (req: Req, res: Res) => {
  try {
    const db = getDb();
    const id = parseInt(req.params.id, 10);
    if (!Number.isInteger(id) || id <= 0)
      return res.status(400).json(standardizeError('Invalid plan id', 'VALIDATION_ERROR', 'id'));
    const plan = db
      .prepare('SELECT * FROM snowball_plans WHERE id = ? AND user_id = ?')
      .get(id, req.user.id);
    if (!plan) return res.status(404).json(standardizeError('Plan not found', 'NOT_FOUND'));

    const { name, notes } = req.body;
    const newName = typeof name === 'string' && name.trim() ? name.trim().slice(0, 100) : plan.name;
    const newNotes = notes !== undefined ? notes || null : plan.notes;

    db.prepare(
      `
      UPDATE snowball_plans SET name = ?, notes = ?, updated_at = datetime('now') WHERE id = ?
    `,
    ).run(newName, newNotes, id);

    const updated = db.prepare('SELECT * FROM snowball_plans WHERE id = ?').get(id);
    res.json(enrichPlanWithProgress(db, updated));
  } catch (err) {
    console.error('[snowball plans] PATCH error:', err.message);
    res.status(500).json(standardizeError('Failed to update plan', 'PLAN_UPDATE_ERROR'));
  }
});

// Shared status-transition handler for pause / resume / complete / abandon.
// `setSql` is a fixed constant per route (never user input).
function transitionPlan(req, res, { allowedFrom, setSql, action, past }) {
  try {
    const db = getDb();
    const id = parseInt(req.params.id, 10);
    if (!Number.isInteger(id) || id <= 0) {
      return res.status(400).json(standardizeError('Invalid plan id', 'VALIDATION_ERROR', 'id'));
    }
    const plan = db
      .prepare('SELECT * FROM snowball_plans WHERE id = ? AND user_id = ?')
      .get(id, req.user.id);
    if (!plan) return res.status(404).json(standardizeError('Plan not found', 'NOT_FOUND'));
    if (!allowedFrom.includes(plan.status)) {
      return res
        .status(400)
        .json(
          standardizeError(
            `Only ${allowedFrom.join(' or ')} plans can be ${past}`,
            'INVALID_PLAN_STATE',
          ),
        );
    }
    db.prepare(
      `UPDATE snowball_plans SET ${setSql}, updated_at = datetime('now') WHERE id = ?`,
    ).run(id);
    const updated = db.prepare('SELECT * FROM snowball_plans WHERE id = ?').get(id);
    res.json(enrichPlanWithProgress(db, updated));
  } catch (err) {
    console.error(`[snowball plans] ${action} error:`, err.message);
    res.status(500).json(standardizeError(`Failed to ${action} plan`, 'PLAN_TRANSITION_ERROR'));
  }
}

// POST /api/snowball/plans/:id/{pause,resume,complete,abandon}
router.post('/plans/:id/pause', (req: Req, res: Res) =>
  transitionPlan(req, res, {
    allowedFrom: ['active'],
    setSql: "status = 'paused', paused_at = datetime('now')",
    action: 'pause',
    past: 'paused',
  }),
);
router.post('/plans/:id/resume', (req: Req, res: Res) =>
  transitionPlan(req, res, {
    allowedFrom: ['paused'],
    setSql: "status = 'active', paused_at = NULL",
    action: 'resume',
    past: 'resumed',
  }),
);
router.post('/plans/:id/complete', (req: Req, res: Res) =>
  transitionPlan(req, res, {
    allowedFrom: ['active', 'paused'],
    setSql: "status = 'completed', completed_at = datetime('now')",
    action: 'complete',
    past: 'completed',
  }),
);
router.post('/plans/:id/abandon', (req: Req, res: Res) =>
  transitionPlan(req, res, {
    allowedFrom: ['active', 'paused'],
    setSql: "status = 'abandoned'",
    action: 'abandon',
    past: 'abandoned',
  }),
);

module.exports = router;
