'use strict';

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

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

type Row = Record<string, any>;

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

function matchError(
  status: number,
  message: string,
  code: string,
  field: string | null = null,
): Error {
  const err = new Error(message) as any;
  err.status = status;
  err.code = code;
  err.field = field;
  return err;
}

function normalizeId(value: unknown, field: string): number {
  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: Db, userId: number, transactionId: unknown): Row {
  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: Db, userId: number, billId: unknown): Row {
  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: Row): string {
  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: Row): number {
  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: Db,
  userId: number,
  transactionId: number,
): Row | undefined {
  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: Db,
  userId: number,
  paymentId: number | bigint | null,
): Row | null {
  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: Db, payment: Row): void {
  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: Db,
  bill: Row,
  amount: number,
): { balance_delta: any; interest_delta: any } {
  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: Db,
  paymentId: number | bigint,
  deltas: { balance_delta: any; interest_delta: any },
): void {
  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: Row, bill: Row): string {
  const label = transaction.payee || transaction.description || `transaction ${transaction.id}`;
  return `Matched transaction to ${bill.name}: ${label}`.slice(0, 500);
}

function createOrUpdateMatchPayment(
  db: Db,
  userId: number,
  transaction: Row,
  bill: Row,
): number | bigint {
  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: Db, userId: number, transactionId: number): Row | null {
  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: Db,
  userId: number,
  transactionId: number,
  paymentId: number | bigint | null = null,
  extra: Row = {},
) {
  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: number,
  transactionId: unknown,
  billId: unknown,
  opts: { learnMerchant?: boolean } = {},
) {
  const db: 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.cts');
      learnMerchantRuleFromMatch(db, userId, bill.id, transaction);
    }

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

  return tx();
}

function unmatchTransaction(userId: number, transactionId: unknown) {
  const db: 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: number, transactionId: unknown) {
  const db: 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: number, transactionId: unknown) {
  const db: 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,
};
