'use strict';

const { getDb } = require('../db/database');
const { computeBalanceDelta, applyBalanceDelta } = require('./billsService');
const {
  applyBankPaymentAsSourceOfTruth,
  reactivatePaymentsOverriddenBy,
} = require('./paymentAccountingService');
const {
  decorateTransaction,
  getTransactionForUser,
} = require('./transactionService');
const { serializePayment } = require('./paymentValidation.cts');
const { markMatched, markUnmatched, markIgnored } = require('./transactionMatchState');

const MATCH_PAYMENT_SOURCE = 'transaction_match';
const MATCH_PAYMENT_METHOD = 'transaction_match';

function matchError(status, message, code, field = null) {
  const err = new Error(message);
  err.status = status;
  err.code = code;
  err.field = field;
  return err;
}

function normalizeId(value, field) {
  const id = typeof value === 'number' ? value : Number(value);
  if (!Number.isSafeInteger(id) || id <= 0) {
    throw matchError(400, `${field} must be a positive integer`, 'VALIDATION_ERROR', field);
  }
  return id;
}

function getOwnedTransaction(db, userId, transactionId) {
  const id = normalizeId(transactionId, 'transaction_id');
  const transaction = getTransactionForUser(db, userId, id);
  if (!transaction) {
    throw matchError(404, 'Transaction not found', 'NOT_FOUND', 'transaction_id');
  }
  return transaction;
}

function getOwnedBill(db, userId, billId) {
  const id = normalizeId(billId, 'bill_id');
  const bill = db.prepare(`
    SELECT *
    FROM bills
    WHERE id = ? AND user_id = ? AND deleted_at IS NULL
  `).get(id, userId);
  if (!bill) {
    throw matchError(404, 'Bill not found', 'NOT_FOUND', 'bill_id');
  }
  return bill;
}

function paymentDateForTransaction(transaction) {
  const date = transaction.posted_date || String(transaction.transacted_at || '').slice(0, 10);
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
    throw matchError(
      400,
      'Transaction must have a posted date before it can be matched to a bill',
      'VALIDATION_ERROR',
      'posted_date',
    );
  }
  return date;
}

function paymentAmountForTransaction(transaction) {
  const cents = Number(transaction.amount);
  if (!Number.isSafeInteger(cents) || cents === 0) {
    throw matchError(
      400,
      'Transaction amount must be a non-zero integer number of cents',
      'VALIDATION_ERROR',
      'amount',
    );
  }
  return Math.round(Math.abs(cents)); // tx.amount and payments.amount are both cents
}

function getActivePaymentForTransaction(db, userId, transactionId) {
  return db.prepare(`
    SELECT p.*
    FROM payments p
    JOIN bills b ON b.id = p.bill_id
    WHERE p.transaction_id = ?
      AND p.deleted_at IS NULL
      AND b.user_id = ?
      AND b.deleted_at IS NULL
    ORDER BY p.id ASC
    LIMIT 1
  `).get(transactionId, userId);
}

function getPaymentForResponse(db, userId, paymentId) {
  if (!paymentId) return null;
  return db.prepare(`
    SELECT p.*
    FROM payments p
    JOIN bills b ON b.id = p.bill_id
    WHERE p.id = ?
      AND b.user_id = ?
  `).get(paymentId, userId) || null;
}

function restorePaymentBalance(db, payment) {
  if (!payment || payment.balance_delta == null) return;
  const bill = db.prepare('SELECT id, current_balance FROM bills WHERE id = ?').get(payment.bill_id);
  if (bill?.current_balance == null) return;

  const restored = Math.max(0, Number(bill.current_balance) - Number(payment.balance_delta)); // cents, exact integer arithmetic
  // Clear interest_accrued_month when reversing a payment that charged interest,
  // so the re-applied payment can accrue interest fresh.
  db.prepare(`
    UPDATE bills
    SET current_balance        = ?,
        interest_accrued_month = CASE WHEN ? THEN NULL ELSE interest_accrued_month END,
        updated_at             = datetime('now')
    WHERE id = ?
  `).run(restored, payment.interest_delta != null ? 1 : 0, bill.id);
}

function applyPaymentBalance(db, bill, amount) {
  const freshBill = db.prepare('SELECT * FROM bills WHERE id = ?').get(bill.id) || bill;
  const balCalc = computeBalanceDelta(freshBill, amount);
  applyBalanceDelta(db, bill.id, balCalc);
  return { balance_delta: balCalc?.balance_delta ?? null, interest_delta: balCalc?.interest_delta ?? null };
}

function updatePaymentBalanceDeltas(db, paymentId, deltas) {
  db.prepare(`
    UPDATE payments
    SET balance_delta = ?,
        interest_delta = ?,
        updated_at = datetime('now')
    WHERE id = ?
  `).run(deltas.balance_delta, deltas.interest_delta, paymentId);
}

function buildMatchPaymentNotes(transaction, bill) {
  const label = transaction.payee || transaction.description || `transaction ${transaction.id}`;
  return `Matched transaction to ${bill.name}: ${label}`.slice(0, 500);
}

function createOrUpdateMatchPayment(db, userId, transaction, bill) {
  const amount = paymentAmountForTransaction(transaction);
  const paidDate = paymentDateForTransaction(transaction);
  const notes = buildMatchPaymentNotes(transaction, bill);
  const existingPayment = getActivePaymentForTransaction(db, userId, transaction.id);

  if (existingPayment && existingPayment.payment_source !== MATCH_PAYMENT_SOURCE) {
    throw matchError(
      409,
      'Transaction is already linked to a non-matching payment. Unlink that payment before matching this transaction.',
      'TRANSACTION_PAYMENT_ALREADY_LINKED',
      'transaction_id',
    );
  }

  if (existingPayment) {
    restorePaymentBalance(db, existingPayment);
    db.prepare(`
      UPDATE payments
      SET bill_id = ?,
          amount = ?,
          paid_date = ?,
          method = ?,
          notes = ?,
          balance_delta = NULL,
          interest_delta = NULL,
          payment_source = ?,
          updated_at = datetime('now')
      WHERE id = ?
    `).run(
      bill.id,
      amount,
      paidDate,
      MATCH_PAYMENT_METHOD,
      notes,
      MATCH_PAYMENT_SOURCE,
      existingPayment.id,
    );
    const updatedPayment = db.prepare('SELECT * FROM payments WHERE id = ?').get(existingPayment.id);
    const deltas = applyBankPaymentAsSourceOfTruth(db, bill, updatedPayment);
    updatePaymentBalanceDeltas(db, existingPayment.id, deltas);
    return existingPayment.id;
  }

  const result = db.prepare(`
    INSERT INTO payments
      (bill_id, amount, paid_date, method, notes, balance_delta, interest_delta, payment_source, transaction_id)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).run(
    bill.id,
    amount,
    paidDate,
    MATCH_PAYMENT_METHOD,
    notes,
    null,
    null,
    MATCH_PAYMENT_SOURCE,
    transaction.id,
  );
  const insertedPayment = db.prepare('SELECT * FROM payments WHERE id = ?').get(result.lastInsertRowid);
  const deltas = applyBankPaymentAsSourceOfTruth(db, bill, insertedPayment);
  updatePaymentBalanceDeltas(db, result.lastInsertRowid, deltas);
  return result.lastInsertRowid;
}

function unlinkPaymentForTransaction(db, userId, transactionId) {
  const existingPayment = getActivePaymentForTransaction(db, userId, transactionId);
  if (!existingPayment) return null;

  if (existingPayment.payment_source === MATCH_PAYMENT_SOURCE) {
    restorePaymentBalance(db, existingPayment);
    reactivatePaymentsOverriddenBy(db, existingPayment.id);
    db.prepare(`
      UPDATE payments
      SET deleted_at = datetime('now'), updated_at = datetime('now')
      WHERE id = ?
    `).run(existingPayment.id);
    return { ...serializePayment(existingPayment), deleted: true };
  }

  db.prepare(`
    UPDATE payments
    SET transaction_id = NULL, updated_at = datetime('now')
    WHERE id = ?
  `).run(existingPayment.id);
  return { ...serializePayment(existingPayment), unlinked: true };
}

function responseForTransaction(db, userId, transactionId, paymentId = null, extra = {}) {
  return {
    success: true,
    transaction: decorateTransaction(getTransactionForUser(db, userId, transactionId)),
    payment: serializePayment(getPaymentForResponse(db, userId, paymentId)),
    ...extra,
  };
}

// opts.learnMerchant — when true (explicit user confirmation), remember a
// merchant→bill rule so future synced transactions from the same merchant
// auto-match. Left false for background auto-matching to avoid compounding
// a wrong auto-match into a permanent rule.
function matchTransactionToBill(userId, transactionId, billId, opts = {}) {
  const db = getDb();
  const tx = db.transaction(() => {
    const transaction = getOwnedTransaction(db, userId, transactionId);
    if (transaction.ignored || transaction.match_status === 'ignored') {
      throw matchError(400, 'Ignored transactions must be unignored before matching', 'TRANSACTION_IGNORED', 'transaction_id');
    }

    const bill = getOwnedBill(db, userId, billId);
    const paymentId = createOrUpdateMatchPayment(db, userId, transaction, bill);

    markMatched(db, userId, transaction.id, bill.id, { resetIgnored: true });

    if (opts.learnMerchant) {
      const { learnMerchantRuleFromMatch } = require('./billMerchantRuleService');
      learnMerchantRuleFromMatch(db, userId, bill.id, transaction);
    }

    return responseForTransaction(db, userId, transaction.id, paymentId);
  });

  return tx();
}

function unmatchTransaction(userId, transactionId) {
  const db = getDb();
  const tx = db.transaction(() => {
    const transaction = getOwnedTransaction(db, userId, transactionId);
    const removedPayment = unlinkPaymentForTransaction(db, userId, transaction.id);

    markUnmatched(db, userId, transaction.id, { resetIgnored: true });

    return responseForTransaction(db, userId, transaction.id, null, { removed_payment: removedPayment });
  });

  return tx();
}

function ignoreTransaction(userId, transactionId) {
  const db = getDb();
  const tx = db.transaction(() => {
    const transaction = getOwnedTransaction(db, userId, transactionId);
    const removedPayment = unlinkPaymentForTransaction(db, userId, transaction.id);

    markIgnored(db, userId, transaction.id);

    return responseForTransaction(db, userId, transaction.id, null, { removed_payment: removedPayment });
  });

  return tx();
}

function unignoreTransaction(userId, transactionId) {
  const db = getDb();
  const tx = db.transaction(() => {
    const transaction = getOwnedTransaction(db, userId, transactionId);

    markUnmatched(db, userId, transaction.id, { resetIgnored: true });

    return responseForTransaction(db, userId, transaction.id);
  });

  return tx();
}

module.exports = {
  MATCH_PAYMENT_METHOD,
  MATCH_PAYMENT_SOURCE,
  ignoreTransaction,
  matchTransactionToBill,
  unignoreTransaction,
  unmatchTransaction,
};
