import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { logAudit } from "../utils/auditLog.js";
import { formatDzdAmount, toNumber } from "../utils/formatters.js";

const MATCHING_ADMIN_ROLES = new Set(["super_admin", "manager", "chief_accountant"]);
const MATCHING_TYPES = {
  client: {
    thirdPartyType: "client",
    queryKey: "client_id",
    route: "clients",
    label: "Client",
    pluralLabel: "Clients",
  },
  supplier: {
    thirdPartyType: "supplier",
    queryKey: "supplier_id",
    route: "suppliers",
    label: "Fournisseur",
    pluralLabel: "Fournisseurs",
  },
};
const LETTERING_TOLERANCE = 0.01;

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

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 roundAmount = (value) => Number(toNumber(value).toFixed(2));

const normalizeMatchingType = (value) => {
  const normalized = cleanString(value).toLowerCase();

  if (!MATCHING_TYPES[normalized]) {
    throw new ApiError(400, "Type de lettrage invalide.");
  }

  return normalized;
};

const getTypeConfig = (type) => {
  const config = MATCHING_TYPES[type];

  if (!config) {
    throw new ApiError(400, "Type de lettrage invalide.");
  }

  return config;
};

const getBalanceExpression = (type, lineAlias = "el") =>
  type === "client"
    ? `${lineAlias}.debit - ${lineAlias}.credit`
    : `${lineAlias}.credit - ${lineAlias}.debit`;

const formatSignedBalance = (value) => formatDzdAmount(roundAmount(value));

const getBalanceTone = (value) => {
  const amount = roundAmount(value);

  if (amount > 0) {
    return "positive";
  }

  if (amount < 0) {
    return "negative";
  }

  return "neutral";
};

const getTypeLabel = (row) => (toNumber(row.debit) > 0 ? "Débit" : "Crédit");

const parseFiscalYearId = (value) => {
  if (value === null || value === undefined || value === "") {
    return null;
  }

  const parsed = Number(value);

  if (!parsed) {
    throw new ApiError(400, "Identifiant d'exercice invalide.");
  }

  return parsed;
};

const encodeLetterIndex = (index) => {
  let value = Number(index);
  let code = "";

  while (value >= 0) {
    code = String.fromCharCode(65 + (value % 26)) + code;
    value = Math.floor(value / 26) - 1;
  }

  return code;
};

const decodeLetterCode = (code) => {
  const normalized = cleanString(code).toUpperCase();

  if (!/^[A-Z]+$/.test(normalized)) {
    return -1;
  }

  let value = 0;

  for (const character of normalized) {
    value = value * 26 + (character.charCodeAt(0) - 64);
  }

  return value - 1;
};

const fetchUserPermissions = async (db, role) => {
  if (!role) {
    return [];
  }

  const { rows } = await db.query(
    `
      SELECT permissions
      FROM roles
      WHERE name = $1
      LIMIT 1
    `,
    [role],
  );

  const permissions = rows[0]?.permissions;

  return Array.isArray(permissions) ? permissions : [];
};

const canManageUnletter = (role, permissions) =>
  MATCHING_ADMIN_ROLES.has(role) ||
  permissions.includes("*") ||
  permissions.includes("matching_admin");

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

  const balanceExpression = getBalanceExpression(type);
  const { rows } = await db.query(
    `
      WITH stats AS (
        SELECT
          aa.id,
          COUNT(el.id) FILTER (
            WHERE je.status = 'validated'
              AND COALESCE(el.is_lettered, FALSE) = FALSE
              AND ($4::bigint IS NULL OR je.fiscal_year_id = $4)
          )::int AS unmatched_lines_count,
          COALESCE(
            SUM(
              CASE
                WHEN je.status = 'validated'
                  AND COALESCE(el.is_lettered, FALSE) = FALSE
                  AND ($4::bigint IS NULL OR je.fiscal_year_id = $4)
                  THEN ${balanceExpression}
                ELSE 0
              END
            ),
            0
          )::numeric AS unmatched_balance
        FROM auxiliary_accounts aa
        LEFT JOIN entry_lines el
          ON el.auxiliary_account_id = aa.id
        LEFT JOIN journal_entries je
          ON je.id = el.entry_id
        WHERE aa.company_id = $1
          AND aa.third_party_type = $2
          AND aa.id = $3
        GROUP BY aa.id
      )
      SELECT
        aa.id,
        aa.ref_code,
        aa.name,
        aa.third_party_type,
        a.code AS collective_account_code,
        a.label AS collective_account_label,
        COALESCE(stats.unmatched_lines_count, 0)::int AS unmatched_lines_count,
        COALESCE(stats.unmatched_balance, 0)::numeric AS unmatched_balance
      FROM auxiliary_accounts aa
      LEFT JOIN accounts a
        ON a.id = aa.account_id
      LEFT JOIN stats
        ON stats.id = aa.id
      WHERE aa.company_id = $1
        AND aa.third_party_type = $2
        AND aa.id = $3
      LIMIT 1
    `,
    [companyId, getTypeConfig(type).thirdPartyType, auxiliaryAccountId, fiscalYearId],
  );

  if (!rows[0]) {
    return null;
  }

  return {
    id: Number(rows[0].id),
    ref_code: rows[0].ref_code,
    name: rows[0].name,
    third_party_type: rows[0].third_party_type,
    collective_account_code: rows[0].collective_account_code,
    collective_account_label: rows[0].collective_account_label,
    unmatched_lines_count: Number(rows[0].unmatched_lines_count || 0),
    unmatched_balance: roundAmount(rows[0].unmatched_balance),
    formatted_unmatched_balance: formatSignedBalance(rows[0].unmatched_balance),
    unmatched_balance_tone: getBalanceTone(rows[0].unmatched_balance),
  };
};

const fetchAuxiliaryOptions = async (
  db,
  companyId,
  type,
  fiscalYearId,
  selectedAuxiliaryId = null,
) => {
  const balanceExpression = getBalanceExpression(type);
  const config = getTypeConfig(type);
  const { rows } = await db.query(
    `
      WITH stats AS (
        SELECT
          aa.id,
          aa.ref_code,
          aa.name,
          COUNT(el.id) FILTER (
            WHERE je.status = 'validated'
              AND COALESCE(el.is_lettered, FALSE) = FALSE
              AND ($3::bigint IS NULL OR je.fiscal_year_id = $3)
          )::int AS unmatched_lines_count,
          COALESCE(
            SUM(
              CASE
                WHEN je.status = 'validated'
                  AND COALESCE(el.is_lettered, FALSE) = FALSE
                  AND ($3::bigint IS NULL OR je.fiscal_year_id = $3)
                  THEN ${balanceExpression}
                ELSE 0
              END
            ),
            0
          )::numeric AS unmatched_balance
        FROM auxiliary_accounts aa
        LEFT JOIN entry_lines el
          ON el.auxiliary_account_id = aa.id
        LEFT JOIN journal_entries je
          ON je.id = el.entry_id
        WHERE aa.company_id = $1
          AND aa.third_party_type = $2
        GROUP BY aa.id, aa.ref_code, aa.name
      )
      SELECT
        id,
        ref_code,
        name,
        unmatched_lines_count,
        unmatched_balance
      FROM stats
      WHERE unmatched_lines_count > 0
         OR id = COALESCE($4::bigint, -1)
      ORDER BY
        CASE WHEN id = COALESCE($4::bigint, -1) THEN 0 ELSE 1 END,
        ABS(unmatched_balance) DESC,
        name ASC,
        ref_code ASC
    `,
    [companyId, config.thirdPartyType, fiscalYearId, selectedAuxiliaryId],
  );

  const seen = new Set();
  const items = rows
    .map((row) => ({
      id: Number(row.id),
      ref_code: row.ref_code,
      name: row.name,
      unmatched_lines_count: Number(row.unmatched_lines_count || 0),
      unmatched_balance: roundAmount(row.unmatched_balance),
      formatted_unmatched_balance: formatSignedBalance(row.unmatched_balance),
      unmatched_balance_tone: getBalanceTone(row.unmatched_balance),
    }))
    .filter((row) => {
      if (seen.has(row.id)) {
        return false;
      }

      seen.add(row.id);
      return true;
    });

  if (
    selectedAuxiliaryId &&
    !items.some((item) => Number(item.id) === Number(selectedAuxiliaryId))
  ) {
    const selectedSummary = await fetchAuxiliarySummaryById(
      db,
      companyId,
      type,
      fiscalYearId,
      selectedAuxiliaryId,
    );

    if (selectedSummary) {
      items.unshift(selectedSummary);
    }
  }

  return items;
};

const fetchUnmatchedLines = async (db, companyId, type, auxiliaryAccountId, fiscalYearId) => {
  if (!auxiliaryAccountId) {
    return [];
  }

  const balanceExpression = getBalanceExpression(type);
  const { rows } = await db.query(
    `
      SELECT
        el.id,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.entry_number,
        je.piece_ref,
        j.code AS journal_code,
        j.label AS journal_label,
        COALESCE(NULLIF(el.label, ''), je.label) AS label,
        el.debit,
        el.credit,
        ${balanceExpression}::numeric AS balance
      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 el.auxiliary_account_id = $2
        AND je.status = 'validated'
        AND COALESCE(el.is_lettered, FALSE) = FALSE
        AND ($3::bigint IS NULL OR je.fiscal_year_id = $3)
      ORDER BY je.entry_date ASC, je.id ASC, el.id ASC
    `,
    [companyId, auxiliaryAccountId, fiscalYearId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    entry_date: row.entry_date,
    entry_number: row.entry_number,
    piece_ref: row.piece_ref,
    journal_code: row.journal_code,
    journal_label: row.journal_label,
    label: row.label,
    debit: roundAmount(row.debit),
    credit: roundAmount(row.credit),
    formatted_debit: formatDzdAmount(row.debit),
    formatted_credit: formatDzdAmount(row.credit),
    balance: roundAmount(row.balance),
    formatted_balance: formatSignedBalance(row.balance),
    balance_tone: getBalanceTone(row.balance),
  }));
};

const fetchLetteredGroups = async (db, companyId, auxiliaryAccountId, fiscalYearId) => {
  if (!auxiliaryAccountId) {
    return [];
  }

  const { rows } = await db.query(
    `
      SELECT
        m.id AS matching_id,
        m.letter_code,
        m.matched_at,
        COALESCE(u.full_name, u.email, 'Système') AS matched_by_name,
        el.id AS entry_line_id,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.entry_number,
        je.piece_ref,
        j.code AS journal_code,
        COALESCE(NULLIF(el.label, ''), je.label) AS label,
        el.debit,
        el.credit
      FROM matching m
      JOIN matching_lines ml
        ON ml.matching_id = m.id
      JOIN entry_lines el
        ON el.id = ml.entry_line_id
      JOIN journal_entries je
        ON je.id = el.entry_id
      JOIN journals j
        ON j.id = je.journal_id
      LEFT JOIN users u
        ON u.id = m.matched_by
      WHERE m.company_id = $1
        AND m.auxiliary_account_id = $2
        AND ($3::bigint IS NULL OR je.fiscal_year_id = $3)
      ORDER BY m.matched_at DESC, m.id DESC, je.entry_date ASC, el.id ASC
    `,
    [companyId, auxiliaryAccountId, fiscalYearId],
  );

  const groups = [];
  const byId = new Map();

  rows.forEach((row) => {
    const matchingId = Number(row.matching_id);

    if (!byId.has(matchingId)) {
      const nextGroup = {
        id: matchingId,
        letter_code: row.letter_code,
        matched_at: row.matched_at?.toISOString?.() || row.matched_at,
        matched_by_name: row.matched_by_name,
        total_debit: 0,
        total_credit: 0,
        items: [],
      };

      byId.set(matchingId, nextGroup);
      groups.push(nextGroup);
    }

    const group = byId.get(matchingId);
    const debit = roundAmount(row.debit);
    const credit = roundAmount(row.credit);

    group.total_debit = roundAmount(group.total_debit + debit);
    group.total_credit = roundAmount(group.total_credit + credit);
    group.items.push({
      entry_line_id: Number(row.entry_line_id),
      entry_date: row.entry_date,
      entry_number: row.entry_number,
      piece_ref: row.piece_ref,
      journal_code: row.journal_code,
      label: row.label,
      debit,
      credit,
      formatted_debit: formatDzdAmount(row.debit),
      formatted_credit: formatDzdAmount(row.credit),
    });
  });

  return groups.map((group) => ({
    ...group,
    formatted_total_debit: formatDzdAmount(group.total_debit),
    formatted_total_credit: formatDzdAmount(group.total_credit),
  }));
};

const fetchAuxiliaryLinesForMatching = async (db, companyId, entryLineIds) => {
  const normalizedLineIds = [...new Set(entryLineIds.map((value) => Number(value)).filter(Boolean))];

  if (!normalizedLineIds.length) {
    throw new ApiError(400, "Aucune ligne sélectionnée pour le lettrage.");
  }

  const { rows } = await db.query(
    `
      SELECT
        el.id,
        el.entry_id,
        el.auxiliary_account_id,
        el.debit,
        el.credit,
        COALESCE(el.is_lettered, FALSE) AS is_lettered,
        el.letter_code,
        COALESCE(NULLIF(el.label, ''), je.label) AS label,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.company_id,
        je.status,
        je.entry_number,
        je.piece_ref,
        j.code AS journal_code,
        aa.third_party_type,
        aa.ref_code,
        aa.name
      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 auxiliary_accounts aa
        ON aa.id = el.auxiliary_account_id
      WHERE el.id = ANY($1::bigint[])
        AND je.company_id = $2
      ORDER BY je.entry_date ASC, el.id ASC
    `,
    [normalizedLineIds, companyId],
  );

  return {
    lineIds: normalizedLineIds,
    rows,
  };
};

const fetchNextLetterCode = async (db, companyId, auxiliaryAccountId) => {
  const { rows } = await db.query(
    `
      SELECT letter_code
      FROM matching
      WHERE company_id = $1
        AND auxiliary_account_id = $2
    `,
    [companyId, auxiliaryAccountId],
  );

  const nextIndex =
    rows.reduce((highestIndex, row) => Math.max(highestIndex, decodeLetterCode(row.letter_code)), -1) +
    1;

  return encodeLetterIndex(nextIndex);
};

const buildSelectionSummary = (rows) => {
  const totalDebit = roundAmount(rows.reduce((sum, row) => sum + toNumber(row.debit), 0));
  const totalCredit = roundAmount(rows.reduce((sum, row) => sum + toNumber(row.credit), 0));
  const difference = roundAmount(totalDebit - totalCredit);

  return {
    total_debit: totalDebit,
    total_credit: totalCredit,
    difference,
    formatted_total_debit: formatDzdAmount(totalDebit),
    formatted_total_credit: formatDzdAmount(totalCredit),
    formatted_difference: formatSignedBalance(difference),
    is_balanced: Math.abs(difference) <= LETTERING_TOLERANCE,
  };
};

const validateLetteringRows = ({ rows, lineIds, companyId, type }) => {
  if (rows.length !== lineIds.length) {
    throw new ApiError(400, "Une ou plusieurs lignes sélectionnées sont introuvables.");
  }

  if (rows.length < 2) {
    throw new ApiError(400, "Le lettrage nécessite au moins deux lignes.");
  }

  const auxiliaryIds = new Set();

  rows.forEach((row) => {
    if (Number(row.company_id) !== Number(companyId)) {
      throw new ApiError(403, "Une ligne appartient à une autre société.");
    }

    if (row.status !== "validated") {
      throw new ApiError(409, "Seules les écritures validées peuvent être lettrées.");
    }

    if (!row.auxiliary_account_id) {
      throw new ApiError(400, "Chaque ligne doit être rattachée à un tiers.");
    }

    if (row.third_party_type !== getTypeConfig(type).thirdPartyType) {
      throw new ApiError(400, "Les lignes sélectionnées ne correspondent pas au type de tiers demandé.");
    }

    if (row.is_lettered) {
      throw new ApiError(409, "Une des lignes sélectionnées est déjà lettrée.");
    }

    auxiliaryIds.add(Number(row.auxiliary_account_id));
  });

  if (auxiliaryIds.size !== 1) {
    throw new ApiError(400, "Les lignes sélectionnées doivent appartenir au même tiers.");
  }

  const summary = buildSelectionSummary(rows);

  if (!summary.is_balanced) {
    throw new ApiError(
      400,
      "Le total débit sélectionné doit être égal au total crédit sélectionné.",
    );
  }

  if (summary.total_debit === 0 || summary.total_credit === 0) {
    throw new ApiError(400, "Le lettrage nécessite au moins un débit et un crédit.");
  }

  return {
    auxiliary_account_id: Number(rows[0].auxiliary_account_id),
    summary,
  };
};

const mapUnmatchedReportRow = (row) => ({
  entry_line_id: Number(row.entry_line_id),
  auxiliary_account_id: Number(row.auxiliary_account_id),
  ref_code: row.ref_code,
  name: row.name,
  third_party_type: row.third_party_type,
  entry_date: row.entry_date,
  effective_date: row.effective_date,
  piece_ref: row.piece_ref,
  entry_number: row.entry_number,
  journal_code: row.journal_code,
  label: row.label,
  type: row.line_type,
  type_label: row.line_type === "debit" ? "Débit" : "Crédit",
  amount: roundAmount(row.amount),
  formatted_amount: formatDzdAmount(row.amount),
  age_days: Number(row.age_days || 0),
});

const fetchAutoProposalLines = async (db, companyId, auxiliaryAccountId) => {
  const { rows } = await db.query(
    `
      SELECT
        el.id,
        el.debit,
        el.credit,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      WHERE je.company_id = $1
        AND el.auxiliary_account_id = $2
        AND je.status = 'validated'
        AND COALESCE(el.is_lettered, FALSE) = FALSE
      ORDER BY je.entry_date ASC, el.id ASC
    `,
    [companyId, auxiliaryAccountId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    debit: roundAmount(row.debit),
    credit: roundAmount(row.credit),
    amount:
      toNumber(row.debit) > 0
        ? roundAmount(row.debit)
        : roundAmount(row.credit),
  }));
};

const buildAutoProposal = (rows) => {
  const debitQueues = new Map();
  const creditQueues = new Map();

  rows.forEach((row) => {
    const amountInCents = Math.round(row.amount * 100);
    const targetMap = row.debit > 0 ? debitQueues : creditQueues;

    if (!targetMap.has(amountInCents)) {
      targetMap.set(amountInCents, []);
    }

    targetMap.get(amountInCents).push(row);
  });

  const selectedIds = [];
  const groups = [];

  debitQueues.forEach((debitRows, amountKey) => {
    const creditRows = creditQueues.get(amountKey) || [];
    const pairsCount = Math.min(debitRows.length, creditRows.length);

    for (let index = 0; index < pairsCount; index += 1) {
      selectedIds.push(debitRows[index].id, creditRows[index].id);
      groups.push({
        debit_line_id: debitRows[index].id,
        credit_line_id: creditRows[index].id,
        amount: roundAmount(amountKey / 100),
        formatted_amount: formatDzdAmount(amountKey / 100),
      });
    }
  });

  return {
    entry_line_ids: selectedIds,
    groups,
  };
};

const fetchMatchingView = async (req, res, type) => {
  const companyId = getCompanyId(req);
  const fiscalYearId = parseFiscalYearId(req.query.fiscal_year_id);
  const config = getTypeConfig(type);
  const selectedAuxiliaryId = Number(req.query[config.queryKey] || 0) || null;
  const permissions = await fetchUserPermissions(pool, req.user?.role);
  const selectorOptions = await fetchAuxiliaryOptions(
    pool,
    companyId,
    type,
    fiscalYearId,
    selectedAuxiliaryId,
  );
  const selectedAuxiliary = selectedAuxiliaryId
    ? await fetchAuxiliarySummaryById(pool, companyId, type, fiscalYearId, selectedAuxiliaryId)
    : null;

  if (selectedAuxiliaryId && !selectedAuxiliary) {
    throw new ApiError(404, `${config.label} introuvable.`);
  }

  const [unmatchedItems, letteredGroups] = selectedAuxiliaryId
    ? await Promise.all([
        fetchUnmatchedLines(pool, companyId, type, selectedAuxiliaryId, fiscalYearId),
        fetchLetteredGroups(pool, companyId, selectedAuxiliaryId, fiscalYearId),
      ])
    : [[], []];

  res.json({
    company_id: companyId,
    fiscal_year_id: fiscalYearId,
    type,
    selector_label: config.label,
    selector_options: selectorOptions,
    selected_auxiliary: selectedAuxiliary,
    unmatched_items: unmatchedItems,
    lettered_groups: letteredGroups,
    permissions: {
      can_unletter: canManageUnletter(req.user?.role, permissions),
    },
  });
};

export const matchingController = {
  getClients: async (req, res) => {
    await fetchMatchingView(req, res, "client");
  },

  getSuppliers: async (req, res) => {
    await fetchMatchingView(req, res, "supplier");
  },

  letter: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const type = normalizeMatchingType(req.body.type);
    const entryLineIds = Array.isArray(req.body.entry_line_ids)
      ? req.body.entry_line_ids.map((value) => Number(value)).filter(Boolean)
      : [];
    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      const { lineIds, rows } = await fetchAuxiliaryLinesForMatching(
        client,
        companyId,
        entryLineIds,
      );
      const validation = validateLetteringRows({
        rows,
        lineIds,
        companyId,
        type,
      });
      const nextLetterCode = await fetchNextLetterCode(
        client,
        companyId,
        validation.auxiliary_account_id,
      );
      const { rows: matchingRows } = await client.query(
        `
          INSERT INTO matching (
            company_id,
            auxiliary_account_id,
            letter_code,
            matched_by
          )
          VALUES ($1, $2, $3, $4)
          RETURNING id, matched_at
        `,
        [companyId, validation.auxiliary_account_id, nextLetterCode, req.user?.sub || null],
      );
      const matchingId = Number(matchingRows[0].id);

      await client.query(
        `
          INSERT INTO matching_lines (matching_id, entry_line_id)
          SELECT $1, UNNEST($2::bigint[])
        `,
        [matchingId, lineIds],
      );

      await client.query(
        `
          UPDATE entry_lines
          SET
            is_lettered = TRUE,
            letter_code = $2
          WHERE id = ANY($1::bigint[])
        `,
        [lineIds, nextLetterCode],
      );

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "matching.letter",
        entityType: "matching",
        entityId: matchingId,
        newData: {
          type,
          auxiliary_account_id: validation.auxiliary_account_id,
          entry_line_ids: lineIds,
          letter_code: nextLetterCode,
          total_debit: validation.summary.total_debit,
          total_credit: validation.summary.total_credit,
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Éléments lettrés avec succès.",
        matching: {
          id: matchingId,
          auxiliary_account_id: validation.auxiliary_account_id,
          letter_code: nextLetterCode,
          matched_at: matchingRows[0].matched_at?.toISOString?.() || matchingRows[0].matched_at,
          lines_count: lineIds.length,
        },
        summary: validation.summary,
      });
    } catch (error) {
      await client.query("ROLLBACK");

      if (error?.code === "23505") {
        throw new ApiError(409, "Une des lignes sélectionnées est déjà utilisée dans un autre lettrage.");
      }

      throw error;
    } finally {
      client.release();
    }
  },

  autoPropose: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    const auxiliaryAccountId = Number(req.body.auxiliary_account_id || 0);

    if (!auxiliaryAccountId) {
      throw new ApiError(400, "Le tiers est obligatoire pour la proposition automatique.");
    }

    const rows = await fetchAutoProposalLines(pool, companyId, auxiliaryAccountId);
    const proposal = buildAutoProposal(rows);
    const selectedRows = rows.filter((row) => proposal.entry_line_ids.includes(row.id));
    const summary = buildSelectionSummary(selectedRows);

    await logAudit(pool, {
      companyId,
      userId: req.user?.sub || null,
      action: "matching.auto_propose",
      entityType: "auxiliary_account",
      entityId: auxiliaryAccountId,
      newData: {
        proposed_entry_line_ids: proposal.entry_line_ids,
        groups_count: proposal.groups.length,
      },
      ipAddress: req.ip,
    });

    res.json({
      message: proposal.entry_line_ids.length
        ? "Proposition automatique générée."
        : "Aucune proposition automatique disponible.",
      auxiliary_account_id: auxiliaryAccountId,
      entry_line_ids: proposal.entry_line_ids,
      groups: proposal.groups,
      summary,
    });
  },

  remove: async (req, res) => {
    const companyId = getCompanyId(req);
    const matchingId = Number(req.params.matching_id || 0);

    if (!matchingId) {
      throw new ApiError(400, "Identifiant de lettrage invalide.");
    }

    const client = await pool.connect();

    try {
      await client.query("BEGIN");

      const permissions = await fetchUserPermissions(client, req.user?.role);

      if (!canManageUnletter(req.user?.role, permissions)) {
        throw new ApiError(403, "Vous n'êtes pas autorisé à délettrer ces éléments.");
      }

      const { rows } = await client.query(
        `
          SELECT
            m.id,
            m.auxiliary_account_id,
            m.letter_code,
            ARRAY_AGG(ml.entry_line_id ORDER BY ml.entry_line_id) AS entry_line_ids
          FROM matching m
          JOIN matching_lines ml
            ON ml.matching_id = m.id
          WHERE m.company_id = $1
            AND m.id = $2
          GROUP BY m.id
          LIMIT 1
        `,
        [companyId, matchingId],
      );

      if (!rows[0]) {
        throw new ApiError(404, "Groupe de lettrage introuvable.");
      }

      const matching = rows[0];

      await client.query(
        `
          UPDATE entry_lines
          SET
            is_lettered = FALSE,
            letter_code = NULL
          WHERE id = ANY($1::bigint[])
        `,
        [matching.entry_line_ids],
      );

      await client.query("DELETE FROM matching WHERE id = $1", [matchingId]);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "matching.unletter",
        entityType: "matching",
        entityId: matchingId,
        oldData: {
          auxiliary_account_id: Number(matching.auxiliary_account_id),
          letter_code: matching.letter_code,
          entry_line_ids: matching.entry_line_ids.map((value) => Number(value)),
        },
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Le groupe de lettrage a été supprimé.",
        matching: {
          id: matchingId,
          auxiliary_account_id: Number(matching.auxiliary_account_id),
          letter_code: matching.letter_code,
        },
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  getUnmatched: async (req, res) => {
    const companyId = getCompanyId(req);
    const type = normalizeMatchingType(req.query.type || "client");
    const config = getTypeConfig(type);
    const amountExpression =
      type === "client" ? "CASE WHEN el.debit > 0 THEN el.debit ELSE el.credit END" : "CASE WHEN el.credit > 0 THEN el.credit ELSE el.debit END";

    const { rows } = await pool.query(
      `
        SELECT
          el.id AS entry_line_id,
          aa.id AS auxiliary_account_id,
          aa.ref_code,
          aa.name,
          aa.third_party_type,
          TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
          TO_CHAR(COALESCE(el.due_date, je.entry_date + COALESCE(aa.payment_terms, 0)), 'YYYY-MM-DD') AS effective_date,
          je.piece_ref,
          je.entry_number,
          j.code AS journal_code,
          COALESCE(NULLIF(el.label, ''), je.label) AS label,
          el.debit,
          el.credit,
          ${amountExpression}::numeric AS amount,
          CASE WHEN el.debit > 0 THEN 'debit' ELSE 'credit' END AS line_type,
          GREATEST((CURRENT_DATE - COALESCE(el.due_date, je.entry_date + COALESCE(aa.payment_terms, 0))), 0)::int AS age_days
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        JOIN journals j
          ON j.id = je.journal_id
        JOIN auxiliary_accounts aa
          ON aa.id = el.auxiliary_account_id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND aa.third_party_type = $2
          AND COALESCE(el.is_lettered, FALSE) = FALSE
        ORDER BY age_days DESC, je.entry_date ASC, el.id ASC
      `,
      [companyId, config.thirdPartyType],
    );

    const items = rows.map(mapUnmatchedReportRow);

    res.json({
      company_id: companyId,
      type,
      title: `Éléments non lettrés ${config.pluralLabel.toLowerCase()}`,
      items,
      summary: {
        count: items.length,
        total_amount: roundAmount(items.reduce((sum, item) => sum + item.amount, 0)),
        formatted_total_amount: formatDzdAmount(items.reduce((sum, item) => sum + item.amount, 0)),
      },
    });
  },
};
