import { ApiError } from "../utils/ApiError.js";
import { ADMIN_ROLES, hasPermission } from "../utils/permissions.js";
import { createCompanyNotifications, emitNotifications } from "../utils/notifications.js";
import { markFiscalDeadlineAsFiled } from "../utils/fiscalDeadlines.js";
import { buildG50PdfBuffer } from "../utils/g50Pdf.js";
import { calculateG50, runG50Preflight } from "./g50CalculationEngine.js";

const RULE_TYPES = new Set([
  "account_range",
  "account_exact",
  "journal_type",
  "entry_label_pattern",
  "auxiliary_type",
]);
const VALIDATION_ROLES = new Set(["super_admin", "manager", "chief_accountant"]);
const ADJUSTMENT_REASON_TYPES = new Set([
  "facturation_error",
  "missing_entry",
  "rate_correction",
  "credit_note_missing",
  "other",
]);
const MONTH_LABELS = [
  "Janvier",
  "Fevrier",
  "Mars",
  "Avril",
  "Mai",
  "Juin",
  "Juillet",
  "Aout",
  "Septembre",
  "Octobre",
  "Novembre",
  "Decembre",
];
const TVA_9_CODES = new Set(["E3B11", "E3B12", "E3B13", "E3B14", "E3B15", "E3B16"]);
const TVA_19_CODES = new Set([
  "E3B21",
  "E3B22",
  "E3B23",
  "E3B24",
  "E3B25",
  "E3B26",
  "E3B28",
  "E3B31",
  "E3B34",
  "E3B35",
  "E3B36",
  "E3B37",
]);
const TVA_BASE_CODES = new Set([...TVA_9_CODES, ...TVA_19_CODES]);
const TVA_DEDUCTION_CODES = new Set(["E3B91", "E3B92", "E3B93", "E3B94", "E3B95", "E3B96"]);
const TVA_PLUS_CODES = new Set(["E3B97", "E3B98"]);

const cleanString = (value) => String(value ?? "").trim();
const roundAmount = (value) => Number(Number(value || 0).toFixed(2));
const toNullableText = (value) => {
  const normalized = cleanString(value);
  return normalized || null;
};

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

  if (!normalized) {
    return null;
  }

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

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

  if (
    Number.isNaN(parsed.getTime()) ||
    parsed.getUTCFullYear() !== year ||
    parsed.getUTCMonth() + 1 !== month ||
    parsed.getUTCDate() !== day
  ) {
    throw new ApiError(400, `${label} invalide.`);
  }

  return normalized;
};

const parseAmountField = (value, label, { min = 0 } = {}) => {
  const normalized = cleanString(value).replace(/\s+/g, "").replace(",", ".");

  if (!normalized) {
    throw new ApiError(400, `${label} est obligatoire.`);
  }

  const parsed = Number(normalized);

  if (!Number.isFinite(parsed) || parsed < min) {
    throw new ApiError(400, `${label} est invalide.`);
  }

  return roundAmount(parsed);
};

const toDateKey = (value) => {
  if (!value) {
    return "";
  }

  const date = value instanceof Date ? value : new Date(value);
  return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
};

const convertLikePatternToRegex = (pattern) => {
  const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  return new RegExp(`^${escaped.replace(/%/g, ".*").replace(/_/g, ".")}$`, "i");
};

const getAmountBySide = (line, amountSide = "credit") => {
  const debit = Number(line.debit || 0);
  const credit = Number(line.credit || 0);

  if (amountSide === "debit") {
    return debit;
  }

  if (amountSide === "credit") {
    return credit;
  }

  if (amountSide === "absolute") {
    return Math.abs(debit - credit);
  }

  return debit - credit;
};

const isDirectVatAccount = (line) => /^445/.test(cleanString(line.account_code));

const formatPeriodLabel = ({ period_type, annee, mois = null, trimestre = null }) => {
  if (period_type === "monthly") {
    return `${MONTH_LABELS[Math.max(Number(mois || 1) - 1, 0)] || "Mois"} ${annee}`;
  }

  return `T${trimestre} ${annee}`;
};

const buildMergedTaxCodesQuery = ({
  companyIdParamIndex = 1,
  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,
      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.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.code,
      cc.label,
      cc.cadre,
      cc.sub_cadre,
      cc.tax_category,
      cc.rate,
      cc.rate_type,
      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
  WHERE is_active = TRUE
  ${orderClause}
`;

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

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

  return rows[0];
};

const fetchFiscalProfile = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        id,
        nif,
        nin,
        article_imposition,
        forme_juridique,
        code_activite,
        activity_description,
        g50_periodicite
      FROM fiscal_profiles
      WHERE company_id = $1
      LIMIT 1
    `,
    [companyId],
  );

  return rows[0] || { g50_periodicite: "mensuel" };
};

const mapHeaderRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  fiscal_period_id: Number(row.fiscal_period_id),
  fiscal_year_id: row.fiscal_year_id ? Number(row.fiscal_year_id) : null,
  fiscal_year_label: row.fiscal_year_label || null,
  version: Number(row.version),
  status: row.status,
  total_cadre1: Number(row.total_cadre1 || 0),
  total_cadre2: Number(row.total_cadre2 || 0),
  total_cadre3: Number(row.total_cadre3 || 0),
  total_cadre4: Number(row.total_cadre4 || 0),
  total_cadre5: Number(row.total_cadre5 || 0),
  total_cadre6: Number(row.total_cadre6 || 0),
  total_cadre7: Number(row.total_cadre7 || 0),
  total_tva_collectee: Number(row.total_tva_collectee || 0),
  total_tva_deductible: Number(row.total_tva_deductible || 0),
  tva_a_payer: Number(row.tva_a_payer || 0),
  tva_precompte_reporter: Number(row.tva_precompte_reporter || 0),
  total_a_payer: Number(row.total_a_payer || 0),
  calculated_at: row.calculated_at,
  calculated_by: row.calculated_by ? Number(row.calculated_by) : null,
  calculated_by_name: row.calculated_by_name || null,
  entries_count: Number(row.entries_count || 0),
  entries_sum_debit: Number(row.entries_sum_debit || 0),
  entries_sum_credit: Number(row.entries_sum_credit || 0),
  validated_at: row.validated_at,
  validated_by: row.validated_by ? Number(row.validated_by) : null,
  validated_by_name: row.validated_by_name || null,
  quittance_numero: row.quittance_numero || null,
  quittance_date: row.quittance_date || null,
  montant_paye: Number(row.montant_paye || 0),
  g50_declaration_id: row.g50_declaration_id ? Number(row.g50_declaration_id) : null,
  created_at: row.created_at,
  updated_at: row.updated_at,
  descriptor: {
    period_type: row.period_type,
    annee: Number(row.annee),
    mois: row.mois === null ? null : Number(row.mois),
    trimestre: row.trimestre === null ? null : Number(row.trimestre),
    date_from: row.period_from,
    date_to: row.period_to,
    deadline_g50: row.deadline_g50 || null,
    is_locked: Boolean(row.period_locked),
    period_label: formatPeriodLabel({
      period_type: row.period_type,
      annee: Number(row.annee),
      mois: row.mois,
      trimestre: row.trimestre,
    }),
  },
});

const fetchHeaderContext = async (db, companyId, headerId) => {
  const { rows } = await db.query(
    `
      SELECT
        gh.id,
        gh.company_id,
        gh.fiscal_period_id,
        gh.version,
        gh.status,
        gh.total_cadre1,
        gh.total_cadre2,
        gh.total_cadre3,
        gh.total_cadre4,
        gh.total_cadre5,
        gh.total_cadre6,
        gh.total_cadre7,
        gh.total_tva_collectee,
        gh.total_tva_deductible,
        gh.tva_a_payer,
        gh.tva_precompte_reporter,
        gh.total_a_payer,
        gh.calculated_at,
        gh.calculated_by,
        gh.entries_count,
        gh.entries_sum_debit,
        gh.entries_sum_credit,
        gh.validated_at,
        gh.validated_by,
        gh.quittance_numero,
        TO_CHAR(gh.quittance_date, 'YYYY-MM-DD') AS quittance_date,
        gh.montant_paye,
        gh.g50_declaration_id,
        gh.created_at,
        gh.updated_at,
        fp.fiscal_year_id,
        fy.label AS fiscal_year_label,
        fp.period_type,
        fp.mois,
        fp.trimestre,
        fp.annee,
        TO_CHAR(fp.date_from, 'YYYY-MM-DD') AS period_from,
        TO_CHAR(fp.date_to, 'YYYY-MM-DD') AS period_to,
        TO_CHAR(fp.deadline_g50, 'YYYY-MM-DD') AS deadline_g50,
        fp.is_locked AS period_locked,
        calc_user.full_name AS calculated_by_name,
        valid_user.full_name AS validated_by_name
      FROM g50_headers gh
      JOIN fiscal_periods fp
        ON fp.id = gh.fiscal_period_id
      LEFT JOIN fiscal_years fy
        ON fy.id = fp.fiscal_year_id
      LEFT JOIN users calc_user
        ON calc_user.id = gh.calculated_by
      LEFT JOIN users valid_user
        ON valid_user.id = gh.validated_by
      WHERE gh.company_id = $1
        AND gh.id = $2
      LIMIT 1
    `,
    [companyId, headerId],
  );

  if (!rows[0]) {
    throw new ApiError(404, "Brouillon G50 introuvable.");
  }

  return mapHeaderRow(rows[0]);
};

const fetchHeaderLines = async (db, headerId) => {
  const { rows } = await db.query(
    `
      SELECT
        gl.id,
        gl.g50_header_id,
        gl.tax_code_id,
        gl.code,
        gl.cadre,
        gl.label,
        gl.ca_global,
        gl.ca_imposable,
        gl.ca_exonere,
        gl.base_imposable,
        gl.taux,
        gl.montant_calcule,
        gl.montant_ajuste,
        gl.montant_final,
        gl.source_entry_ids,
        gl.source_entry_count,
        gl.is_manually_adjusted,
        gl.adjustment_reason,
        gl.created_at,
        gl.updated_at,
        ftc.tax_category,
        ftc.sub_cadre
      FROM g50_lines gl
      LEFT JOIN fiscal_tax_codes ftc
        ON ftc.id = gl.tax_code_id
      WHERE gl.g50_header_id = $1
      ORDER BY gl.cadre ASC, gl.code ASC
    `,
    [headerId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    g50_header_id: Number(row.g50_header_id),
    tax_code_id: Number(row.tax_code_id),
    code: row.code,
    cadre: Number(row.cadre),
    label: row.label,
    ca_global: Number(row.ca_global || 0),
    ca_imposable: Number(row.ca_imposable || 0),
    ca_exonere: Number(row.ca_exonere || 0),
    base_imposable: Number(row.base_imposable || 0),
    taux: row.taux === null ? null : Number(row.taux),
    montant_calcule: Number(row.montant_calcule || 0),
    montant_ajuste: row.montant_ajuste === null ? null : Number(row.montant_ajuste),
    montant_final: Number(row.montant_final || 0),
    source_entry_ids: Array.isArray(row.source_entry_ids)
      ? row.source_entry_ids.map((value) => Number(value))
      : [],
    source_entry_count: Number(row.source_entry_count || 0),
    is_manually_adjusted: Boolean(row.is_manually_adjusted),
    adjustment_reason: row.adjustment_reason || null,
    tax_category: row.tax_category || "",
    sub_cadre: row.sub_cadre || "",
    created_at: row.created_at,
    updated_at: row.updated_at,
  }));
};

const fetchHeaderAdjustments = async (db, headerId) => {
  const { rows } = await db.query(
    `
      SELECT
        ga.id,
        ga.g50_header_id,
        ga.g50_line_id,
        ga.tax_code_id,
        ga.code,
        ga.montant_avant,
        ga.montant_apres,
        ga.difference,
        ga.reason_type,
        ga.justification,
        ga.supporting_document_url,
        ga.adjusted_by,
        TO_CHAR(ga.adjusted_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS adjusted_at,
        ga.requires_approval,
        ga.approved_by,
        TO_CHAR(ga.approved_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS approved_at,
        u.full_name AS adjusted_by_name
      FROM g50_adjustments ga
      LEFT JOIN users u
        ON u.id = ga.adjusted_by
      WHERE ga.g50_header_id = $1
      ORDER BY ga.adjusted_at DESC, ga.id DESC
    `,
    [headerId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    g50_header_id: Number(row.g50_header_id),
    g50_line_id: Number(row.g50_line_id),
    tax_code_id: row.tax_code_id ? Number(row.tax_code_id) : null,
    code: row.code || null,
    montant_avant: Number(row.montant_avant || 0),
    montant_apres: Number(row.montant_apres || 0),
    difference: Number(row.difference || 0),
    reason_type: row.reason_type || "other",
    justification: row.justification || "",
    supporting_document_url: row.supporting_document_url || null,
    adjusted_by: row.adjusted_by ? Number(row.adjusted_by) : null,
    adjusted_by_name: row.adjusted_by_name || null,
    adjusted_at: row.adjusted_at,
    requires_approval: Boolean(row.requires_approval),
    approved_by: row.approved_by ? Number(row.approved_by) : null,
    approved_at: row.approved_at || null,
  }));
};

const fetchHeaderAuditLog = async (db, headerId) => {
  const { rows } = await db.query(
    `
      SELECT
        gal.id,
        gal.action,
        gal.actor_id,
        gal.details,
        gal.ip_address,
        TO_CHAR(gal.created_at, 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS created_at,
        u.full_name AS actor_name
      FROM g50_audit_log gal
      LEFT JOIN users u
        ON u.id = gal.actor_id
      WHERE gal.g50_header_id = $1
      ORDER BY gal.created_at DESC, gal.id DESC
    `,
    [headerId],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    action: row.action,
    actor_id: row.actor_id ? Number(row.actor_id) : null,
    actor_name: row.actor_name || null,
    details: row.details || {},
    ip_address: row.ip_address || null,
    created_at: row.created_at,
  }));
};

const attachAdjustmentsToLines = (lines, adjustments) => {
  const adjustmentsByLineId = adjustments.reduce((accumulator, item) => {
    accumulator[item.g50_line_id] ||= [];
    accumulator[item.g50_line_id].push(item);
    return accumulator;
  }, {});

  return lines.map((line) => ({
    ...line,
    adjustments: adjustmentsByLineId[line.id] || [],
  }));
};

const buildTvaSummaryFromLines = (lines) => {
  const amountByCode = new Map(lines.map((line) => [line.code, Number(line.montant_final || 0)]));
  const sumSet = (codes) =>
    roundAmount([...codes].reduce((total, code) => total + Number(amountByCode.get(code) || 0), 0));

  const totalCollectee = roundAmount(sumSet(TVA_BASE_CODES));
  const precompteAnterieur = roundAmount(Number(amountByCode.get("E3B91") || 0));
  const totalDeductible = roundAmount(
    [...TVA_DEDUCTION_CODES]
      .filter((code) => code !== "E3B91")
      .reduce((total, code) => total + Number(amountByCode.get(code) || 0), 0),
  );
  const totalPlus = roundAmount(sumSet(TVA_PLUS_CODES));
  const totalDeductionsAvecPrecompte = roundAmount(precompteAnterieur + totalDeductible);
  const totalRappel = roundAmount(totalCollectee + totalPlus);
  const tvaAPayer = roundAmount(Math.max(totalRappel - totalDeductionsAvecPrecompte, 0));
  const precompteReporter = roundAmount(Math.max(totalDeductionsAvecPrecompte - totalRappel, 0));

  return {
    total_collectee: totalCollectee,
    precompte_anterieur: precompteAnterieur,
    total_deductible: totalDeductible,
    total_deductions_avec_precompte: totalDeductionsAvecPrecompte,
    total_rappel: totalRappel,
    total_plus: totalPlus,
    tva_a_payer: tvaAPayer,
    precompte_reporter: precompteReporter,
  };
};

const buildRecapRows = (header) => [
  { cadre: 1, label: "IBS/ICBS", montant: Number(header.total_cadre1 || 0) },
  { cadre: 2, label: "IBS retenues source", montant: Number(header.total_cadre2 || 0) },
  { cadre: 3, label: "IRG BIC/BNC", montant: Number(header.total_cadre3 || 0) },
  { cadre: 4, label: "IRG/Salaires", montant: Number(header.total_cadre4 || 0) },
  { cadre: 5, label: "TLS", montant: Number(header.total_cadre5 || 0) },
  { cadre: 6, label: "Droit de timbre", montant: Number(header.total_cadre6 || 0) },
  { cadre: 7, label: "Autres taxes", montant: Number(header.total_cadre7 || 0) },
  { cadre: 8, label: "TVA", montant: Number(header.tva_a_payer || 0) },
];

const computePermissions = ({ role, permissions = [] }) => ({
  can_adjust: true,
  can_validate:
    VALIDATION_ROLES.has(role) ||
    hasPermission(permissions, "*", "closure:update", "entries:validate"),
  can_reopen:
    ADMIN_ROLES.has(role) ||
    hasPermission(permissions, "*", "closure:update", "system_settings:update"),
});

const fetchMergedTaxCodeByCode = async (db, companyId, code) => {
  const { rows } = await db.query(
    `
      ${buildMergedTaxCodesQuery({
        orderClause: "ORDER BY code ASC",
      })}
    `,
    [companyId],
  );

  const match = rows.find((row) => row.code === code);
  return match
    ? {
        id: Number(match.id),
        code: match.code,
        label: match.label,
        cadre: Number(match.cadre),
        sub_cadre: match.sub_cadre || "",
        tax_category: match.tax_category || "",
        rate: match.rate === null ? null : Number(match.rate),
        is_deduction: Boolean(match.is_deduction),
      }
    : null;
};

const upsertSyntheticLine = async ({
  db,
  companyId,
  headerId,
  code,
  amount,
}) => {
  const existing = await db.query(
    `
      SELECT id
      FROM g50_lines
      WHERE g50_header_id = $1
        AND code = $2
      LIMIT 1
    `,
    [headerId, code],
  );

  if (!existing.rows[0] && amount <= 0) {
    return null;
  }

  const taxCode = await fetchMergedTaxCodeByCode(db, companyId, code);

  if (!taxCode) {
    return null;
  }

  if (existing.rows[0]) {
    await db.query(
      `
        UPDATE g50_lines
        SET
          montant_calcule = $3,
          montant_ajuste = NULL,
          montant_final = $3,
          is_manually_adjusted = FALSE,
          adjustment_reason = NULL,
          updated_at = CURRENT_TIMESTAMP
        WHERE id = $1
      `,
      [Number(existing.rows[0].id), headerId, roundAmount(amount)],
    );

    return Number(existing.rows[0].id);
  }

  const insert = await db.query(
    `
      INSERT INTO g50_lines (
        g50_header_id,
        tax_code_id,
        code,
        cadre,
        label,
        ca_global,
        ca_imposable,
        ca_exonere,
        base_imposable,
        taux,
        montant_calcule,
        montant_final,
        source_entry_ids,
        source_entry_count,
        is_manually_adjusted
      )
      VALUES (
        $1, $2, $3, $4, $5,
        0, 0, 0, 0, $6, $7, $7,
        NULL, 0, FALSE
      )
      RETURNING id
    `,
    [
      headerId,
      taxCode.id,
      taxCode.code,
      taxCode.cadre,
      taxCode.label,
      taxCode.rate,
      roundAmount(amount),
    ],
  );

  return Number(insert.rows[0].id);
};

const refreshHeaderTotals = async (db, companyId, headerId) => {
  await upsertSyntheticLine({
    db,
    companyId,
    headerId,
    code: "E3B99",
    amount: 0,
  });
  await upsertSyntheticLine({
    db,
    companyId,
    headerId,
    code: "E3B00",
    amount: 0,
  });

  let lines = await fetchHeaderLines(db, headerId);
  const tvaSummary = buildTvaSummaryFromLines(lines);

  await upsertSyntheticLine({
    db,
    companyId,
    headerId,
    code: "E3B99",
    amount: tvaSummary.tva_a_payer,
  });
  await upsertSyntheticLine({
    db,
    companyId,
    headerId,
    code: "E3B00",
    amount: tvaSummary.precompte_reporter,
  });

  lines = await fetchHeaderLines(db, headerId);
  const refreshedTvaSummary = buildTvaSummaryFromLines(lines);
  const totalsByCadre = lines.reduce(
    (accumulator, line) => {
      accumulator[line.cadre] = roundAmount(
        Number(accumulator[line.cadre] || 0) + Number(line.montant_final || 0),
      );
      return accumulator;
    },
    { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0 },
  );
  const totalAPayer = roundAmount(
    Number(totalsByCadre[1] || 0) +
      Number(totalsByCadre[2] || 0) +
      Number(totalsByCadre[3] || 0) +
      Number(totalsByCadre[4] || 0) +
      Number(totalsByCadre[5] || 0) +
      Number(totalsByCadre[6] || 0) +
      Number(totalsByCadre[7] || 0) +
      Number(refreshedTvaSummary.tva_a_payer || 0),
  );

  await db.query(
    `
      UPDATE g50_headers
      SET
        total_cadre1 = $2,
        total_cadre2 = $3,
        total_cadre3 = $4,
        total_cadre4 = $5,
        total_cadre5 = $6,
        total_cadre6 = $7,
        total_cadre7 = $8,
        total_tva_collectee = $9,
        total_tva_deductible = $10,
        tva_a_payer = $11,
        tva_precompte_reporter = $12,
        total_a_payer = $13,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
    `,
    [
      headerId,
      totalsByCadre[1] || 0,
      totalsByCadre[2] || 0,
      totalsByCadre[3] || 0,
      totalsByCadre[4] || 0,
      totalsByCadre[5] || 0,
      totalsByCadre[6] || 0,
      totalsByCadre[7] || 0,
      refreshedTvaSummary.total_collectee,
      refreshedTvaSummary.total_deductions_avec_precompte,
      refreshedTvaSummary.tva_a_payer,
      refreshedTvaSummary.precompte_reporter,
      totalAPayer,
    ],
  );

  return {
    lines,
    tva_summary: refreshedTvaSummary,
  };
};

const syncEditableHeaderStatus = async (db, headerId) => {
  const { rows } = await db.query(
    `
      SELECT
        gh.status,
        COUNT(*) FILTER (WHERE gl.is_manually_adjusted) AS adjusted_count
      FROM g50_headers gh
      LEFT JOIN g50_lines gl
        ON gl.g50_header_id = gh.id
      WHERE gh.id = $1
      GROUP BY gh.status
      LIMIT 1
    `,
    [headerId],
  );

  const currentStatus = rows[0]?.status || "draft";

  if (!["draft", "adjusted"].includes(currentStatus)) {
    return currentStatus;
  }

  const nextStatus = Number(rows[0]?.adjusted_count || 0) > 0 ? "adjusted" : "draft";

  if (nextStatus !== currentStatus) {
    await db.query(
      `
        UPDATE g50_headers
        SET
          status = $2,
          updated_at = CURRENT_TIMESTAMP
        WHERE id = $1
      `,
      [headerId, nextStatus],
    );
  }

  return nextStatus;
};

const fetchEditableLineContext = async (db, companyId, headerId, lineId) => {
  const header = await fetchHeaderContext(db, companyId, headerId);

  if (!["draft", "adjusted"].includes(header.status)) {
    throw new ApiError(409, "Ce brouillon G50 est verrouillé.");
  }

  const lines = await fetchHeaderLines(db, headerId);
  const line = lines.find((item) => item.id === Number(lineId));

  if (!line) {
    throw new ApiError(404, "Rubrique G50 introuvable.");
  }

  return { header, line };
};

const fetchAccountMappings = async (db, companyId, periodFrom, periodTo) => {
  const { rows } = await db.query(
    `
      SELECT
        fam.id,
        fam.account_id,
        fam.tax_code_id,
        fam.amount_side,
        fam.sign,
        fam.notes,
        fam.effective_from,
        fam.effective_to
      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
    `,
    [companyId, periodFrom, periodTo],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    account_id: Number(row.account_id),
    tax_code_id: Number(row.tax_code_id),
    amount_side: row.amount_side || "credit",
    sign: Number(row.sign || 1),
    notes: row.notes || "",
    effective_from: toDateKey(row.effective_from),
    effective_to: row.effective_to ? toDateKey(row.effective_to) : "",
  }));
};

const fetchJournalMappings = async (db, companyId, periodFrom, periodTo) => {
  const { rows } = await db.query(
    `
      SELECT
        fjm.id,
        fjm.journal_id,
        fjm.tax_code_id,
        fjm.mapping_type,
        fjm.amount_side,
        fjm.sign,
        fjm.effective_from,
        fjm.effective_to
      FROM fiscal_journal_mapping fjm
      WHERE fjm.company_id = $1
        AND fjm.is_active = TRUE
        AND fjm.effective_from <= $3
        AND COALESCE(fjm.effective_to, DATE '2999-12-31') >= $2
    `,
    [companyId, periodFrom, periodTo],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    journal_id: Number(row.journal_id),
    tax_code_id: Number(row.tax_code_id),
    mapping_type: row.mapping_type || "include",
    amount_side: row.amount_side || "credit",
    sign: Number(row.sign || 1),
    effective_from: toDateKey(row.effective_from),
    effective_to: row.effective_to ? toDateKey(row.effective_to) : "",
  }));
};

const fetchFiscalRules = async (db, companyId, periodFrom, periodTo) => {
  const { rows } = await db.query(
    `
      SELECT
        fr.id,
        fr.tax_code_id,
        fr.rule_name,
        fr.rule_type,
        fr.account_code_from,
        fr.account_code_to,
        fr.account_code_exact,
        fr.account_code_exact_list,
        fr.journal_type,
        fr.journal_type_list,
        fr.entry_label_pattern,
        fr.auxiliary_type,
        fr.amount_side,
        fr.sign,
        fr.effective_from,
        fr.effective_to,
        fr.description,
        fr.legal_reference
      FROM fiscal_rules fr
      WHERE fr.company_id = $1
        AND fr.is_active = TRUE
        AND fr.effective_from <= $3
        AND COALESCE(fr.effective_to, DATE '2999-12-31') >= $2
    `,
    [companyId, periodFrom, periodTo],
  );

  return rows.map((row) => ({
    id: Number(row.id),
    tax_code_id: Number(row.tax_code_id),
    rule_name: row.rule_name || "",
    rule_type: row.rule_type,
    account_code_from: row.account_code_from || "",
    account_code_to: row.account_code_to || "",
    account_code_exact: row.account_code_exact || "",
    account_code_exact_list: Array.isArray(row.account_code_exact_list)
      ? row.account_code_exact_list.map((value) => cleanString(value)).filter(Boolean)
      : [],
    journal_type: row.journal_type || "",
    journal_type_list: Array.isArray(row.journal_type_list)
      ? row.journal_type_list.map((value) => cleanString(value)).filter(Boolean)
      : [],
    entry_label_pattern: row.entry_label_pattern || "",
    auxiliary_type: row.auxiliary_type || "",
    amount_side: row.amount_side || "credit",
    sign: Number(row.sign || 1),
    effective_from: toDateKey(row.effective_from),
    effective_to: row.effective_to ? toDateKey(row.effective_to) : "",
    description: row.description || "",
    legal_reference: row.legal_reference || "",
  }));
};

const isWithinEffectiveWindow = (entryDate, item) => {
  const dateKey = String(entryDate).slice(0, 10);
  return (
    item.effective_from <= dateKey &&
    (!item.effective_to || item.effective_to >= dateKey)
  );
};

const doesRuleMatchLine = (rule, line) => {
  if (!RULE_TYPES.has(rule.rule_type) || !isWithinEffectiveWindow(line.entry_date, rule)) {
    return false;
  }

  if (rule.rule_type === "account_range") {
    return (
      cleanString(line.account_code) >= cleanString(rule.account_code_from) &&
      cleanString(line.account_code) <= cleanString(rule.account_code_to)
    );
  }

  if (rule.rule_type === "account_exact") {
    const candidates = rule.account_code_exact_list.length
      ? rule.account_code_exact_list
      : [rule.account_code_exact].filter(Boolean);
    return candidates.includes(cleanString(line.account_code));
  }

  if (rule.rule_type === "journal_type") {
    const candidates = rule.journal_type_list.length
      ? rule.journal_type_list
      : [rule.journal_type].filter(Boolean);
    return candidates.includes(cleanString(line.journal_type));
  }

  if (rule.rule_type === "entry_label_pattern") {
    const regex = convertLikePatternToRegex(rule.entry_label_pattern || "%");
    return regex.test(line.entry_label || "") || regex.test(line.line_label || "");
  }

  if (rule.rule_type === "auxiliary_type") {
    return cleanString(line.third_party_type) === cleanString(rule.auxiliary_type);
  }

  return false;
};

const fetchSourceLinesForHeaderLine = async (db, header, line) => {
  if (!line.source_entry_ids.length) {
    return [];
  }

  const { rows } = await db.query(
    `
      SELECT
        el.id AS entry_line_id,
        el.entry_id,
        el.debit,
        el.credit,
        je.entry_number,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.piece_ref,
        je.label AS entry_label,
        COALESCE(el.label, je.label) AS line_label,
        je.journal_id,
        j.code AS journal_code,
        j.label AS journal_label,
        j.journal_type,
        a.id AS account_id,
        a.code AS account_code,
        a.label AS account_label,
        aa.third_party_type
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      JOIN journals j
        ON j.id = je.journal_id
      JOIN accounts a
        ON a.id = el.account_id
      LEFT JOIN auxiliary_accounts aa
        ON aa.id = el.auxiliary_account_id
      WHERE je.company_id = $1
        AND je.status = 'validated'
        AND je.entry_date BETWEEN $2 AND $3
        AND je.id = ANY($4::bigint[])
      ORDER BY je.entry_date ASC, je.id ASC, el.id ASC
    `,
    [
      header.company_id,
      header.descriptor.date_from,
      header.descriptor.date_to,
      line.source_entry_ids,
    ],
  );

  return rows.map((row) => ({
    entry_line_id: Number(row.entry_line_id),
    entry_id: Number(row.entry_id),
    debit: Number(row.debit || 0),
    credit: Number(row.credit || 0),
    entry_number: row.entry_number || null,
    entry_date: row.entry_date,
    piece_ref: row.piece_ref || null,
    entry_label: row.entry_label || "",
    line_label: row.line_label || "",
    journal_id: Number(row.journal_id),
    journal_code: row.journal_code,
    journal_label: row.journal_label,
    journal_type: row.journal_type,
    account_id: Number(row.account_id),
    account_code: row.account_code,
    account_label: row.account_label,
    third_party_type: row.third_party_type || "",
  }));
};

const buildLineEntriesAnalysis = async ({ db, header, line }) => {
  if (line.code === "E3B91") {
    const previousPrecompte = await fetchPreviousPrecompteForHeader(db, header.company_id, header);

    return {
      line_id: line.id,
      calculated_amount: line.montant_calcule,
      source_accounts: [],
      applied_rules: previousPrecompte.amount
        ? ["Précompte TVA du mois précédent"]
        : [],
      entries_count: 0,
      entries: [],
      synthetic_message: previousPrecompte.amount
        ? `Précompte récupéré depuis ${previousPrecompte.period_label || "la période précédente"}`
        : "Aucun précompte antérieur disponible.",
    };
  }

  if (["E3B99", "E3B00"].includes(line.code)) {
    return {
      line_id: line.id,
      calculated_amount: line.montant_calcule,
      source_accounts: [],
      applied_rules: ["Formule de synthèse TVA"],
      entries_count: 0,
      entries: [],
      synthetic_message:
        line.code === "E3B99"
          ? "Rubrique de synthèse TVA à payer."
          : "Rubrique de synthèse précompte à reporter.",
    };
  }

  const [accountMappings, journalMappings, fiscalRules, sourceLines] = await Promise.all([
    fetchAccountMappings(db, header.company_id, header.descriptor.date_from, header.descriptor.date_to),
    fetchJournalMappings(db, header.company_id, header.descriptor.date_from, header.descriptor.date_to),
    fetchFiscalRules(db, header.company_id, header.descriptor.date_from, header.descriptor.date_to),
    fetchSourceLinesForHeaderLine(db, header, line),
  ]);

  const accountMappingsByAccountId = accountMappings.reduce((accumulator, item) => {
    accumulator[item.account_id] ||= [];
    accumulator[item.account_id].push(item);
    return accumulator;
  }, {});
  const journalMappingsByJournalId = journalMappings.reduce((accumulator, item) => {
    accumulator[item.journal_id] ||= [];
    accumulator[item.journal_id].push(item);
    return accumulator;
  }, {});

  const sourceAccounts = new Map();
  const appliedRules = new Set();
  const retainedEntries = sourceLines
    .map((sourceLine) => {
      let retainedAmount = 0;

      (accountMappingsByAccountId[sourceLine.account_id] || [])
        .filter((item) => item.tax_code_id === line.tax_code_id && isWithinEffectiveWindow(sourceLine.entry_date, item))
        .forEach((mapping) => {
          retainedAmount += roundAmount(getAmountBySide(sourceLine, mapping.amount_side) * mapping.sign);
          appliedRules.add(`Mapping compte ${sourceLine.account_code}`);
        });

      (journalMappingsByJournalId[sourceLine.journal_id] || [])
        .filter((item) => item.tax_code_id === line.tax_code_id && isWithinEffectiveWindow(sourceLine.entry_date, item))
        .forEach((mapping) => {
          const multiplier = mapping.mapping_type === "exclude" ? -1 : 1;
          retainedAmount += roundAmount(
            getAmountBySide(sourceLine, mapping.amount_side) * mapping.sign * multiplier,
          );
          appliedRules.add(`Mapping journal ${sourceLine.journal_code}`);
        });

      fiscalRules
        .filter((rule) => rule.tax_code_id === line.tax_code_id && doesRuleMatchLine(rule, sourceLine))
        .forEach((rule) => {
          retainedAmount += roundAmount(getAmountBySide(sourceLine, rule.amount_side) * rule.sign);
          appliedRules.add(rule.rule_name || `Règle ${rule.id}`);
        });

      if (retainedAmount === 0) {
        return null;
      }

      sourceAccounts.set(sourceLine.account_id, `${sourceLine.account_code} · ${sourceLine.account_label}`);

      return {
        entry_line_id: sourceLine.entry_line_id,
        entry_id: sourceLine.entry_id,
        entry_number: sourceLine.entry_number,
        entry_date: sourceLine.entry_date,
        piece_ref: sourceLine.piece_ref || null,
        journal_code: sourceLine.journal_code,
        journal_label: sourceLine.journal_label,
        account_code: sourceLine.account_code,
        account_label: sourceLine.account_label,
        label: sourceLine.line_label || sourceLine.entry_label,
        debit: Number(sourceLine.debit || 0),
        credit: Number(sourceLine.credit || 0),
        retained_amount: roundAmount(retainedAmount),
      };
    })
    .filter(Boolean);

  return {
    line_id: line.id,
    calculated_amount: line.montant_calcule,
    source_accounts: [...sourceAccounts.values()],
    applied_rules: [...appliedRules],
    entries_count: retainedEntries.length,
    entries: retainedEntries,
    synthetic_message: "",
  };
};

const fetchPreviousPrecompteForHeader = async (db, companyId, header) => {
  const { period_type, annee, mois, trimestre } = header.descriptor;
  let previousPeriodWhere = "";
  const params = [companyId, annee];

  if (period_type === "monthly") {
    const current = new Date(Date.UTC(annee, Number(mois || 1) - 1, 1));
    current.setUTCMonth(current.getUTCMonth() - 1);
    params[1] = current.getUTCFullYear();
    params.push(current.getUTCMonth() + 1);
    previousPeriodWhere = "fp.period_type = 'monthly' AND fp.annee = $2 AND fp.mois = $3";
  } else {
    let previousQuarter = Number(trimestre || 1) - 1;
    let previousYear = annee;

    if (previousQuarter < 1) {
      previousQuarter = 4;
      previousYear -= 1;
    }

    params[1] = previousYear;
    params.push(previousQuarter);
    previousPeriodWhere = "fp.period_type = 'quarterly' AND fp.annee = $2 AND fp.trimestre = $3";
  }

  const { rows } = await db.query(
    `
      SELECT
        gh.id,
        gh.tva_precompte_reporter,
        fp.period_type,
        fp.annee,
        fp.mois,
        fp.trimestre
      FROM g50_headers gh
      JOIN fiscal_periods fp
        ON fp.id = gh.fiscal_period_id
      WHERE gh.company_id = $1
        AND ${previousPeriodWhere}
      ORDER BY gh.version DESC, gh.created_at DESC
      LIMIT 1
    `,
    params,
  );

  if (!rows[0]) {
    return {
      amount: 0,
      source_header_id: null,
      period_label: null,
    };
  }

  return {
    amount: Number(rows[0].tva_precompte_reporter || 0),
    source_header_id: Number(rows[0].id),
    period_label: formatPeriodLabel({
      period_type: rows[0].period_type,
      annee: Number(rows[0].annee),
      mois: rows[0].mois,
      trimestre: rows[0].trimestre,
    }),
  };
};

const ensureBankAccount = async (db, companyId) => {
  const { rows } = await db.query(
    `
      SELECT
        ba.id,
        ba.account_id,
        ba.bank_name,
        ba.account_number,
        a.code AS accounting_code,
        a.label AS accounting_label
      FROM bank_accounts ba
      LEFT JOIN accounts a
        ON a.id = ba.account_id
      WHERE ba.company_id = $1
        AND ba.is_active = TRUE
      ORDER BY ba.id ASC
      LIMIT 1
    `,
    [companyId],
  );

  if (!rows[0]) {
    throw new ApiError(409, "Aucun compte bancaire actif n'est configuré pour la société.");
  }

  return {
    id: Number(rows[0].id),
    account_id: rows[0].account_id ? Number(rows[0].account_id) : null,
    bank_name: rows[0].bank_name,
    account_number: rows[0].account_number,
    accounting_code: rows[0].accounting_code || null,
    accounting_label: rows[0].accounting_label || null,
  };
};

const fetchPostingAccount = async (db, companyId, { exactCode, fallbackPrefixes = [], label }) => {
  const prefixes = fallbackPrefixes
    .map((value) => cleanString(value).toUpperCase())
    .filter(Boolean);
  const exact = cleanString(exactCode).toUpperCase();
  const likePatterns = prefixes.map((value) => `${value}%`);

  const { rows } = await db.query(
    `
      SELECT
        id,
        code,
        label,
        is_movable,
        is_active
      FROM accounts
      WHERE company_id = $1
        AND is_active = TRUE
        AND (
          UPPER(code) = $2
          OR (cardinality($3::text[]) > 0 AND UPPER(code) LIKE ANY($3::text[]))
        )
      ORDER BY
        CASE
          WHEN UPPER(code) = $2 THEN 0
          ELSE 1
        END,
        CASE
          WHEN is_movable THEN 0
          ELSE 1
        END,
        LENGTH(code) ASC,
        code ASC
      LIMIT 1
    `,
    [companyId, exact, likePatterns],
  );

  if (!rows[0]) {
    throw new ApiError(404, `${label} introuvable dans le plan comptable (${exactCode}).`);
  }

  if (!rows[0].is_movable) {
    throw new ApiError(409, `${label} doit être un compte mouvementable.`);
  }

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

const resolveBankCreditAccount = async (db, companyId, bankAccount) => {
  if (bankAccount.account_id) {
    const { rows } = await db.query(
      `
        SELECT id, code, label, is_movable, is_active
        FROM accounts
        WHERE company_id = $1
          AND id = $2
        LIMIT 1
      `,
      [companyId, bankAccount.account_id],
    );

    if (rows[0]?.is_active && rows[0]?.is_movable) {
      return {
        id: Number(rows[0].id),
        code: rows[0].code,
        label: rows[0].label,
      };
    }
  }

  return fetchPostingAccount(db, companyId, {
    exactCode: "512000",
    fallbackPrefixes: ["512"],
    label: "Compte banque",
  });
};

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

  if (!rows[0]) {
    throw new ApiError(404, "Aucun journal correspondant n'est configuré.");
  }

  if (!rows[0].is_active) {
    throw new ApiError(409, "Le journal sélectionné est inactif.");
  }

  return rows[0];
};

const resolveAccountingContext = async (db, companyId, entryDate) => {
  const { rows } = await db.query(
    `
      SELECT
        fy.id AS fiscal_year_id,
        fy.label AS fiscal_year_label,
        fy.is_closed AS fiscal_year_is_closed,
        ap.id AS period_id,
        ap.is_closed AS period_is_closed
      FROM fiscal_years fy
      LEFT JOIN accounting_periods ap
        ON ap.fiscal_year_id = fy.id
        AND ap.company_id = fy.company_id
        AND ap.month = EXTRACT(MONTH FROM $2::date)::int
        AND ap.year = EXTRACT(YEAR FROM $2::date)::int
      WHERE fy.company_id = $1
        AND $2::date BETWEEN fy.start_date AND fy.end_date
      ORDER BY fy.is_current DESC, fy.start_date DESC
      LIMIT 1
    `,
    [companyId, entryDate],
  );

  const context = rows[0] || null;

  if (!context?.fiscal_year_id || !context?.period_id) {
    throw new ApiError(
      400,
      "La date d'écriture doit appartenir à un exercice et une période comptables existants.",
    );
  }

  if (context.fiscal_year_is_closed || context.period_is_closed) {
    throw new ApiError(400, "La date d'écriture doit appartenir à une période ouverte.");
  }

  return {
    fiscal_year_id: Number(context.fiscal_year_id),
    fiscal_year_label: context.fiscal_year_label,
    period_id: Number(context.period_id),
  };
};

const buildEntryNumber = async (db, { companyId, journal, fiscalYearId, fiscalYearLabel }) => {
  const fiscalSegment =
    (
      cleanString(fiscalYearLabel).replace(/[^0-9A-Za-z]+/g, "") || String(fiscalYearId)
    ).toUpperCase();
  const prefix = `${cleanString(journal.code).toUpperCase()}-${fiscalSegment}-`;
  const { rows } = await db.query(
    `
      SELECT
        COALESCE(
          MAX(
            NULLIF(SUBSTRING(entry_number FROM '([0-9]+)$'), '')::int
          ),
          0
        ) + 1 AS next_sequence
      FROM journal_entries
      WHERE company_id = $1
        AND journal_id = $2
        AND fiscal_year_id = $3
        AND UPPER(entry_number) LIKE UPPER($4)
    `,
    [companyId, journal.id, fiscalYearId, `${prefix}%`],
  );

  return `${prefix}${String(Number(rows[0]?.next_sequence || 1)).padStart(5, "0")}`;
};

const insertEntryLines = async (db, entryId, lines) => {
  for (const line of lines) {
    await db.query(
      `
        INSERT INTO entry_lines (
          entry_id,
          account_id,
          auxiliary_account_id,
          label,
          debit,
          credit,
          due_date
        )
        VALUES ($1, $2, NULL, $3, $4, $5, NULL)
      `,
      [entryId, line.account_id, line.label, line.debit, line.credit],
    );
  }
};

const fetchExistingEntryKinds = async (db, companyId, declarationId) => {
  const { rows } = await db.query(
    `
      SELECT entry_kind
      FROM fiscal_declaration_entries
      WHERE company_id = $1
        AND declaration_type = 'G50'
        AND declaration_id = $2
    `,
    [companyId, declarationId],
  );

  return new Set(rows.map((row) => row.entry_kind));
};

const buildLegacyG50PayloadFromDraft = async ({ db, companyId, header, lines }) => {
  const company = await fetchCompany(db, companyId);
  const profile = await fetchFiscalProfile(db, companyId);
  const previousPrecompte = await fetchPreviousPrecompteForHeader(db, companyId, header);
  const byCode = new Map(lines.map((line) => [line.code, line]));
  const getAmount = (code) => Number(byCode.get(code)?.montant_final || 0);
  const toRows = (codes) =>
    codes
      .map((code) => byCode.get(code))
      .filter(Boolean)
      .map((line) => ({
        code: line.code,
        label: line.label,
        base: line.base_imposable || line.ca_imposable || line.ca_global || 0,
        rate: line.taux || 0,
        amount: line.montant_final || 0,
        ca_total: line.ca_global || 0,
        ca_exonere: line.ca_exonere || 0,
        ca_imposable: line.ca_imposable || line.base_imposable || 0,
        montant_droits: line.montant_final || 0,
      }));
  const tvaSummary = buildTvaSummaryFromLines(lines);

  return {
    company,
    profile,
    previousPrecompte,
    declaration: {
      company_id: companyId,
      mois: header.descriptor.mois,
      trimestre: header.descriptor.trimestre,
      annee: header.descriptor.annee,
      periodicite: header.descriptor.period_type === "monthly" ? "mensuel" : "trimestriel",
      period_label: header.descriptor.period_label,
      nif: profile.nif || company.nif || null,
      nin: profile.nin || null,
      article_imposition: profile.article_imposition || company.ai || null,
      forme_juridique: profile.forme_juridique || null,
      code_activite: profile.code_activite || null,
      cadre1_detail: { rows: toRows(["E1M10"]) },
      e1m10_acompte_ibs: getAmount("E1M10"),
      solde_liquidation_ibs: 0,
      acompte_icbs: 0,
      solde_liquidation_icbs: 0,
      total_cadre1: header.total_cadre1,
      cadre2_detail: { rows: toRows(["E1M30", "E1M40"]) },
      e1m30_ibs_entreprises_etrangeres: getAmount("E1M30"),
      taux_e1m30: Number(byCode.get("E1M30")?.taux || 0),
      e1m40_ibs_autres_retenues: getAmount("E1M40"),
      taux_e1m40: Number(byCode.get("E1M40")?.taux || 0),
      total_cadre2: header.total_cadre2,
      cadre3_detail: { rows: toRows(["E1N10", "E1N20"]) },
      acompte_irg_bic_bnc: getAmount("E1N10"),
      solde_liquidation_irg: getAmount("E1N20"),
      total_cadre3: header.total_cadre3,
      irg_retenues_detail: {
        e1l20: { amount: getAmount("E1L20"), base: Number(byCode.get("E1L20")?.base_imposable || 0) },
        e1l30: { amount: getAmount("E1L30"), base: Number(byCode.get("E1L30")?.base_imposable || 0) },
        e1l40: { amount: getAmount("E1L40"), base: Number(byCode.get("E1L40")?.base_imposable || 0) },
        e1l60: { amount: getAmount("E1L60"), base: Number(byCode.get("E1L60")?.base_imposable || 0) },
        e1l80: { amount: getAmount("E1L80"), base: Number(byCode.get("E1L80")?.base_imposable || 0) },
      },
      total_cadre4: header.total_cadre4,
      tls_detail: { monthly_rows: toRows(["E2D10", "E2D20", "E2D21"]) },
      total_cadre5: header.total_cadre5,
      droits_timbre_detail: { rows: toRows(["E2E00"]) },
      total_cadre6: header.total_cadre6,
      autres_taxes_detail: { rows: lines.filter((line) => line.cadre === 7).map((line) => ({
        code: line.code,
        label: line.label,
        base: line.base_imposable || 0,
        rate: line.taux || 0,
        amount: line.montant_final || 0,
      })) },
      total_cadre7: header.total_cadre7,
      tva_ca_imposable_detail: {
        rows9: lines.filter((line) => TVA_9_CODES.has(line.code)).map((line) => ({
          code: line.code,
          label: line.label,
          ca_total: line.ca_global || 0,
          ca_exonere: line.ca_exonere || 0,
          ca_imposable: line.ca_imposable || line.base_imposable || 0,
          rate: line.taux || 0,
          montant_droits: line.montant_final || 0,
        })),
        rows19: lines.filter((line) => TVA_19_CODES.has(line.code)).map((line) => ({
          code: line.code,
          label: line.label,
          ca_total: line.ca_global || 0,
          ca_exonere: line.ca_exonere || 0,
          ca_imposable: line.ca_imposable || line.base_imposable || 0,
          rate: line.taux || 0,
          montant_droits: line.montant_final || 0,
        })),
      },
      tva_deductions_detail: {
        e3b91: getAmount("E3B91"),
        e3b92: getAmount("E3B92"),
        e3b93: getAmount("E3B93"),
        e3b94: getAmount("E3B94"),
        e3b95: getAmount("E3B95"),
        e3b96: getAmount("E3B96"),
        e3b97: getAmount("E3B97"),
        e3b98: getAmount("E3B98"),
      },
      tva_total_droits: tvaSummary.total_collectee,
      tva_total_deductions: tvaSummary.total_deductions_avec_precompte,
      tva_a_payer: tvaSummary.tva_a_payer,
      tva_precompte_reporter: tvaSummary.precompte_reporter,
      total_cadre8: tvaSummary.tva_a_payer,
      total_a_payer: header.total_a_payer,
      mode_paiement: "bank",
      cheque_bancaire_no: null,
      cheque_bancaire_agence: null,
      cheque_postal_no: null,
      montant_numeraire: 0,
      quittance_numero: header.quittance_numero || null,
      quittance_date: header.quittance_date || null,
      lieu_declaration: company.city || null,
      date_signature: header.validated_at ? String(header.validated_at).slice(0, 10) : null,
      statut: "validee",
      observations: "Synchronisé depuis le moteur automatique G50.",
    },
  };
};

const upsertLegacyG50DeclarationFromDraft = async ({
  db,
  companyId,
  header,
  lines,
  userId,
}) => {
  const payload = await buildLegacyG50PayloadFromDraft({
    db,
    companyId,
    header,
    lines,
  });
  const declaration = payload.declaration;
  let existingId = header.g50_declaration_id;

  if (!existingId) {
    const { rows } = await db.query(
      `
        SELECT id
        FROM g50_declarations
        WHERE company_id = $1
          AND annee = $2
          AND periodicite = $3
          AND COALESCE(mois, 0) = COALESCE($4, 0)
          AND COALESCE(trimestre, 0) = COALESCE($5, 0)
        LIMIT 1
      `,
      [
        companyId,
        declaration.annee,
        declaration.periodicite,
        declaration.mois,
        declaration.trimestre,
      ],
    );

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

  if (existingId) {
    await db.query(
      `
        UPDATE g50_declarations
        SET
          nif = $2,
          nin = $3,
          article_imposition = $4,
          forme_juridique = $5,
          code_activite = $6,
          cadre1_detail = $7::jsonb,
          e1m10_acompte_ibs = $8,
          solde_liquidation_ibs = $9,
          acompte_icbs = $10,
          solde_liquidation_icbs = $11,
          total_cadre1 = $12,
          cadre2_detail = $13::jsonb,
          e1m30_ibs_entreprises_etrangeres = $14,
          taux_e1m30 = $15,
          e1m40_ibs_autres_retenues = $16,
          taux_e1m40 = $17,
          total_cadre2 = $18,
          cadre3_detail = $19::jsonb,
          acompte_irg_bic_bnc = $20,
          solde_liquidation_irg = $21,
          total_cadre3 = $22,
          irg_retenues_detail = $23::jsonb,
          total_cadre4 = $24,
          tls_detail = $25::jsonb,
          total_cadre5 = $26,
          droits_timbre_detail = $27::jsonb,
          total_cadre6 = $28,
          autres_taxes_detail = $29::jsonb,
          total_cadre7 = $30,
          tva_ca_imposable_detail = $31::jsonb,
          tva_deductions_detail = $32::jsonb,
          tva_total_droits = $33,
          tva_total_deductions = $34,
          tva_a_payer = $35,
          tva_precompte_reporter = $36,
          total_cadre8 = $37,
          total_a_payer = $38,
          quittance_numero = $39,
          quittance_date = $40,
          lieu_declaration = $41,
          date_signature = $42,
          statut = 'validee',
          submitted_at = CURRENT_TIMESTAMP,
          observations = $43,
          updated_at = CURRENT_TIMESTAMP
        WHERE id = $1
      `,
      [
        existingId,
        declaration.nif,
        declaration.nin,
        declaration.article_imposition,
        declaration.forme_juridique,
        declaration.code_activite,
        JSON.stringify(declaration.cadre1_detail),
        declaration.e1m10_acompte_ibs,
        declaration.solde_liquidation_ibs,
        declaration.acompte_icbs,
        declaration.solde_liquidation_icbs,
        declaration.total_cadre1,
        JSON.stringify(declaration.cadre2_detail),
        declaration.e1m30_ibs_entreprises_etrangeres,
        declaration.taux_e1m30,
        declaration.e1m40_ibs_autres_retenues,
        declaration.taux_e1m40,
        declaration.total_cadre2,
        JSON.stringify(declaration.cadre3_detail),
        declaration.acompte_irg_bic_bnc,
        declaration.solde_liquidation_irg,
        declaration.total_cadre3,
        JSON.stringify(declaration.irg_retenues_detail),
        declaration.total_cadre4,
        JSON.stringify(declaration.tls_detail),
        declaration.total_cadre5,
        JSON.stringify(declaration.droits_timbre_detail),
        declaration.total_cadre6,
        JSON.stringify(declaration.autres_taxes_detail),
        declaration.total_cadre7,
        JSON.stringify(declaration.tva_ca_imposable_detail),
        JSON.stringify(declaration.tva_deductions_detail),
        declaration.tva_total_droits,
        declaration.tva_total_deductions,
        declaration.tva_a_payer,
        declaration.tva_precompte_reporter,
        declaration.total_cadre8,
        declaration.total_a_payer,
        declaration.quittance_numero,
        declaration.quittance_date,
        declaration.lieu_declaration,
        declaration.date_signature,
        declaration.observations,
      ],
    );
  } else {
    const insert = await db.query(
      `
        INSERT INTO g50_declarations (
          company_id,
          mois,
          trimestre,
          annee,
          periodicite,
          nif,
          nin,
          article_imposition,
          forme_juridique,
          code_activite,
          cadre1_detail,
          e1m10_acompte_ibs,
          solde_liquidation_ibs,
          acompte_icbs,
          solde_liquidation_icbs,
          total_cadre1,
          cadre2_detail,
          e1m30_ibs_entreprises_etrangeres,
          taux_e1m30,
          e1m40_ibs_autres_retenues,
          taux_e1m40,
          total_cadre2,
          cadre3_detail,
          acompte_irg_bic_bnc,
          solde_liquidation_irg,
          total_cadre3,
          irg_retenues_detail,
          total_cadre4,
          tls_detail,
          total_cadre5,
          droits_timbre_detail,
          total_cadre6,
          autres_taxes_detail,
          total_cadre7,
          tva_ca_imposable_detail,
          tva_deductions_detail,
          tva_total_droits,
          tva_total_deductions,
          tva_a_payer,
          tva_precompte_reporter,
          total_cadre8,
          total_a_payer,
          quittance_numero,
          quittance_date,
          lieu_declaration,
          date_signature,
          statut,
          observations,
          created_by
        )
        VALUES (
          $1, $2, $3, $4, $5,
          $6, $7, $8, $9, $10,
          $11::jsonb, $12, $13, $14, $15, $16,
          $17::jsonb, $18, $19, $20, $21, $22,
          $23::jsonb, $24, $25, $26,
          $27::jsonb, $28, $29::jsonb, $30,
          $31::jsonb, $32, $33::jsonb, $34,
          $35::jsonb, $36::jsonb, $37, $38, $39, $40, $41, $42,
          $43, $44, $45, $46, 'validee', $47, $48
        )
        RETURNING id
      `,
      [
        companyId,
        declaration.mois,
        declaration.trimestre,
        declaration.annee,
        declaration.periodicite,
        declaration.nif,
        declaration.nin,
        declaration.article_imposition,
        declaration.forme_juridique,
        declaration.code_activite,
        JSON.stringify(declaration.cadre1_detail),
        declaration.e1m10_acompte_ibs,
        declaration.solde_liquidation_ibs,
        declaration.acompte_icbs,
        declaration.solde_liquidation_icbs,
        declaration.total_cadre1,
        JSON.stringify(declaration.cadre2_detail),
        declaration.e1m30_ibs_entreprises_etrangeres,
        declaration.taux_e1m30,
        declaration.e1m40_ibs_autres_retenues,
        declaration.taux_e1m40,
        declaration.total_cadre2,
        JSON.stringify(declaration.cadre3_detail),
        declaration.acompte_irg_bic_bnc,
        declaration.solde_liquidation_irg,
        declaration.total_cadre3,
        JSON.stringify(declaration.irg_retenues_detail),
        declaration.total_cadre4,
        JSON.stringify(declaration.tls_detail),
        declaration.total_cadre5,
        JSON.stringify(declaration.droits_timbre_detail),
        declaration.total_cadre6,
        JSON.stringify(declaration.autres_taxes_detail),
        declaration.total_cadre7,
        JSON.stringify(declaration.tva_ca_imposable_detail),
        JSON.stringify(declaration.tva_deductions_detail),
        declaration.tva_total_droits,
        declaration.tva_total_deductions,
        declaration.tva_a_payer,
        declaration.tva_precompte_reporter,
        declaration.total_cadre8,
        declaration.total_a_payer,
        declaration.quittance_numero,
        declaration.quittance_date,
        declaration.lieu_declaration,
        declaration.date_signature,
        declaration.observations,
        userId || null,
      ],
    );

    existingId = Number(insert.rows[0].id);
  }

  await db.query(
    `
      UPDATE g50_headers
      SET
        g50_declaration_id = $2,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
    `,
    [header.id, existingId],
  );

  return {
    id: existingId,
    declaration,
    company: payload.company,
    profile: payload.profile,
    previousPrecompte: payload.previousPrecompte,
  };
};

const createPaymentEntry = async ({
  db,
  companyId,
  declarationId,
  declarationLabel,
  periodLabel,
  entryKind,
  label,
  amount,
  entryDate,
  pieceRef,
  debitAccount,
  bankAccount,
  userId,
}) => {
  if (roundAmount(amount) <= 0) {
    return null;
  }

  const existingKinds = await fetchExistingEntryKinds(db, companyId, declarationId);

  if (existingKinds.has(entryKind)) {
    return null;
  }

  const journal = await fetchJournalByType(db, companyId, "bank");
  const creditAccount = await resolveBankCreditAccount(db, companyId, bankAccount);
  const accountingContext = await resolveAccountingContext(db, companyId, entryDate);
  const entryNumber = await buildEntryNumber(db, {
    companyId,
    journal,
    fiscalYearId: accountingContext.fiscal_year_id,
    fiscalYearLabel: accountingContext.fiscal_year_label,
  });
  const { rows } = await db.query(
    `
      INSERT INTO journal_entries (
        company_id,
        journal_id,
        fiscal_year_id,
        period_id,
        entry_number,
        entry_date,
        label,
        status,
        piece_ref,
        created_by,
        created_at,
        updated_at
      )
      VALUES (
        $1,
        $2,
        $3,
        $4,
        $5,
        $6::date,
        $7,
        'draft',
        $8,
        $9,
        CURRENT_TIMESTAMP,
        CURRENT_TIMESTAMP
      )
      RETURNING id
    `,
    [
      companyId,
      journal.id,
      accountingContext.fiscal_year_id,
      accountingContext.period_id,
      entryNumber,
      entryDate,
      label,
      pieceRef || null,
      userId || null,
    ],
  );

  const journalEntryId = Number(rows[0].id);
  await insertEntryLines(db, journalEntryId, [
    {
      account_id: debitAccount.id,
      label,
      debit: roundAmount(amount),
      credit: 0,
    },
    {
      account_id: creditAccount.id,
      label,
      debit: 0,
      credit: roundAmount(amount),
    },
  ]);

  await db.query(
    `
      INSERT INTO fiscal_declaration_entries (
        company_id,
        declaration_type,
        declaration_id,
        declaration_year,
        entry_kind,
        declaration_label,
        period_label,
        amount,
        journal_entry_id,
        generated_by,
        metadata
      )
      VALUES (
        $1,
        'G50',
        $2,
        $3,
        $4,
        $5,
        $6,
        $7,
        $8,
        $9,
        $10::jsonb
      )
    `,
    [
      companyId,
      declarationId,
      Number(String(periodLabel).slice(-4)) || null,
      entryKind,
      declarationLabel,
      periodLabel,
      roundAmount(amount),
      journalEntryId,
      userId || null,
      JSON.stringify({
        piece_ref: pieceRef || null,
        label,
        journal_code: journal.code,
      }),
    ],
  );

  return {
    journal_entry_id: journalEntryId,
    entry_kind: entryKind,
    amount: roundAmount(amount),
  };
};

const generatePaymentEntriesForValidatedG50 = async ({
  db,
  companyId,
  header,
  legacyDeclarationId,
  userId,
}) => {
  const bankAccount = await ensureBankAccount(db, companyId);
  const declarationLabel = `G50 ${header.descriptor.period_label}`;
  const periodLabel = header.descriptor.period_label;
  const debitTvaAccount = await fetchPostingAccount(db, companyId, {
    exactCode: "445500",
    fallbackPrefixes: ["4455", "445"],
    label: "Compte TVA à décaisser",
  });
  const debitIbsAccount = await fetchPostingAccount(db, companyId, {
    exactCode: "444000",
    fallbackPrefixes: ["444"],
    label: "Compte IBS à payer",
  });
  const createdEntries = [];
  const tvaEntry = await createPaymentEntry({
    db,
    companyId,
    declarationId: legacyDeclarationId,
    declarationLabel,
    periodLabel,
    entryKind: "g50_tva",
    label: `Paiement TVA ${periodLabel} G50`,
    amount: header.tva_a_payer,
    entryDate: header.quittance_date || header.descriptor.deadline_g50 || header.descriptor.date_to,
    pieceRef: header.quittance_numero || null,
    debitAccount: debitTvaAccount,
    bankAccount,
    userId,
  });

  if (tvaEntry) {
    createdEntries.push(tvaEntry);
  }

  const ibsAmount = roundAmount(Number(header.total_cadre1 || 0) + Number(header.total_cadre2 || 0));
  const ibsEntry = await createPaymentEntry({
    db,
    companyId,
    declarationId: legacyDeclarationId,
    declarationLabel,
    periodLabel,
    entryKind: "g50_ibs",
    label: `Acompte IBS ${periodLabel} G50`,
    amount: ibsAmount,
    entryDate: header.quittance_date || header.descriptor.deadline_g50 || header.descriptor.date_to,
    pieceRef: header.quittance_numero || null,
    debitAccount: debitIbsAccount,
    bankAccount,
    userId,
  });

  if (ibsEntry) {
    createdEntries.push(ibsEntry);
  }

  return createdEntries;
};

const fetchCoherenceLines = async ({
  db,
  companyId,
  dateFrom,
  dateTo,
  accountPrefixes = [],
  journalType = null,
}) => {
  const params = [companyId, dateFrom, dateTo];
  const conditions = [
    "je.company_id = $1",
    "je.status = 'validated'",
    "je.entry_date BETWEEN $2 AND $3",
  ];

  if (journalType) {
    params.push(journalType);
    conditions.push(`j.journal_type = $${params.length}`);
  }

  if (accountPrefixes.length) {
    const prefixConditions = accountPrefixes.map((prefix) => {
      params.push(`${prefix}%`);
      return `a.code LIKE $${params.length}`;
    });

    conditions.push(`(${prefixConditions.join(" OR ")})`);
  }

  const { rows } = await db.query(
    `
      SELECT
        je.id,
        je.entry_number,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.piece_ref,
        je.label,
        j.label AS journal_label,
        COALESCE(SUM(ABS(COALESCE(el.debit, 0) - COALESCE(el.credit, 0))), 0) AS amount
      FROM entry_lines el
      INNER JOIN journal_entries je ON je.id = el.entry_id
      INNER JOIN journals j ON j.id = je.journal_id
      INNER JOIN accounts a ON a.id = el.account_id
      WHERE ${conditions.join(" AND ")}
      GROUP BY je.id, je.entry_number, je.entry_date, je.piece_ref, je.label, j.label
      ORDER BY je.entry_date DESC, je.id DESC
      LIMIT 10
    `,
    params,
  );

  const total = rows.reduce((sum, row) => sum + Number(row.amount || 0), 0);

  return {
    total: roundAmount(total),
    entries_count: rows.length,
    entries: rows.map((row) => ({
      id: Number(row.id),
      entry_number: row.entry_number,
      entry_date: row.entry_date,
      piece_ref: row.piece_ref || null,
      label: row.label,
      journal_label: row.journal_label,
      amount: Number(row.amount || 0),
    })),
  };
};

const fetchG50terComparison = async (db, companyId, header) => {
  const annee = header.descriptor.annee;
  const trimestre =
    header.descriptor.period_type === "quarterly"
      ? Number(header.descriptor.trimestre || 1)
      : Math.floor((Number(header.descriptor.mois || 1) - 1) / 3) + 1;

  const { rows } = await db.query(
    `
      SELECT
        COALESCE(SUM(montant_irg), 0) AS total_irg,
        COUNT(*)::int AS declarations_count
      FROM g50ter_declarations
      WHERE company_id = $1
        AND annee = $2
        AND trimestre = $3
        AND statut IN ('deposee', 'validee')
    `,
    [companyId, annee, trimestre],
  );

  return {
    trimestre,
    total_irg: Number(rows[0]?.total_irg || 0),
    declarations_count: Number(rows[0]?.declarations_count || 0),
  };
};

const buildDeadlineStatusForReopened = (deadlineDate) => {
  if (!deadlineDate) {
    return "upcoming";
  }

  const today = new Date();
  const deadline = new Date(`${String(deadlineDate).slice(0, 10)}T00:00:00Z`);
  const diffDays = Math.ceil((deadline.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));

  if (diffDays < 0) {
    return "overdue";
  }

  if (diffDays <= 7) {
    return "due_soon";
  }

  return "upcoming";
};

const buildG50DraftResponse = async ({
  db,
  companyId,
  headerId,
  role,
  permissions = [],
}) => {
  const header = await fetchHeaderContext(db, companyId, headerId);
  const [rawLines, adjustments, auditLog, previousPrecompte] = await Promise.all([
    fetchHeaderLines(db, headerId),
    fetchHeaderAdjustments(db, headerId),
    fetchHeaderAuditLog(db, headerId),
    fetchPreviousPrecompteForHeader(db, companyId, header),
  ]);
  const lines = attachAdjustmentsToLines(rawLines, adjustments);
  const tvaSummary = buildTvaSummaryFromLines(lines);
  const zeroLines = lines.filter((line) => Number(line.montant_final || 0) === 0);

  return {
    header,
    lines,
    tva_summary: tvaSummary,
    recap: buildRecapRows(header),
    zero_lines: zeroLines.map((line) => ({
      id: line.id,
      code: line.code,
      label: line.label,
    })),
    audit_log: auditLog,
    permissions: computePermissions({ role, permissions }),
    previous_precompte: previousPrecompte,
  };
};

const fetchHeaderDescriptorOnly = async (db, companyId, headerId) =>
  fetchHeaderContext(db, companyId, headerId);

export const getG50Draft = async ({ db, companyId, headerId, role, permissions = [] }) =>
  buildG50DraftResponse({
    db,
    companyId,
    headerId,
    role,
    permissions,
  });

export const getG50DraftLineEntries = async ({ db, companyId, headerId, lineId }) => {
  const header = await fetchHeaderDescriptorOnly(db, companyId, headerId);
  const lines = await fetchHeaderLines(db, headerId);
  const line = lines.find((item) => item.id === Number(lineId));

  if (!line) {
    throw new ApiError(404, "Rubrique G50 introuvable.");
  }

  const item = await buildLineEntriesAnalysis({
    db,
    header,
    line,
  });

  return {
    header_id: headerId,
    line: {
      id: line.id,
      code: line.code,
      label: line.label,
      montant_calcule: line.montant_calcule,
      montant_final: line.montant_final,
      adjustment_reason: line.adjustment_reason,
    },
    ...item,
  };
};

export const adjustG50DraftLine = async ({
  db,
  companyId,
  headerId,
  lineId,
  newAmount,
  reasonType,
  justification,
  supportingDocumentUrl,
  userId,
  ipAddress,
  role,
  permissions = [],
}) => {
  const { header, line } = await fetchEditableLineContext(db, companyId, headerId, lineId);
  const normalizedReason = cleanString(reasonType);
  const normalizedJustification = cleanString(justification);

  if (!ADJUSTMENT_REASON_TYPES.has(normalizedReason)) {
    throw new ApiError(400, "Le type de correction est invalide.");
  }

  if (normalizedJustification.length < 20) {
    throw new ApiError(400, "La justification doit contenir au moins 20 caractères.");
  }

  const amount = parseAmountField(newAmount, "Le nouveau montant");
  const previousAmount = Number(line.montant_final || 0);
  const difference = roundAmount(amount - previousAmount);

  await db.query(
    `
      INSERT INTO g50_adjustments (
        g50_header_id,
        g50_line_id,
        tax_code_id,
        code,
        montant_avant,
        montant_apres,
        difference,
        reason_type,
        justification,
        supporting_document_url,
        adjusted_by
      )
      VALUES (
        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
      )
    `,
    [
      headerId,
      line.id,
      line.tax_code_id,
      line.code,
      previousAmount,
      amount,
      difference,
      normalizedReason,
      normalizedJustification,
      toNullableText(supportingDocumentUrl),
      userId || null,
    ],
  );

  await db.query(
    `
      UPDATE g50_lines
      SET
        montant_ajuste = $3,
        montant_final = $3,
        is_manually_adjusted = TRUE,
        adjustment_reason = $4,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
        AND g50_header_id = $2
    `,
    [line.id, headerId, amount, normalizedReason],
  );

  await refreshHeaderTotals(db, companyId, headerId);
  await syncEditableHeaderStatus(db, headerId);

  await db.query(
    `
      INSERT INTO g50_audit_log (
        g50_header_id,
        action,
        actor_id,
        details,
        ip_address
      )
      VALUES ($1, 'line_adjusted', $2, $3::jsonb, $4::inet)
    `,
    [
      headerId,
      userId || null,
      JSON.stringify({
        line_id: line.id,
        code: line.code,
        montant_avant: previousAmount,
        montant_apres: amount,
        difference,
        reason_type: normalizedReason,
        justification: normalizedJustification,
      }),
      ipAddress || null,
    ],
  );

  return getG50Draft({
    db,
    companyId,
    headerId,
    role,
    permissions,
  });
};

export const revertG50DraftAdjustment = async ({
  db,
  companyId,
  headerId,
  lineId,
  userId,
  ipAddress,
  role,
  permissions = [],
}) => {
  const { line } = await fetchEditableLineContext(db, companyId, headerId, lineId);

  if (!line.is_manually_adjusted) {
    throw new ApiError(409, "Cette rubrique ne comporte aucun ajustement manuel actif.");
  }

  const previousAmount = Number(line.montant_final || 0);
  const restoredAmount = Number(line.montant_calcule || 0);

  await db.query(
    `
      INSERT INTO g50_adjustments (
        g50_header_id,
        g50_line_id,
        tax_code_id,
        code,
        montant_avant,
        montant_apres,
        difference,
        reason_type,
        justification,
        adjusted_by
      )
      VALUES (
        $1, $2, $3, $4, $5, $6, $7, 'other', $8, $9
      )
    `,
    [
      headerId,
      line.id,
      line.tax_code_id,
      line.code,
      previousAmount,
      restoredAmount,
      roundAmount(restoredAmount - previousAmount),
      "Ajustement annulé — retour au montant calculé automatiquement.",
      userId || null,
    ],
  );

  await db.query(
    `
      UPDATE g50_lines
      SET
        montant_ajuste = NULL,
        montant_final = montant_calcule,
        is_manually_adjusted = FALSE,
        adjustment_reason = NULL,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
        AND g50_header_id = $2
    `,
    [line.id, headerId],
  );

  await refreshHeaderTotals(db, companyId, headerId);
  await syncEditableHeaderStatus(db, headerId);

  await db.query(
    `
      INSERT INTO g50_audit_log (
        g50_header_id,
        action,
        actor_id,
        details,
        ip_address
      )
      VALUES ($1, 'line_adjusted', $2, $3::jsonb, $4::inet)
    `,
    [
      headerId,
      userId || null,
      JSON.stringify({
        line_id: line.id,
        code: line.code,
        reverted: true,
        montant_avant: previousAmount,
        montant_apres: restoredAmount,
      }),
      ipAddress || null,
    ],
  );

  return getG50Draft({
    db,
    companyId,
    headerId,
    role,
    permissions,
  });
};

export const getG50DraftControlChecks = async ({ db, companyId, headerId }) => {
  const header = await fetchHeaderContext(db, companyId, headerId);
  const [draft, preflight, tvaLedger, g50terComparison] = await Promise.all([
    getG50Draft({
      db,
      companyId,
      headerId,
      role: "viewer",
      permissions: [],
    }),
    runG50Preflight({
      db,
      companyId,
      periodFrom: header.descriptor.date_from,
      periodTo: header.descriptor.date_to,
    }),
    fetchCoherenceLines({
      db,
      companyId,
      dateFrom: header.descriptor.date_from,
      dateTo: header.descriptor.date_to,
      accountPrefixes: ["445"],
    }),
    fetchG50terComparison(db, companyId, header),
  ]);
  const computedTva = buildTvaSummaryFromLines(draft.lines);
  const tolerance = 1;
  const zeroLines = draft.zero_lines;
  const checks = [
    {
      key: "tva_grand_livre",
      label: "TVA cohérente avec grand livre",
      ok: roundAmount(Math.abs(Number(header.tva_a_payer || 0) - Number(tvaLedger.total || 0))) <= tolerance,
      is_critical: true,
      result_label:
        roundAmount(Math.abs(Number(header.tva_a_payer || 0) - Number(tvaLedger.total || 0))) <= tolerance
          ? "Écart < 1 DA"
          : `Écart ${roundAmount(Math.abs(Number(header.tva_a_payer || 0) - Number(tvaLedger.total || 0)))} DA`,
      action_path: null,
    },
    {
      key: "g50ter_match",
      label: "IRG salaires = somme G50ter",
      ok: roundAmount(Math.abs(Number(header.total_cadre4 || 0) - Number(g50terComparison.total_irg || 0))) <= tolerance,
      is_critical: false,
      result_label:
        g50terComparison.declarations_count > 0
          ? `Concordance T${g50terComparison.trimestre}`
          : "Aucune G50ter déposée sur le trimestre",
      action_path: "/fiscal/g50ter",
    },
    {
      key: "draft_entries",
      label: "Aucune écriture brouillon non incluse",
      ok: preflight.checks.draft_entries.ok,
      is_critical: true,
      result_label: preflight.checks.draft_entries.message,
      action_path: preflight.checks.draft_entries.ok ? null : "/entries",
    },
    {
      key: "unmapped_accounts",
      label: "Comptes non mappés avec mouvement",
      ok: preflight.checks.unmapped_accounts.ok,
      is_critical: true,
      result_label: preflight.checks.unmapped_accounts.message,
      action_path: preflight.checks.unmapped_accounts.ok
        ? null
        : "/fiscal/config/account-mapping",
    },
    {
      key: "previous_precompte",
      label: "Précompte TVA correct",
      ok:
        roundAmount(
          Math.abs(
            Number(computedTva.precompte_anterieur || 0) -
              Number(preflight.checks.previous_precompte.amount || 0),
          ),
        ) <= tolerance,
      is_critical: true,
      result_label: `${Number(preflight.checks.previous_precompte.amount || 0)} DA`,
      action_path: null,
    },
    {
      key: "header_consistency",
      label: "Solde TVA comptable = TVA G50",
      ok:
        roundAmount(Math.abs(Number(header.tva_a_payer || 0) - Number(computedTva.tva_a_payer || 0))) <= tolerance &&
        roundAmount(
          Math.abs(
            Number(header.tva_precompte_reporter || 0) -
              Number(computedTva.precompte_reporter || 0),
          ),
        ) <= tolerance,
      is_critical: true,
      result_label: "Synthèse interne cohérente",
      action_path: null,
    },
  ];

  return {
    header_id: headerId,
    period_label: header.descriptor.period_label,
    can_validate: checks.filter((check) => check.is_critical).every((check) => check.ok),
    zero_lines,
    checks,
  };
};

export const validateG50Draft = async ({
  db,
  companyId,
  headerId,
  userId,
  role,
  permissions = [],
  confirmationText,
  io,
  ipAddress,
}) => {
  if (
    !(
      VALIDATION_ROLES.has(role) ||
      hasPermission(permissions, "*", "closure:update", "entries:validate")
    )
  ) {
    throw new ApiError(403, "Vous n'avez pas l'autorisation de valider une G50.");
  }

  if (cleanString(confirmationText).toUpperCase() !== "VALIDER") {
    throw new ApiError(400, "La confirmation de validation doit être égale à VALIDER.");
  }

  const header = await fetchHeaderContext(db, companyId, headerId);

  if (!["draft", "adjusted"].includes(header.status)) {
    throw new ApiError(409, "Cette G50 n'est plus modifiable.");
  }

  const control = await getG50DraftControlChecks({
    db,
    companyId,
    headerId,
  });

  if (!control.can_validate) {
    throw new ApiError(
      409,
      "La G50 ne peut pas être validée tant que les contrôles critiques ne sont pas conformes.",
    );
  }

  const draft = await getG50Draft({
    db,
    companyId,
    headerId,
    role,
    permissions,
  });
  const legacyDeclaration = await upsertLegacyG50DeclarationFromDraft({
    db,
    companyId,
    header: draft.header,
    lines: draft.lines,
    userId,
  });
  const createdEntries = await generatePaymentEntriesForValidatedG50({
    db,
    companyId,
    header: draft.header,
    legacyDeclarationId: legacyDeclaration.id,
    userId,
  });

  await db.query(
    `
      UPDATE g50_headers
      SET
        status = 'validated',
        validated_at = CURRENT_TIMESTAMP,
        validated_by = $2,
        g50_declaration_id = $3,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
    `,
    [headerId, userId || null, legacyDeclaration.id],
  );

  await db.query(
    `
      INSERT INTO g50_audit_log (
        g50_header_id,
        action,
        actor_id,
        details,
        ip_address
      )
      VALUES ($1, 'validated', $2, $3::jsonb, $4::inet)
    `,
    [
      headerId,
      userId || null,
      JSON.stringify({
        total_a_payer: draft.header.total_a_payer,
        total_corrections: draft.lines.filter((line) => line.is_manually_adjusted).length,
        zero_lines: control.zero_lines.map((line) => line.code),
        generated_entries: createdEntries,
      }),
      ipAddress || null,
    ],
  );

  await markFiscalDeadlineAsFiled(db, {
    companyId,
    declarationType: "G50",
    fiscalYearId: draft.header.fiscal_year_id,
    periodeLabel: draft.header.descriptor.period_label,
    declarationId: legacyDeclaration.id,
  });

  const notifications = await createCompanyNotifications(db, {
    companyId,
    type: "g50_validated",
    message: `G50 ${draft.header.descriptor.period_label} validée`,
    entityId: headerId,
  });

  emitNotifications(io, notifications);
  io?.to(`company:${companyId}`)?.emit("g50:validated", {
    header_id: headerId,
    total_a_payer: draft.header.total_a_payer,
    period_label: draft.header.descriptor.period_label,
  });

  return getG50Draft({
    db,
    companyId,
    headerId,
    role,
    permissions,
  });
};

export const reopenG50Draft = async ({
  db,
  companyId,
  headerId,
  userId,
  role,
  permissions = [],
  justification,
  ipAddress,
}) => {
  if (!(ADMIN_ROLES.has(role) || hasPermission(permissions, "*", "closure:update"))) {
    throw new ApiError(403, "Seuls les administrateurs peuvent rouvrir une G50 validée.");
  }

  const header = await fetchHeaderContext(db, companyId, headerId);

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

  const normalizedJustification = cleanString(justification);

  if (normalizedJustification.length < 12) {
    throw new ApiError(400, "Une justification de réouverture est requise.");
  }

  const draftLines = await fetchHeaderLines(db, headerId);
  const nextStatus = draftLines.some((line) => line.is_manually_adjusted) ? "adjusted" : "draft";

  await db.query(
    `
      UPDATE g50_headers
      SET
        status = $2,
        validated_at = NULL,
        validated_by = NULL,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
    `,
    [headerId, nextStatus],
  );

  if (header.g50_declaration_id) {
    await db.query(
      `
        UPDATE g50_declarations
        SET
          statut = 'brouillon',
          updated_at = CURRENT_TIMESTAMP
        WHERE id = $1
      `,
      [header.g50_declaration_id],
    );

    await db.query(
      `
        UPDATE fiscal_deadlines
        SET
          statut = $4,
          declaration_id = NULL
        WHERE company_id = $1
          AND declaration_type = 'G50'
          AND periode_label = $2
          AND COALESCE(fiscal_year_id, 0) = COALESCE($3, 0)
      `,
      [
        companyId,
        header.descriptor.period_label,
        header.fiscal_year_id,
        buildDeadlineStatusForReopened(header.descriptor.deadline_g50),
      ],
    );
  }

  await db.query(
    `
      INSERT INTO g50_audit_log (
        g50_header_id,
        action,
        actor_id,
        details,
        ip_address
      )
      VALUES ($1, 'reopened', $2, $3::jsonb, $4::inet)
    `,
    [
      headerId,
      userId || null,
      JSON.stringify({
        justification: normalizedJustification,
        previous_status: header.status,
      }),
      ipAddress || null,
    ],
  );

  return getG50Draft({
    db,
    companyId,
    headerId,
    role,
    permissions,
  });
};

export const recalculateG50Draft = async ({
  db,
  companyId,
  headerId,
  userId,
  role,
  permissions = [],
  io,
  ipAddress,
}) => {
  const header = await fetchHeaderContext(db, companyId, headerId);

  if (!["draft", "adjusted"].includes(header.status)) {
    throw new ApiError(409, "Seuls les brouillons G50 peuvent être recalculés.");
  }

  await db.query(
    `
      UPDATE g50_headers
      SET
        status = 'locked',
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
    `,
    [headerId],
  );

  await db.query(
    `
      INSERT INTO g50_audit_log (
        g50_header_id,
        action,
        actor_id,
        details,
        ip_address
      )
      VALUES ($1, 'locked', $2, $3::jsonb, $4::inet)
    `,
    [
      headerId,
      userId || null,
      JSON.stringify({
        reason: "recalculation",
        period_label: header.descriptor.period_label,
      }),
      ipAddress || null,
    ],
  );

  const result = await calculateG50({
    db,
    companyId,
    periodFrom: header.descriptor.date_from,
    periodTo: header.descriptor.date_to,
    userId,
    io,
    ipAddress,
  });

  return getG50Draft({
    db,
    companyId,
    headerId: result.header.id,
    role,
    permissions,
  });
};

export const listG50Corrections = async ({
  db,
  companyId,
  periodFrom = "",
  periodTo = "",
}) => {
  const params = [companyId];
  const conditions = ["gh.company_id = $1"];

  if (periodFrom) {
    params.push(periodFrom);
    conditions.push(`fp.date_to >= $${params.length}::date`);
  }

  if (periodTo) {
    params.push(periodTo);
    conditions.push(`fp.date_from <= $${params.length}::date`);
  }

  const { rows } = await db.query(
    `
      SELECT
        ga.id,
        ga.g50_header_id,
        ga.g50_line_id,
        ga.code,
        gl.label,
        ga.montant_avant,
        ga.montant_apres,
        ga.difference,
        ga.reason_type,
        ga.justification,
        ga.supporting_document_url,
        ga.adjusted_at,
        u.full_name AS adjusted_by_name,
        fp.period_type,
        fp.annee,
        fp.mois,
        fp.trimestre
      FROM g50_adjustments ga
      JOIN g50_headers gh
        ON gh.id = ga.g50_header_id
      JOIN g50_lines gl
        ON gl.id = ga.g50_line_id
      JOIN fiscal_periods fp
        ON fp.id = gh.fiscal_period_id
      LEFT JOIN users u
        ON u.id = ga.adjusted_by
      WHERE ${conditions.join(" AND ")}
      ORDER BY ga.adjusted_at DESC, ga.id DESC
    `,
    params,
  );

  const items = rows.map((row) => ({
    id: Number(row.id),
    g50_header_id: Number(row.g50_header_id),
    g50_line_id: Number(row.g50_line_id),
    period_label: formatPeriodLabel({
      period_type: row.period_type,
      annee: Number(row.annee),
      mois: row.mois,
      trimestre: row.trimestre,
    }),
    code: row.code,
    label: row.label,
    amount_before: Number(row.montant_avant || 0),
    amount_after: Number(row.montant_apres || 0),
    delta: Number(row.difference || 0),
    reason_type: row.reason_type || "other",
    justification: row.justification || "",
    supporting_document_url: row.supporting_document_url || null,
    adjusted_by_name: row.adjusted_by_name || null,
    adjusted_at: toDateKey(row.adjusted_at) ? new Date(row.adjusted_at).toISOString() : row.adjusted_at,
  }));

  return {
    stats: {
      total_corrections: items.length,
      total_adjusted_amount: roundAmount(
        items.reduce((total, item) => total + Math.abs(Number(item.delta || 0)), 0),
      ),
      g50_count: new Set(items.map((item) => item.g50_header_id)).size,
    },
    items,
  };
};

export const listG50History = async ({
  db,
  companyId,
  annee = null,
  role,
  permissions = [],
}) => {
  const params = [companyId];
  const conditions = ["gh.company_id = $1"];

  if (annee) {
    params.push(Number(annee));
    conditions.push(`fp.annee = $${params.length}`);
  }

  const { rows } = await db.query(
    `
      SELECT
        gh.id,
        gh.version,
        gh.status,
        gh.total_a_payer,
        gh.tva_a_payer,
        gh.total_cadre4,
        gh.calculated_at,
        gh.validated_at,
        calc_user.full_name AS calculated_by_name,
        valid_user.full_name AS validated_by_name,
        fp.period_type,
        fp.annee,
        fp.mois,
        fp.trimestre,
        COUNT(ga.id)::int AS adjustments_count
      FROM g50_headers gh
      JOIN fiscal_periods fp
        ON fp.id = gh.fiscal_period_id
      LEFT JOIN users calc_user
        ON calc_user.id = gh.calculated_by
      LEFT JOIN users valid_user
        ON valid_user.id = gh.validated_by
      LEFT JOIN g50_adjustments ga
        ON ga.g50_header_id = gh.id
      WHERE ${conditions.join(" AND ")}
      GROUP BY
        gh.id,
        gh.version,
        gh.status,
        gh.total_a_payer,
        gh.tva_a_payer,
        gh.total_cadre4,
        gh.calculated_at,
        gh.validated_at,
        calc_user.full_name,
        valid_user.full_name,
        fp.period_type,
        fp.annee,
        fp.mois,
        fp.trimestre
      ORDER BY fp.annee DESC, COALESCE(fp.mois, fp.trimestre) DESC, gh.version DESC
    `,
    params,
  );

  return {
    permissions: computePermissions({ role, permissions }),
    items: rows.map((row) => ({
      id: Number(row.id),
      period_label: formatPeriodLabel({
        period_type: row.period_type,
        annee: Number(row.annee),
        mois: row.mois,
        trimestre: row.trimestre,
      }),
      version: Number(row.version),
      status: row.status,
      total_a_payer: Number(row.total_a_payer || 0),
      tva_a_payer: Number(row.tva_a_payer || 0),
      irg_total: Number(row.total_cadre4 || 0),
      calculated_at: row.calculated_at,
      calculated_by_name: row.calculated_by_name || null,
      validated_at: row.validated_at,
      validated_by_name: row.validated_by_name || null,
      adjustments_count: Number(row.adjustments_count || 0),
    })),
  };
};

export const buildG50DraftPdf = async ({
  db,
  companyId,
  headerId,
  generatedBy,
}) => {
  const header = await fetchHeaderContext(db, companyId, headerId);
  const lines = await fetchHeaderLines(db, headerId);
  const payload = await buildLegacyG50PayloadFromDraft({
    db,
    companyId,
    header,
    lines,
  });

  return buildG50PdfBuffer({
    company: payload.company,
    profile: payload.profile,
    declaration: payload.declaration,
    previousPrecompte: payload.previousPrecompte,
    generatedBy,
  });
};
