import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";
import { logAudit } from "../utils/auditLog.js";

const ACCOUNT_MAPPING_SIDES = new Set(["debit", "credit", "net", "absolute"]);
const JOURNAL_MAPPING_SIDES = new Set(["debit", "credit", "net"]);
const JOURNAL_MAPPING_TYPES = new Set(["include", "exclude"]);

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

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

  if (typeof value === "boolean") {
    return value;
  }

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

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

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

  return fallback;
};

const normalizeOptionalDate = (value, label) => {
  const normalized = cleanString(value);

  if (!normalized) {
    return null;
  }

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

  return normalized;
};

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

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

  return companyId;
};

const ensureCompanyAccess = (req, companyId) => {
  if (req.user?.role === "super_admin") {
    return;
  }

  const allowedCompanyIds = new Set(
    [req.user?.companyId, ...(Array.isArray(req.user?.companyIds) ? req.user.companyIds : [])]
      .map((value) => Number(value || 0))
      .filter(Boolean),
  );

  if (!allowedCompanyIds.has(companyId)) {
    throw new ApiError(403, "Accès refusé à cette société.");
  }
};

const toDateKey = (value) => {
  const date = value instanceof Date ? value : new Date(value);

  if (Number.isNaN(date.getTime())) {
    return "";
  }

  return date.toISOString().slice(0, 10);
};

const getTodayKey = () => toDateKey(new Date());

const getCurrentMonthRange = () => {
  const now = new Date();
  const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));

  return {
    from: toDateKey(start),
    to: toDateKey(now),
  };
};

const getLastThreeMonthsRange = () => {
  const now = new Date();
  const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 2, 1));

  return {
    from: toDateKey(start),
    to: toDateKey(now),
  };
};

const buildMergedTaxCodesQuery = ({
  companyIdParamIndex = 1,
  onlyActive = true,
  orderClause = `
    ORDER BY
      cadre ASC,
      COALESCE(sub_cadre, '') ASC,
      COALESCE(sort_order, 999999) ASC,
      code ASC
  `,
} = {}) => `
  WITH official_codes AS (
    SELECT *
    FROM fiscal_tax_codes
    WHERE company_id IS NULL
  ),
  company_codes AS (
    SELECT *
    FROM fiscal_tax_codes
    WHERE company_id = $${companyIdParamIndex}
  ),
  merged_codes AS (
    SELECT
      COALESCE(cc.id, oc.id) AS id,
      cc.company_id AS company_id,
      cc.id AS company_row_id,
      oc.id AS official_row_id,
      COALESCE(cc.code, oc.code) AS code,
      COALESCE(cc.label, oc.label) AS label,
      COALESCE(cc.cadre, oc.cadre) AS cadre,
      COALESCE(cc.sub_cadre, oc.sub_cadre) AS sub_cadre,
      COALESCE(cc.tax_category, oc.tax_category) AS tax_category,
      COALESCE(cc.rate, oc.rate) AS rate,
      COALESCE(cc.rate_type, oc.rate_type) AS rate_type,
      COALESCE(cc.base_formula, oc.base_formula) AS base_formula,
      COALESCE(cc.legal_reference, oc.legal_reference) AS legal_reference,
      COALESCE(cc.is_deduction, oc.is_deduction) AS is_deduction,
      COALESCE(cc.is_active, oc.is_active) AS is_active,
      COALESCE(cc.sort_order, oc.sort_order) AS sort_order
    FROM official_codes oc
    LEFT JOIN company_codes cc
      ON cc.code = oc.code

    UNION ALL

    SELECT
      cc.id,
      cc.company_id,
      cc.id AS company_row_id,
      NULL AS official_row_id,
      cc.code,
      cc.label,
      cc.cadre,
      cc.sub_cadre,
      cc.tax_category,
      cc.rate,
      cc.rate_type,
      cc.base_formula,
      cc.legal_reference,
      cc.is_deduction,
      cc.is_active,
      cc.sort_order
    FROM company_codes cc
    LEFT JOIN official_codes oc
      ON oc.code = cc.code
    WHERE oc.id IS NULL
  )
  SELECT *
  FROM merged_codes
  ${onlyActive ? "WHERE is_active = TRUE" : ""}
  ${orderClause}
`;

const mapTaxCodeOption = (row) => ({
  id: Number(row.id),
  company_id: row.company_id ? Number(row.company_id) : null,
  code: row.code,
  label: row.label,
  cadre: Number(row.cadre),
  sub_cadre: row.sub_cadre || "",
  tax_category: row.tax_category || "",
  rate: row.rate === null || row.rate === undefined ? null : Number(row.rate),
  rate_type: row.rate_type || "",
  is_deduction: Boolean(row.is_deduction),
});

const fetchTaxCodeOptions = async (db, companyId) => {
  const { rows } = await db.query(buildMergedTaxCodesQuery(), [companyId]);
  return rows.map(mapTaxCodeOption);
};

const fetchAccessibleTaxCode = async (db, companyId, taxCodeId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        company_id,
        code,
        label,
        cadre,
        sub_cadre,
        tax_category,
        rate,
        rate_type,
        is_deduction,
        is_active
      FROM fiscal_tax_codes
      WHERE id = $1
        AND (company_id = $2 OR company_id IS NULL)
      LIMIT 1
    `,
    [taxCodeId, companyId],
  );

  return rows[0] ? mapTaxCodeOption(rows[0]) : null;
};

const CLASS_LABELS = {
  "4": "Classe 4 — Tiers",
  "5": "Classe 5 — Trésorerie",
  "6": "Classe 6 — Charges",
  "7": "Classe 7 — Produits",
};

const mapAccountRow = (row) => ({
  id: Number(row.id),
  code: row.code,
  label: row.label,
  class_code: row.class_code,
  class_label: CLASS_LABELS[row.class_code] || `Classe ${row.class_code}`,
  is_active: Boolean(row.is_active),
  has_mapping: Boolean(row.has_mapping),
  mapping_count: Number(row.mapping_count || 0),
  active_mapping_count: Number(row.active_mapping_count || 0),
});

const fetchEligibleAccounts = async (db, companyId) => {
  const { rows } = await db.query(
    `
      WITH mapping_stats AS (
        SELECT
          fam.account_id,
          COUNT(*)::int AS mapping_count,
          COUNT(*) FILTER (
            WHERE fam.is_active = TRUE
              AND fam.effective_from <= CURRENT_DATE
              AND COALESCE(fam.effective_to, DATE '2999-12-31') >= CURRENT_DATE
          )::int AS active_mapping_count
        FROM fiscal_account_mapping fam
        WHERE fam.company_id = $1
        GROUP BY fam.account_id
      )
      SELECT
        a.id,
        a.code,
        a.label,
        a.is_active,
        LEFT(a.code, 1) AS class_code,
        COALESCE(ms.mapping_count, 0) AS mapping_count,
        COALESCE(ms.active_mapping_count, 0) AS active_mapping_count,
        (COALESCE(ms.active_mapping_count, 0) > 0) AS has_mapping
      FROM accounts a
      LEFT JOIN mapping_stats ms
        ON ms.account_id = a.id
      WHERE a.company_id = $1
        AND LEFT(a.code, 1) BETWEEN '4' AND '7'
      ORDER BY a.code ASC
    `,
    [companyId],
  );

  return rows.map(mapAccountRow);
};

const fetchAccountById = async (db, companyId, accountId) => {
  const { rows } = await db.query(
    `
      SELECT
        a.id,
        a.code,
        a.label,
        a.is_active,
        LEFT(a.code, 1) AS class_code
      FROM accounts a
      WHERE a.id = $1
        AND a.company_id = $2
        AND LEFT(a.code, 1) BETWEEN '4' AND '7'
      LIMIT 1
    `,
    [accountId, companyId],
  );

  return rows[0] ? mapAccountRow(rows[0]) : null;
};

const mapAccountMappingRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  account_id: Number(row.account_id),
  tax_code_id: Number(row.tax_code_id),
  amount_side: row.amount_side,
  sign: Number(row.sign || 1),
  applies_to_tva_collected: Boolean(row.applies_to_tva_collected),
  applies_to_tva_deductible: Boolean(row.applies_to_tva_deductible),
  effective_from: toDateKey(row.effective_from),
  effective_to: row.effective_to ? toDateKey(row.effective_to) : "",
  is_active: Boolean(row.is_active),
  notes: row.notes || "",
  created_at: row.created_at,
  tax_code: {
    id: Number(row.tax_code_id),
    code: row.tax_code_code,
    label: row.tax_code_label,
    cadre: Number(row.tax_code_cadre),
    sub_cadre: row.tax_code_sub_cadre || "",
    tax_category: row.tax_code_category || "",
  },
});

const fetchAccountMappingsForAccount = async (db, companyId, accountId) => {
  const { rows } = await db.query(
    `
      SELECT
        fam.*,
        tc.code AS tax_code_code,
        tc.label AS tax_code_label,
        tc.cadre AS tax_code_cadre,
        tc.sub_cadre AS tax_code_sub_cadre,
        tc.tax_category AS tax_code_category
      FROM fiscal_account_mapping fam
      JOIN fiscal_tax_codes tc
        ON tc.id = fam.tax_code_id
      WHERE fam.company_id = $1
        AND fam.account_id = $2
      ORDER BY fam.effective_from DESC, fam.created_at DESC, fam.id DESC
    `,
    [companyId, accountId],
  );

  return rows.map(mapAccountMappingRow);
};

const fetchAccountPreview = async (db, companyId, accountId) => {
  const period = getLastThreeMonthsRange();
  const { rows } = await db.query(
    `
      SELECT
        COUNT(*)::int AS line_count,
        COUNT(DISTINCT je.id)::int AS entry_count,
        COALESCE(SUM(el.debit), 0)::numeric(18, 2) AS debit_total,
        COALESCE(SUM(el.credit), 0)::numeric(18, 2) AS credit_total,
        COALESCE(SUM(el.debit - el.credit), 0)::numeric(18, 2) AS net_total,
        COALESCE(SUM(ABS(el.debit - el.credit)), 0)::numeric(18, 2) AS absolute_total
      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 AND $3
        AND el.account_id = $4
    `,
    [companyId, period.from, period.to, accountId],
  );

  const row = rows[0] || {};

  return {
    period_from: period.from,
    period_to: period.to,
    line_count: Number(row.line_count || 0),
    entry_count: Number(row.entry_count || 0),
    totals: {
      debit: Number(row.debit_total || 0),
      credit: Number(row.credit_total || 0),
      net: Number(row.net_total || 0),
      absolute: Number(row.absolute_total || 0),
    },
  };
};

const normalizeAccountMappingPayload = async ({ db, companyId, body, existing = null }) => {
  const accountId = Number(body.account_id || body.accountId || existing?.account_id || 0);
  const taxCodeId = Number(body.tax_code_id || body.taxCodeId || existing?.tax_code_id || 0);
  const amountSide = cleanString(body.amount_side || body.amountSide || existing?.amount_side || "credit").toLowerCase();
  const sign = Number(body.sign ?? existing?.sign ?? 1) === -1 ? -1 : 1;
  const effectiveFrom = normalizeOptionalDate(
    body.effective_from || body.effectiveFrom || existing?.effective_from,
    "La date d'entrée en vigueur",
  );
  const effectiveTo = normalizeOptionalDate(
    body.effective_to || body.effectiveTo || existing?.effective_to,
    "La date de fin",
  );
  const notes = cleanString(body.notes || existing?.notes);
  const isActive = normalizeBoolean(body.is_active ?? body.isActive, existing?.is_active ?? true);

  if (!accountId) {
    throw new ApiError(400, "Le compte comptable est obligatoire.");
  }

  if (!taxCodeId) {
    throw new ApiError(400, "Le code G50 est obligatoire.");
  }

  if (!ACCOUNT_MAPPING_SIDES.has(amountSide)) {
    throw new ApiError(400, "Le côté du montant est invalide.");
  }

  if (!effectiveFrom) {
    throw new ApiError(400, "La date d'entrée en vigueur est obligatoire.");
  }

  if (effectiveTo && effectiveTo < effectiveFrom) {
    throw new ApiError(400, "La date de fin ne peut pas précéder la date de début.");
  }

  const [account, taxCode] = await Promise.all([
    fetchAccountById(db, companyId, accountId),
    fetchAccessibleTaxCode(db, companyId, taxCodeId),
  ]);

  if (!account) {
    throw new ApiError(400, "Le compte sélectionné est introuvable ou non éligible au mapping fiscal.");
  }

  if (!taxCode) {
    throw new ApiError(400, "Le code G50 sélectionné est introuvable.");
  }

  return {
    account_id: accountId,
    tax_code_id: taxCodeId,
    amount_side: amountSide,
    sign,
    effective_from: effectiveFrom,
    effective_to: effectiveTo,
    notes,
    is_active: isActive,
    applies_to_tva_collected: taxCode.tax_category === "TVA" && !taxCode.is_deduction,
    applies_to_tva_deductible: taxCode.tax_category === "TVA" && taxCode.is_deduction,
    account,
    tax_code: taxCode,
  };
};

const fetchAccountMappingById = async (db, companyId, mappingId) => {
  const { rows } = await db.query(
    `
      SELECT
        fam.*,
        tc.code AS tax_code_code,
        tc.label AS tax_code_label,
        tc.cadre AS tax_code_cadre,
        tc.sub_cadre AS tax_code_sub_cadre,
        tc.tax_category AS tax_code_category
      FROM fiscal_account_mapping fam
      JOIN fiscal_tax_codes tc
        ON tc.id = fam.tax_code_id
      WHERE fam.id = $1
        AND fam.company_id = $2
      LIMIT 1
    `,
    [mappingId, companyId],
  );

  return rows[0] ? mapAccountMappingRow(rows[0]) : null;
};

const getCoverageRange = (req) => {
  const fallback = getCurrentMonthRange();
  const periodFrom =
    normalizeOptionalDate(req.query.period_from || req.query.periodFrom, "La date de début") ||
    fallback.from;
  const periodTo =
    normalizeOptionalDate(req.query.period_to || req.query.periodTo, "La date de fin") ||
    fallback.to;

  if (periodTo < periodFrom) {
    throw new ApiError(400, "La période de couverture est invalide.");
  }

  return {
    from: periodFrom,
    to: periodTo,
  };
};

const getAccountMappingCoverage = async (db, companyId, period) => {
  const { rows } = await db.query(
    `
      WITH movement_accounts AS (
        SELECT
          a.id AS account_id
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        JOIN accounts a
          ON a.id = el.account_id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND je.entry_date BETWEEN $2 AND $3
          AND a.company_id = $1
          AND LEFT(a.code, 1) BETWEEN '4' AND '7'
        GROUP BY a.id
      ),
      mapped_accounts AS (
        SELECT DISTINCT fam.account_id
        FROM fiscal_account_mapping fam
        WHERE fam.company_id = $1
          AND fam.is_active = TRUE
          AND fam.effective_from <= $3
          AND COALESCE(fam.effective_to, DATE '2999-12-31') >= $2
      )
      SELECT
        COUNT(*)::int AS accounts_with_movement,
        COUNT(*) FILTER (WHERE ma.account_id IS NOT NULL)::int AS mapped_accounts,
        COUNT(*) FILTER (WHERE ma.account_id IS NULL)::int AS unmapped_accounts
      FROM movement_accounts mov
      LEFT JOIN mapped_accounts ma
        ON ma.account_id = mov.account_id
    `,
    [companyId, period.from, period.to],
  );

  const row = rows[0] || {};

  return {
    period_from: period.from,
    period_to: period.to,
    accounts_with_movement: Number(row.accounts_with_movement || 0),
    mapped_accounts: Number(row.mapped_accounts || 0),
    unmapped_accounts: Number(row.unmapped_accounts || 0),
  };
};

const getUnmappedAccounts = async (db, companyId, period) => {
  const { rows } = await db.query(
    `
      WITH movement_accounts AS (
        SELECT
          a.id,
          a.code,
          a.label,
          LEFT(a.code, 1) AS class_code,
          COUNT(*)::int AS line_count,
          COUNT(DISTINCT je.id)::int AS entry_count,
          COALESCE(SUM(el.debit), 0)::numeric(18, 2) AS debit_total,
          COALESCE(SUM(el.credit), 0)::numeric(18, 2) AS credit_total
        FROM entry_lines el
        JOIN journal_entries je
          ON je.id = el.entry_id
        JOIN accounts a
          ON a.id = el.account_id
        WHERE je.company_id = $1
          AND je.status = 'validated'
          AND je.entry_date BETWEEN $2 AND $3
          AND a.company_id = $1
          AND LEFT(a.code, 1) BETWEEN '4' AND '7'
        GROUP BY a.id, a.code, a.label
      )
      SELECT
        mov.*
      FROM movement_accounts mov
      WHERE NOT EXISTS (
        SELECT 1
        FROM fiscal_account_mapping fam
        WHERE fam.company_id = $1
          AND fam.account_id = mov.id
          AND fam.is_active = TRUE
          AND fam.effective_from <= $3
          AND COALESCE(fam.effective_to, DATE '2999-12-31') >= $2
      )
      ORDER BY mov.code ASC
    `,
    [companyId, period.from, period.to],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    code: row.code,
    label: row.label,
    class_code: row.class_code,
    class_label: CLASS_LABELS[row.class_code] || `Classe ${row.class_code}`,
    line_count: Number(row.line_count || 0),
    entry_count: Number(row.entry_count || 0),
    debit_total: Number(row.debit_total || 0),
    credit_total: Number(row.credit_total || 0),
  }));
};

const mapJournalRow = (row) => ({
  id: Number(row.id),
  code: row.code,
  label: row.label,
  journal_type: row.journal_type,
  is_active: Boolean(row.is_active),
});

const fetchJournals = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        code,
        label,
        journal_type,
        is_active
      FROM journals
      WHERE company_id = $1
      ORDER BY code ASC
    `,
    [companyId],
  );

  return rows.map(mapJournalRow);
};

const mapJournalMappingRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  journal_id: Number(row.journal_id),
  tax_code_id: Number(row.tax_code_id),
  mapping_type: row.mapping_type,
  amount_side: row.amount_side,
  sign: Number(row.sign || 1),
  effective_from: toDateKey(row.effective_from),
  effective_to: row.effective_to ? toDateKey(row.effective_to) : "",
  is_active: Boolean(row.is_active),
  journal: {
    id: Number(row.journal_id),
    code: row.journal_code,
    label: row.journal_label,
    journal_type: row.journal_type,
  },
  tax_code: {
    id: Number(row.tax_code_id),
    code: row.tax_code_code,
    label: row.tax_code_label,
    cadre: Number(row.tax_code_cadre),
    sub_cadre: row.tax_code_sub_cadre || "",
  },
});

const fetchJournalMappings = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        fjm.*,
        j.code AS journal_code,
        j.label AS journal_label,
        j.journal_type,
        tc.code AS tax_code_code,
        tc.label AS tax_code_label,
        tc.cadre AS tax_code_cadre,
        tc.sub_cadre AS tax_code_sub_cadre
      FROM fiscal_journal_mapping fjm
      JOIN journals j
        ON j.id = fjm.journal_id
      JOIN fiscal_tax_codes tc
        ON tc.id = fjm.tax_code_id
      WHERE fjm.company_id = $1
      ORDER BY j.code ASC, fjm.effective_from DESC, fjm.id DESC
    `,
    [companyId],
  );

  return rows.map(mapJournalMappingRow);
};

const fetchJournalMappingById = async (db, companyId, mappingId) => {
  const { rows } = await db.query(
    `
      SELECT
        fjm.*,
        j.code AS journal_code,
        j.label AS journal_label,
        j.journal_type,
        tc.code AS tax_code_code,
        tc.label AS tax_code_label,
        tc.cadre AS tax_code_cadre,
        tc.sub_cadre AS tax_code_sub_cadre
      FROM fiscal_journal_mapping fjm
      JOIN journals j
        ON j.id = fjm.journal_id
      JOIN fiscal_tax_codes tc
        ON tc.id = fjm.tax_code_id
      WHERE fjm.id = $1
        AND fjm.company_id = $2
      LIMIT 1
    `,
    [mappingId, companyId],
  );

  return rows[0] ? mapJournalMappingRow(rows[0]) : null;
};

const normalizeJournalMappingPayload = async ({ db, companyId, body, existing = null }) => {
  const journalId = Number(body.journal_id || body.journalId || existing?.journal_id || 0);
  const taxCodeId = Number(body.tax_code_id || body.taxCodeId || existing?.tax_code_id || 0);
  const mappingType = cleanString(
    body.mapping_type || body.mappingType || existing?.mapping_type || "include",
  ).toLowerCase();
  const amountSide = cleanString(
    body.amount_side || body.amountSide || existing?.amount_side || "credit",
  ).toLowerCase();
  const sign = Number(body.sign ?? existing?.sign ?? 1) === -1 ? -1 : 1;
  const effectiveFrom = normalizeOptionalDate(
    body.effective_from || body.effectiveFrom || existing?.effective_from,
    "La date d'entrée en vigueur",
  );
  const effectiveTo = normalizeOptionalDate(
    body.effective_to || body.effectiveTo || existing?.effective_to,
    "La date de fin",
  );
  const isActive = normalizeBoolean(body.is_active ?? body.isActive, existing?.is_active ?? true);

  if (!journalId) {
    throw new ApiError(400, "Le journal est obligatoire.");
  }

  if (!taxCodeId) {
    throw new ApiError(400, "Le code G50 est obligatoire.");
  }

  if (!JOURNAL_MAPPING_TYPES.has(mappingType)) {
    throw new ApiError(400, "Le type de mapping de journal est invalide.");
  }

  if (!JOURNAL_MAPPING_SIDES.has(amountSide)) {
    throw new ApiError(400, "Le côté du montant est invalide.");
  }

  if (!effectiveFrom) {
    throw new ApiError(400, "La date d'entrée en vigueur est obligatoire.");
  }

  if (effectiveTo && effectiveTo < effectiveFrom) {
    throw new ApiError(400, "La date de fin ne peut pas précéder la date de début.");
  }

  const [taxCode, journalResponse] = await Promise.all([
    fetchAccessibleTaxCode(db, companyId, taxCodeId),
    db.query(
      `
        SELECT
          id,
          code,
          label,
          journal_type,
          is_active
        FROM journals
        WHERE id = $1
          AND company_id = $2
        LIMIT 1
      `,
      [journalId, companyId],
    ),
  ]);

  if (!taxCode) {
    throw new ApiError(400, "Le code G50 sélectionné est introuvable.");
  }

  if (!journalResponse.rows[0]) {
    throw new ApiError(400, "Le journal sélectionné est introuvable.");
  }

  return {
    journal_id: journalId,
    tax_code_id: taxCodeId,
    mapping_type: mappingType,
    amount_side: amountSide,
    sign,
    effective_from: effectiveFrom,
    effective_to: effectiveTo,
    is_active: isActive,
    tax_code: taxCode,
    journal: mapJournalRow(journalResponse.rows[0]),
  };
};

const findTaxCodeByCode = (taxCodes, code) => taxCodes.find((item) => item.code === code) || null;

const buildJournalAutoSuggestions = ({ journals, taxCodes, currentMappings }) => {
  const today = getTodayKey();
  const activeKeys = new Set(
    currentMappings
      .filter(
        (item) =>
          item.is_active &&
          item.effective_from <= today &&
          (!item.effective_to || item.effective_to >= today),
      )
      .map((item) => `${item.journal_id}:${item.tax_code.code}:${item.amount_side}:${item.sign}`),
  );

  const suggestions = [];

  journals.forEach((journal) => {
    let taxCode = null;
    let amountSide = "credit";
    let sign = 1;
    let reason = "";

    if (journal.journal_type === "sale") {
      taxCode = findTaxCodeByCode(taxCodes, "E3B21");
      amountSide = "credit";
      sign = 1;
      reason = "Journal Ventes → rubrique TVA/CA ventes 19 %.";
    } else if (journal.journal_type === "purchase") {
      taxCode = findTaxCodeByCode(taxCodes, "E3B92");
      amountSide = "debit";
      sign = 1;
      reason = "Journal Achats → TVA déductible sur achats.";
    } else if (journal.journal_type === "payroll") {
      taxCode = findTaxCodeByCode(taxCodes, "E1L20");
      amountSide = "credit";
      sign = 1;
      reason = "Journal Paie → IRG / traitements et salaires.";
    }

    if (!taxCode) {
      return;
    }

    const dedupeKey = `${journal.id}:${taxCode.code}:${amountSide}:${sign}`;

    if (activeKeys.has(dedupeKey)) {
      return;
    }

    suggestions.push({
      journal_id: journal.id,
      journal,
      tax_code_id: taxCode.id,
      tax_code: taxCode,
      amount_side: amountSide,
      sign,
      mapping_type: "include",
      effective_from: today,
      reason,
    });
  });

  return suggestions;
};

export const fiscalMappingController = {
  listAccountMappings: async (req, res) => {
    const companyId = getCompanyId(req);
    ensureCompanyAccess(req, companyId);

    const accountId = Number(req.query.account_id || req.query.accountId || 0) || null;

    const [accounts, taxCodes] = await Promise.all([
      fetchEligibleAccounts(pool, companyId),
      fetchTaxCodeOptions(pool, companyId),
    ]);

    let selectedAccount = null;
    let items = [];
    let preview = null;

    if (accountId) {
      selectedAccount = await fetchAccountById(pool, companyId, accountId);

      if (!selectedAccount) {
        throw new ApiError(404, "Compte introuvable pour le mapping fiscal.");
      }

      const [mappingItems, previewData] = await Promise.all([
        fetchAccountMappingsForAccount(pool, companyId, accountId),
        fetchAccountPreview(pool, companyId, accountId),
      ]);

      items = mappingItems;
      preview = previewData;
    }

    res.json({
      accounts,
      tax_codes: taxCodes,
      selected_account: selectedAccount,
      items,
      preview,
      meta: {
        company_id: companyId,
        total_accounts: accounts.length,
      },
    });
  },

  createAccountMapping: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const client = await pool.connect();

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

      const payload = await normalizeAccountMappingPayload({
        db: client,
        companyId,
        body: req.body,
      });

      const duplicateCheck = await client.query(
        `
          SELECT id
          FROM fiscal_account_mapping
          WHERE company_id = $1
            AND account_id = $2
            AND tax_code_id = $3
            AND effective_from = $4
          LIMIT 1
        `,
        [companyId, payload.account_id, payload.tax_code_id, payload.effective_from],
      );

      if (duplicateCheck.rows[0]) {
        throw new ApiError(409, "Un mapping identique existe déjà pour cette date d'effet.");
      }

      const { rows } = await client.query(
        `
          INSERT INTO fiscal_account_mapping (
            company_id,
            account_id,
            tax_code_id,
            amount_side,
            sign,
            applies_to_tva_collected,
            applies_to_tva_deductible,
            effective_from,
            effective_to,
            is_active,
            notes
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
          RETURNING id
        `,
        [
          companyId,
          payload.account_id,
          payload.tax_code_id,
          payload.amount_side,
          payload.sign,
          payload.applies_to_tva_collected,
          payload.applies_to_tva_deductible,
          payload.effective_from,
          payload.effective_to,
          payload.is_active,
          payload.notes || null,
        ],
      );

      const item = await fetchAccountMappingById(client, companyId, Number(rows[0].id));

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "CREATE",
        entityType: "fiscal_account_mapping",
        entityId: item.id,
        oldData: null,
        newData: item,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Mapping de compte créé.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  updateAccountMapping: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const mappingId = Number(req.params.id || 0);

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

    const client = await pool.connect();

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

      const existing = await fetchAccountMappingById(client, companyId, mappingId);

      if (!existing) {
        throw new ApiError(404, "Mapping de compte introuvable.");
      }

      const payload = await normalizeAccountMappingPayload({
        db: client,
        companyId,
        body: req.body,
        existing,
      });

      const duplicateCheck = await client.query(
        `
          SELECT id
          FROM fiscal_account_mapping
          WHERE company_id = $1
            AND account_id = $2
            AND tax_code_id = $3
            AND effective_from = $4
            AND id <> $5
          LIMIT 1
        `,
        [companyId, payload.account_id, payload.tax_code_id, payload.effective_from, mappingId],
      );

      if (duplicateCheck.rows[0]) {
        throw new ApiError(409, "Un mapping identique existe déjà pour cette date d'effet.");
      }

      await client.query(
        `
          UPDATE fiscal_account_mapping
          SET
            account_id = $2,
            tax_code_id = $3,
            amount_side = $4,
            sign = $5,
            applies_to_tva_collected = $6,
            applies_to_tva_deductible = $7,
            effective_from = $8,
            effective_to = $9,
            is_active = $10,
            notes = $11
          WHERE id = $1
        `,
        [
          mappingId,
          payload.account_id,
          payload.tax_code_id,
          payload.amount_side,
          payload.sign,
          payload.applies_to_tva_collected,
          payload.applies_to_tva_deductible,
          payload.effective_from,
          payload.effective_to,
          payload.is_active,
          payload.notes || null,
        ],
      );

      const item = await fetchAccountMappingById(client, companyId, mappingId);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "UPDATE",
        entityType: "fiscal_account_mapping",
        entityId: mappingId,
        oldData: existing,
        newData: item,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Mapping de compte mis à jour.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  deleteAccountMapping: async (req, res) => {
    const companyId = getCompanyId(req);
    ensureCompanyAccess(req, companyId);

    const mappingId = Number(req.params.id || 0);

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

    const client = await pool.connect();

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

      const existing = await fetchAccountMappingById(client, companyId, mappingId);

      if (!existing) {
        throw new ApiError(404, "Mapping de compte introuvable.");
      }

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

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "DELETE",
        entityType: "fiscal_account_mapping",
        entityId: mappingId,
        oldData: existing,
        newData: null,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Mapping de compte supprimé.",
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  getAccountMappingCoverage: async (req, res) => {
    const companyId = getCompanyId(req);
    ensureCompanyAccess(req, companyId);

    const period = getCoverageRange(req);
    const coverage = await getAccountMappingCoverage(pool, companyId, period);

    res.json({ item: coverage });
  },

  getUnmappedAccounts: async (req, res) => {
    const companyId = getCompanyId(req);
    ensureCompanyAccess(req, companyId);

    const period = getCoverageRange(req);
    const items = await getUnmappedAccounts(pool, companyId, period);

    res.json({
      items,
      meta: {
        company_id: companyId,
        total: items.length,
        period_from: period.from,
        period_to: period.to,
      },
    });
  },

  listJournalMappings: async (req, res) => {
    const companyId = getCompanyId(req);
    ensureCompanyAccess(req, companyId);

    const [journals, taxCodes, items] = await Promise.all([
      fetchJournals(pool, companyId),
      fetchTaxCodeOptions(pool, companyId),
      fetchJournalMappings(pool, companyId),
    ]);

    res.json({
      journals,
      tax_codes: taxCodes,
      items,
      meta: {
        company_id: companyId,
        total: items.length,
      },
    });
  },

  createJournalMapping: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const client = await pool.connect();

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

      const payload = await normalizeJournalMappingPayload({
        db: client,
        companyId,
        body: req.body,
      });

      const duplicateCheck = await client.query(
        `
          SELECT id
          FROM fiscal_journal_mapping
          WHERE company_id = $1
            AND journal_id = $2
            AND tax_code_id = $3
            AND effective_from = $4
          LIMIT 1
        `,
        [companyId, payload.journal_id, payload.tax_code_id, payload.effective_from],
      );

      if (duplicateCheck.rows[0]) {
        throw new ApiError(409, "Un mapping identique existe déjà pour cette date d'effet.");
      }

      const { rows } = await client.query(
        `
          INSERT INTO fiscal_journal_mapping (
            company_id,
            journal_id,
            tax_code_id,
            mapping_type,
            amount_side,
            sign,
            effective_from,
            effective_to,
            is_active
          )
          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
          RETURNING id
        `,
        [
          companyId,
          payload.journal_id,
          payload.tax_code_id,
          payload.mapping_type,
          payload.amount_side,
          payload.sign,
          payload.effective_from,
          payload.effective_to,
          payload.is_active,
        ],
      );

      const item = await fetchJournalMappingById(client, companyId, Number(rows[0].id));

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "CREATE",
        entityType: "fiscal_journal_mapping",
        entityId: item.id,
        oldData: null,
        newData: item,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.status(201).json({
        message: "Mapping de journal créé.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  updateJournalMapping: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const mappingId = Number(req.params.id || 0);

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

    const client = await pool.connect();

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

      const existing = await fetchJournalMappingById(client, companyId, mappingId);

      if (!existing) {
        throw new ApiError(404, "Mapping de journal introuvable.");
      }

      const payload = await normalizeJournalMappingPayload({
        db: client,
        companyId,
        body: req.body,
        existing,
      });

      const duplicateCheck = await client.query(
        `
          SELECT id
          FROM fiscal_journal_mapping
          WHERE company_id = $1
            AND journal_id = $2
            AND tax_code_id = $3
            AND effective_from = $4
            AND id <> $5
          LIMIT 1
        `,
        [companyId, payload.journal_id, payload.tax_code_id, payload.effective_from, mappingId],
      );

      if (duplicateCheck.rows[0]) {
        throw new ApiError(409, "Un mapping identique existe déjà pour cette date d'effet.");
      }

      await client.query(
        `
          UPDATE fiscal_journal_mapping
          SET
            journal_id = $2,
            tax_code_id = $3,
            mapping_type = $4,
            amount_side = $5,
            sign = $6,
            effective_from = $7,
            effective_to = $8,
            is_active = $9
          WHERE id = $1
        `,
        [
          mappingId,
          payload.journal_id,
          payload.tax_code_id,
          payload.mapping_type,
          payload.amount_side,
          payload.sign,
          payload.effective_from,
          payload.effective_to,
          payload.is_active,
        ],
      );

      const item = await fetchJournalMappingById(client, companyId, mappingId);

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "UPDATE",
        entityType: "fiscal_journal_mapping",
        entityId: mappingId,
        oldData: existing,
        newData: item,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Mapping de journal mis à jour.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  deleteJournalMapping: async (req, res) => {
    const companyId = getCompanyId(req);
    ensureCompanyAccess(req, companyId);

    const mappingId = Number(req.params.id || 0);

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

    const client = await pool.connect();

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

      const existing = await fetchJournalMappingById(client, companyId, mappingId);

      if (!existing) {
        throw new ApiError(404, "Mapping de journal introuvable.");
      }

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

      await logAudit(client, {
        companyId,
        userId: req.user?.sub || null,
        action: "DELETE",
        entityType: "fiscal_journal_mapping",
        entityId: mappingId,
        oldData: existing,
        newData: null,
        ipAddress: req.ip,
      });

      await client.query("COMMIT");

      res.json({
        message: "Mapping de journal supprimé.",
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

  autoProposeJournalMappings: async (req, res) => {
    const companyId = getCompanyId(req, req.body);
    ensureCompanyAccess(req, companyId);

    const [journals, taxCodes, currentMappings] = await Promise.all([
      fetchJournals(pool, companyId),
      fetchTaxCodeOptions(pool, companyId),
      fetchJournalMappings(pool, companyId),
    ]);

    const items = buildJournalAutoSuggestions({
      journals,
      taxCodes,
      currentMappings,
    });

    res.json({
      items,
      meta: {
        company_id: companyId,
        total: items.length,
      },
    });
  },
};
