import { randomUUID } from "crypto";

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

const RULE_TYPES = new Set([
  "account_range",
  "account_exact",
  "journal_type",
  "entry_label_pattern",
  "auxiliary_type",
]);
const JOURNAL_TYPES = new Set([
  "purchase",
  "sale",
  "cash",
  "bank",
  "misc",
  "payroll",
  "assets",
]);
const AUXILIARY_TYPES = new Set(["client", "supplier"]);
const AMOUNT_SIDES = new Set(["debit", "credit", "net", "absolute"]);
const STATUS_FILTERS = new Set(["active", "expired", "future", "all"]);

const JOURNAL_TYPE_LABELS = {
  purchase: "Achats",
  sale: "Ventes",
  cash: "Caisse",
  bank: "Banque",
  misc: "OD",
  payroll: "Paie",
  assets: "Immobilisations",
};

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 subtractDays = (dateKey, days) => {
  const date = new Date(`${dateKey}T00:00:00Z`);
  date.setUTCDate(date.getUTCDate() - days);
  return toDateKey(date);
};

const getLastMonthRange = () => {
  const now = new Date();
  const firstCurrentMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
  const lastPreviousMonth = new Date(firstCurrentMonth.getTime() - 24 * 60 * 60 * 1000);
  const firstPreviousMonth = new Date(
    Date.UTC(lastPreviousMonth.getUTCFullYear(), lastPreviousMonth.getUTCMonth(), 1),
  );

  return {
    from: toDateKey(firstPreviousMonth),
    to: toDateKey(lastPreviousMonth),
  };
};

const normalizeArrayValues = (value) => {
  if (Array.isArray(value)) {
    return value;
  }

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

  return [value];
};

const uniqueStrings = (values) => [...new Set(values.map((value) => cleanString(value)).filter(Boolean))];

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

  return rows[0] || null;
};

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

  return rows[0] || null;
};

const deriveRuleStatus = (rule, referenceDate = getTodayKey()) => {
  if (!rule.is_active) {
    return "expired";
  }

  if (rule.effective_from > referenceDate) {
    return "future";
  }

  if (rule.effective_to && rule.effective_to < referenceDate) {
    return "expired";
  }

  return "active";
};

const formatJournalTypeList = (journalTypes = []) =>
  journalTypes.map((type) => JOURNAL_TYPE_LABELS[type] || type);

const buildScopeSummary = (rule) => {
  const amountSideLabel =
    rule.amount_side === "debit"
      ? "débit"
      : rule.amount_side === "credit"
        ? "crédit"
        : rule.amount_side === "net"
          ? "net"
          : "valeur absolue";

  if (rule.rule_type === "account_range") {
    return `Comptes ${rule.account_code_from} → ${rule.account_code_to} (${amountSideLabel})`;
  }

  if (rule.rule_type === "account_exact") {
    const accounts = rule.account_code_exact_list?.length
      ? rule.account_code_exact_list
      : rule.account_code_exact
        ? [rule.account_code_exact]
        : [];

    return `Comptes ${accounts.join(", ")} (${amountSideLabel})`;
  }

  if (rule.rule_type === "journal_type") {
    return `Journal ${formatJournalTypeList(rule.journal_type_list || []).join(", ")}`;
  }

  if (rule.rule_type === "entry_label_pattern") {
    return `Libellé ${rule.entry_label_pattern || "—"}`;
  }

  if (rule.rule_type === "auxiliary_type") {
    return `Tiers ${rule.auxiliary_type === "client" ? "clients" : "fournisseurs"}`;
  }

  return "Portée non définie";
};

const mapRuleRow = (row, referenceDate = getTodayKey()) => {
  const accountExactList = Array.isArray(row.account_code_exact_list)
    ? row.account_code_exact_list.map((value) => cleanString(value)).filter(Boolean)
    : [];
  const journalTypeList = Array.isArray(row.journal_type_list)
    ? row.journal_type_list.map((value) => cleanString(value)).filter(Boolean)
    : [];

  const item = {
    id: Number(row.id),
    company_id: Number(row.company_id),
    tax_code_id: Number(row.tax_code_id),
    version_group_key: row.version_group_key || `legacy-${row.id}`,
    version_number: Number(row.version_number || 1),
    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: accountExactList,
    journal_type: row.journal_type || "",
    journal_type_list: journalTypeList.length
      ? journalTypeList
      : row.journal_type
        ? [row.journal_type]
        : [],
    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) : "",
    is_active: Boolean(row.is_active),
    description: row.description || "",
    legal_reference: row.legal_reference || "",
    created_by: row.created_by ? Number(row.created_by) : null,
    created_at: row.created_at,
    updated_at: row.updated_at,
    tax_code: row.tax_code_id
      ? {
          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 || "",
        }
      : null,
  };

  return {
    ...item,
    status: deriveRuleStatus(item, referenceDate),
    scope_summary: buildScopeSummary(item),
  };
};

const fetchRuleById = async (db, companyId, ruleId) => {
  const { rows } = await db.query(
    `
      SELECT
        fr.*,
        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_rules fr
      JOIN fiscal_tax_codes tc
        ON tc.id = fr.tax_code_id
      WHERE fr.id = $1
        AND fr.company_id = $2
      LIMIT 1
    `,
    [ruleId, companyId],
  );

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

const normalizeRulePayload = async ({
  db,
  companyId,
  body,
  existingRule = null,
  forNewVersion = false,
}) => {
  const ruleName = cleanString(body.rule_name || body.ruleName || existingRule?.rule_name);
  const taxCodeId = Number(body.tax_code_id || body.taxCodeId || existingRule?.tax_code_id || 0);
  const ruleType = cleanString(body.rule_type || body.ruleType || existingRule?.rule_type).toLowerCase();
  const legalReference = cleanString(
    body.legal_reference || body.legalReference || existingRule?.legal_reference,
  );
  const description = cleanString(body.description || existingRule?.description);
  const amountSide = cleanString(body.amount_side || body.amountSide || existingRule?.amount_side).toLowerCase();
  const sign = Number(body.sign ?? existingRule?.sign ?? 1) === -1 ? -1 : 1;
  const effectiveFrom = normalizeOptionalDate(
    body.effective_from || body.effectiveFrom || existingRule?.effective_from,
    "La date d'entrée en vigueur",
  );
  const effectiveTo = normalizeOptionalDate(
    body.effective_to || body.effectiveTo || existingRule?.effective_to,
    "La date de fin d'effet",
  );
  const isActive = normalizeBoolean(body.is_active ?? body.isActive, existingRule?.is_active ?? true);

  if (!ruleName) {
    throw new ApiError(400, "Le nom de la règle est obligatoire.");
  }

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

  const taxCode = await fetchTaxCodeById(db, companyId, taxCodeId);

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

  if (!RULE_TYPES.has(ruleType)) {
    throw new ApiError(400, "Le type de règle fiscale est invalide.");
  }

  if (!AMOUNT_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 d'effet ne peut pas précéder la date de début.");
  }

  const payload = {
    tax_code_id: taxCodeId,
    rule_name: ruleName,
    rule_type: ruleType,
    account_code_from: "",
    account_code_to: "",
    account_code_exact: "",
    account_code_exact_list: [],
    journal_type: "",
    journal_type_list: [],
    entry_label_pattern: "",
    auxiliary_type: "",
    amount_side: amountSide,
    sign,
    effective_from: effectiveFrom,
    effective_to: effectiveTo,
    is_active: isActive,
    description,
    legal_reference: legalReference,
  };

  if (ruleType === "account_range") {
    const accountCodeFrom = cleanString(
      body.account_code_from || body.accountCodeFrom || existingRule?.account_code_from,
    );
    const accountCodeTo = cleanString(
      body.account_code_to || body.accountCodeTo || existingRule?.account_code_to,
    );

    if (!accountCodeFrom || !accountCodeTo) {
      throw new ApiError(400, "La plage de comptes est obligatoire.");
    }

    const [fromAccount, toAccount] = await Promise.all([
      fetchAccountByCode(db, companyId, accountCodeFrom),
      fetchAccountByCode(db, companyId, accountCodeTo),
    ]);

    if (!fromAccount || !toAccount) {
      throw new ApiError(400, "Un compte de la plage sélectionnée est introuvable.");
    }

    if (accountCodeFrom > accountCodeTo) {
      throw new ApiError(400, "Le compte de début doit être inférieur au compte de fin.");
    }

    payload.account_code_from = accountCodeFrom;
    payload.account_code_to = accountCodeTo;
  } else if (ruleType === "account_exact") {
    const rawExactCodes = normalizeArrayValues(
      body.account_code_exact_list ||
        body.accountCodeExactList ||
        body.account_code_exact ||
        body.accountCodeExact ||
        existingRule?.account_code_exact_list ||
        existingRule?.account_code_exact,
    );
    const exactCodes = uniqueStrings(rawExactCodes);

    if (!exactCodes.length) {
      throw new ApiError(400, "Au moins un compte exact est obligatoire.");
    }

    const accountRows = await db.query(
      `
        SELECT code
        FROM accounts
        WHERE company_id = $1
          AND code = ANY($2::text[])
      `,
      [companyId, exactCodes],
    );

    if (accountRows.rows.length !== exactCodes.length) {
      throw new ApiError(400, "Un des comptes exacts sélectionnés est introuvable.");
    }

    payload.account_code_exact = exactCodes[0];
    payload.account_code_exact_list = exactCodes;
  } else if (ruleType === "journal_type") {
    const journalTypes = uniqueStrings(
      normalizeArrayValues(
        body.journal_type_list ||
          body.journalTypeList ||
          body.journal_type ||
          body.journalType ||
          existingRule?.journal_type_list ||
          existingRule?.journal_type,
      ).map((value) => cleanString(value).toLowerCase()),
    );

    if (!journalTypes.length) {
      throw new ApiError(400, "Au moins un type de journal est obligatoire.");
    }

    if (journalTypes.some((value) => !JOURNAL_TYPES.has(value))) {
      throw new ApiError(400, "Un type de journal sélectionné est invalide.");
    }

    payload.journal_type = journalTypes[0];
    payload.journal_type_list = journalTypes;
  } else if (ruleType === "entry_label_pattern") {
    const pattern = cleanString(
      body.entry_label_pattern || body.entryLabelPattern || existingRule?.entry_label_pattern,
    );

    if (!pattern) {
      throw new ApiError(400, "Le pattern de libellé est obligatoire.");
    }

    payload.entry_label_pattern = pattern;
  } else if (ruleType === "auxiliary_type") {
    const auxiliaryType = cleanString(
      body.auxiliary_type || body.auxiliaryType || existingRule?.auxiliary_type,
    ).toLowerCase();

    if (!AUXILIARY_TYPES.has(auxiliaryType)) {
      throw new ApiError(400, "Le type de tiers est invalide.");
    }

    payload.auxiliary_type = auxiliaryType;
  }

  if (!forNewVersion && existingRule?.version_group_key) {
    payload.version_group_key = existingRule.version_group_key;
    payload.version_number = existingRule.version_number;
  }

  return payload;
};

const buildRuleTestQuery = ({ rule, companyId, periodFrom, periodTo }) => {
  const params = [companyId, periodFrom, periodTo];
  const whereParts = [
    "je.company_id = $1",
    "je.status = 'validated'",
    "je.entry_date BETWEEN $2 AND $3",
  ];

  if (rule.rule_type === "account_range") {
    params.push(rule.account_code_from, rule.account_code_to);
    whereParts.push(`a.code BETWEEN $${params.length - 1} AND $${params.length}`);
  } else if (rule.rule_type === "account_exact") {
    const exactCodes = rule.account_code_exact_list?.length
      ? rule.account_code_exact_list
      : [rule.account_code_exact].filter(Boolean);
    params.push(exactCodes);
    whereParts.push(`a.code = ANY($${params.length}::text[])`);
  } else if (rule.rule_type === "journal_type") {
    const journalTypes = rule.journal_type_list?.length
      ? rule.journal_type_list
      : [rule.journal_type].filter(Boolean);
    params.push(journalTypes);
    whereParts.push(`j.journal_type = ANY($${params.length}::text[])`);
  } else if (rule.rule_type === "entry_label_pattern") {
    params.push(rule.entry_label_pattern);
    whereParts.push(
      `(COALESCE(je.label, '') ILIKE $${params.length} OR COALESCE(el.label, '') ILIKE $${params.length})`,
    );
  } else if (rule.rule_type === "auxiliary_type") {
    params.push(rule.auxiliary_type);
    whereParts.push(`aa.third_party_type = $${params.length}`);
  }

  const amountExpression =
    rule.amount_side === "debit"
      ? "el.debit"
      : rule.amount_side === "credit"
        ? "el.credit"
        : rule.amount_side === "net"
          ? "(el.debit - el.credit)"
          : "ABS(el.debit - el.credit)";

  const signedExpression = `(${amountExpression}) * ${rule.sign}`;

  return {
    params,
    totalQuery: `
      SELECT
        COUNT(*)::int AS matching_lines_count,
        COUNT(DISTINCT je.id)::int AS matching_entries_count,
        COALESCE(SUM(${signedExpression}), 0)::numeric(18, 2) AS total_amount
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      JOIN accounts a
        ON a.id = el.account_id
      JOIN journals j
        ON j.id = je.journal_id
      LEFT JOIN auxiliary_accounts aa
        ON aa.id = el.auxiliary_account_id
      WHERE ${whereParts.join(" AND ")}
    `,
    sampleQuery: `
      SELECT
        el.id AS entry_line_id,
        je.id AS entry_id,
        TO_CHAR(je.entry_date, 'YYYY-MM-DD') AS entry_date,
        je.entry_number,
        je.label AS entry_label,
        COALESCE(el.label, je.label) AS line_label,
        a.code AS account_code,
        a.label AS account_label,
        j.code AS journal_code,
        j.label AS journal_label,
        ${signedExpression}::numeric(18, 2) AS matched_amount
      FROM entry_lines el
      JOIN journal_entries je
        ON je.id = el.entry_id
      JOIN accounts a
        ON a.id = el.account_id
      JOIN journals j
        ON j.id = je.journal_id
      LEFT JOIN auxiliary_accounts aa
        ON aa.id = el.auxiliary_account_id
      WHERE ${whereParts.join(" AND ")}
      ORDER BY je.entry_date DESC, je.id DESC, el.id DESC
      LIMIT 10
    `,
  };
};

const runRuleTest = async ({ db, rule, companyId, periodFrom, periodTo }) => {
  const { params, totalQuery, sampleQuery } = buildRuleTestQuery({
    rule,
    companyId,
    periodFrom,
    periodTo,
  });

  const [totalResult, sampleResult] = await Promise.all([
    db.query(totalQuery, params),
    db.query(sampleQuery, params),
  ]);

  const totals = totalResult.rows[0] || {};

  return {
    period_from: periodFrom,
    period_to: periodTo,
    matching_lines_count: Number(totals.matching_lines_count || 0),
    matching_entries_count: Number(totals.matching_entries_count || 0),
    total_amount: Number(totals.total_amount || 0),
    tone:
      Number(totals.matching_lines_count || 0) > 0
        ? "success"
        : "warning",
    sample_lines: sampleResult.rows.map((row) => ({
      entry_line_id: Number(row.entry_line_id),
      entry_id: Number(row.entry_id),
      entry_date: row.entry_date,
      entry_number: row.entry_number,
      entry_label: row.entry_label,
      line_label: row.line_label,
      account_code: row.account_code,
      account_label: row.account_label,
      journal_code: row.journal_code,
      journal_label: row.journal_label,
      matched_amount: Number(row.matched_amount || 0),
    })),
  };
};

const fetchHistoryRows = async (db, companyId, versionGroupKey, fallbackRuleId) => {
  if (versionGroupKey) {
    const { rows } = await db.query(
      `
        SELECT
          fr.*,
          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_rules fr
        JOIN fiscal_tax_codes tc
          ON tc.id = fr.tax_code_id
        WHERE fr.company_id = $1
          AND fr.version_group_key = $2
        ORDER BY fr.version_number ASC, fr.effective_from ASC, fr.created_at ASC
      `,
      [companyId, versionGroupKey],
    );

    return rows;
  }

  const { rows } = await db.query(
    `
      SELECT
        fr.*,
        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_rules fr
      JOIN fiscal_tax_codes tc
        ON tc.id = fr.tax_code_id
      WHERE fr.company_id = $1
        AND fr.id = $2
    `,
    [companyId, fallbackRuleId],
  );

  return rows;
};

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

    const search = cleanString(req.query.search);
    const taxCodeId = Number(req.query.tax_code_id || req.query.taxCodeId || 0) || null;
    const status = cleanString(req.query.status || "all").toLowerCase() || "all";
    const referenceDate = normalizeOptionalDate(req.query.date, "La date de référence") || getTodayKey();
    const effectiveFrom = normalizeOptionalDate(
      req.query.effective_from || req.query.effectiveFrom,
      "La date de début",
    );
    const effectiveTo = normalizeOptionalDate(
      req.query.effective_to || req.query.effectiveTo,
      "La date de fin",
    );
    const ruleType = cleanString(req.query.rule_type || req.query.ruleType).toLowerCase();

    if (!STATUS_FILTERS.has(status)) {
      throw new ApiError(400, "Le filtre de statut est invalide.");
    }

    const params = [companyId];
    const whereParts = ["fr.company_id = $1"];

    if (search) {
      params.push(search);
      whereParts.push(`fr.rule_name ILIKE '%' || $${params.length} || '%'`);
    }

    if (taxCodeId) {
      params.push(taxCodeId);
      whereParts.push(`fr.tax_code_id = $${params.length}`);
    }

    if (ruleType) {
      if (!RULE_TYPES.has(ruleType)) {
        throw new ApiError(400, "Le type de règle fiscale est invalide.");
      }
      params.push(ruleType);
      whereParts.push(`fr.rule_type = $${params.length}`);
    }

    if (effectiveFrom) {
      params.push(effectiveFrom);
      whereParts.push(`COALESCE(fr.effective_to, '2999-12-31') >= $${params.length}`);
    }

    if (effectiveTo) {
      params.push(effectiveTo);
      whereParts.push(`fr.effective_from <= $${params.length}`);
    }

    const { rows } = await pool.query(
      `
        SELECT
          fr.*,
          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_rules fr
        JOIN fiscal_tax_codes tc
          ON tc.id = fr.tax_code_id
        WHERE ${whereParts.join(" AND ")}
        ORDER BY fr.effective_from DESC, fr.created_at DESC
      `,
      params,
    );

    let items = rows.map((row) => mapRuleRow(row, referenceDate));

    if (status !== "all") {
      items = items.filter((item) => item.status === status);
    }

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

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

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

    if (!ruleId) {
      throw new ApiError(400, "Identifiant de règle invalide.");
    }

    const item = await fetchRuleById(pool, companyId, ruleId);

    if (!item) {
      throw new ApiError(404, "Règle fiscale introuvable.");
    }

    res.json({ item });
  },

  create: 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 normalizeRulePayload({
        db: client,
        companyId,
        body: req.body,
      });
      const versionGroupKey = randomUUID();

      const { rows } = await client.query(
        `
          INSERT INTO fiscal_rules (
            company_id,
            tax_code_id,
            version_group_key,
            version_number,
            rule_name,
            rule_type,
            account_code_from,
            account_code_to,
            account_code_exact,
            account_code_exact_list,
            journal_type,
            journal_type_list,
            entry_label_pattern,
            auxiliary_type,
            amount_side,
            sign,
            effective_from,
            effective_to,
            is_active,
            description,
            legal_reference,
            created_by
          )
          VALUES (
            $1, $2, $3, 1, $4, $5, $6, $7, $8, $9::jsonb, $10, $11::jsonb, $12, $13, $14, $15,
            $16, $17, $18, $19, $20, $21
          )
          RETURNING id
        `,
        [
          companyId,
          payload.tax_code_id,
          versionGroupKey,
          payload.rule_name,
          payload.rule_type,
          payload.account_code_from || null,
          payload.account_code_to || null,
          payload.account_code_exact || null,
          payload.account_code_exact_list.length
            ? JSON.stringify(payload.account_code_exact_list)
            : null,
          payload.journal_type || null,
          payload.journal_type_list.length ? JSON.stringify(payload.journal_type_list) : null,
          payload.entry_label_pattern || null,
          payload.auxiliary_type || null,
          payload.amount_side,
          payload.sign,
          payload.effective_from,
          payload.effective_to || null,
          payload.is_active,
          payload.description || null,
          payload.legal_reference || null,
          req.user?.sub || null,
        ],
      );

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

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

      await client.query("COMMIT");

      res.status(201).json({
        message: "Règle fiscale créée.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

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

    if (!ruleId) {
      throw new ApiError(400, "Identifiant de règle invalide.");
    }

    const client = await pool.connect();

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

      const existingRule = await fetchRuleById(client, companyId, ruleId);

      if (!existingRule) {
        throw new ApiError(404, "Règle fiscale introuvable.");
      }

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

      await client.query(
        `
          UPDATE fiscal_rules
          SET
            tax_code_id = $2,
            rule_name = $3,
            rule_type = $4,
            account_code_from = $5,
            account_code_to = $6,
            account_code_exact = $7,
            account_code_exact_list = $8::jsonb,
            journal_type = $9,
            journal_type_list = $10::jsonb,
            entry_label_pattern = $11,
            auxiliary_type = $12,
            amount_side = $13,
            sign = $14,
            effective_from = $15,
            effective_to = $16,
            is_active = $17,
            description = $18,
            legal_reference = $19,
            updated_at = NOW()
          WHERE id = $1
        `,
        [
          ruleId,
          payload.tax_code_id,
          payload.rule_name,
          payload.rule_type,
          payload.account_code_from || null,
          payload.account_code_to || null,
          payload.account_code_exact || null,
          payload.account_code_exact_list.length
            ? JSON.stringify(payload.account_code_exact_list)
            : null,
          payload.journal_type || null,
          payload.journal_type_list.length ? JSON.stringify(payload.journal_type_list) : null,
          payload.entry_label_pattern || null,
          payload.auxiliary_type || null,
          payload.amount_side,
          payload.sign,
          payload.effective_from,
          payload.effective_to || null,
          payload.is_active,
          payload.description || null,
          payload.legal_reference || null,
        ],
      );

      const item = await fetchRuleById(client, companyId, ruleId);

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

      await client.query("COMMIT");

      res.json({
        message: "Règle fiscale mise à jour.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

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

    if (!ruleId) {
      throw new ApiError(400, "Identifiant de règle invalide.");
    }

    const client = await pool.connect();

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

      const existingRule = await fetchRuleById(client, companyId, ruleId);

      if (!existingRule) {
        throw new ApiError(404, "Règle fiscale introuvable.");
      }

      if (deriveRuleStatus(existingRule) === "active") {
        throw new ApiError(
          400,
          "Seules les règles futures, expirées ou désactivées peuvent être supprimées.",
        );
      }

      await client.query(
        `
          DELETE FROM fiscal_rules
          WHERE id = $1
            AND company_id = $2
        `,
        [ruleId, companyId],
      );

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

      await client.query("COMMIT");

      res.json({
        message: "Règle fiscale supprimée.",
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

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

    if (!ruleId) {
      throw new ApiError(400, "Identifiant de règle invalide.");
    }

    const client = await pool.connect();

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

      const existingRule = await fetchRuleById(client, companyId, ruleId);

      if (!existingRule) {
        throw new ApiError(404, "Règle fiscale introuvable.");
      }

      const versionGroupKey = existingRule.version_group_key?.startsWith("legacy-")
        ? randomUUID()
        : existingRule.version_group_key || randomUUID();

      if (!existingRule.version_group_key || existingRule.version_group_key.startsWith("legacy-")) {
        await client.query(
          `
            UPDATE fiscal_rules
            SET version_group_key = $2
            WHERE id = $1
          `,
          [ruleId, versionGroupKey],
        );
      }

      const payload = await normalizeRulePayload({
        db: client,
        companyId,
        body: {
          ...existingRule,
          ...req.body,
          effective_from: req.body.effective_from || req.body.effectiveFrom || getTodayKey(),
        },
        existingRule: {
          ...existingRule,
          version_group_key: versionGroupKey,
        },
        forNewVersion: true,
      });

      const newVersionNumber = Number(existingRule.version_number || 1) + 1;
      const previousRuleEffectiveTo = subtractDays(payload.effective_from, 1);

      await client.query(
        `
          UPDATE fiscal_rules
          SET
            effective_to = $2,
            updated_at = NOW()
          WHERE id = $1
            AND (
              effective_to IS NULL
              OR effective_to >= $3
            )
        `,
        [ruleId, previousRuleEffectiveTo, payload.effective_from],
      );

      const { rows } = await client.query(
        `
          INSERT INTO fiscal_rules (
            company_id,
            tax_code_id,
            version_group_key,
            version_number,
            rule_name,
            rule_type,
            account_code_from,
            account_code_to,
            account_code_exact,
            account_code_exact_list,
            journal_type,
            journal_type_list,
            entry_label_pattern,
            auxiliary_type,
            amount_side,
            sign,
            effective_from,
            effective_to,
            is_active,
            description,
            legal_reference,
            created_by
          )
          VALUES (
            $1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11, $12::jsonb, $13, $14, $15, $16,
            $17, $18, $19, $20, $21, $22
          )
          RETURNING id
        `,
        [
          companyId,
          payload.tax_code_id,
          versionGroupKey,
          newVersionNumber,
          payload.rule_name,
          payload.rule_type,
          payload.account_code_from || null,
          payload.account_code_to || null,
          payload.account_code_exact || null,
          payload.account_code_exact_list.length
            ? JSON.stringify(payload.account_code_exact_list)
            : null,
          payload.journal_type || null,
          payload.journal_type_list.length ? JSON.stringify(payload.journal_type_list) : null,
          payload.entry_label_pattern || null,
          payload.auxiliary_type || null,
          payload.amount_side,
          payload.sign,
          payload.effective_from,
          payload.effective_to || null,
          payload.is_active,
          payload.description || null,
          payload.legal_reference || null,
          req.user?.sub || null,
        ],
      );

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

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

      await client.query("COMMIT");

      res.status(201).json({
        message: "Nouvelle version de règle créée.",
        item,
      });
    } catch (error) {
      await client.query("ROLLBACK");
      throw error;
    } finally {
      client.release();
    }
  },

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

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

    if (!ruleId) {
      throw new ApiError(400, "Identifiant de règle invalide.");
    }

    const item = await fetchRuleById(pool, companyId, ruleId);

    if (!item) {
      throw new ApiError(404, "Règle fiscale introuvable.");
    }

    const fallbackRange = getLastMonthRange();
    const periodFrom =
      normalizeOptionalDate(
        req.query.period_from || req.query.periodFrom || req.body?.period_from || req.body?.periodFrom,
        "La date de début de test",
      ) || fallbackRange.from;
    const periodTo =
      normalizeOptionalDate(
        req.query.period_to || req.query.periodTo || req.body?.period_to || req.body?.periodTo,
        "La date de fin de test",
      ) || fallbackRange.to;

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

    const preview = await runRuleTest({
      db: pool,
      rule: item,
      companyId,
      periodFrom,
      periodTo,
    });

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

    res.json({
      item,
      preview,
    });
  },

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

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

    const fallbackRange = getLastMonthRange();
    const periodFrom =
      normalizeOptionalDate(req.body.period_from || req.body.periodFrom, "La date de début de test") ||
      fallbackRange.from;
    const periodTo =
      normalizeOptionalDate(req.body.period_to || req.body.periodTo, "La date de fin de test") ||
      fallbackRange.to;

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

    const virtualRule = {
      ...payload,
      id: null,
      company_id: companyId,
      version_group_key: null,
      version_number: 1,
      created_by: null,
      created_at: null,
      updated_at: null,
    };

    const preview = await runRuleTest({
      db: pool,
      rule: virtualRule,
      companyId,
      periodFrom,
      periodTo,
    });

    res.json({
      preview,
    });
  },

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

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

    if (!ruleId) {
      throw new ApiError(400, "Identifiant de règle invalide.");
    }

    const item = await fetchRuleById(pool, companyId, ruleId);

    if (!item) {
      throw new ApiError(404, "Règle fiscale introuvable.");
    }

    const rows = await fetchHistoryRows(
      pool,
      companyId,
      item.version_group_key?.startsWith("legacy-") ? null : item.version_group_key,
      item.id,
    );

    res.json({
      items: rows.map((row) => mapRuleRow(row)),
    });
  },
};
