import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { exportReportAsExcel, exportReportAsPdf } from "../utils/reportExport.js";

const TODAY = new Date().toISOString().slice(0, 10);
const TOLERANCE = 0.01;

const REPORT_CATALOG = [
  {
    key: "brouillard",
    path: "/reports/brouillard",
    title: "Brouillard de saisie",
    description: "Liste chronologique de toutes les écritures, y compris les brouillons.",
    landscape: true,
  },
  {
    key: "journal",
    path: "/reports/journal",
    title: "Journal comptable",
    description: "Journal validé par journal et par période.",
    landscape: true,
  },
  {
    key: "grand-livre",
    path: "/reports/grand-livre",
    title: "Grand Livre",
    description: "Mouvements détaillés et soldes courants par compte.",
    landscape: true,
  },
  {
    key: "balance",
    path: "/reports/balance",
    title: "Balance Générale",
    description: "Balance des comptes avec contrôle d'équilibre.",
    landscape: true,
  },
  {
    key: "balance-auxiliaire",
    path: "/reports/balance-auxiliaire",
    title: "Balance Auxiliaire",
    description: "Balance détaillée des tiers clients et fournisseurs.",
    landscape: true,
  },
  {
    key: "aged-clients",
    path: "/reports/aged-clients",
    title: "Balance Âgée Clients",
    description: "Encours clients ventilés par ancienneté.",
    landscape: true,
  },
  {
    key: "aged-suppliers",
    path: "/reports/aged-suppliers",
    title: "Balance Âgée Fournisseurs",
    description: "Encours fournisseurs ventilés par ancienneté.",
    landscape: true,
  },
  {
    key: "cash-state",
    path: "/reports/cash-state",
    title: "État de Caisse",
    description: "Solde d'ouverture, mouvements et solde de clôture d'une caisse.",
    landscape: false,
  },
  {
    key: "bank-reconciliation-state",
    path: "/reports/bank-reconciliation-state",
    title: "État de Rapprochement Bancaire",
    description: "Synthèse détaillée d'un rapprochement bancaire.",
    landscape: false,
  },
  {
    key: "assets-book",
    path: "/reports/assets-book",
    title: "Livre des Immobilisations",
    description: "Registre des immobilisations avec amortissements et VNC.",
    landscape: true,
  },
];

const REPORT_DEFINITIONS = new Map(REPORT_CATALOG.map((item) => [item.key, item]));

const cleanString = (value) => String(value ?? "").trim();

const roundAmount = (value) => Number(Number(value || 0).toFixed(2));

const isAmountZero = (value) => Math.abs(roundAmount(value)) <= TOLERANCE;

const toIsoDate = (value) => {
  const normalized = cleanString(value);

  if (!normalized) {
    return "";
  }

  if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) {
    throw new ApiError(400, "Date invalide.");
  }

  const date = new Date(`${normalized}T00:00:00Z`);
  const [year, month, day] = normalized.split("-").map(Number);

  if (
    Number.isNaN(date.getTime()) ||
    date.getUTCFullYear() !== year ||
    date.getUTCMonth() + 1 !== month ||
    date.getUTCDate() !== day
  ) {
    throw new ApiError(400, "Date invalide.");
  }

  return normalized;
};

const parseBoolean = (value, fallback = false) => {
  if (typeof value === "boolean") {
    return value;
  }

  if (value === null || value === undefined || value === "") {
    return fallback;
  }

  const normalized = cleanString(value).toLowerCase();

  if (["1", "true", "yes", "oui", "on"].includes(normalized)) {
    return true;
  }

  if (["0", "false", "no", "non", "off"].includes(normalized)) {
    return false;
  }

  return fallback;
};

const parseInteger = (value, fallback = 0) => {
  if (value === null || value === undefined || value === "") {
    return fallback;
  }

  const parsed = Number.parseInt(value, 10);

  if (!Number.isFinite(parsed)) {
    throw new ApiError(400, "Valeur entière invalide.");
  }

  return parsed;
};

const parseIdList = (value) => {
  if (!cleanString(value)) {
    return [];
  }

  return cleanString(value)
    .split(",")
    .map((item) => Number.parseInt(item, 10))
    .filter((item) => Number.isFinite(item) && item > 0);
};

const getCompanyId = (req, source = req.query) => {
  const companyId = Number(source.company_id || req.user?.companyId || 0);

  if (!companyId) {
    throw new ApiError(400, "company_id is required.");
  }

  return companyId;
};

const formatGeneratedAt = () =>
  new Intl.DateTimeFormat("fr-FR", {
    dateStyle: "short",
    timeStyle: "short",
  }).format(new Date());

const firstDayOfMonth = (year, month) =>
  new Date(Date.UTC(year, month - 1, 1)).toISOString().slice(0, 10);

const lastDayOfMonth = (year, month) =>
  new Date(Date.UTC(year, month, 0)).toISOString().slice(0, 10);

const minIsoDate = (left, right) => (left <= right ? left : right);

const getSignedBalance = (normalSide, debit, credit) =>
  roundAmount(normalSide === "credit" ? credit - debit : debit - credit);

const getStandardBalance = (debit, credit) => roundAmount(debit - credit);

const makeAmountSummary = (label, value) => ({
  label,
  value: roundAmount(value),
  type: "amount",
});

const makeTextSummary = (label, value) => ({
  label,
  value: value ?? "",
  type: "text",
});

const buildColumn = (key, label, type = "text", align = null) => ({
  key,
  label,
  type,
  align: align || (type === "amount" ? "right" : "left"),
});

const fetchCompany = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id, name, currency, nif, rc, logo_url, address, city
      FROM companies
      WHERE id = $1
      LIMIT 1
    `,
    [companyId],
  );

  if (!rows[0]) {
    throw new ApiError(404, "Société introuvable.");
  }

  return {
    id: Number(rows[0].id),
    name: rows[0].name,
    currency: rows[0].currency,
    nif: rows[0].nif,
    rc: rows[0].rc,
    logo_url: rows[0].logo_url,
    address: rows[0].address,
    city: rows[0].city,
  };
};

const fetchFiscalYearById = async (db, companyId, fiscalYearId) => {
  if (!fiscalYearId) {
    return null;
  }

  const { rows } = await db.query(
    `
      SELECT id, label, TO_CHAR(start_date, 'YYYY-MM-DD') AS start_date,
        TO_CHAR(end_date, 'YYYY-MM-DD') AS end_date, is_current, is_closed
      FROM fiscal_years
      WHERE company_id = $1
        AND id = $2
      LIMIT 1
    `,
    [companyId, fiscalYearId],
  );

  return rows[0]
    ? {
        id: Number(rows[0].id),
        label: rows[0].label,
        start_date: rows[0].start_date,
        end_date: rows[0].end_date,
        is_current: rows[0].is_current,
        is_closed: rows[0].is_closed,
      }
    : null;
};

const fetchDefaultFiscalYear = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id, label, TO_CHAR(start_date, 'YYYY-MM-DD') AS start_date,
        TO_CHAR(end_date, 'YYYY-MM-DD') AS end_date, is_current, is_closed
      FROM fiscal_years
      WHERE company_id = $1
      ORDER BY is_current DESC, start_date DESC
      LIMIT 1
    `,
    [companyId],
  );

  if (!rows[0]) {
    throw new ApiError(404, "Aucun exercice comptable disponible.");
  }

  return {
    id: Number(rows[0].id),
    label: rows[0].label,
    start_date: rows[0].start_date,
    end_date: rows[0].end_date,
    is_current: rows[0].is_current,
    is_closed: rows[0].is_closed,
  };
};

const fetchPeriodById = async (db, companyId, periodId) => {
  if (!periodId) {
    return null;
  }

  const { rows } = await db.query(
    `
      SELECT id, fiscal_year_id, month, year, is_closed
      FROM accounting_periods
      WHERE company_id = $1
        AND id = $2
      LIMIT 1
    `,
    [companyId, periodId],
  );

  return rows[0]
    ? {
        id: Number(rows[0].id),
        fiscal_year_id: Number(rows[0].fiscal_year_id),
        month: Number(rows[0].month),
        year: Number(rows[0].year),
        is_closed: rows[0].is_closed,
        label: `${String(rows[0].month).padStart(2, "0")}/${rows[0].year}`,
      }
    : null;
};

const fetchJournalById = async (db, companyId, journalId) => {
  if (!journalId) {
    return null;
  }

  const { rows } = await db.query(
    `
      SELECT id, code, label
      FROM journals
      WHERE company_id = $1
        AND id = $2
      LIMIT 1
    `,
    [companyId, journalId],
  );

  return rows[0]
    ? {
        id: Number(rows[0].id),
        code: rows[0].code,
        label: rows[0].label,
      }
    : null;
};

const fetchCashRegisterById = async (db, companyId, cashRegisterId) => {
  const { rows } = await db.query(
    `
      SELECT id, name, currency, current_balance
      FROM cash_registers
      WHERE company_id = $1
        AND id = $2
      LIMIT 1
    `,
    [companyId, cashRegisterId],
  );

  return rows[0]
    ? {
        id: Number(rows[0].id),
        name: rows[0].name,
        currency: rows[0].currency,
        current_balance: roundAmount(rows[0].current_balance),
      }
    : null;
};

const fetchFirstCashRegister = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id
      FROM cash_registers
      WHERE company_id = $1
        AND is_active = TRUE
      ORDER BY name ASC, id ASC
      LIMIT 1
    `,
    [companyId],
  );

  return rows[0] ? Number(rows[0].id) : null;
};

const fetchBankAccountById = async (db, companyId, bankAccountId) => {
  const { rows } = await db.query(
    `
      SELECT
        ba.id,
        ba.bank_name,
        ba.account_number,
        ba.rib,
        ba.currency,
        ba.current_balance,
        ba.account_id,
        a.code AS account_code,
        a.label AS account_label,
        a.balance_normal_side
      FROM bank_accounts ba
      LEFT JOIN accounts a
        ON a.id = ba.account_id
      WHERE ba.company_id = $1
        AND ba.id = $2
      LIMIT 1
    `,
    [companyId, bankAccountId],
  );

  return rows[0]
    ? {
        id: Number(rows[0].id),
        bank_name: rows[0].bank_name,
        account_number: rows[0].account_number,
        rib: rows[0].rib,
        currency: rows[0].currency,
        current_balance: roundAmount(rows[0].current_balance),
        account_id: rows[0].account_id ? Number(rows[0].account_id) : null,
        account_code: rows[0].account_code,
        account_label: rows[0].account_label,
        balance_normal_side: rows[0].balance_normal_side || "debit",
      }
    : null;
};

const fetchFirstBankAccountId = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT id
      FROM bank_accounts
      WHERE company_id = $1
        AND is_active = TRUE
      ORDER BY bank_name ASC, id ASC
      LIMIT 1
    `,
    [companyId],
  );

  return rows[0] ? Number(rows[0].id) : null;
};

const resolveDateRange = async (db, companyId, query) => {
  const requestedFiscalYearId = parseInteger(query.fiscal_year_id, 0);
  const requestedPeriodId = parseInteger(query.period_id, 0);
  let fiscalYear = requestedFiscalYearId
    ? await fetchFiscalYearById(db, companyId, requestedFiscalYearId)
    : await fetchDefaultFiscalYear(db, companyId);
  let period = null;
  let dateFrom = "";
  let dateTo = "";

  if (!fiscalYear) {
    throw new ApiError(404, "Exercice introuvable.");
  }

  if (requestedPeriodId) {
    period = await fetchPeriodById(db, companyId, requestedPeriodId);

    if (!period) {
      throw new ApiError(404, "Période introuvable.");
    }

    fiscalYear = await fetchFiscalYearById(db, companyId, period.fiscal_year_id);
    dateFrom = firstDayOfMonth(period.year, period.month);
    dateTo = lastDayOfMonth(period.year, period.month);
  } else {
    dateFrom = cleanString(query.date_from || query.from)
      ? toIsoDate(query.date_from || query.from)
      : fiscalYear.start_date;
    dateTo = cleanString(query.date_to || query.to)
      ? toIsoDate(query.date_to || query.to)
      : fiscalYear.is_current
        ? minIsoDate(TODAY, fiscalYear.end_date)
        : fiscalYear.end_date;
  }

  if (dateFrom && dateTo && dateFrom > dateTo) {
    throw new ApiError(400, "La date de début doit être antérieure à la date de fin.");
  }

  return {
    fiscal_year: fiscalYear,
    period,
    date_from: dateFrom,
    date_to: dateTo,
  };
};

const buildDateParameters = ({ date_from, date_to, fiscal_year, period }) => {
  const parameters = [];

  if (fiscal_year) {
    parameters.push({
      label: "Exercice",
      value: fiscal_year.label,
    });
  }

  if (period) {
    parameters.push({
      label: "Période",
      value: period.label,
    });
  } else {
    parameters.push({
      label: "Du",
      value: date_from || "—",
    });
    parameters.push({
      label: "Au",
      value: date_to || "—",
    });
  }

  return parameters;
};

const groupBy = (items, getKey) => {
  const map = new Map();

  items.forEach((item) => {
    const key = getKey(item);

    if (!map.has(key)) {
      map.set(key, []);
    }

    map.get(key).push(item);
  });

  return map;
};

const computeAgingBucket = (daysLate) => {
  if (daysLate < 0) {
    return "not_due";
  }

  if (daysLate <= 30) {
    return "bucket_0_30";
  }

  if (daysLate <= 60) {
    return "bucket_31_60";
  }

  if (daysLate <= 90) {
    return "bucket_61_90";
  }

  return "bucket_90_plus";
};

const diffDays = (fromDate, toDate) => {
  const from = new Date(`${fromDate}T00:00:00Z`);
  const to = new Date(`${toDate}T00:00:00Z`);

  return Math.floor((to.getTime() - from.getTime()) / 86400000);
};

const buildReportBase = async (db, companyId, reportKey) => {
  const company = await fetchCompany(db, companyId);
  const definition = REPORT_DEFINITIONS.get(reportKey);

  if (!definition) {
    throw new ApiError(404, "Rapport introuvable.");
  }

  return {
    report_key: reportKey,
    title: definition.title,
    description: definition.description,
    export_filename: definition.key,
    landscape: definition.landscape,
    generated_at: formatGeneratedAt(),
    company,
    parameters: [],
    warnings: [],
    cards: [],
    table: null,
    sections: [],
    summary: [],
  };
};

const buildBrouillardReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "brouillard");
  const range = await resolveDateRange(db, companyId, query);
  const journalId = parseInteger(query.journal_id, 0);
  const journal = journalId ? await fetchJournalById(db, companyId, journalId) : null;
  const values = [companyId];
  const conditions = ["je.company_id = $1"];

  if (range.date_from) {
    values.push(range.date_from);
    conditions.push(`je.entry_date >= $${values.length}::date`);
  }

  if (range.date_to) {
    values.push(range.date_to);
    conditions.push(`je.entry_date <= $${values.length}::date`);
  }

  if (journal) {
    values.push(journal.id);
    conditions.push(`je.journal_id = $${values.length}`);
  }

  const { rows } = await db.query(
    `
      SELECT
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.entry_number,
        je.status,
        j.code AS journal_code,
        j.label AS journal_label,
        je.label AS entry_label,
        a.code AS account_code,
        aa.ref_code AS auxiliary_code,
        el.debit,
        el.credit
      FROM journal_entries je
      JOIN journals j
        ON j.id = je.journal_id
      JOIN entry_lines el
        ON el.entry_id = je.id
      JOIN accounts a
        ON a.id = el.account_id
      LEFT JOIN auxiliary_accounts aa
        ON aa.id = el.auxiliary_account_id
      WHERE ${conditions.join(" AND ")}
      ORDER BY je.entry_date ASC, je.id ASC, el.id ASC
    `,
    values,
  );

  const tableRows = rows.map((row) => ({
    date: row.entry_date,
    number: row.entry_number,
    journal: `${row.journal_code} · ${row.journal_label}`,
    label: row.entry_label,
    account: row.account_code,
    auxiliary: row.auxiliary_code || "",
    debit: roundAmount(row.debit),
    credit: roundAmount(row.credit),
    row_type: row.status === "cancelled" ? "cancelled" : "detail",
  }));
  const totalDebit = roundAmount(tableRows.reduce((sum, row) => sum + row.debit, 0));
  const totalCredit = roundAmount(tableRows.reduce((sum, row) => sum + row.credit, 0));

  report.parameters = [
    ...buildDateParameters(range),
    makeTextSummary("Journal", journal ? `${journal.code} · ${journal.label}` : "Tous les journaux"),
  ];
  report.table = {
    columns: [
      buildColumn("date", "Date", "date"),
      buildColumn("number", "N°"),
      buildColumn("journal", "Journal"),
      buildColumn("label", "Libellé"),
      buildColumn("account", "Compte"),
      buildColumn("auxiliary", "Aux"),
      buildColumn("debit", "Débit", "amount"),
      buildColumn("credit", "Crédit", "amount"),
    ],
    rows: tableRows,
    emptyMessage: "Aucune écriture disponible sur la période sélectionnée.",
  };
  report.summary = [
    makeAmountSummary("Total débit", totalDebit),
    makeAmountSummary("Total crédit", totalCredit),
    makeTextSummary("Lignes", String(tableRows.length)),
  ];

  return report;
};

const buildJournalReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "journal");
  const range = await resolveDateRange(db, companyId, query);
  const journalId = parseInteger(query.journal_id, 0);
  const journal = journalId ? await fetchJournalById(db, companyId, journalId) : null;
  const values = [companyId];
  const conditions = ["je.company_id = $1", "je.status = 'validated'"];

  if (range.date_from) {
    values.push(range.date_from);
    conditions.push(`je.entry_date >= $${values.length}::date`);
  }

  if (range.date_to) {
    values.push(range.date_to);
    conditions.push(`je.entry_date <= $${values.length}::date`);
  }

  if (journal) {
    values.push(journal.id);
    conditions.push(`je.journal_id = $${values.length}`);
  }

  const { rows } = await db.query(
    `
      SELECT
        je.id AS entry_id,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        COALESCE(je.piece_ref, je.entry_number) AS piece_number,
        je.entry_number,
        je.label AS entry_label,
        j.code AS journal_code,
        j.label AS journal_label,
        a.code AS account_code,
        a.label AS account_label,
        el.debit,
        el.credit
      FROM journal_entries je
      JOIN journals j
        ON j.id = je.journal_id
      JOIN entry_lines el
        ON el.entry_id = je.id
      JOIN accounts a
        ON a.id = el.account_id
      WHERE ${conditions.join(" AND ")}
      ORDER BY je.entry_date ASC, je.id ASC, el.id ASC
    `,
    values,
  );

  const sectionsByEntry = new Map();
  let grandDebit = 0;
  let grandCredit = 0;

  rows.forEach((row) => {
    if (!sectionsByEntry.has(row.entry_id)) {
      sectionsByEntry.set(row.entry_id, {
        title: `${row.piece_number} · ${row.entry_label}`,
        subtitle: `${row.journal_code} · ${row.journal_label}`,
        meta: [
          { label: "Date", value: row.entry_date },
          { label: "N° écriture", value: row.entry_number },
        ],
        columns: [
          buildColumn("date", "Date", "date"),
          buildColumn("piece", "N° Pièce"),
          buildColumn("label", "Libellé"),
          buildColumn("account_code", "Compte"),
          buildColumn("account_label", "Libellé compte"),
          buildColumn("debit", "Débit", "amount"),
          buildColumn("credit", "Crédit", "amount"),
        ],
        rows: [],
        summary: [],
      });
    }

    const section = sectionsByEntry.get(row.entry_id);
    const debit = roundAmount(row.debit);
    const credit = roundAmount(row.credit);

    section.rows.push({
      date: row.entry_date,
      piece: row.piece_number,
      label: row.entry_label,
      account_code: row.account_code,
      account_label: row.account_label,
      debit,
      credit,
    });

    grandDebit += debit;
    grandCredit += credit;
  });

  const sections = Array.from(sectionsByEntry.values()).map((section) => {
    const totalDebit = roundAmount(section.rows.reduce((sum, row) => sum + row.debit, 0));
    const totalCredit = roundAmount(section.rows.reduce((sum, row) => sum + row.credit, 0));

    return {
      ...section,
      summary: [
        makeAmountSummary("Sous-total débit", totalDebit),
        makeAmountSummary("Sous-total crédit", totalCredit),
      ],
    };
  });

  report.parameters = [
    ...buildDateParameters(range),
    makeTextSummary("Journal", journal ? `${journal.code} · ${journal.label}` : "Tous les journaux"),
  ];
  report.sections = sections;
  report.summary = [
    makeAmountSummary("Total débit", grandDebit),
    makeAmountSummary("Total crédit", grandCredit),
    makeTextSummary("Écritures", String(sections.length)),
  ];

  return report;
};

const buildGrandLivreReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "grand-livre");
  const range = await resolveDateRange(db, companyId, query);
  const accountIds = parseIdList(query.account_ids);
  const codeFrom = cleanString(query.account_code_from).toUpperCase();
  const codeTo = cleanString(query.account_code_to).toUpperCase();
  const values = [companyId];
  const conditions = ["a.company_id = $1", "a.is_active = TRUE"];

  if (accountIds.length) {
    values.push(accountIds);
    conditions.push(`a.id = ANY($${values.length}::bigint[])`);
  }

  if (codeFrom) {
    values.push(codeFrom);
    conditions.push(`a.code >= $${values.length}`);
  }

  if (codeTo) {
    values.push(codeTo);
    conditions.push(`a.code <= $${values.length}`);
  }

  const accountResult = await db.query(
    `
      SELECT id, code, label, balance_normal_side
      FROM accounts a
      WHERE ${conditions.join(" AND ")}
      ORDER BY a.code ASC
    `,
    values,
  );

  const sections = [];
  let grandDebit = 0;
  let grandCredit = 0;

  for (const account of accountResult.rows) {
    const openingRow = await db.query(
      `
        SELECT
          COALESCE(SUM(el.debit), 0)::numeric AS total_debit,
          COALESCE(SUM(el.credit), 0)::numeric AS total_credit
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND el.account_id = $2
          AND je.entry_date < $3::date
      `,
      [companyId, account.id, range.date_from],
    );
    const movements = await db.query(
      `
        SELECT
          TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
          COALESCE(je.piece_ref, je.entry_number) AS piece_number,
          j.code AS journal_code,
          COALESCE(el.label, je.label) AS line_label,
          el.debit,
          el.credit
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        JOIN journals j
          ON j.id = je.journal_id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND el.account_id = $2
          AND je.entry_date BETWEEN $3::date AND $4::date
        ORDER BY je.entry_date ASC, je.id ASC, el.id ASC
      `,
      [companyId, account.id, range.date_from, range.date_to],
    );

    const openingDebit = roundAmount(openingRow.rows[0]?.total_debit || 0);
    const openingCredit = roundAmount(openingRow.rows[0]?.total_credit || 0);
    const openingBalance = getSignedBalance(
      account.balance_normal_side,
      openingDebit,
      openingCredit,
    );
    let runningBalance = openingBalance;
    const rows = movements.rows.map((row) => {
      const debit = roundAmount(row.debit);
      const credit = roundAmount(row.credit);
      runningBalance = roundAmount(
        runningBalance +
          getSignedBalance(account.balance_normal_side, debit, credit),
      );

      return {
        date: row.entry_date,
        piece: row.piece_number,
        journal: row.journal_code,
        label: row.line_label,
        debit,
        credit,
        balance: runningBalance,
      };
    });

    const totalDebit = roundAmount(rows.reduce((sum, row) => sum + row.debit, 0));
    const totalCredit = roundAmount(rows.reduce((sum, row) => sum + row.credit, 0));
    const finalBalance = rows.length ? rows[rows.length - 1].balance : openingBalance;

    if (!rows.length && isAmountZero(openingBalance)) {
      continue;
    }

    grandDebit += totalDebit;
    grandCredit += totalCredit;

    sections.push({
      title: `${account.code} · ${account.label}`,
      subtitle: "Mouvements du compte",
      meta: [
        { label: "Solde d'ouverture", value: openingBalance, type: "amount" },
      ],
      columns: [
        buildColumn("date", "Date", "date"),
        buildColumn("piece", "N° Pièce"),
        buildColumn("journal", "Journal"),
        buildColumn("label", "Libellé"),
        buildColumn("debit", "Débit", "amount"),
        buildColumn("credit", "Crédit", "amount"),
        buildColumn("balance", "Solde", "amount"),
      ],
      rows,
      summary: [
        makeAmountSummary("Total débit", totalDebit),
        makeAmountSummary("Total crédit", totalCredit),
        makeAmountSummary("Solde final", finalBalance),
      ],
      page_break_after: true,
    });
  }

  report.parameters = [
    ...buildDateParameters(range),
    makeTextSummary("Compte de", codeFrom || "Premier"),
    makeTextSummary("Compte à", codeTo || "Dernier"),
  ];
  report.sections = sections;
  report.summary = [
    makeAmountSummary("Total débit", grandDebit),
    makeAmountSummary("Total crédit", grandCredit),
    makeTextSummary("Comptes", String(sections.length)),
  ];

  return report;
};

const buildBalanceReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "balance");
  const range = await resolveDateRange(db, companyId, query);
  const showZero = parseBoolean(query.show_zero, false);
  const level = cleanString(query.level || "all").toLowerCase();
  const prefixLength = ["2", "3", "4"].includes(level) ? Number(level) : null;
  const movementRows = await db.query(
    `
      SELECT
        a.code,
        a.label,
        COALESCE(m.total_debit, 0)::numeric AS total_debit,
        COALESCE(m.total_credit, 0)::numeric AS total_credit
      FROM accounts a
      LEFT JOIN (
        SELECT
          el.account_id,
          SUM(el.debit) AS total_debit,
          SUM(el.credit) AS total_credit
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND je.entry_date BETWEEN $2::date AND $3::date
        GROUP BY el.account_id
      ) m
        ON m.account_id = a.id
      WHERE a.company_id = $1
        AND a.is_active = TRUE
      ORDER BY a.code ASC
    `,
    [companyId, range.date_from, range.date_to],
  );

  const exactLabels = new Map(movementRows.rows.map((row) => [row.code, row.label]));
  const aggregation = new Map();

  movementRows.rows.forEach((row) => {
    const code = prefixLength ? row.code.slice(0, prefixLength) : row.code;
    const totalDebit = roundAmount(row.total_debit);
    const totalCredit = roundAmount(row.total_credit);

    if (!aggregation.has(code)) {
      aggregation.set(code, {
        code,
        label: exactLabels.get(code) || row.label,
        total_debit: 0,
        total_credit: 0,
      });
    }

    const target = aggregation.get(code);
    target.total_debit = roundAmount(target.total_debit + totalDebit);
    target.total_credit = roundAmount(target.total_credit + totalCredit);
  });

  const detailRows = Array.from(aggregation.values())
    .map((item) => {
      const balance = getStandardBalance(item.total_debit, item.total_credit);

      return {
        code: item.code,
        label: item.label,
        debit_total: item.total_debit,
        credit_total: item.total_credit,
        debit_balance: balance > 0 ? balance : 0,
        credit_balance: balance < 0 ? Math.abs(balance) : 0,
        class_code: item.code.slice(0, 1),
        row_type: "detail",
      };
    })
    .filter((item) =>
      showZero
        ? true
        : !(
            isAmountZero(item.debit_total) &&
            isAmountZero(item.credit_total) &&
            isAmountZero(item.debit_balance) &&
            isAmountZero(item.credit_balance)
          ),
    )
    .sort((left, right) => left.code.localeCompare(right.code));

  const groupedRows = [];
  const groupedByClass = groupBy(detailRows, (row) => row.class_code);
  const orderedClassCodes = Array.from(groupedByClass.keys()).sort();

  orderedClassCodes.forEach((classCode) => {
    const rows = groupedByClass.get(classCode);
    const subtotal = rows.reduce(
      (accumulator, row) => ({
        debit_total: roundAmount(accumulator.debit_total + row.debit_total),
        credit_total: roundAmount(accumulator.credit_total + row.credit_total),
        debit_balance: roundAmount(accumulator.debit_balance + row.debit_balance),
        credit_balance: roundAmount(accumulator.credit_balance + row.credit_balance),
      }),
      { debit_total: 0, credit_total: 0, debit_balance: 0, credit_balance: 0 },
    );

    groupedRows.push(...rows);
    groupedRows.push({
      code: `Classe ${classCode}`,
      label: `Sous-total classe ${classCode}`,
      ...subtotal,
      row_type: "class_total",
    });
  });

  const grandTotals = detailRows.reduce(
    (accumulator, row) => ({
      debit_total: roundAmount(accumulator.debit_total + row.debit_total),
      credit_total: roundAmount(accumulator.credit_total + row.credit_total),
      debit_balance: roundAmount(accumulator.debit_balance + row.debit_balance),
      credit_balance: roundAmount(accumulator.credit_balance + row.credit_balance),
    }),
    { debit_total: 0, credit_total: 0, debit_balance: 0, credit_balance: 0 },
  );
  groupedRows.push({
    code: "TOTAL",
    label: "Total général",
    ...grandTotals,
    row_type: "grand_total",
  });

  if (
    !isAmountZero(grandTotals.debit_total - grandTotals.credit_total) ||
    !isAmountZero(grandTotals.debit_balance - grandTotals.credit_balance)
  ) {
    report.warnings.push({
      tone: "danger",
      message: "ATTENTION: La balance n'est pas équilibrée",
    });
  }

  report.parameters = [
    ...buildDateParameters(range),
    makeTextSummary("Niveau", prefixLength ? `${prefixLength} chiffres` : "Tous les digits"),
    makeTextSummary("Comptes à zéro", showZero ? "Inclus" : "Masqués"),
  ];
  report.table = {
    columns: [
      buildColumn("code", "Code"),
      buildColumn("label", "Libellé"),
      buildColumn("debit_total", "Débit cumulé", "amount"),
      buildColumn("credit_total", "Crédit cumulé", "amount"),
      buildColumn("debit_balance", "Solde débiteur", "amount"),
      buildColumn("credit_balance", "Solde créditeur", "amount"),
    ],
    rows: groupedRows,
    emptyMessage: "Aucune ligne de balance sur la période sélectionnée.",
  };
  report.summary = [
    makeAmountSummary("Débit cumulé", grandTotals.debit_total),
    makeAmountSummary("Crédit cumulé", grandTotals.credit_total),
    makeAmountSummary("Solde débiteur", grandTotals.debit_balance),
    makeAmountSummary("Solde créditeur", grandTotals.credit_balance),
  ];

  return report;
};

const buildAuxiliaryBalanceReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "balance-auxiliaire");
  const range = await resolveDateRange(db, companyId, query);
  const type = cleanString(query.type || "client").toLowerCase() === "supplier" ? "supplier" : "client";
  const rowsResult = await db.query(
    `
      SELECT
        aa.ref_code,
        aa.name,
        a.code AS collective_code,
        COALESCE(m.total_debit, 0)::numeric AS total_debit,
        COALESCE(m.total_credit, 0)::numeric AS total_credit
      FROM auxiliary_accounts aa
      JOIN accounts a
        ON a.id = aa.account_id
      LEFT JOIN (
        SELECT
          el.auxiliary_account_id,
          SUM(el.debit) AS total_debit,
          SUM(el.credit) AS total_credit
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND je.entry_date BETWEEN $2::date AND $3::date
        GROUP BY el.auxiliary_account_id
      ) m
        ON m.auxiliary_account_id = aa.id
      WHERE aa.company_id = $1
        AND aa.third_party_type = $4
        AND aa.is_active = TRUE
      ORDER BY aa.ref_code ASC
    `,
    [companyId, range.date_from, range.date_to, type],
  );

  const tableRows = rowsResult.rows.map((row) => {
    const totalDebit = roundAmount(row.total_debit);
    const totalCredit = roundAmount(row.total_credit);
    const balance = getStandardBalance(totalDebit, totalCredit);

    return {
      code: row.ref_code,
      name: row.name,
      collective_code: row.collective_code,
      debit_total: totalDebit,
      credit_total: totalCredit,
      debit_balance: balance > 0 ? balance : 0,
      credit_balance: balance < 0 ? Math.abs(balance) : 0,
    };
  });

  const totals = tableRows.reduce(
    (accumulator, row) => ({
      debit_total: roundAmount(accumulator.debit_total + row.debit_total),
      credit_total: roundAmount(accumulator.credit_total + row.credit_total),
      debit_balance: roundAmount(accumulator.debit_balance + row.debit_balance),
      credit_balance: roundAmount(accumulator.credit_balance + row.credit_balance),
    }),
    { debit_total: 0, credit_total: 0, debit_balance: 0, credit_balance: 0 },
  );

  report.title = `Balance Auxiliaire ${type === "client" ? "Clients" : "Fournisseurs"}`;
  report.parameters = [
    ...buildDateParameters(range),
    makeTextSummary("Type", type === "client" ? "Clients" : "Fournisseurs"),
  ];
  report.table = {
    columns: [
      buildColumn("code", "Code auxiliaire"),
      buildColumn("name", "Nom"),
      buildColumn("collective_code", "Compte collectif"),
      buildColumn("debit_total", "Débit cumulé", "amount"),
      buildColumn("credit_total", "Crédit cumulé", "amount"),
      buildColumn("debit_balance", "Solde déb", "amount"),
      buildColumn("credit_balance", "Solde créd", "amount"),
    ],
    rows: tableRows,
    emptyMessage: "Aucun mouvement auxiliaire disponible.",
  };
  report.summary = [
    makeAmountSummary("Débit cumulé", totals.debit_total),
    makeAmountSummary("Crédit cumulé", totals.credit_total),
    makeAmountSummary("Solde débiteur", totals.debit_balance),
    makeAmountSummary("Solde créditeur", totals.credit_balance),
  ];

  return report;
};

const buildAgedBalanceReport = async (db, companyId, query, reportKey) => {
  const isClient = reportKey === "aged-clients";
  const report = await buildReportBase(db, companyId, reportKey);
  const asOfDate = cleanString(query.as_of_date) ? toIsoDate(query.as_of_date) : TODAY;
  const type = isClient ? "client" : "supplier";
  const lines = await db.query(
    `
      SELECT
        aa.ref_code,
        aa.name,
        a.code AS collective_code,
        TO_CHAR(COALESCE(el.due_date, je.entry_date), 'YYYY-MM-DD') AS due_date,
        el.debit,
        el.credit
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      JOIN auxiliary_accounts aa
        ON aa.id = el.auxiliary_account_id
      JOIN accounts a
        ON a.id = aa.account_id
      WHERE je.company_id = $1
        AND je.status = 'validated'
        AND je.entry_date <= $2::date
        AND aa.third_party_type = $3
        AND el.is_lettered = FALSE
        AND aa.is_active = TRUE
      ORDER BY aa.ref_code ASC, due_date ASC
    `,
    [companyId, asOfDate, type],
  );

  const grouped = new Map();

  lines.rows.forEach((row) => {
    const amount = roundAmount(
      isClient ? Number(row.debit || 0) - Number(row.credit || 0) : Number(row.credit || 0) - Number(row.debit || 0),
    );

    if (amount <= 0) {
      return;
    }

    const key = row.ref_code;

    if (!grouped.has(key)) {
      grouped.set(key, {
        code: row.ref_code,
        name: row.name,
        collective_code: row.collective_code,
        total_balance: 0,
        not_due: 0,
        bucket_0_30: 0,
        bucket_31_60: 0,
        bucket_61_90: 0,
        bucket_90_plus: 0,
      });
    }

    const target = grouped.get(key);
    const bucket = computeAgingBucket(diffDays(row.due_date, asOfDate));
    target.total_balance = roundAmount(target.total_balance + amount);
    target[bucket] = roundAmount(target[bucket] + amount);
  });

  const tableRows = Array.from(grouped.values()).sort((left, right) => left.code.localeCompare(right.code));
  const totals = tableRows.reduce(
    (accumulator, row) => ({
      total_balance: roundAmount(accumulator.total_balance + row.total_balance),
      not_due: roundAmount(accumulator.not_due + row.not_due),
      bucket_0_30: roundAmount(accumulator.bucket_0_30 + row.bucket_0_30),
      bucket_31_60: roundAmount(accumulator.bucket_31_60 + row.bucket_31_60),
      bucket_61_90: roundAmount(accumulator.bucket_61_90 + row.bucket_61_90),
      bucket_90_plus: roundAmount(accumulator.bucket_90_plus + row.bucket_90_plus),
    }),
    {
      total_balance: 0,
      not_due: 0,
      bucket_0_30: 0,
      bucket_31_60: 0,
      bucket_61_90: 0,
      bucket_90_plus: 0,
    },
  );

  report.parameters = [
    { label: "À la date du", value: asOfDate },
  ];
  report.table = {
    columns: [
      buildColumn("code", isClient ? "Client" : "Fournisseur"),
      buildColumn("name", "Nom"),
      buildColumn("collective_code", "Compte collectif"),
      buildColumn("total_balance", "Solde total", "amount"),
      buildColumn("not_due", "Non échu", "amount"),
      buildColumn("bucket_0_30", "0-30j", "amount"),
      buildColumn("bucket_31_60", "31-60j", "amount"),
      buildColumn("bucket_61_90", "61-90j", "amount"),
      buildColumn("bucket_90_plus", "+90j", "amount"),
    ],
    rows: tableRows,
    emptyMessage: "Aucun encours à analyser.",
  };
  report.summary = [
    makeAmountSummary("Solde total", totals.total_balance),
    makeAmountSummary("Non échu", totals.not_due),
    makeAmountSummary("0-30j", totals.bucket_0_30),
    makeAmountSummary("31-60j", totals.bucket_31_60),
    makeAmountSummary("61-90j", totals.bucket_61_90),
    makeAmountSummary("+90j", totals.bucket_90_plus),
  ];

  return report;
};

const buildCashStateReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "cash-state");
  const range = await resolveDateRange(db, companyId, query);
  let cashRegisterId = parseInteger(query.cash_register_id, 0);

  if (!cashRegisterId) {
    cashRegisterId = await fetchFirstCashRegister(db, companyId);
  }

  if (!cashRegisterId) {
    report.parameters = buildDateParameters(range);
    report.warnings.push({
      tone: "warning",
      message: "Aucune caisse active n'est configurée pour cette société.",
    });
    report.table = {
      columns: [
        buildColumn("date", "Date", "date"),
        buildColumn("piece", "Pièce"),
        buildColumn("label", "Libellé"),
        buildColumn("entry", "Entrée (+)", "amount"),
        buildColumn("exit", "Sortie (-)", "amount"),
        buildColumn("balance", "Solde cumulé", "amount"),
      ],
      rows: [],
      emptyMessage: "Aucune caisse disponible.",
    };
    return report;
  }

  const register = await fetchCashRegisterById(db, companyId, cashRegisterId);

  if (!register) {
    throw new ApiError(404, "Caisse introuvable.");
  }

  const [openingAdjustments, closingAdjustments, movements] = await Promise.all([
    db.query(
      `
        SELECT COALESCE(SUM(CASE WHEN direction = 'credit' THEN amount ELSE -amount END), 0)::numeric AS amount
        FROM cash_movements
        WHERE company_id = $1
          AND cash_register_id = $2
          AND movement_date >= $3::date
      `,
      [companyId, cashRegisterId, range.date_from],
    ),
    db.query(
      `
        SELECT COALESCE(SUM(CASE WHEN direction = 'credit' THEN amount ELSE -amount END), 0)::numeric AS amount
        FROM cash_movements
        WHERE company_id = $1
          AND cash_register_id = $2
          AND movement_date > $3::date
      `,
      [companyId, cashRegisterId, range.date_to],
    ),
    db.query(
      `
        SELECT
          TO_CHAR(cm.movement_date, 'YYYY-MM-DD') AS movement_date,
          cm.piece_ref,
          cm.label,
          cm.direction,
          cm.amount
        FROM cash_movements cm
        WHERE cm.company_id = $1
          AND cm.cash_register_id = $2
          AND cm.movement_date BETWEEN $3::date AND $4::date
        ORDER BY cm.movement_date ASC, cm.id ASC
      `,
      [companyId, cashRegisterId, range.date_from, range.date_to],
    ),
  ]);

  const openingBalance = roundAmount(register.current_balance - Number(openingAdjustments.rows[0]?.amount || 0));
  const closingBalance = roundAmount(register.current_balance - Number(closingAdjustments.rows[0]?.amount || 0));
  let runningBalance = openingBalance;
  const tableRows = movements.rows.map((row) => {
    const amount = roundAmount(row.amount);
    const signedAmount = row.direction === "credit" ? amount : -amount;
    runningBalance = roundAmount(runningBalance + signedAmount);

    return {
      date: row.movement_date,
      piece: row.piece_ref || "",
      label: row.label,
      entry: row.direction === "credit" ? amount : 0,
      exit: row.direction === "debit" ? amount : 0,
      balance: runningBalance,
    };
  });
  const totalEntry = roundAmount(tableRows.reduce((sum, row) => sum + row.entry, 0));
  const totalExit = roundAmount(tableRows.reduce((sum, row) => sum + row.exit, 0));

  report.parameters = [
    ...buildDateParameters(range),
    { label: "Caisse", value: register.name },
  ];
  report.cards = [
    makeAmountSummary("Solde d'ouverture", openingBalance),
    makeAmountSummary("Solde de clôture", closingBalance),
  ];
  report.table = {
    columns: [
      buildColumn("date", "Date", "date"),
      buildColumn("piece", "Pièce"),
      buildColumn("label", "Libellé"),
      buildColumn("entry", "Entrée (+)", "amount"),
      buildColumn("exit", "Sortie (-)", "amount"),
      buildColumn("balance", "Solde cumulé", "amount"),
    ],
    rows: tableRows,
    emptyMessage: "Aucun mouvement de caisse sur la période sélectionnée.",
  };
  report.summary = [
    makeAmountSummary("Total entrées", totalEntry),
    makeAmountSummary("Total sorties", totalExit),
    makeAmountSummary("Solde final", closingBalance),
  ];

  return report;
};

const fetchReconciliationHeader = async (db, companyId, bankAccountId, reconciliationId) => {
  if (reconciliationId) {
    const { rows } = await db.query(
      `
        SELECT
          r.id,
          r.bank_account_id,
          TO_CHAR(r.reconciliation_date, 'YYYY-MM-DD') AS reconciliation_date,
          r.bank_balance,
          r.book_balance,
          r.difference,
          r.status
        FROM reconciliations r
        WHERE r.company_id = $1
          AND r.id = $2
        LIMIT 1
      `,
      [companyId, reconciliationId],
    );

    return rows[0]
      ? {
          id: Number(rows[0].id),
          bank_account_id: Number(rows[0].bank_account_id),
          reconciliation_date: rows[0].reconciliation_date,
          bank_balance: roundAmount(rows[0].bank_balance),
          book_balance: roundAmount(rows[0].book_balance),
          difference: roundAmount(rows[0].difference),
          status: rows[0].status,
        }
      : null;
  }

  const { rows } = await db.query(
    `
      SELECT
        r.id,
        r.bank_account_id,
        TO_CHAR(r.reconciliation_date, 'YYYY-MM-DD') AS reconciliation_date,
        r.bank_balance,
        r.book_balance,
        r.difference,
        r.status
      FROM reconciliations r
      WHERE r.company_id = $1
        AND r.bank_account_id = $2
      ORDER BY
        CASE WHEN r.status = 'open' THEN 0 ELSE 1 END,
        r.reconciliation_date DESC,
        r.id DESC
      LIMIT 1
    `,
    [companyId, bankAccountId],
  );

  return rows[0]
    ? {
        id: Number(rows[0].id),
        bank_account_id: Number(rows[0].bank_account_id),
        reconciliation_date: rows[0].reconciliation_date,
        bank_balance: roundAmount(rows[0].bank_balance),
        book_balance: roundAmount(rows[0].book_balance),
        difference: roundAmount(rows[0].difference),
        status: rows[0].status,
      }
    : null;
};

const buildBankReconciliationStateReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "bank-reconciliation-state");
  let bankAccountId = parseInteger(query.bank_account_id, 0);
  const reconciliationId = parseInteger(query.reconciliation_id, 0);

  if (!bankAccountId && !reconciliationId) {
    bankAccountId = await fetchFirstBankAccountId(db, companyId);
  }

  if (!bankAccountId && !reconciliationId) {
    report.warnings.push({
      tone: "warning",
      message: "Aucun compte bancaire actif n'est configuré pour cette société.",
    });
    report.sections = [
      {
        title: "Section B · Éléments à régulariser (bancaire)",
        columns: [
          buildColumn("date", "Date", "date"),
          buildColumn("operation_number", "N° opération"),
          buildColumn("label", "Libellé"),
          buildColumn("debit", "Débit", "amount"),
          buildColumn("credit", "Crédit", "amount"),
        ],
        rows: [],
        summary: [],
        empty_message: "Aucun compte bancaire disponible.",
      },
      {
        title: "Section D · Éléments à régulariser (comptable)",
        columns: [
          buildColumn("date", "Date", "date"),
          buildColumn("piece_number", "N° Pièce"),
          buildColumn("journal", "Journal"),
          buildColumn("label", "Libellé"),
          buildColumn("debit", "Débit", "amount"),
          buildColumn("credit", "Crédit", "amount"),
        ],
        rows: [],
        summary: [],
        empty_message: "Aucun rapprochement disponible.",
      },
    ];
    return report;
  }

  const reconciliation = await fetchReconciliationHeader(
    db,
    companyId,
    bankAccountId,
    reconciliationId,
  );

  if (!reconciliation) {
    const account = bankAccountId ? await fetchBankAccountById(db, companyId, bankAccountId) : null;

    report.parameters = account
      ? [{ label: "Compte bancaire", value: `${account.bank_name} · ${account.account_number}` }]
      : [];
    report.warnings.push({
      tone: "warning",
      message: "Aucun rapprochement n'est encore disponible pour ce compte bancaire.",
    });
    report.sections = [
      {
        title: "Section B · Éléments à régulariser (bancaire)",
        columns: [
          buildColumn("date", "Date", "date"),
          buildColumn("operation_number", "N° opération"),
          buildColumn("label", "Libellé"),
          buildColumn("debit", "Débit", "amount"),
          buildColumn("credit", "Crédit", "amount"),
        ],
        rows: [],
        summary: [],
        empty_message: "Aucun rapprochement n'est encore disponible.",
      },
      {
        title: "Section D · Éléments à régulariser (comptable)",
        columns: [
          buildColumn("date", "Date", "date"),
          buildColumn("piece_number", "N° Pièce"),
          buildColumn("journal", "Journal"),
          buildColumn("label", "Libellé"),
          buildColumn("debit", "Débit", "amount"),
          buildColumn("credit", "Crédit", "amount"),
        ],
        rows: [],
        summary: [],
        empty_message: "Aucun rapprochement n'est encore disponible.",
      },
    ];
    return report;
  }

  const bankAccount = await fetchBankAccountById(db, companyId, reconciliation.bank_account_id);

  if (!bankAccount) {
    throw new ApiError(404, "Compte bancaire introuvable.");
  }

  const bankMovementConditions =
    reconciliation.status === "closed"
      ? "bm.reconciliation_id = $4"
      : "(bm.reconciliation_id IS NULL OR bm.reconciliation_id = $4)";
  const bankMovementsResult = await db.query(
    `
      SELECT
        bm.id,
        TO_CHAR(bm.movement_date, 'YYYY-MM-DD') AS movement_date,
        bm.operation_number,
        bm.label,
        bm.direction,
        bm.amount,
        bm.reconciliation_id
      FROM bank_movements bm
      WHERE bm.company_id = $1
        AND bm.bank_account_id = $2
        AND bm.movement_date <= $3::date
        AND ${bankMovementConditions}
      ORDER BY bm.movement_date ASC, bm.id ASC
    `,
    [companyId, reconciliation.bank_account_id, reconciliation.reconciliation_date, reconciliation.id],
  );
  const bankMovements = bankMovementsResult.rows.map((row) => {
    const amount = roundAmount(row.amount);
    const balanceEffect = row.direction === "credit" ? amount : -amount;

    return {
      id: Number(row.id),
      date: row.movement_date,
      operation_number: row.operation_number || "",
      label: row.label,
      debit: row.direction === "debit" ? amount : 0,
      credit: row.direction === "credit" ? amount : 0,
      balance_effect: roundAmount(balanceEffect),
      selected: Number(row.reconciliation_id || 0) === reconciliation.id,
    };
  });

  const ledgerLines = [];

  if (bankAccount.account_id) {
    const ledgerConditions =
      reconciliation.status === "closed"
        ? "rel.reconciliation_id = $4"
        : "(rel.entry_line_id IS NULL OR rel.reconciliation_id = $4)";
    const ledgerResult = await db.query(
      `
        SELECT
          el.id,
          TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
          COALESCE(je.piece_ref, je.entry_number) AS piece_number,
          COALESCE(el.label, je.label) AS line_label,
          j.code AS journal_code,
          el.debit,
          el.credit,
          rel.reconciliation_id
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        JOIN journals j
          ON j.id = je.journal_id
        LEFT JOIN reconciliation_entry_lines rel
          ON rel.entry_line_id = el.id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND el.account_id = $2
          AND je.entry_date <= $3::date
          AND ${ledgerConditions}
        ORDER BY je.entry_date ASC, je.id ASC, el.id ASC
      `,
      [companyId, bankAccount.account_id, reconciliation.reconciliation_date, reconciliation.id],
    );

    ledgerResult.rows.forEach((row) => {
      const debit = roundAmount(row.debit);
      const credit = roundAmount(row.credit);

      ledgerLines.push({
        id: Number(row.id),
        date: row.entry_date,
        piece_number: row.piece_number,
        journal: row.journal_code,
        label: row.line_label,
        debit,
        credit,
        balance_effect: getSignedBalance(bankAccount.balance_normal_side, debit, credit),
        selected: Number(row.reconciliation_id || 0) === reconciliation.id,
      });
    });
  }

  const bookBalanceResult = bankAccount.account_id
    ? await db.query(
        `
          SELECT
            COALESCE(SUM(el.debit), 0)::numeric AS total_debit,
            COALESCE(SUM(el.credit), 0)::numeric AS total_credit
          FROM entry_lines el
          JOIN journal_entries je
            ON je.id = el.entry_id
          WHERE je.company_id = $1
            AND je.status = 'validated'
            AND el.account_id = $2
            AND je.entry_date <= $3::date
        `,
        [companyId, bankAccount.account_id, reconciliation.reconciliation_date],
      )
    : { rows: [{ total_debit: 0, total_credit: 0 }] };
  const computedBookBalance = getSignedBalance(
    bankAccount.balance_normal_side,
    roundAmount(bookBalanceResult.rows[0]?.total_debit || 0),
    roundAmount(bookBalanceResult.rows[0]?.total_credit || 0),
  );
  const effectiveBookBalance =
    reconciliation.status === "closed" ? reconciliation.book_balance : computedBookBalance;
  const unmatchedBankRows = bankMovements.filter((item) => !item.selected);
  const unmatchedBookRows = ledgerLines.filter((item) => !item.selected);
  const unmatchedBankAmount = roundAmount(
    unmatchedBankRows.reduce((sum, item) => sum + item.balance_effect, 0),
  );
  const unmatchedBookAmount = roundAmount(
    unmatchedBookRows.reduce((sum, item) => sum + item.balance_effect, 0),
  );
  const reconciledBalance = roundAmount(reconciliation.bank_balance - unmatchedBankAmount);
  const difference =
    reconciliation.status === "closed"
      ? reconciliation.difference
      : roundAmount(
          reconciliation.bank_balance -
            effectiveBookBalance -
            unmatchedBankAmount +
            unmatchedBookAmount,
        );

  report.parameters = [
    { label: "Compte bancaire", value: `${bankAccount.bank_name} · ${bankAccount.account_number}` },
    { label: "Rapprochement", value: reconciliation.reconciliation_date },
    { label: "Statut", value: reconciliation.status === "closed" ? "Clôturé" : "Ouvert" },
  ];
  if (!isAmountZero(difference)) {
    report.warnings.push({
      tone: "danger",
      message: "Le rapprochement présente encore un écart.",
    });
  }
  report.cards = [
    makeAmountSummary("Section A · Solde relevé bancaire", reconciliation.bank_balance),
    makeAmountSummary("Section B · Éléments à régulariser bancaire", unmatchedBankAmount),
    makeAmountSummary("Section C · Solde comptable", effectiveBookBalance),
    makeAmountSummary("Section D · Éléments à régulariser comptable", unmatchedBookAmount),
    makeAmountSummary("Conclusion · Solde rapproché", reconciledBalance),
    makeAmountSummary("Écart", difference),
  ];
  report.sections = [
    {
      title: "Section B · Éléments à régulariser (bancaire)",
      columns: [
        buildColumn("date", "Date", "date"),
        buildColumn("operation_number", "N° opération"),
        buildColumn("label", "Libellé"),
        buildColumn("debit", "Débit", "amount"),
        buildColumn("credit", "Crédit", "amount"),
      ],
      rows: unmatchedBankRows,
      summary: [
        makeAmountSummary("Total à régulariser", unmatchedBankAmount),
      ],
      empty_message: "Aucun élément bancaire à régulariser.",
    },
    {
      title: "Section D · Éléments à régulariser (comptable)",
      columns: [
        buildColumn("date", "Date", "date"),
        buildColumn("piece_number", "N° Pièce"),
        buildColumn("journal", "Journal"),
        buildColumn("label", "Libellé"),
        buildColumn("debit", "Débit", "amount"),
        buildColumn("credit", "Crédit", "amount"),
      ],
      rows: unmatchedBookRows,
      summary: [
        makeAmountSummary("Total à régulariser", unmatchedBookAmount),
      ],
      empty_message: "Aucun élément comptable à régulariser.",
    },
  ];

  return report;
};

const buildAssetsBookReport = async (db, companyId, query) => {
  const report = await buildReportBase(db, companyId, "assets-book");
  const fiscalYearId = parseInteger(query.fiscal_year_id, 0);
  const fiscalYear = fiscalYearId
    ? await fetchFiscalYearById(db, companyId, fiscalYearId)
    : await fetchDefaultFiscalYear(db, companyId);

  if (!fiscalYear) {
    throw new ApiError(404, "Exercice introuvable.");
  }

  const asOfDate = fiscalYear.is_current ? minIsoDate(TODAY, fiscalYear.end_date) : fiscalYear.end_date;
  const assetsResult = await db.query(
    `
      SELECT
        a.code,
        a.label,
        TO_CHAR(a.acquisition_date, 'YYYY-MM-DD') AS acquisition_date,
        a.acquisition_value,
        a.duration_years,
        a.depreciation_method,
        ac.code AS category_code,
        ac.label AS category_label,
        COALESCE(ex.exercise_amount, 0)::numeric AS exercise_amount,
        COALESCE(cum.cumulated_amount, 0)::numeric AS cumulated_amount
      FROM assets a
      JOIN asset_categories ac
        ON ac.id = a.category_id
      LEFT JOIN LATERAL (
        SELECT SUM(dl.amount_depreciation) AS exercise_amount
        FROM depreciation_lines dl
        WHERE dl.asset_id = a.id
          AND dl.fiscal_year_id = $2
      ) ex
        ON TRUE
      LEFT JOIN LATERAL (
        SELECT SUM(dl.amount_depreciation) AS cumulated_amount
        FROM depreciation_lines dl
        WHERE dl.asset_id = a.id
          AND dl.period_date <= $3::date
      ) cum
        ON TRUE
      WHERE a.company_id = $1
        AND a.acquisition_date <= $3::date
      ORDER BY ac.code ASC, a.code ASC
    `,
    [companyId, fiscalYear.id, asOfDate],
  );

  const grouped = groupBy(assetsResult.rows, (row) => row.category_code);
  const sections = [];
  const grandTotals = {
    acquisition_value: 0,
    exercise_amount: 0,
    cumulated_amount: 0,
    net_book_value: 0,
  };

  Array.from(grouped.keys())
    .sort()
    .forEach((categoryCode, index, array) => {
      const rows = grouped.get(categoryCode);
      const tableRows = rows.map((row) => {
        const acquisitionValue = roundAmount(row.acquisition_value);
        const exerciseAmount = roundAmount(row.exercise_amount);
        const cumulatedAmount = roundAmount(row.cumulated_amount);
        const linearRate = Number((100 / Math.max(1, Number(row.duration_years))).toFixed(2));
        const rate = roundAmount(
          row.depreciation_method === "degressive"
            ? Math.min(100, linearRate * 1.75)
            : linearRate,
        );
        const netBookValue = roundAmount(acquisitionValue - cumulatedAmount);

        grandTotals.acquisition_value = roundAmount(grandTotals.acquisition_value + acquisitionValue);
        grandTotals.exercise_amount = roundAmount(grandTotals.exercise_amount + exerciseAmount);
        grandTotals.cumulated_amount = roundAmount(grandTotals.cumulated_amount + cumulatedAmount);
        grandTotals.net_book_value = roundAmount(grandTotals.net_book_value + netBookValue);

        return {
          code: row.code,
          label: row.label,
          acquisition_date: row.acquisition_date,
          acquisition_value: acquisitionValue,
          rate,
          exercise_amount: exerciseAmount,
          cumulated_amount: cumulatedAmount,
          net_book_value: netBookValue,
        };
      });

      const subtotal = tableRows.reduce(
        (accumulator, row) => ({
          acquisition_value: roundAmount(accumulator.acquisition_value + row.acquisition_value),
          exercise_amount: roundAmount(accumulator.exercise_amount + row.exercise_amount),
          cumulated_amount: roundAmount(accumulator.cumulated_amount + row.cumulated_amount),
          net_book_value: roundAmount(accumulator.net_book_value + row.net_book_value),
        }),
        { acquisition_value: 0, exercise_amount: 0, cumulated_amount: 0, net_book_value: 0 },
      );

      sections.push({
        title: `${rows[0].category_code} · ${rows[0].category_label}`,
        columns: [
          buildColumn("code", "Code"),
          buildColumn("label", "Libellé"),
          buildColumn("acquisition_date", "Date acq.", "date"),
          buildColumn("acquisition_value", "Valeur origine", "amount"),
          buildColumn("rate", "Taux", "amount"),
          buildColumn("exercise_amount", "Amort exercice", "amount"),
          buildColumn("cumulated_amount", "Amort cumulé", "amount"),
          buildColumn("net_book_value", "VNC", "amount"),
        ],
        rows: tableRows,
        summary: [
          makeAmountSummary("Valeur origine", subtotal.acquisition_value),
          makeAmountSummary("Amort exercice", subtotal.exercise_amount),
          makeAmountSummary("Amort cumulé", subtotal.cumulated_amount),
          makeAmountSummary("VNC", subtotal.net_book_value),
        ],
        page_break_after: index < array.length - 1,
      });
    });

  report.parameters = [
    { label: "Exercice", value: fiscalYear.label },
    { label: "À la date du", value: asOfDate },
  ];
  report.sections = sections;
  report.summary = [
    makeAmountSummary("Valeur origine", grandTotals.acquisition_value),
    makeAmountSummary("Amort exercice", grandTotals.exercise_amount),
    makeAmountSummary("Amort cumulé", grandTotals.cumulated_amount),
    makeAmountSummary("VNC", grandTotals.net_book_value),
  ];

  return report;
};

const buildBalanceAgedTitle = (isClient) =>
  isClient ? "Balance Âgée Clients" : "Balance Âgée Fournisseurs";

const buildCashOrBankRangeParameters = (range, label, value) => [
  ...buildDateParameters(range),
  { label, value },
];

const buildBankOrCashEmptyMessage = (type) =>
  type === "cash"
    ? "Aucun mouvement de caisse sur la période sélectionnée."
    : "Aucun mouvement bancaire sur la période sélectionnée.";

const buildReportByKey = async (db, companyId, reportKey, query) => {
  switch (reportKey) {
    case "brouillard":
      return buildBrouillardReport(db, companyId, query);
    case "journal":
      return buildJournalReport(db, companyId, query);
    case "grand-livre":
      return buildGrandLivreReport(db, companyId, query);
    case "balance":
      return buildBalanceReport(db, companyId, query);
    case "balance-auxiliaire":
      return buildAuxiliaryBalanceReport(db, companyId, query);
    case "aged-clients": {
      const report = await buildAgedBalanceReport(db, companyId, query, reportKey);
      report.title = buildBalanceAgedTitle(true);
      return report;
    }
    case "aged-suppliers": {
      const report = await buildAgedBalanceReport(db, companyId, query, reportKey);
      report.title = buildBalanceAgedTitle(false);
      return report;
    }
    case "cash-state":
      return buildCashStateReport(db, companyId, query);
    case "bank-reconciliation-state":
      return buildBankReconciliationStateReport(db, companyId, query);
    case "assets-book":
      return buildAssetsBookReport(db, companyId, query);
    default:
      throw new ApiError(404, "Rapport introuvable.");
  }
};

const buildFileName = (report, extension) =>
  `${cleanString(report.export_filename || report.report_key || "rapport")
    .toLowerCase()
    .replace(/[^a-z0-9-_]+/gi, "-")}.${extension}`;

const sendExport = async (res, report, exportType) => {
  if (exportType === "pdf") {
    const buffer = await exportReportAsPdf(report);
    res.setHeader("Content-Type", "application/pdf");
    res.setHeader("Content-Disposition", `attachment; filename="${buildFileName(report, "pdf")}"`);
    res.send(Buffer.from(buffer));
    return;
  }

  if (exportType === "excel" || exportType === "xlsx") {
    const buffer = await exportReportAsExcel(report);
    res.setHeader(
      "Content-Type",
      "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
    );
    res.setHeader("Content-Disposition", `attachment; filename="${buildFileName(report, "xlsx")}"`);
    res.send(Buffer.from(buffer));
    return;
  }

  throw new ApiError(400, "Format d'export non supporté.");
};

export const reportsController = {
  list: async (req, res) => {
    const companyId = getCompanyId(req);
    const company = await fetchCompany(pool, companyId);

    res.json({
      company_id: companyId,
      company,
      items: REPORT_CATALOG.map((item) => ({
        key: item.key,
        path: item.path,
        title: item.title,
        description: item.description,
      })),
    });
  },

  get: async (req, res) => {
    const companyId = getCompanyId(req);
    const reportKey = cleanString(req.params.reportKey).toLowerCase();
    const exportType = cleanString(req.query.export || req.query.format).toLowerCase();
    const report = await buildReportByKey(pool, companyId, reportKey, req.query);
    report.generated_by = req.user?.email || req.user?.sub || "Système";

    if (exportType) {
      await sendExport(res, report, exportType);
      return;
    }

    res.json(report);
  },
};
