import { getDb } from "./db";
import { categorizeBatch } from "./categorizationService";
import {
  isLinkedTransactionFkError,
  type LinkedTransferTooltipRow,
} from "./balance.service";
import type {
  Transaction,
  TransactionRow,
  TransactionFilters,
  TransactionSort,
  TransactionPageResult,
  Category,
  ImportSource,
  SplitChild,
} from "../shared/types";

/**
 * Thrown when a deletion path is blocked by `balance_account_transfers.transaction_id`
 * FK RESTRICT. Carries the offending `transaction_id`(s) so the UI can format a
 * precise message ("Cette transaction est liée au compte de bilan X — déliez-la
 * avant de supprimer") and can offer a deep link to the linked account.
 *
 * `linkedAccounts` is best-effort: when known (single-row delete) the array
 * lists every account currently linking the transaction. For bulk deletes
 * the array may be empty — the UI just shows the generic message in that
 * case.
 */
export class TransactionLinkedToBalanceError extends Error {
  readonly code = "transaction_linked_to_balance_account" as const;
  readonly transactionId: number | null;
  readonly linkedAccounts: LinkedTransferTooltipRow[];
  constructor(
    transactionId: number | null,
    linkedAccounts: LinkedTransferTooltipRow[],
    message?: string
  ) {
    super(
      message ??
        "Transaction is linked to one or more balance accounts; unlink before deleting"
    );
    this.name = "TransactionLinkedToBalanceError";
    this.transactionId = transactionId;
    this.linkedAccounts = linkedAccounts;
  }
}

/**
 * Delete one transaction by id. Throws `TransactionLinkedToBalanceError` if
 * the transaction has any row in `balance_account_transfers` (FK RESTRICT)
 * — UI surfaces "Cette transaction est liée au compte de bilan X — déliez-la
 * avant de supprimer" with a link to the offending account.
 *
 * Pre-checks the link table so the error carries account names; falls back
 * to the FK-error pattern matcher if the constraint fires for any other
 * reason.
 */
export async function deleteTransaction(transactionId: number): Promise<void> {
  const db = await getDb();
  // Pre-check: if any transfer references this transaction, surface a clean
  // typed error WITHOUT touching the row. Cheaper than catching the FK
  // exception and provides the account names for the UI message.
  const linked = await db.select<
    Array<{ account_id: number; account_name: string; direction: "in" | "out" }>
  >(
    `SELECT bat.account_id AS account_id,
            a.name AS account_name,
            bat.direction AS direction
       FROM balance_account_transfers bat
       JOIN balance_accounts a ON a.id = bat.account_id
      WHERE bat.transaction_id = $1`,
    [transactionId]
  );
  if (linked.length > 0) {
    throw new TransactionLinkedToBalanceError(
      transactionId,
      linked.map((l) => ({
        transaction_id: transactionId,
        account_id: l.account_id,
        account_name: l.account_name,
        direction: l.direction,
      }))
    );
  }
  try {
    await db.execute("DELETE FROM transactions WHERE id = $1", [transactionId]);
  } catch (err) {
    // Defensive: a race could have linked the transaction between the
    // SELECT and the DELETE. Surface the typed error in that case too.
    if (isLinkedTransactionFkError(err)) {
      throw new TransactionLinkedToBalanceError(transactionId, []);
    }
    throw err;
  }
}

export async function insertBatch(
  transactions: Array<{
    date: string;
    description: string;
    amount: number;
    source_id: number;
    file_id: number;
    original_description: string;
    category_id?: number | null;
    supplier_id?: number | null;
  }>,
  onProgress?: (inserted: number) => void
): Promise<number> {
  const db = await getDb();
  let insertedCount = 0;

  for (const tx of transactions) {
    await db.execute(
      `INSERT INTO transactions (date, description, amount, source_id, file_id, original_description, category_id, supplier_id)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
      [
        tx.date,
        tx.description,
        tx.amount,
        tx.source_id,
        tx.file_id,
        tx.original_description,
        tx.category_id ?? null,
        tx.supplier_id ?? null,
      ]
    );
    insertedCount++;
    if (onProgress && insertedCount % 10 === 0) {
      onProgress(insertedCount);
    }
  }

  if (onProgress && insertedCount % 10 !== 0) {
    onProgress(insertedCount);
  }

  return insertedCount;
}

export async function findDuplicates(
  rows: Array<{ date: string; description: string; amount: number }>
): Promise<
  Array<{
    rowIndex: number;
    existingTransactionId: number;
    date: string;
    description: string;
    amount: number;
  }>
> {
  const db = await getDb();
  const duplicates: Array<{
    rowIndex: number;
    existingTransactionId: number;
    date: string;
    description: string;
    amount: number;
  }> = [];

  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];
    const existing = await db.select<Transaction[]>(
      `SELECT id FROM transactions WHERE date = $1 AND description = $2 AND amount = $3 LIMIT 1`,
      [row.date, row.description, row.amount]
    );
    if (existing.length > 0) {
      duplicates.push({
        rowIndex: i,
        existingTransactionId: existing[0].id,
        date: row.date,
        description: row.description,
        amount: row.amount,
      });
    }
  }

  return duplicates;
}

export async function getTransactionPage(
  filters: TransactionFilters,
  sort: TransactionSort,
  page: number,
  pageSize: number
): Promise<TransactionPageResult> {
  const db = await getDb();

  const whereClauses: string[] = [];
  const params: unknown[] = [];
  let paramIndex = 1;

  if (filters.search) {
    whereClauses.push(`t.description LIKE $${paramIndex}`);
    params.push(`%${filters.search}%`);
    paramIndex++;
  }

  if (filters.uncategorizedOnly) {
    whereClauses.push(`t.category_id IS NULL`);
  } else if (filters.categoryId !== null) {
    whereClauses.push(`t.category_id = $${paramIndex}`);
    params.push(filters.categoryId);
    paramIndex++;
  }

  if (filters.sourceId !== null) {
    whereClauses.push(`t.source_id = $${paramIndex}`);
    params.push(filters.sourceId);
    paramIndex++;
  }

  if (filters.dateFrom) {
    whereClauses.push(`t.date >= $${paramIndex}`);
    params.push(filters.dateFrom);
    paramIndex++;
  }

  if (filters.dateTo) {
    whereClauses.push(`t.date <= $${paramIndex}`);
    params.push(filters.dateTo);
    paramIndex++;
  }

  // Always exclude split children from the transaction list
  whereClauses.push(`t.parent_transaction_id IS NULL`);

  const whereSQL = `WHERE ${whereClauses.join(" AND ")}`;

  // Map sort column to SQL
  const sortColumnMap: Record<string, string> = {
    date: "t.date",
    description: "t.description",
    amount: "t.amount",
    category_name: "c.name",
  };
  const orderSQL = `ORDER BY ${sortColumnMap[sort.column]} ${sort.direction}`;

  const offset = (page - 1) * pageSize;

  // Rows query
  const rowsSQL = `
    SELECT t.id, t.date, t.description, t.amount, t.category_id,
           c.name AS category_name, c.color AS category_color,
           s.name AS source_name, t.notes, t.is_manually_categorized,
           t.is_split
    FROM transactions t
    LEFT JOIN categories c ON t.category_id = c.id
    LEFT JOIN import_sources s ON t.source_id = s.id
    ${whereSQL}
    ${orderSQL}
    LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
  `;
  const rowsParams = [...params, pageSize, offset];

  // Totals query
  const totalsSQL = `
    SELECT COUNT(*) AS totalCount,
           COALESCE(SUM(t.amount), 0) AS totalAmount,
           COALESCE(SUM(CASE WHEN t.amount > 0 THEN t.amount ELSE 0 END), 0) AS incomeTotal,
           COALESCE(SUM(CASE WHEN t.amount < 0 THEN t.amount ELSE 0 END), 0) AS expenseTotal
    FROM transactions t
    LEFT JOIN categories c ON t.category_id = c.id
    LEFT JOIN import_sources s ON t.source_id = s.id
    ${whereSQL}
  `;

  const [rows, totals] = await Promise.all([
    db.select<TransactionRow[]>(rowsSQL, rowsParams),
    db.select<
      Array<{
        totalCount: number;
        totalAmount: number;
        incomeTotal: number;
        expenseTotal: number;
      }>
    >(totalsSQL, params),
  ]);

  const t = totals[0] ?? {
    totalCount: 0,
    totalAmount: 0,
    incomeTotal: 0,
    expenseTotal: 0,
  };

  return {
    rows,
    totalCount: t.totalCount,
    totalAmount: t.totalAmount,
    incomeTotal: t.incomeTotal,
    expenseTotal: t.expenseTotal,
  };
}

export async function updateTransactionCategory(
  txId: number,
  categoryId: number | null,
  isManual: boolean
): Promise<void> {
  const db = await getDb();
  await db.execute(
    `UPDATE transactions SET category_id = $1, is_manually_categorized = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3`,
    [categoryId, isManual, txId]
  );
}

export async function updateTransactionNotes(
  txId: number,
  notes: string
): Promise<void> {
  const db = await getDb();
  await db.execute(
    `UPDATE transactions SET notes = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`,
    [notes, txId]
  );
}

export async function getAllCategories(): Promise<Category[]> {
  const db = await getDb();
  return db.select<Category[]>(
    `SELECT * FROM categories WHERE is_active = 1 AND is_inputable = 1 ORDER BY sort_order, name`
  );
}

export async function getAllImportSources(): Promise<ImportSource[]> {
  const db = await getDb();
  return db.select<ImportSource[]>(
    `SELECT * FROM import_sources ORDER BY name`
  );
}

export async function autoCategorizeTransactions(): Promise<number> {
  const db = await getDb();
  const uncategorized = await db.select<Array<{ id: number; description: string }>>(
    `SELECT id, description FROM transactions WHERE category_id IS NULL AND is_manually_categorized = 0`
  );

  if (uncategorized.length === 0) return 0;

  const results = await categorizeBatch(uncategorized.map((tx) => tx.description));

  let count = 0;
  for (let i = 0; i < uncategorized.length; i++) {
    const result = results[i];
    if (result.category_id !== null) {
      await db.execute(
        `UPDATE transactions SET category_id = $1, supplier_id = $2, updated_at = CURRENT_TIMESTAMP WHERE id = $3`,
        [result.category_id, result.supplier_id, uncategorized[i].id]
      );
      count++;
    }
  }

  return count;
}

export async function getSplitParentTransactions(): Promise<TransactionRow[]> {
  const db = await getDb();
  return db.select<TransactionRow[]>(
    `SELECT t.id, t.date, t.description, t.amount, t.category_id,
            c.name AS category_name, c.color AS category_color,
            s.name AS source_name, t.notes, t.is_manually_categorized,
            t.is_split
     FROM transactions t
     LEFT JOIN categories c ON t.category_id = c.id
     LEFT JOIN import_sources s ON t.source_id = s.id
     WHERE t.is_split = 1 AND t.parent_transaction_id IS NULL
     ORDER BY t.date DESC`
  );
}

export async function getSplitChildren(parentId: number): Promise<SplitChild[]> {
  const db = await getDb();
  return db.select<SplitChild[]>(
    `SELECT t.id, t.category_id, c.name AS category_name, c.color AS category_color,
            t.amount, t.description
     FROM transactions t
     LEFT JOIN categories c ON t.category_id = c.id
     WHERE t.parent_transaction_id = $1
     ORDER BY t.id`,
    [parentId]
  );
}

export async function saveSplitAdjustment(
  parentId: number,
  entries: Array<{ category_id: number; amount: number; description: string }>
): Promise<void> {
  const db = await getDb();

  // Delete any existing children
  await db.execute(
    `DELETE FROM transactions WHERE parent_transaction_id = $1`,
    [parentId]
  );

  // Fetch parent transaction
  const [parent] = await db.select<Transaction[]>(
    `SELECT * FROM transactions WHERE id = $1`,
    [parentId]
  );
  if (!parent) throw new Error("Parent transaction not found");

  // Insert each split child
  let offsetTotal = 0;
  for (const entry of entries) {
    await db.execute(
      `INSERT INTO transactions (date, description, amount, category_id, source_id, file_id, original_description, parent_transaction_id, is_split)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 1)`,
      [
        parent.date,
        entry.description,
        entry.amount,
        entry.category_id,
        parent.source_id ?? null,
        parent.file_id ?? null,
        parent.original_description ?? "",
        parentId,
      ]
    );
    offsetTotal += entry.amount;
  }

  // Insert offset child (cancels the redistributed portion from the original category)
  await db.execute(
    `INSERT INTO transactions (date, description, amount, category_id, source_id, file_id, original_description, parent_transaction_id, is_split)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 1)`,
    [
      parent.date,
      parent.description,
      -offsetTotal,
      parent.category_id ?? null,
      parent.source_id ?? null,
      parent.file_id ?? null,
      parent.original_description ?? "",
      parentId,
    ]
  );

  // Mark parent as split
  await db.execute(
    `UPDATE transactions SET is_split = 1, updated_at = CURRENT_TIMESTAMP WHERE id = $1`,
    [parentId]
  );
}

export async function deleteSplitAdjustment(parentId: number): Promise<void> {
  const db = await getDb();
  await db.execute(
    `DELETE FROM transactions WHERE parent_transaction_id = $1`,
    [parentId]
  );
  await db.execute(
    `UPDATE transactions SET is_split = 0, updated_at = CURRENT_TIMESTAMP WHERE id = $1`,
    [parentId]
  );
}
