// @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, ensureUserDefaultCategories } = require('../db/database.cts');
const { standardizeError } = require('../middleware/errorFormatter.cts');
const { fromCents } = require('../utils/money.mts');
const {
  createSubscriptionFromRecommendation,
  declineRecommendation,
  decorateSubscription,
  getSubscriptionRecommendations,
  getSubscriptionSummary,
  getSubscriptions,
  monthlyEquivalent,
  recordSubscriptionFeedback,
  searchSubscriptionTransactions,
} = require('../services/subscriptionService.cts');
const { addMerchantRule, applyMerchantRules } = require('../services/billMerchantRuleService.cts');

router.get('/', (req: Req, res: Res) => {
  const db = getDb();
  ensureUserDefaultCategories(req.user.id);
  const subscriptions = getSubscriptions(db, req.user.id);
  res.json({
    summary: getSubscriptionSummary(subscriptions),
    subscriptions,
  });
});

router.get('/recommendations', (req: Req, res: Res) => {
  const db = getDb();
  res.json({
    recommendations: getSubscriptionRecommendations(db, req.user.id),
  });
});

router.get('/transaction-matches', (req: Req, res: Res) => {
  try {
    res.json({
      transactions: searchSubscriptionTransactions(getDb(), req.user.id, req.query),
    });
  } catch (err) {
    res
      .status(500)
      .json(
        standardizeError(
          err.message || 'Failed to search subscription transactions',
          'SUBSCRIPTION_SEARCH_ERROR',
        ),
      );
  }
});

router.post('/recommendations/decline', (req: Req, res: Res) => {
  const { decline_key } = req.body || {};
  if (!decline_key || typeof decline_key !== 'string' || decline_key.length > 200) {
    return res
      .status(400)
      .json(standardizeError('decline_key is required', 'VALIDATION_ERROR', 'decline_key'));
  }
  try {
    const db = getDb();
    declineRecommendation(db, req.user.id, decline_key);
    recordSubscriptionFeedback(db, req.user.id, {
      action: 'decline',
      catalog_id: req.body?.catalog_id,
      merchant: req.body?.merchant,
      confidence: req.body?.confidence,
      metadata: { decline_key },
    });
    res.json({ ok: true });
  } catch (err) {
    res
      .status(500)
      .json(standardizeError(err.message || 'Failed to decline recommendation', 'DECLINE_ERROR'));
  }
});

// POST /api/subscriptions/recommendations/match-bill
// Link an existing bill to all transactions in a recommendation (no new bill created).
router.post('/recommendations/match-bill', (req: Req, res: Res) => {
  const billId = parseInt(req.body?.bill_id, 10);
  const rawIds = Array.isArray(req.body?.transaction_ids) ? req.body.transaction_ids : [];
  const txIds = rawIds
    .map((id) => parseInt(id, 10))
    .filter((n) => Number.isInteger(n) && n > 0)
    .slice(0, 50);

  if (!Number.isInteger(billId) || billId < 1) {
    return res
      .status(400)
      .json(standardizeError('bill_id is required', 'VALIDATION_ERROR', 'bill_id'));
  }
  if (txIds.length === 0) {
    return res
      .status(400)
      .json(
        standardizeError(
          'transaction_ids must be a non-empty array',
          'VALIDATION_ERROR',
          'transaction_ids',
        ),
      );
  }

  const db = getDb();
  const bill = db
    .prepare(
      'SELECT id, name, catalog_id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
    )
    .get(billId, req.user.id);
  if (!bill)
    return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id'));
  const catalogId = parseInt(req.body?.catalog_id, 10);
  const catalogEntry =
    Number.isInteger(catalogId) && catalogId > 0
      ? db.prepare('SELECT id FROM subscription_catalog WHERE id = ?').get(catalogId)
      : null;

  const placeholders = txIds.map(() => '?').join(',');
  const txRows = db
    .prepare(
      `
    SELECT id, amount, posted_date, transacted_at
    FROM transactions
    WHERE user_id = ? AND id IN (${placeholders}) AND ignored = 0 AND match_status != 'matched'
  `,
    )
    .all(req.user.id, ...txIds);

  const updateTx = db.prepare(`
    UPDATE transactions
    SET matched_bill_id = ?, match_status = 'matched', updated_at = CURRENT_TIMESTAMP
    WHERE id = ? AND user_id = ? AND ignored = 0 AND match_status != 'matched'
  `);
  const insertPayment = db.prepare(`
    INSERT OR IGNORE INTO payments (bill_id, amount, paid_date, payment_source, transaction_id)
    VALUES (?, ?, ?, 'auto_match', ?)
  `);
  let matchedCount = 0;
  db.transaction(() => {
    for (const tx of txRows) {
      const paidDate =
        tx.posted_date || (tx.transacted_at ? String(tx.transacted_at).slice(0, 10) : null);
      const amount = Math.round(Math.abs(tx.amount)); // tx.amount and payments.amount are both cents
      matchedCount += updateTx.run(billId, tx.id, req.user.id).changes;
      if (paidDate) insertPayment.run(billId, amount, paidDate, tx.id);
    }
  })();

  // Store merchant rule for ongoing auto-matching on future syncs
  const merchant = typeof req.body?.merchant === 'string' ? req.body.merchant.trim() : '';
  if (merchant) addMerchantRule(db, req.user.id, billId, merchant);
  if (catalogEntry && !bill.catalog_id) {
    try {
      db.prepare(
        "UPDATE bills SET catalog_id = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
      ).run(catalogEntry.id, billId, req.user.id);
    } catch {
      /* pre-v0.96 */
    }
  }
  recordSubscriptionFeedback(db, req.user.id, {
    action: 'link_existing_bill',
    catalog_id: catalogEntry?.id,
    bill_id: billId,
    merchant,
    confidence: req.body?.confidence,
    metadata: { transaction_ids: txIds },
  });

  // Apply rules immediately to catch any unmatched transactions beyond the explicit list
  const { matched: autoMatched } = applyMerchantRules(db, req.user.id);

  res.json({ ok: true, matched_count: matchedCount + autoMatched, bill_name: bill.name });
});

router.post('/recommendations/create', (req: Req, res: Res) => {
  const db = getDb();
  ensureUserDefaultCategories(req.user.id);
  if (req.body?.category_id) {
    const categoryId = parseInt(req.body.category_id, 10);
    const category = Number.isInteger(categoryId)
      ? db
          .prepare('SELECT id FROM categories WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
          .get(categoryId, req.user.id)
      : null;
    if (!category) {
      return res
        .status(400)
        .json(
          standardizeError(
            'category_id is invalid for this user',
            'VALIDATION_ERROR',
            'category_id',
          ),
        );
    }
  }
  try {
    const created = createSubscriptionFromRecommendation(db, req.user.id, req.body || {});
    // Store merchant rule so future SimpleFIN transactions auto-match this bill
    if (req.body?.merchant) addMerchantRule(db, req.user.id, created.id, req.body.merchant);
    recordSubscriptionFeedback(db, req.user.id, {
      action: 'accept_track_new',
      catalog_id: req.body?.catalog_match?.id,
      bill_id: created.id,
      merchant: req.body?.merchant,
      confidence: req.body?.confidence,
      metadata: { transaction_ids: req.body?.transaction_ids || [] },
    });
    res.status(201).json(created);
  } catch (err) {
    res
      .status(err.status || 400)
      .json(
        standardizeError(
          err.message || 'Could not create subscription',
          err.status ? 'VALIDATION_ERROR' : 'SUBSCRIPTION_CREATE_ERROR',
          err.field || null,
        ),
      );
  }
});

// ── Catalog browser ───────────────────────────────────────────────────────────

router.get('/catalog', (req: Req, res: Res) => {
  const db = getDb();
  try {
    const catalogEntries = db
      .prepare(
        `
      SELECT id, rank, name, category, subcategory, subscription_type,
             website, starting_monthly_usd, starting_annual_usd
      FROM subscription_catalog
      ORDER BY rank ASC
    `,
      )
      .all();

    if (!catalogEntries.length) return res.json({ catalog: [] });

    // User's subscription bills that are linked to a catalog entry
    const matchedBills = db
      .prepare(
        `
      SELECT b.id, b.name, b.expected_amount, b.active, b.catalog_id,
             b.cycle_type, b.billing_cycle
      FROM bills b
      WHERE b.user_id = ?
        AND b.is_subscription = 1
        AND b.deleted_at IS NULL
        AND b.catalog_id IS NOT NULL
    `,
      )
      .all(req.user.id);

    const billByCatalogId = new Map(matchedBills.map((b) => [b.catalog_id, b]));

    // User's custom descriptors
    let userDescriptors = [];
    try {
      userDescriptors = db
        .prepare(
          'SELECT id, catalog_id, descriptor FROM user_catalog_descriptors WHERE user_id = ?',
        )
        .all(req.user.id);
    } catch {
      /* pre-v0.96 */
    }

    const userDescsByCatalogId = new Map();
    for (const d of userDescriptors) {
      if (!userDescsByCatalogId.has(d.catalog_id)) userDescsByCatalogId.set(d.catalog_id, []);
      userDescsByCatalogId.get(d.catalog_id).push({ id: d.id, descriptor: d.descriptor });
    }

    const catalog = catalogEntries.map((entry) => {
      const bill = billByCatalogId.get(entry.id) ?? null;
      return {
        ...entry,
        matched_bill: bill
          ? {
              id: bill.id,
              name: bill.name,
              expected_amount: fromCents(bill.expected_amount),
              active: !!bill.active,
              monthly_equivalent: monthlyEquivalent(
                fromCents(bill.expected_amount),
                bill.cycle_type,
                bill.billing_cycle,
              ),
            }
          : null,
        user_descriptors: userDescsByCatalogId.get(entry.id) ?? [],
      };
    });

    res.json({ catalog });
  } catch (err) {
    res
      .status(500)
      .json(standardizeError(err.message || 'Failed to load catalog', 'CATALOG_ERROR'));
  }
});

// Update which catalog entry a bill is linked to (or unlink with null)
router.put('/:id/catalog-link', (req: Req, res: Res) => {
  const db = getDb();
  const billId = parseInt(req.params.id, 10);
  if (!Number.isInteger(billId) || billId < 1) {
    return res.status(400).json(standardizeError('Invalid bill ID', 'VALIDATION_ERROR'));
  }

  const bill = db
    .prepare(
      'SELECT id, name, catalog_id FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL',
    )
    .get(billId, req.user.id);
  if (!bill) return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND'));

  const rawCatalogId = req.body?.catalog_id;

  if (rawCatalogId === null || rawCatalogId === undefined) {
    db.prepare(
      "UPDATE bills SET catalog_id = NULL, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
    ).run(billId, req.user.id);
    recordSubscriptionFeedback(db, req.user.id, {
      action: 'catalog_unlink',
      catalog_id: bill.catalog_id,
      bill_id: billId,
      merchant: bill.name,
    });
    return res.json({ ok: true, catalog_id: null });
  }

  const catalogId = parseInt(rawCatalogId, 10);
  if (!Number.isInteger(catalogId) || catalogId < 1) {
    return res
      .status(400)
      .json(
        standardizeError(
          'catalog_id must be a positive integer or null',
          'VALIDATION_ERROR',
          'catalog_id',
        ),
      );
  }
  const catalogEntry = db
    .prepare('SELECT id FROM subscription_catalog WHERE id = ?')
    .get(catalogId);
  if (!catalogEntry)
    return res
      .status(404)
      .json(standardizeError('Catalog entry not found', 'NOT_FOUND', 'catalog_id'));

  db.prepare(
    "UPDATE bills SET catalog_id = ?, updated_at = datetime('now') WHERE id = ? AND user_id = ?",
  ).run(catalogId, billId, req.user.id);
  recordSubscriptionFeedback(db, req.user.id, {
    action: 'catalog_relink',
    catalog_id: catalogId,
    bill_id: billId,
    merchant: bill.name,
    metadata: { previous_catalog_id: bill.catalog_id || null },
  });
  res.json({ ok: true, catalog_id: catalogId });
});

// Add a custom bank descriptor for a catalog entry (per-user)
router.post('/catalog/:catalogId/descriptors', (req: Req, res: Res) => {
  const db = getDb();
  const catalogId = parseInt(req.params.catalogId, 10);
  if (!Number.isInteger(catalogId) || catalogId < 1) {
    return res.status(400).json(standardizeError('Invalid catalog ID', 'VALIDATION_ERROR'));
  }

  const catalogEntry = db
    .prepare('SELECT id FROM subscription_catalog WHERE id = ?')
    .get(catalogId);
  if (!catalogEntry)
    return res.status(404).json(standardizeError('Catalog entry not found', 'NOT_FOUND'));

  const descriptor = String(req.body?.descriptor ?? '').trim();
  if (!descriptor) {
    return res
      .status(400)
      .json(standardizeError('descriptor is required', 'VALIDATION_ERROR', 'descriptor'));
  }
  if (descriptor.length > 100) {
    return res
      .status(400)
      .json(
        standardizeError(
          'descriptor must be 100 characters or less',
          'VALIDATION_ERROR',
          'descriptor',
        ),
      );
  }

  try {
    // Check for case-insensitive duplicate
    const exists = db
      .prepare(
        'SELECT id FROM user_catalog_descriptors WHERE user_id = ? AND catalog_id = ? AND LOWER(descriptor) = LOWER(?)',
      )
      .get(req.user.id, catalogId, descriptor);
    if (exists) {
      return res
        .status(409)
        .json(
          standardizeError(
            'Descriptor already exists for this service',
            'DUPLICATE_ERROR',
            'descriptor',
          ),
        );
    }

    const result = db
      .prepare(
        'INSERT INTO user_catalog_descriptors (user_id, catalog_id, descriptor) VALUES (?, ?, ?)',
      )
      .run(req.user.id, catalogId, descriptor);
    recordSubscriptionFeedback(db, req.user.id, {
      action: 'add_descriptor',
      catalog_id: catalogId,
      merchant: descriptor,
      descriptor,
    });

    res.status(201).json({ id: result.lastInsertRowid, descriptor, catalog_id: catalogId });
  } catch (err) {
    res
      .status(500)
      .json(standardizeError(err.message || 'Failed to add descriptor', 'DESCRIPTOR_ADD_ERROR'));
  }
});

// Delete a user-added catalog descriptor
router.delete('/catalog/descriptors/:id', (req: Req, res: Res) => {
  const db = getDb();
  const descriptorId = parseInt(req.params.id, 10);
  if (!Number.isInteger(descriptorId) || descriptorId < 1) {
    return res.status(400).json(standardizeError('Invalid descriptor ID', 'VALIDATION_ERROR'));
  }

  try {
    const existing = db
      .prepare(
        'SELECT catalog_id, descriptor FROM user_catalog_descriptors WHERE id = ? AND user_id = ?',
      )
      .get(descriptorId, req.user.id);
    const result = db
      .prepare('DELETE FROM user_catalog_descriptors WHERE id = ? AND user_id = ?')
      .run(descriptorId, req.user.id);
    if (result.changes === 0) {
      return res.status(404).json(standardizeError('Descriptor not found', 'NOT_FOUND'));
    }
    if (existing) {
      recordSubscriptionFeedback(db, req.user.id, {
        action: 'delete_descriptor',
        catalog_id: existing.catalog_id,
        merchant: existing.descriptor,
        descriptor: existing.descriptor,
      });
    }
    res.json({ ok: true });
  } catch (err) {
    res
      .status(500)
      .json(
        standardizeError(err.message || 'Failed to delete descriptor', 'DESCRIPTOR_DELETE_ERROR'),
      );
  }
});

router.patch('/:id', (req: Req, res: Res) => {
  const db = getDb();
  const billId = parseInt(req.params.id, 10);
  if (!Number.isInteger(billId)) {
    return res
      .status(400)
      .json(standardizeError('bill_id must be an integer', 'VALIDATION_ERROR', 'bill_id'));
  }

  const existing = db
    .prepare('SELECT * FROM bills WHERE id = ? AND user_id = ? AND deleted_at IS NULL')
    .get(billId, req.user.id);
  if (!existing)
    return res.status(404).json(standardizeError('Bill not found', 'NOT_FOUND', 'bill_id'));

  const allowedTypes = new Set([
    'streaming',
    'software',
    'cloud',
    'music',
    'news',
    'fitness',
    'gaming',
    'utilities',
    'insurance',
    'other',
  ]);
  const next = {
    is_subscription:
      req.body.is_subscription !== undefined
        ? req.body.is_subscription
          ? 1
          : 0
        : existing.is_subscription,
    subscription_type:
      req.body.subscription_type !== undefined
        ? allowedTypes.has(req.body.subscription_type)
          ? req.body.subscription_type
          : 'other'
        : existing.subscription_type,
    reminder_days_before:
      req.body.reminder_days_before !== undefined
        ? Number(req.body.reminder_days_before)
        : existing.reminder_days_before,
    active: req.body.active !== undefined ? (req.body.active ? 1 : 0) : existing.active,
  };

  if (
    !Number.isInteger(next.reminder_days_before) ||
    next.reminder_days_before < 0 ||
    next.reminder_days_before > 30
  ) {
    return res
      .status(400)
      .json(
        standardizeError(
          'reminder_days_before must be between 0 and 30',
          'VALIDATION_ERROR',
          'reminder_days_before',
        ),
      );
  }

  db.prepare(
    `
    UPDATE bills
    SET is_subscription = ?,
        subscription_type = ?,
        reminder_days_before = ?,
        active = ?,
        updated_at = datetime('now')
    WHERE id = ? AND user_id = ?
  `,
  ).run(
    next.is_subscription,
    next.subscription_type,
    next.reminder_days_before,
    next.active,
    billId,
    req.user.id,
  );

  const updated = db
    .prepare(
      `
    SELECT b.*, c.name AS category_name
    FROM bills b
    LEFT JOIN categories c ON c.id = b.category_id AND c.user_id = b.user_id AND c.deleted_at IS NULL
    WHERE b.id = ? AND b.user_id = ?
  `,
    )
    .get(billId, req.user.id);

  res.json(decorateSubscription(updated));
});

module.exports = router;
